Fix generator teardown leaks, add RETURN-in-GEN and LOOP conditions on DO EACH

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:
Ishikawa
2026-08-06 11:29:17 -04:00
committed by Andrew Kesterson
parent f7e8d4b82b
commit a29c7f34fe
17 changed files with 426 additions and 13 deletions

View File

@@ -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