Release the scope a skipped BEGIN block's loop pushed
`akbasic_parse_for()` and `akbasic_parse_do()` create their environment while the line is *parsed*; whether to skip it is decided afterwards, when the line is evaluated. So a loop inside a block that was not taken pushed a scope, its body was skipped, and the `NEXT` or `LOOP` that would have popped it was skipped too. Nothing else ever would. At the top level that exhausted the pool after thirty-two skips. Inside a routine it was far more confusing: the orphan sat between the routine and its caller, so the `RETURN` after the block reported "RETURN outside the context of GOSUB" from a routine that plainly *was* entered by a `GOSUB` -- naming the one construct that was not at fault, which is why it cost an evening to find. The skip now releases what parsing pushed. **Narrower than it first looks.** Releasing on any skip breaks tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas: a zero-iteration `FOR` skips its body by the same mechanism, and there the orphan is load-bearing -- it absorbs the inner `NEXT` so the outer `NEXT` still finds its own `FOR`. Releasing it turns that case into "NEXT outside the context of FOR". So the release is conditional on the skip being a *block* skip, which is decidable because nothing inside a skipped block ever runs to arm a `NEXT` wait. Both halves are asserted side by side in tests/structure_verbs.c, the second one citing the golden case that caught it. The forty-skip case names its own step budget: a skipped line is not free, and forty passes over a five-line block cost about 2700 steps against the shared runner's 2000. Chapter 18's trap 3 becomes history rather than a warning, and the note in Step 5 that called `GOTO`-guarded loops "not a style choice" now says why the shape is kept anyway. TODO.md section 9 item 2, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
21
TODO.md
21
TODO.md
@@ -2511,9 +2511,24 @@ reduced against `build/basic`, the stdio build, unless it says otherwise.
|
||||
`include/akbasic/value.h`. A test belongs in `tests/user_functions.c`: call a `DEF` ten
|
||||
thousand times and then `DIM` an array.
|
||||
|
||||
2. **A `BEGIN` block that is skipped leaks a scope for every loop inside it**, because `FOR`
|
||||
and `DO` create their environment at *parse* time and the skip is decided at *evaluate*
|
||||
time.
|
||||
2. ~~**A `BEGIN` block that is skipped leaks a scope for every loop inside it**, because
|
||||
`FOR` and `DO` create their environment at *parse* time and the skip is decided at
|
||||
*evaluate* time.~~ **Done** — the skip now releases what parsing pushed
|
||||
(`akbasic_runtime_interpret()`, `src/runtime.c`), taking the second of the two routes
|
||||
below. `tests/structure_verbs.c` covers both loop forms inside a routine and the
|
||||
forty-skip top-level case.
|
||||
|
||||
**It is narrower than it looks, and the golden corpus is why.** The first attempt
|
||||
released the scope on *any* skip, which broke
|
||||
`tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas`: a zero-
|
||||
iteration `FOR` skips its body by the same mechanism, and there the orphan is
|
||||
load-bearing -- it is what absorbs the inner `NEXT` so the outer one still finds its
|
||||
`FOR`. Releasing it turns that case into "NEXT outside the context of FOR". So the
|
||||
release is conditional on the skip being a *block* skip, which is testable because a
|
||||
skipped block can never contain a live `NEXT` wait. Both halves are now asserted side by
|
||||
side in `tests/structure_verbs.c`.
|
||||
|
||||
The original report:
|
||||
|
||||
```basic
|
||||
N# = 0
|
||||
|
||||
Reference in New Issue
Block a user