Skip to content

Commit 31b2969

Browse files
committed
do loop goto bug fixed
1 parent a78f494 commit 31b2969

6 files changed

Lines changed: 281 additions & 113 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
## Changelog
22

3+
### Fix: GOTO inside a FUNCTION no longer corrupts the caller's DO/LOOP (2026-06-01)
4+
5+
A `GOTO` executed inside a `FUNCTION` while the caller was sitting inside a
6+
`DO ... LOOP` used to clear the `DO` bookkeeping globally (`do_top = 0` in
7+
`goto_unwind_structured_stacks()`), so the caller's matching `LOOP` later halted
8+
with `LOOP without DO`. `WHILE`/`FOR`/`IF` were already snapshotted per function
9+
call (`udf_call_frame`), but `DO` was not, and the unwind cleared it to zero
10+
instead of to the current function's floor.
11+
12+
Fix mirrors the earlier FOR-body fix (2026-05-23): the `udf_call_frame` now also
13+
saves `do_top` on entry, `statement_return` / `statement_end_function` restore
14+
it, and `goto_unwind_structured_stacks()` clears `do_top` down to the running
15+
UDF's floor (0 at top level) rather than unconditionally to zero. A `GOTO` now
16+
only unwinds the `DO` blocks the running function itself opened; the caller's
17+
loop frame survives the call.
18+
19+
This is what lets a state-machine main loop (`DO ... LOOP UNTIL done`) dispatch
20+
to handler functions that use their own internal `GOTO`s. Surfaced while
21+
rewriting `examples/trek-new.bas` into a `GOSUB`-free state machine. Regression
22+
test: `tests/do_loop_func_goto_test.bas`.
23+
324
### `tests/run-wasm.js` headless WASM runner (2026-05-23)
425

526
Wishlist §3f. A permanent node runner that drives the `basic-wasm` build

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,10 @@ The `examples` folder (included in release archives) contains:
600600
- You may `THEN` jump to a line number (`IF A>10 THEN 100`) or execute inline statements after `THEN`.
601601
- **Random numbers**:
602602
- `RND(X)` behaves like classic BASIC; a negative argument reseeds the generator.
603+
- **Structured loops and `GOTO`**:
604+
- `DO ... LOOP [UNTIL]`, `WHILE ... WEND`, `FOR ... NEXT` and block `IF ... ELSE IF ... END IF` can be mixed freely with `GOTO`.
605+
- A `GOTO` abandons any block frames opened since the current routine started (the classic unstructured behaviour), but it no longer disturbs blocks owned by a *caller*.
606+
- **Fixed bug (2026-06-01):** a `GOTO` inside a `FUNCTION` that ran while the caller was sitting inside a `DO ... LOOP` used to clear the loop bookkeeping globally, so the caller's matching `LOOP` later failed with `LOOP without DO`. The block stack (`DO`/`WHILE`/`FOR`/`IF`) is now snapshotted per function call and restored on return, so `GOTO` only unwinds the blocks that the running function itself opened. This is what lets a state-machine main loop (`DO ... LOOP UNTIL done`) dispatch to handler functions that use their own internal `GOTO`s, as in `examples/trek-new.bas`. Regression test: `tests/do_loop_func_goto_test.bas`.
603607

604608
This README describes the current feature set of the interpreter as implemented in `basic.c` and is subject to change without notice.
605609

basic.c

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,6 +2103,7 @@ struct udf_call_frame {
21032103
int saved_while_top; /* WHILE/WEND nesting at call site */
21042104
int saved_for_top; /* FOR/NEXT nesting at call site */
21052105
int saved_if_depth; /* block IF/END IF nesting at call site */
2106+
int saved_do_top; /* DO/LOOP nesting at call site */
21062107
struct value saved_params[MAX_UDF_PARAMS];
21072108
};
21082109

