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>
Adds generator support per the plan in issue 57:
- environment.h: isGenerator/generatorFn on a GEN call's own environment,
isEachLoop and forGeneratorEnv on a FOR EACH/DO EACH loop's own
environment.
- runtime.c: splits akbasic_runtime_prev_environment() into
akbasic_runtime_detach_environment() (return to parent without releasing)
and akbasic_runtime_release_environment() (give variables and the pool
slot back, on any environment); prev_environment() is now the two in
sequence. akbasic_runtime_call_function() refuses to call a GEN like an
ordinary function.
- verbs.c/verbs.h/scanner: new keywords GEN, EMIT, EACH, IN and the compound
verb "END GEN" (built by a new akbasic_parse_end(), the same trick
akbasic_parse_print() uses for PRINT #).
- parser_commands.c: akbasic_parse_gen() (modelled on multi-line DEF),
akbasic_parse_end(), and EACH branches in akbasic_parse_for()/
akbasic_parse_do().
- runtime_generator.c (new): akbasic_cmd_gen, akbasic_cmd_emit,
akbasic_cmd_end_gen, and the invoke/pump/release machinery FOR EACH, DO
EACH, NEXT and LOOP share. EMIT walks up to the nearest isGenerator
ancestor rather than assuming it is standing directly in the GEN's own
call frame, because a GEN body may nest its own FOR/DO/GOSUB around an
EMIT -- the issue's own ROOMOBJECTS example does exactly that.
- runtime_commands.c/runtime_structure.c: EACH branches in cmd_for/cmd_do,
matching EACH branches in cmd_next/cmd_loop, and forGeneratorEnv release
on every path that can abandon a live generator (EXIT, a NEXT that pops
for a mismatched loop variable).
Deviates from the plan in one place: FunctionDef gained an isGenerator flag
(not in the plan's field list) because refusing a GEN called like a
function has to happen before anything is pushed. Relying on EMIT's own
isGenerator check for that case doesn't work: akbasic_runtime_call_function()
drives its own step loop the same way akbasic_runtime_pump_generator() does,
and a BASIC-level error inside that loop is swallowed by process_line_run()
as reported-but-not-propagated, so the call would silently "succeed" with a
meaningless return value instead of failing.
Also: a zero-argument parameter list is not supported by the DEF/GEN
parameter parser this reuses (a pre-existing limitation, not
generator-specific); every generator in the tests takes at least one
parameter as a result.
Tests: tests/generators.c (pool exhaustion under repeated EXIT, calling a
GEN like a function, EMIT outside a GEN, self-recursion, sibling/nested
invocations) and tests/language/flowcontrol/generators_*.bas -- the
issue's own ROOMOBJECTS example in both loop shapes, an empty generator,
non-numeric EMIT, nested/interleaved invocations, and three error-path
golden cases. Docs: control-flow chapter 4 gets a GEN/EMIT/FOR EACH/DO EACH
section, the verb reference gets GEN/EMIT/END GEN entries and updated
FOR/DO/NEXT/LOOP/EXIT rows, and architecture chapter 14 documents the
detach/release split and the two-environment generator invocation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reflow the approved reference cases, shorten local fixture comments, and split the executable docs line so the 80-byte input buffer can read every source line.
Co-authored-by: andrew <andrew@aklabs.net>
Move every program and expectation out of tests/reference and register the unified tests/language corpus as local cases. Remove the old immutable-corpus protections from build, maintenance, and documentation paths.
Co-authored-by: andrew <andrew@aklabs.net>