Fix generator teardown leaks, add RETURN-in-GEN and LOOP conditions on DO EACH
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m26s
akbasic CI Build / coverage (push) Successful in 4m0s
akbasic CI Build / sanitizers (push) Successful in 6m33s
akbasic CI Build / akgl_build (push) Successful in 9m32s
akbasic CI Build / mutation_test (push) Successful in 18m47s
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m26s
akbasic CI Build / coverage (push) Successful in 4m0s
akbasic CI Build / sanitizers (push) Successful in 6m33s
akbasic CI Build / akgl_build (push) Successful in 9m32s
akbasic CI Build / mutation_test (push) Successful in 18m47s
Review findings and follow-ups from PR #61 review: - runtime_generator.c: akbasic_runtime_release_generator() now releases the forGeneratorEnv of every scope it walks through. Abandoning a generator that was itself suspended inside a FOR EACH over another generator stranded the inner generator's pool slot; a loop doing so exhausted the twelve-slot pool and died far from the cause. - runtime.c/runtime.h: new akbasic_runtime_unwind_to_environment(), the shared teardown for the error unwinds in pump_generator() and call_function() -- both previously bare prev_environment() loops with the same suspended-generator blindness. - runtime_commands.c: bare RETURN standing in a GEN's own frame ends the generator exactly as END GEN does -- a GEN is a function at heart. RETURN with a value there is refused (values leave a GEN only through EMIT). The no-frame error message now says "GOSUB, DEF, or GEN". - runtime_structure.c: LOOP WHILE/UNTIL composes with DO EACH -- checked after each trip with the loop variable still holding that trip's value; a condition that stops the loop abandons the generator exactly as EXIT does. Previously the condition was silently ignored, while the verb reference documented it as working. - parser_commands.c: trailing tokens after the generator call on a FOR EACH/DO EACH line are refused at parse. Previously they sat unparsed and blew up only after the loop completed, when the parent scope resumed the line mid-statement -- an error at the loop's end pointing at its start. - tests/generators.c: pool-exhaustion tests for the nested-abandonment and LOOP-condition paths, RETURN semantics tests, and a direct test of the unwind primitive. Three new golden pairs cover RETURN, LOOP conditions and the misplaced-condition parse error. - docs: RETURN and LOOP-condition semantics in 04-control-flow.md and 11-verb-reference.md; corrected the self-recursion analogy (functions are re-entrant here). TODO.md 1.10 records the generator design decisions the code comments were already citing, plus the zero-arg parameter-list limitation. MAINTENANCE.md gains the abandoned-generators invariant those comments also cited. Co-Authored-By: Andrew Kesterson <andrew@aklabs.net> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -197,8 +197,9 @@ A `GEN`'s body is ordinary BASIC: it may hold its own `FOR`, `DO`, `IF` or `GOSU
|
||||
around the `EMIT`s, and even invoke another `GEN` with its own `FOR EACH`/`DO EACH` --
|
||||
the same generator invoked with different arguments, nested or side by side, is not
|
||||
recursion. A `GEN` invoking *itself* from within its own currently-running body is
|
||||
refused, the same way it would be an error to call a function that has not returned yet
|
||||
by name from inside its own body without meaning to recurse.
|
||||
refused: unlike a function call, which runs to completion and returns, the outer
|
||||
invocation is suspended mid-body waiting on the same loop, and there is no answer to
|
||||
"which EMIT feeds which loop" that is not a surprise.
|
||||
|
||||
`EXIT` leaves a `FOR EACH`/`DO EACH` loop early, exactly as it does a plain `FOR` or
|
||||
`DO`, and the generator it was consuming stops there -- nothing forces the rest of it to
|
||||
@@ -224,6 +225,59 @@ run just because the loop started it:
|
||||
STOPPED
|
||||
```
|
||||
|
||||
`RETURN` ends a `GEN` from the inside, exactly as it ends a multi-line `DEF` or a
|
||||
`GOSUB`: the generator is done, the loop consuming it ends, and the program carries on
|
||||
after the loop. What a generator's `RETURN` cannot do is carry a value -- values leave
|
||||
a `GEN` one at a time, through `EMIT`, and `RETURN 99` inside one is an error. Like a
|
||||
`GOSUB`'s or a `DEF`'s, the `RETURN` must stand in the `GEN`'s own scope: from inside
|
||||
a `FOR` or `DO` the body opened, it is an error, though `IF ... THEN RETURN` is fine
|
||||
because `IF` opens no scope of its own.
|
||||
|
||||
```basic
|
||||
10 GEN FIRSTFEW(N#)
|
||||
20 EMIT 1
|
||||
30 IF N# < 2 THEN RETURN
|
||||
40 EMIT 2
|
||||
50 IF N# < 3 THEN RETURN
|
||||
60 EMIT 3
|
||||
70 END GEN
|
||||
80 FOR EACH V# IN FIRSTFEW(2)
|
||||
90 PRINT V#
|
||||
100 NEXT V#
|
||||
110 PRINT "DONE"
|
||||
```
|
||||
|
||||
```output
|
||||
1
|
||||
2
|
||||
DONE
|
||||
```
|
||||
|
||||
A `DO EACH`'s `LOOP` may carry a `WHILE` or `UNTIL`, and the two compose: the condition
|
||||
is checked after each trip through the body, with the loop variable still holding that
|
||||
trip's value, and a condition that says stop abandons the rest of the generator exactly
|
||||
as `EXIT` does. The condition belongs on the `LOOP` -- putting it on the `DO` line is
|
||||
an error, since the `DO` line already says what the loop consumes.
|
||||
|
||||
```basic
|
||||
10 GEN COUNTUP(N#)
|
||||
20 FOR I# = 1 TO N#
|
||||
30 EMIT I#
|
||||
40 NEXT I#
|
||||
50 END GEN
|
||||
60 DO EACH V# IN COUNTUP(10)
|
||||
70 PRINT V#
|
||||
80 LOOP UNTIL V# = 3
|
||||
90 PRINT "STOPPED"
|
||||
```
|
||||
|
||||
```output
|
||||
1
|
||||
2
|
||||
3
|
||||
STOPPED
|
||||
```
|
||||
|
||||
A `GEN` shares its namespace with `DEF` -- the same name cannot be both -- but it is not
|
||||
a function and cannot be called like one: `X# = COUNTUP(3)` is refused, because nothing
|
||||
about an ordinary call means "resume where the last `EMIT` left off." `FOR EACH`/`DO
|
||||
|
||||
Reference in New Issue
Block a user