Files
akbasic/docs/04-control-flow.md
Ishikawa a29c7f34fe Fix generator teardown leaks, add RETURN-in-GEN and LOOP conditions on DO EACH
Review findings and follow-ups from PR #61 review:

- runtime_generator.c: akbasic_runtime_release_generator() now releases the
  forGeneratorEnv of every scope it walks through. Abandoning a generator
  that was itself suspended inside a FOR EACH over another generator
  stranded the inner generator's pool slot; a loop doing so exhausted the
  twelve-slot pool and died far from the cause.
- runtime.c/runtime.h: new akbasic_runtime_unwind_to_environment(), the
  shared teardown for the error unwinds in pump_generator() and
  call_function() -- both previously bare prev_environment() loops with the
  same suspended-generator blindness.
- runtime_commands.c: bare RETURN standing in a GEN's own frame ends the
  generator exactly as END GEN does -- a GEN is a function at heart. RETURN
  with a value there is refused (values leave a GEN only through EMIT). The
  no-frame error message now says "GOSUB, DEF, or GEN".
- runtime_structure.c: LOOP WHILE/UNTIL composes with DO EACH -- checked
  after each trip with the loop variable still holding that trip's value; a
  condition that stops the loop abandons the generator exactly as EXIT
  does. Previously the condition was silently ignored, while the verb
  reference documented it as working.
- parser_commands.c: trailing tokens after the generator call on a FOR
  EACH/DO EACH line are refused at parse. Previously they sat unparsed and
  blew up only after the loop completed, when the parent scope resumed the
  line mid-statement -- an error at the loop's end pointing at its start.
- tests/generators.c: pool-exhaustion tests for the nested-abandonment and
  LOOP-condition paths, RETURN semantics tests, and a direct test of the
  unwind primitive. Three new golden pairs cover RETURN, LOOP conditions
  and the misplaced-condition parse error.
- docs: RETURN and LOOP-condition semantics in 04-control-flow.md and
  11-verb-reference.md; corrected the self-recursion analogy (functions
  are re-entrant here). TODO.md 1.10 records the generator design
  decisions the code comments were already citing, plus the zero-arg
  parameter-list limitation. MAINTENANCE.md gains the abandoned-generators
  invariant those comments also cited.

Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:55:59 -04:00

421 lines
9.0 KiB
Markdown

