Implement generators: GEN, EMIT, END GEN, FOR EACH and DO EACH (issue 57)
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m32s
akbasic CI Build / coverage (push) Successful in 4m7s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / akgl_build (push) Successful in 8m12s
akbasic CI Build / mutation_test (push) Successful in 23m3s

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>
This commit is contained in:
2026-08-06 10:02:43 -04:00
parent 11e4e96daf
commit f7e8d4b82b
32 changed files with 1322 additions and 23 deletions

View File

@@ -0,0 +1,6 @@
10 REM A GEN invoked like an ordinary function is refused, not silently run.
20 GEN ONE(N#)
30 EMIT N#
40 END GEN
50 X# = ONE(5)
60 PRINT "UNREACHABLE"

View File

@@ -0,0 +1,2 @@
? 50 : RUNTIME ERROR ONE is a GEN; call it with FOR EACH or DO EACH, not as a function

View File

@@ -0,0 +1,14 @@
10 REM Same generator as generators_foreach.bas, via DO EACH ... LOOP.
20 DIM OBJ#(3)
30 OBJ#(0) = 100
40 OBJ#(1) = 200
50 OBJ#(2) = 300
60 GEN ROOMOBJECTS(R#)
70 FOR I# = 0 TO 2
80 IF I# <> 1 THEN EMIT OBJ#(I#)
90 NEXT I#
100 END GEN
110 DO EACH O# IN ROOMOBJECTS(0)
120 PRINT O#
130 LOOP
140 PRINT "DONE"

View File

@@ -0,0 +1,3 @@
100
300
DONE

View File

@@ -0,0 +1,3 @@
10 REM EMIT outside any GEN invocation is refused cleanly.
20 EMIT 5
30 PRINT "UNREACHABLE"

View File

@@ -0,0 +1,2 @@
? 20 : RUNTIME ERROR EMIT outside the context of a GEN body

View File

@@ -0,0 +1,11 @@
10 REM A GEN with no EMIT at all runs its FOR EACH/DO EACH body zero times.
20 GEN NOTHING(N#)
30 END GEN
40 FOR EACH X# IN NOTHING(0)
50 PRINT "NEVER FOR"
60 NEXT X#
70 PRINT "AFTER FOR"
80 DO EACH Y# IN NOTHING(0)
90 PRINT "NEVER DO"
100 LOOP
110 PRINT "AFTER DO"

View File

@@ -0,0 +1,2 @@
AFTER FOR
AFTER DO

View File

@@ -0,0 +1,15 @@
10 REM The GEN/FOR EACH example from issue #57, with real arrays standing in
20 REM for OBJCOUNT%/VISIBLE/OBJ%.
30 DIM OBJ#(3)
40 OBJ#(0) = 100
50 OBJ#(1) = 200
60 OBJ#(2) = 300
70 GEN ROOMOBJECTS(R#)
80 FOR I# = 0 TO 2
90 IF I# <> 1 THEN EMIT OBJ#(I#)
100 NEXT I#
110 END GEN
120 FOR EACH O# IN ROOMOBJECTS(0)
130 PRINT O#
140 NEXT O#
150 PRINT "DONE"

View File

@@ -0,0 +1,3 @@
100
300
DONE

View File

@@ -0,0 +1,10 @@
10 REM A stray NEXT with nothing open behaves like the plain-FOR case: the
20 REM FOR EACH's own NEXT closes it cleanly, and a second one errors.
30 GEN ONE(N#)
40 EMIT N#
50 END GEN
60 FOR EACH O# IN ONE(1)
70 PRINT O#
80 NEXT O#
90 NEXT O#
100 PRINT "UNREACHABLE"

View File

@@ -0,0 +1,3 @@
1
? 90 : RUNTIME ERROR NEXT outside the context of FOR

View File

@@ -0,0 +1,22 @@
10 REM Nested FOR EACH/DO EACH over the same GEN with different arguments,
20 REM in every combination of the two loop shapes.
30 GEN COUNTUP(N#)
40 FOR I# = 1 TO N#
50 EMIT I#
60 NEXT I#
70 END GEN
80 FOR EACH A# IN COUNTUP(2)
90 FOR EACH B# IN COUNTUP(3)
100 PRINT A# * 10 + B#
110 NEXT B#
120 NEXT A#
130 DO EACH C# IN COUNTUP(2)
140 DO EACH D# IN COUNTUP(2)
150 PRINT C# * 100 + D#
160 LOOP
170 LOOP
180 FOR EACH E# IN COUNTUP(2)
190 DO EACH F# IN COUNTUP(2)
200 PRINT E# * 1000 + F#
210 LOOP
220 NEXT E#

View File

@@ -0,0 +1,14 @@
11
12
13
21
22
23
101
102
201
202
1001
1002
2001
2002

View File

@@ -0,0 +1,11 @@
10 REM FOR EACH/DO EACH accept any emitted type, not just numeric.
20 GEN WORDS(N#)
30 EMIT "HELLO"
40 EMIT "WORLD"
50 END GEN
60 FOR EACH W$ IN WORDS(0)
70 PRINT W$
80 NEXT W$
90 DO EACH V$ IN WORDS(0)
100 PRINT V$
110 LOOP

View File

@@ -0,0 +1,4 @@
HELLO
WORLD
HELLO
WORLD