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:
@@ -816,6 +816,45 @@ akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTL
|
||||
if ( akbasic_environment_is_waiting_for_any(obj->environment) ) {
|
||||
if ( expr->leaftype != AKBASIC_LEAF_COMMAND ||
|
||||
!akbasic_environment_is_waiting_for(obj->environment, expr->identifier) ) {
|
||||
/*
|
||||
* **A skipped loop has already pushed its scope, and this is where it
|
||||
* comes back.**
|
||||
*
|
||||
* akbasic_parse_for() and akbasic_parse_do() create the environment
|
||||
* while the line is *parsed*; whether to skip is decided here, after
|
||||
* parsing. So a `FOR` inside a block that is not taken pushes a scope,
|
||||
* its body is skipped, and the `NEXT` that would pop it is skipped
|
||||
* too. Nothing else ever will.
|
||||
*
|
||||
* Left behind, thirty-two of them exhausted the pool -- and inside a
|
||||
* routine it was much more confusing than that: 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`. The error named the one construct that
|
||||
* was not at fault. TODO.md section 9 item 2.
|
||||
*
|
||||
* `loopFirstLine` is what makes this safe: it is set by both parse
|
||||
* handlers and by nothing else, so it identifies the scope this very
|
||||
* line pushed rather than whatever happened to be on top.
|
||||
*
|
||||
* **Only a block skip**, which is what the `BEND` test is for. A
|
||||
* zero-iteration `FOR` skips its body the same way, and there the
|
||||
* orphan is load-bearing: `tests/reference/.../nestedforloopwaiting
|
||||
* forcommand.bas` nests a loop inside one that runs zero times, and
|
||||
* the inner scope is what absorbs the inner `NEXT` so the outer `NEXT`
|
||||
* still finds its `FOR`. Popping it there turns that case into "NEXT
|
||||
* outside the context of FOR". A `FOR` inside a skipped block cannot
|
||||
* be in that position, because nothing in a skipped block ever runs to
|
||||
* arm a `NEXT` wait in the first place.
|
||||
*/
|
||||
if ( expr->leaftype == AKBASIC_LEAF_COMMAND &&
|
||||
obj->environment->loopFirstLine != 0 &&
|
||||
obj->environment->waitingForCommand[0] == '\0' &&
|
||||
akbasic_environment_is_waiting_for(obj->environment, "BEND") &&
|
||||
(strcmp(expr->identifier, "FOR") == 0 ||
|
||||
strcmp(expr->identifier, "DO") == 0) ) {
|
||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||
}
|
||||
*dest = &obj->staticTrueValue;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user