@@ -14435,6 +14436,7 @@ static struct value invoke_udf(int func_index, struct value *args, int nargs)
1443514436
udf_call_stack[udf_call_depth].saved_while_top = while_top;
1443614437
udf_call_stack[udf_call_depth].saved_for_top = for_top;
1443714438
udf_call_stack[udf_call_depth].saved_if_depth = if_depth;
14439+
udf_call_stack[udf_call_depth].saved_do_top = do_top;
1443814440
for (i = 0; i < uf->param_count; i++) {
1443914441
param_var = find_or_create_var(uf->param_names[i], uf->param_is_string[i], 0, 0, NULL, 0);
1444014442
if (param_var && i < nargs) {
@@ -17555,16 +17557,20 @@ static void goto_unwind_structured_stacks(void)
1755517557
{
1755617558
int floor_while = 0;
1755717559
int floor_if = 0;
17560+
int floor_do = 0;
1755817561
if (udf_call_depth > 0) {
1755917562
floor_while = udf_call_stack[udf_call_depth - 1].saved_while_top;
1756017563
floor_if = udf_call_stack[udf_call_depth - 1].saved_if_depth;
17564+
floor_do = udf_call_stack[udf_call_depth - 1].saved_do_top;
1756117565
}
1756217566
if (while_top > floor_while) while_top = floor_while;
1756317567
if (if_depth > floor_if) if_depth = floor_if;
17564-
/* DO frames are not saved per-UDF (yet) — clear to 0. Re-entering
17565-
* the DO body via GOTO is the same misuse as IF/WHILE: caller has
17566-
* to RUN the program from a clean state if they want it back. */
17567-
do_top = 0;
17568+
/* DO frames clear down to the current UDF's floor — a GOTO inside a
17569+
* function discards only the DO blocks that function opened, leaving
17570+
* the caller's DO/LOOP frame (and any outer ones) intact. At top
17571+
* level the floor is 0, matching classic line-numbered BASIC where
17572+
* GOTO abandons structured state. */
17573+
if (do_top > floor_do) do_top = floor_do;
1756817574
}
1756917575

1757017576
static void statement_goto(char **p)
@@ -17686,11 +17692,12 @@ static void statement_return(char **p)
1768617692
udf_call_depth--;
1768717693
current_line = udf_call_stack[udf_call_depth].saved_line;
1768817694
statement_pos = udf_call_stack[udf_call_depth].saved_pos;
17689-
/* Unwind any WHILE / FOR / IF blocks the UDF entered but did
17695+
/* Unwind any WHILE / FOR / IF / DO blocks the UDF entered but did
1769017696
* not close — the caller resumes with its own stacks intact. */
1769117697
while_top = udf_call_stack[udf_call_depth].saved_while_top;
1769217698
for_top = udf_call_stack[udf_call_depth].saved_for_top;
1769317699
if_depth = udf_call_stack[udf_call_depth].saved_if_depth;
17700+
do_top = udf_call_stack[udf_call_depth].saved_do_top;
1769417701
return;
1769517702
}
1769617703
if (gosub_top <= 0) {
@@ -18134,10 +18141,11 @@ static void statement_end_function(char **p)
1813418141
udf_call_depth--;
1813518142
current_line = udf_call_stack[udf_call_depth].saved_line;
1813618143
statement_pos = udf_call_stack[udf_call_depth].saved_pos;
18137-
/* Unwind WHILE / FOR / IF stacks the UDF body left dangling. */
18144+
/* Unwind WHILE / FOR / IF / DO stacks the UDF body left dangling. */
1813818145
while_top = udf_call_stack[udf_call_depth].saved_while_top;
1813918146
for_top = udf_call_stack[udf_call_depth].saved_for_top;
1814018147
if_depth = udf_call_stack[udf_call_depth].saved_if_depth;
18148+
do_top = udf_call_stack[udf_call_depth].saved_do_top;
1814118149
}
1814218150

1814318151
static void statement_while(char **p, char *while_pos)

0 commit comments

Comments
 (0)