# 4. Control flow
## IF ... THEN ... ELSE
```basic
10 A# = 5
20 IF A# = 5 THEN PRINT "FIVE" ELSE PRINT "NOT FIVE"
```
```output
FIVE
```
The condition is a whole expression, so `AND`, `OR` and `NOT` all work in one:
```basic
10 A# = 5
20 IF A# > 0 AND A# < 10 THEN PRINT "IN RANGE"
```
```output
IN RANGE
```
**Everything after `THEN` on the line belongs to the condition**, which matters as soon
as you put several statements on a line:
| Line | Condition | What runs |
|---|---|---|
| `IF C THEN A : B` | true | `A`, `B` |
| `IF C THEN A : B` | false | nothing |
| `IF C THEN A ELSE B : D` | true | `A` |
| `IF C THEN A ELSE B : D` | false | `B`, `D` |
The remainder always belongs to whichever arm was written *last*.
### Blocks
`BEGIN` and `BEND` make an `IF` span lines:
```basic
10 A# = 5
20 IF A# = 5 THEN BEGIN
30 PRINT "IN THE BLOCK"
40 PRINT "STILL IN IT"
50 BEND
60 PRINT "AFTER"
```
```output
IN THE BLOCK
STILL IN IT
AFTER
```
When the condition is false every line up to the `BEND` is skipped.
## FOR ... NEXT
```basic
10 FOR I# = 1 TO 5
20 PRINT I#
30 NEXT I#
```
```output
1
2
3
4
5
```
`STEP` sets the stride, and a negative one counts down:
```basic
10 FOR I# = 10 TO 0 STEP -2
20 PRINT I#
30 NEXT I#
```
```output
10
8
6
4
2
0
```
The counter is an ordinary variable and the body may assign to it. Two things to know:
- **The counter does not survive the loop.** It lives in the loop's own scope, so
reading it afterwards gives zero. On a C128 it keeps its final value.
- **A step that overshoots runs the body one extra time.** `FOR I = 1 TO 9 STEP 3` runs
with 1, 4, 7 *and 10*. Both are recorded as known defects; see Chapter 13.
`EXIT` leaves the loop early:
```basic
10 FOR I# = 1 TO 100
20 IF I# = 5 THEN EXIT
30 NEXT I#
40 PRINT "OUT"
```
```output
OUT
```
## DO ... LOOP
The condition can go on either end, or neither:
```basic
10 I# = 0
20 DO WHILE I# < 3
30 PRINT I#
40 I# = I# + 1
50 LOOP
```
```output
0
1
2
```
```basic
10 I# = 0
20 DO
30 PRINT I#
40 I# = I# + 1
50 LOOP UNTIL I# = 3
```
```output
0
1
2
```
A condition on the `DO` is tested before the body, so the body may run zero times. A
condition on the `LOOP` is tested after, so it runs at least once. `DO` with no
condition at all loops forever until an `EXIT` or a `GOTO` leaves it.
`EXIT` works here too.
## GEN ... EMIT and FOR EACH / DO EACH
A `GEN` is a subroutine that hands back more than one value, one at a time, instead of
returning once. It looks like a multi-line `DEF`, except its body runs `EMIT` where a
function would `RETURN`, and it ends in `END GEN` rather than `RETURN`:
```basic
10 GEN COUNTUP(N#)
20 FOR I# = 1 TO N#
30 EMIT I#
40 NEXT I#
50 END GEN
60 FOR EACH V# IN COUNTUP(3)
70 PRINT V#
80 NEXT V#
```
```output
1
2
3
```
`FOR EACH` is what runs a `GEN`: it calls `COUNTUP(3)` the way `FOR EACH V# IN` says,
and each `EMIT` inside the generator's body becomes one trip through the loop, with `V#`
holding whatever was emitted. When the generator's body reaches `END GEN` with nothing
left to emit, the loop ends -- there is no separate "no more values" check to write.
`DO EACH ... LOOP` does the same thing:
```basic
10 GEN COUNTUP(N#)
20 FOR I# = 1 TO N#
30 EMIT I#
40 NEXT I#
50 END GEN
60 DO EACH V# IN COUNTUP(3)
70 PRINT V#
80 LOOP
```
```output
1
2
3
```
A `GEN`'s body is ordinary BASIC: it may hold its own `FOR`, `DO`, `IF` or `GOSUB`
around the `EMIT`s, and even invoke another `GEN` with its own `FOR EACH`/`DO EACH` --
the same generator invoked with different arguments, nested or side by side, is not
recursion. A `GEN` invoking *itself* from within its own currently-running body is
refused: unlike a function call, which runs to completion and returns, the outer
invocation is suspended mid-body waiting on the same loop, and there is no answer to
"which EMIT feeds which loop" that is not a surprise.
`EXIT` leaves a `FOR EACH`/`DO EACH` loop early, exactly as it does a plain `FOR` or
`DO`, and the generator it was consuming stops there -- nothing forces the rest of it to
run just because the loop started it:
```basic
10 GEN COUNTUP(N#)
20 FOR I# = 1 TO N#
30 EMIT I#
40 NEXT I#
50 END GEN
60 FOR EACH V# IN COUNTUP(100)
70 PRINT V#
80 IF V# = 3 THEN EXIT
90 NEXT V#
100 PRINT "STOPPED"
```
```output
1
2
3
STOPPED
```
`RETURN` ends a `GEN` from the inside, exactly as it ends a multi-line `DEF` or a
`GOSUB`: the generator is done, the loop consuming it ends, and the program carries on
after the loop. What a generator's `RETURN` cannot do is carry a value -- values leave
a `GEN` one at a time, through `EMIT`, and `RETURN 99` inside one is an error. Like a
`GOSUB`'s or a `DEF`'s, the `RETURN` must stand in the `GEN`'s own scope: from inside
a `FOR` or `DO` the body opened, it is an error, though `IF ... THEN RETURN` is fine
because `IF` opens no scope of its own.
```basic
10 GEN FIRSTFEW(N#)
20 EMIT 1
30 IF N# < 2 THEN RETURN
40 EMIT 2
50 IF N# < 3 THEN RETURN
60 EMIT 3
70 END GEN
80 FOR EACH V# IN FIRSTFEW(2)
90 PRINT V#
100 NEXT V#
110 PRINT "DONE"
```
```output
1
2
DONE
```
A `DO EACH`'s `LOOP` may carry a `WHILE` or `UNTIL`, and the two compose: the condition
is checked after each trip through the body, with the loop variable still holding that
trip's value, and a condition that says stop abandons the rest of the generator exactly
as `EXIT` does. The condition belongs on the `LOOP` -- putting it on the `DO` line is
an error, since the `DO` line already says what the loop consumes.
```basic
10 GEN COUNTUP(N#)
20 FOR I# = 1 TO N#
30 EMIT I#
40 NEXT I#
50 END GEN
60 DO EACH V# IN COUNTUP(10)
70 PRINT V#
80 LOOP UNTIL V# = 3
90 PRINT "STOPPED"
```
```output
1
2
3
STOPPED
```
A `GEN` shares its namespace with `DEF` -- the same name cannot be both -- but it is not
a function and cannot be called like one: `X# = COUNTUP(3)` is refused, because nothing
about an ordinary call means "resume where the last `EMIT` left off." `FOR EACH`/`DO
EACH` is the only thing that consumes a `GEN`.
## GOTO and GOSUB
```basic
10 GOSUB 100
20 PRINT "BACK"
30 END
100 PRINT "IN THE SUBROUTINE"
110 RETURN
```
```output
IN THE SUBROUTINE
BACK
```
`RETURN` goes back to the line after the `GOSUB`.
## Labels
A label is a name with no type suffix. It marks a line, and anything that takes a line
number takes a label instead:
```basic
10 GOTO SETUP
20 PRINT "SKIPPED"
100 LABEL SETUP
110 PRINT "ARRIVED"
```
```output
ARRIVED
```
**Labels are filed before the program runs**, so a forward `GOTO` works. This is worth
using for its own sake: a program written with labels is immune to `RENUMBER`, because
there is no number to rewrite.
Follow that one step further and the numbers stop earning their keep at all. A program
in a file can leave them out — see
[Chapter 2](02-getting-started.md#a-program-in-a-file-does-not-need-them) — and then
every branch it makes is by name:
```basic
GOSUB GREET
GOTO FINISH
PRINT "NOT REACHED"
LABEL GREET
PRINT "HELLO"
RETURN
LABEL FINISH
PRINT "DONE"
```
```output
HELLO
DONE
```
Nothing in that program can be broken by inserting a line into it, which is the whole
argument.
## ON
`ON` picks the *n*th target from a list, counting from one:
```basic
10 CHOICE# = 2
20 ON CHOICE# GOTO 100, 200, 300
30 PRINT "CHOICE WAS OUT OF RANGE"
40 END
100 PRINT "FIRST"
110 END
200 PRINT "SECOND"
210 END
300 PRINT "THIRD"
```
```output
SECOND
```
Out of range is not an error — it falls through to the next statement, which is what
lets you write the check as the line after. `ON ... GOSUB` works the same way and
returns.
## Trapping errors
`TRAP` sends an error to a handler instead of stopping the program:
```basic
10 TRAP HANDLER
20 DIM Q#(2)
30 PRINT Q#(9)
40 PRINT "CARRIED ON"
50 END
100 LABEL HANDLER
110 PRINT "CAUGHT " + ERR(ER#) + " ON LINE " + EL#
120 RESUME NEXT
```
```output
CAUGHT Out Of Bounds ON LINE 30
CARRIED ON
```
Two variables are set when the trap fires: **`ER#`** is the error code and **`EL#`** is
the line it happened on. `ERR(ER#)` gives the message text. On a C128 these are called
`ER` and `EL` with no suffix; this dialect has no bare variable names.
**[Chapter 15](15-error-codes.md) is the complete list** of what `ER#` can hold and
what each code means. The short version is that the interpreter's own codes are 512 to
519 and are the only numbers worth comparing against; two codes render as the same
`ERR()` text, so compare the number rather than the text.
`RESUME` comes in three forms:
| Form | Where it goes |
|---|---|
| `RESUME` | back to the line that failed, and tries again |
| `RESUME NEXT` | on to the line after the one that failed |
| `RESUME (line)` | to a line you name |
Bare `RESUME` only terminates if the handler fixed whatever was wrong — that is what
it is for.
`TRAP` with no argument turns trapping off. An error *inside* a handler is reported
normally rather than re-entering it, so a broken handler cannot loop forever.
## STOP, END and CONT
`STOP` stops the program and returns to the prompt; `CONT` resumes from there. `END`
stops it without arming `CONT`. `QUIT` ends the interpreter itself.