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
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:
@@ -407,6 +407,8 @@ block structure is executing: the `FOR` bounds and step, the `DO`/`LOOP` conditi
|
||||
| `GOSUB` | `RETURN` |
|
||||
| A call to a multi-line user function | that function's `RETURN` |
|
||||
| An interrupt firing | the handler's `RETURN` |
|
||||
| `FOR EACH`/`DO EACH` — the loop's own scope, during parsing | the `NEXT`/`LOOP` that finds the generator exhausted, or an `EXIT` |
|
||||
| A `FOR EACH`/`DO EACH` invoking a `GEN` | `END GEN` reached for real, or the loop's own abandonment |
|
||||
|
||||
That `FOR` entry is not a typo. `akbasic_parse_for()` pushes the new environment while
|
||||
parsing the line, parks `TO` and `STEP` in it as unevaluated leaves, and makes it active
|
||||
@@ -454,6 +456,59 @@ Three consequences follow, and all three are things people report as bugs:
|
||||
the value correctly inside the loop and gets `0` immediately after it, with nothing
|
||||
raised anywhere.
|
||||
|
||||
### Generators: a scope that outlives the verb that pushed it
|
||||
|
||||
Everything above pops a scope by *releasing* it — `akbasic_runtime_prev_environment()`
|
||||
gives its variables and its own pool slot back in the same motion that hands control to
|
||||
its parent. A `GEN` needed a third option, because `EMIT` has to survive being
|
||||
"returned" from: the next value comes from resuming exactly where the last `EMIT` left
|
||||
off, not from starting over.
|
||||
|
||||
`akbasic_runtime_prev_environment()` is now built from two smaller pieces:
|
||||
|
||||
- `akbasic_runtime_detach_environment()` — moves `obj->environment` to the parent,
|
||||
*without* releasing anything.
|
||||
- `akbasic_runtime_release_environment()` — gives a scope's variables and pool slot
|
||||
back, callable on a scope that is not necessarily the active one.
|
||||
|
||||
A `FOR EACH`/`DO EACH` invocation therefore holds **two** environments at once, for as
|
||||
long as the loop is running:
|
||||
|
||||
```text
|
||||
loop environment (isEachLoop) <- pushed like a plain FOR/DO's, at parse time
|
||||
forGeneratorEnv -----------> generator environment (isGenerator)
|
||||
<- pushed once, by akbasic_runtime_generator_invoke(),
|
||||
and never released until the generator is
|
||||
exhausted or abandoned
|
||||
```
|
||||
|
||||
`EMIT` finds its generator environment by walking *up* from wherever it is actually
|
||||
standing — a `GEN` body is ordinary BASIC and may nest its own `FOR`, `DO` or `GOSUB`
|
||||
around an `EMIT`, each pushing scopes of its own — to the nearest ancestor with
|
||||
`isGenerator` set. It assigns the emitted value into the loop's `forNextVariable`,
|
||||
records *exactly* where it is standing (which may be several environments below the
|
||||
generator's own call frame) as `forGeneratorEnv`, and moves `obj->environment` straight
|
||||
to the loop environment — detaching, not popping, so every environment between the two
|
||||
survives untouched.
|
||||
|
||||
`NEXT`/`LOOP` reactivate a suspended generator by setting `obj->environment` back to
|
||||
`forGeneratorEnv` and driving the step loop (`akbasic_runtime_pump_generator()`) until
|
||||
either another `EMIT` detaches it again or `END GEN` is reached for real — meaning
|
||||
`isGenerator` is set and nothing is skipping forward to it, exactly the same test
|
||||
`RETURN` makes for a multi-line `DEF`. Real exhaustion releases the generator
|
||||
environment (`akbasic_runtime_prev_environment()`, same as anything else that pops) and
|
||||
clears `forGeneratorEnv`, which is what tells the loop apart from one still waiting to
|
||||
resume.
|
||||
|
||||
**Abandoning a live generator has to release it explicitly.** `EXIT` out of a `FOR
|
||||
EACH`/`DO EACH` pops the loop environment the same way it always has, but a generator
|
||||
paused mid-run is not on that direct parent chain from the loop back to the root — it
|
||||
hangs off `forGeneratorEnv` instead, possibly several environments deep if `EMIT` last
|
||||
ran inside a nested `FOR`/`DO` in the `GEN`'s own body. `akbasic_runtime_release_generator()`
|
||||
is what walks that chain and releases all of it; every place that pops a `FOR EACH`/`DO
|
||||
EACH` loop out from under a live generator calls it first, or the pool leaks one
|
||||
generator at a time.
|
||||
|
||||
## Values
|
||||
|
||||
`akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct
|
||||
|
||||
Reference in New Issue
Block a user