Merge branch 'main' into 36
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m25s
akbasic CI Build / coverage (push) Successful in 4m18s
akbasic CI Build / sanitizers (push) Successful in 5m4s
akbasic CI Build / akgl_build (push) Successful in 8m17s
akbasic CI Build / mutation_test (push) Successful in 23m54s

This commit is contained in:
2026-08-06 12:38:05 -04:00
committed by Starfort Source Vault
71 changed files with 2145 additions and 302 deletions

View File

@@ -409,6 +409,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
@@ -456,6 +458,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