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:
2026-08-01 23:55:10 -04:00
parent f5a35b4af6
commit 5d33237eed
4 changed files with 183 additions and 24 deletions

View File

@@ -343,9 +343,10 @@ The game sets `DPLAY# = 1` or `DHUD# = 1` when something changes and never draws
directly. One consequence is visible and deliberate: **the score lags the bricks by one
frame**, because the field goes first. At thirty frames a second nobody can see it.
Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks, and again that is not a style
choice a `RETURN` inside a block leaves the interpreter with no `GOSUB` to return from.
The last trap in this chapter is exactly that.
Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks, and when this was written that
was not a style choice: a loop inside a block that was skipped left the interpreter with
no `GOSUB` to return from. That is fixed — see trap 3 below — and the shape is kept
because it costs nothing and reads the same.
## Step 6: Draw the lettering, because text has no colour
@@ -672,7 +673,13 @@ NEXT I#
RETURN
```
### 3. A skipped `BEGIN` block containing a loop breaks the enclosing `RETURN`
### 3. A skipped `BEGIN` block containing a loop — *fixed*
This one is here as history rather than as a warning. A loop inside a block that was not
taken used to leave a scope behind, and inside a routine the orphan sat between it 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`. The error named the one
construct that was not at fault, which is why it cost an evening.
```basic
T# = 0
@@ -689,18 +696,13 @@ RETURN
```
```output
? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
CAME BACK
```
The routine plainly *was* called by a `GOSUB`. `FOR` creates its environment when the
line is **parsed**, and the block skip is decided when it is **evaluated**, so a skipped
loop pushes a scope that its skipped `NEXT` never pops — and the orphan sits between the
routine and its caller. Outside a routine the same thing exhausts the pool of 32 after
thirty-two skips instead.
Guard loops with `GOTO` rather than wrapping them in a block, which is what every draw
routine in this game does.
`FOR` creates its environment when the line is **parsed** and the skip is decided when it
is **evaluated**; the skip now releases what parsing pushed. **The listing still guards
its loops with `GOTO`**, which is where this chapter's own history shows: written that way
because it had to be, and left that way because it works.
### 4. `SSHAPE` and `GSHAPE` ignore the subscript on a string array