Merge pull request 'Implement generators: GEN, EMIT, END GEN, FOR EACH and DO EACH' (#61) from 57-implement-generators into main
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m29s
akbasic CI Build / coverage (push) Successful in 3m58s
akbasic CI Build / sanitizers (push) Successful in 6m21s
akbasic CI Build / akgl_build (push) Successful in 8m23s
akbasic CI Build / mutation_test (push) Successful in 18m29s
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m29s
akbasic CI Build / coverage (push) Successful in 3m58s
akbasic CI Build / sanitizers (push) Successful in 6m21s
akbasic CI Build / akgl_build (push) Successful in 8m23s
akbasic CI Build / mutation_test (push) Successful in 18m29s
This commit is contained in:
@@ -143,6 +143,7 @@ set(AKBASIC_SOURCES
|
|||||||
src/runtime_disk.c
|
src/runtime_disk.c
|
||||||
src/runtime_format.c
|
src/runtime_format.c
|
||||||
src/runtime_functions.c
|
src/runtime_functions.c
|
||||||
|
src/runtime_generator.c
|
||||||
src/runtime_graphics.c
|
src/runtime_graphics.c
|
||||||
src/runtime_housekeeping.c
|
src/runtime_housekeeping.c
|
||||||
src/runtime_machine.c
|
src/runtime_machine.c
|
||||||
@@ -352,6 +353,7 @@ set(AKBASIC_TESTS
|
|||||||
error_codes
|
error_codes
|
||||||
for_next
|
for_next
|
||||||
format_verbs
|
format_verbs
|
||||||
|
generators
|
||||||
grammar_leaves
|
grammar_leaves
|
||||||
graphics_verbs
|
graphics_verbs
|
||||||
hoststruct
|
hoststruct
|
||||||
|
|||||||
@@ -698,6 +698,26 @@ as a commit co-author, and this repository follows the same rule.
|
|||||||
Each dependency carries its own `AGENTS.md` with authoritative per-repo rules. Read the
|
Each dependency carries its own `AGENTS.md` with authoritative per-repo rules. Read the
|
||||||
relevant one before editing a submodule.
|
relevant one before editing a submodule.
|
||||||
|
|
||||||
|
### Abandoned generators
|
||||||
|
|
||||||
|
The one invariant generators (`GEN`/`EMIT`, `FOR EACH`/`DO EACH`) add to the environment
|
||||||
|
pool: **a suspended generator hangs off its loop scope's `forGeneratorEnv` as a *child*,
|
||||||
|
off the parent chain, so no walk up `->parent` ever finds it.** Any path that discards a
|
||||||
|
loop scope — `EXIT`, a mismatched `NEXT`, a `LOOP` condition saying stop, an error unwind
|
||||||
|
through `akbasic_runtime_pump_generator()` or `akbasic_runtime_call_function()` — must
|
||||||
|
release that generator too, or its pool slot is stranded until the next `RUN`; with
|
||||||
|
twelve slots, a loop that abandons a dozen of them kills the program with "Environment
|
||||||
|
pool exhausted" far from the cause.
|
||||||
|
|
||||||
|
Two functions own the invariant, and every discard path goes through one of them:
|
||||||
|
`akbasic_runtime_release_generator()` (walks a detached generator chain up through its
|
||||||
|
call frame, recursing into any `forGeneratorEnv` it passes) and
|
||||||
|
`akbasic_runtime_unwind_to_environment()` (pops the *active* chain down to a target,
|
||||||
|
releasing each popped scope's suspended generator on the way). If you add a new path
|
||||||
|
that pops or discards environments, use one of these — a bare
|
||||||
|
`akbasic_runtime_prev_environment()` loop reintroduces the leak, and
|
||||||
|
`tests/generators.c` holds the pool-exhaustion tests that will say so.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Editing the documentation
|
## Editing the documentation
|
||||||
|
|||||||
30
TODO.md
30
TODO.md
@@ -366,6 +366,36 @@ One caveat survives the upgrade unchanged: `aksl_strhash_djb2` still sign-extend
|
|||||||
high-bit byte hashes differently from the `unsigned char` answer. BASIC identifiers are 7-bit
|
high-bit byte hashes differently from the `unsigned char` answer. BASIC identifiers are 7-bit
|
||||||
ASCII so the symbol tables cannot reach it — see §1.3, which is still accurate.
|
ASCII so the symbol tables cannot reach it — see §1.3, which is still accurate.
|
||||||
|
|
||||||
|
### 1.10 Generators share the `DEF` namespace, and the rest of their v1 semantics
|
||||||
|
|
||||||
|
Decided with issue #57 and its review. `GEN` and `DEF` live in the same functions table —
|
||||||
|
one lookup, one "unknown function" error path, a name cannot be both. What follows from
|
||||||
|
that and from the review of PR #61, all settled:
|
||||||
|
|
||||||
|
- A `GEN` called like a function (`X# = COUNTUP(3)`) is refused at the call, before
|
||||||
|
anything is pushed — `akbasic_FunctionDef.isGenerator` exists for exactly this check.
|
||||||
|
- Bare `RETURN` standing in a `GEN`'s own frame ends the generator exactly as `END GEN`
|
||||||
|
does; `RETURN expr` there is an error, because values leave a `GEN` only through `EMIT`.
|
||||||
|
Both inherit the interpreter-wide restriction that `RETURN` does not unwind nested
|
||||||
|
`FOR`/`DO` scopes — lifting that everywhere (the C64 reference *does* unwind) is filed
|
||||||
|
separately.
|
||||||
|
- `DO EACH ... LOOP WHILE c | UNTIL c` composes: the condition is checked after each trip,
|
||||||
|
with the loop variable still holding that trip's value, and stopping abandons the
|
||||||
|
generator exactly as `EXIT` does. A condition on the `DO EACH` line itself is a parse
|
||||||
|
error.
|
||||||
|
- Self-recursion — a `GEN` reached again down its own parent chain — is refused; sibling
|
||||||
|
and nested invocations of the same `GEN` are each a fresh pool environment and are fine.
|
||||||
|
- Every path that discards a `FOR EACH`/`DO EACH` scope must release the generator
|
||||||
|
suspended off it; see MAINTENANCE.md's "Abandoned generators" note for the invariant
|
||||||
|
and `akbasic_runtime_unwind_to_environment()` for the one primitive that enforces it.
|
||||||
|
|
||||||
|
One known limitation, pre-existing and shared with `DEF`: `parse_def_parameters()` does
|
||||||
|
not accept an empty parameter list, so `GEN NAME()` cannot be written — every generator
|
||||||
|
takes at least one parameter whether it wants one or not. Location:
|
||||||
|
`src/parser_commands.c`, `parse_def_parameters()`. Consequence: pointless parameters in
|
||||||
|
programs. Blast radius: cosmetic, both `DEF` and `GEN` headers. Closure: teach the shared
|
||||||
|
helper to accept `()`, one test each for `DEF` and `GEN`; filed as its own issue.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. What exists — **the core port is complete and green**
|
## 2. What exists — **the core port is complete and green**
|
||||||
|
|||||||
@@ -146,6 +146,143 @@ condition at all loops forever until an `EXIT` or a `GOTO` leaves it.
|
|||||||
|
|
||||||
`EXIT` works here too.
|
`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
|
## GOTO and GOSUB
|
||||||
|
|
||||||
```basic
|
```basic
|
||||||
|
|||||||
@@ -37,17 +37,20 @@ for the reasoning in each case.
|
|||||||
| `DIM` … `AS` | `DIM S@ AS T`, `DIM P@ AS PTR TO T` | Make a structure, or a strict pointer to one. See Chapter 16. |
|
| `DIM` … `AS` | `DIM S@ AS T`, `DIM P@ AS PTR TO T` | Make a structure, or a strict pointer to one. See Chapter 16. |
|
||||||
| `DIRECTORY` | `DIRECTORY` | **Refused.** Not written yet; the standard-library wrapper it waited on has landed. |
|
| `DIRECTORY` | `DIRECTORY` | **Refused.** Not written yet; the standard-library wrapper it waited on has landed. |
|
||||||
| `DLOAD` | `DLOAD "name"` | Load a program from a file. |
|
| `DLOAD` | `DLOAD "name"` | Load a program from a file. |
|
||||||
| `DO` | `DO [WHILE c | UNTIL c]` | Start a loop. The condition may be here, on the `LOOP`, or neither. |
|
| `DO` | `DO [WHILE c | UNTIL c]`, `DO EACH V IN gen(args)` | Start a loop. The condition may be here, on the `LOOP`, or neither. `EACH` consumes a `GEN` instead, and takes its condition only on the `LOOP`; see Chapter 4. |
|
||||||
| `DOPEN` | `DOPEN n, "name" [,W]` | Open a file on channel `n`. `W` opens it for writing. |
|
| `DOPEN` | `DOPEN n, "name" [,W]` | Open a file on channel `n`. `W` opens it for writing. |
|
||||||
| `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. |
|
| `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. |
|
||||||
| `DSAVE` | `DSAVE "name"` | Save the program to a file. |
|
| `DSAVE` | `DSAVE "name"` | Save the program to a file. |
|
||||||
| `DVERIFY` | `DVERIFY "name"` | The other name for `VERIFY`. |
|
| `DVERIFY` | `DVERIFY "name"` | The other name for `VERIFY`. |
|
||||||
|
| `EMIT` | `EMIT expr` | Yield one value from a `GEN` body. Only valid inside one; see Chapter 4. |
|
||||||
| `END` | `END` | Stop the program. Does not arm `CONT`. |
|
| `END` | `END` | Stop the program. Does not arm `CONT`. |
|
||||||
|
| `END GEN` | `END GEN` | Close a `GEN` body, the way `RETURN` closes a multi-line `DEF`. |
|
||||||
| `ENVELOPE` | `ENVELOPE n, a, d, s, r` | Define one of `PLAY`'s ten envelope presets. |
|
| `ENVELOPE` | `ENVELOPE n, a, d, s, r` | Define one of `PLAY`'s ten envelope presets. |
|
||||||
| `EXIT` | `EXIT` | Leave the innermost `FOR` or `DO` loop. |
|
| `EXIT` | `EXIT` | Leave the innermost `FOR`, `FOR EACH`, `DO` or `DO EACH` loop. |
|
||||||
| `FETCH` | `FETCH count, from, to` | Copy bytes. The same as `STASH`; there is no expansion RAM. |
|
| `FETCH` | `FETCH count, from, to` | Copy bytes. The same as `STASH`; there is no expansion RAM. |
|
||||||
| `FILTER` | `FILTER ...` | **Refused.** There is no filter stage in the audio backend. |
|
| `FILTER` | `FILTER ...` | **Refused.** There is no filter stage in the audio backend. |
|
||||||
| `FOR` | `FOR V = a TO b [STEP c]` | Start a counted loop, ended by `NEXT`. |
|
| `FOR` | `FOR V = a TO b [STEP c]`, `FOR EACH V IN gen(args)` | Start a counted loop, ended by `NEXT`. `EACH` consumes a `GEN` instead; see Chapter 4. |
|
||||||
|
| `GEN` | `GEN NAME(args) ... END GEN` | Define a generator: a subroutine that yields more than once via `EMIT`, consumed by `FOR EACH`/`DO EACH`. See Chapter 4. |
|
||||||
| `GET` | `GET V` | Take a keystroke if one is waiting, without stopping. |
|
| `GET` | `GET V` | Take a keystroke if one is waiting, without stopping. |
|
||||||
| `GETKEY` | `GETKEY V` | Wait for a keystroke, holding the program but not the host. |
|
| `GETKEY` | `GETKEY V` | Wait for a keystroke, holding the program but not the host. |
|
||||||
| `GETMENU` | `GETMENU n, V%` | Wait for a menu choice, holding the program but not the host. Assigns the entry number. See Chapter 19. |
|
| `GETMENU` | `GETMENU n, V%` | Wait for a menu choice, holding the program but not the host. Assigns the entry number. See Chapter 19. |
|
||||||
@@ -67,11 +70,11 @@ for the reasoning in each case.
|
|||||||
| `LIST` | `LIST [n][-n]` | List the program, or part of it. |
|
| `LIST` | `LIST [n][-n]` | List the program, or part of it. |
|
||||||
| `LOAD` | `LOAD "name"` | The other name for `DLOAD`. |
|
| `LOAD` | `LOAD "name"` | The other name for `DLOAD`. |
|
||||||
| `LOCATE` | `LOCATE x, y` | Move the pixel cursor. |
|
| `LOCATE` | `LOCATE x, y` | Move the pixel cursor. |
|
||||||
| `LOOP` | `LOOP [WHILE c | UNTIL c]` | End a `DO` loop. |
|
| `LOOP` | `LOOP [WHILE c | UNTIL c]` | End a `DO` loop, including a `DO EACH`. |
|
||||||
| `MENU` | `MENU [n [,"item", ...]]` | Show a menu the player picks from. No entries retires it; no arguments retire them all. See Chapter 19. |
|
| `MENU` | `MENU [n [,"item", ...]]` | Show a menu the player picks from. No entries retires it; no arguments retire them all. See Chapter 19. |
|
||||||
| `MOVSPR` | `MOVSPR n, ...` | Move a sprite. Four forms; see Chapter 8. |
|
| `MOVSPR` | `MOVSPR n, ...` | Move a sprite. Four forms; see Chapter 8. |
|
||||||
| `NEW` | `NEW` | Erase the program and every variable. |
|
| `NEW` | `NEW` | Erase the program and every variable. |
|
||||||
| `NEXT` | `NEXT V` | End a `FOR` loop and advance its counter. |
|
| `NEXT` | `NEXT V` | End a `FOR` loop and advance its counter, or resume a `FOR EACH` for its next value. |
|
||||||
| `ON` | `ON e GOTO|GOSUB t [,...]` | Branch to the `e`th target, counting from one. |
|
| `ON` | `ON e GOTO|GOSUB t [,...]` | Branch to the `e`th target, counting from one. |
|
||||||
| `PAINT` | `PAINT src, x, y` | Flood-fill the region containing a point. |
|
| `PAINT` | `PAINT src, x, y` | Flood-fill the region containing a point. |
|
||||||
| `PLAY` | `PLAY "notes"` | Queue notes. Does not block. |
|
| `PLAY` | `PLAY "notes"` | Queue notes. Does not block. |
|
||||||
@@ -87,7 +90,7 @@ for the reasoning in each case.
|
|||||||
| `RENUMBER` | `RENUMBER [start [,step [,from]]]` | Renumber lines, rewriting every branch to match. |
|
| `RENUMBER` | `RENUMBER [start [,step [,from]]]` | Renumber lines, rewriting every branch to match. |
|
||||||
| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. |
|
| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. |
|
||||||
| `RESUME` | `RESUME [NEXT | line]` | Return from a `TRAP` handler. |
|
| `RESUME` | `RESUME [NEXT | line]` | Return from a `TRAP` handler. |
|
||||||
| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. |
|
| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. Inside a `GEN`, a bare `RETURN` ends the generator early; `RETURN expr` there is an error. |
|
||||||
| `RUN` | `RUN [line]` | Run the program, optionally from a line. |
|
| `RUN` | `RUN [line]` | Run the program, optionally from a line. |
|
||||||
| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. |
|
| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. |
|
||||||
| `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. |
|
| `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. |
|
||||||
|
|||||||
@@ -407,6 +407,8 @@ block structure is executing: the `FOR` bounds and step, the `DO`/`LOOP` conditi
|
|||||||
| `GOSUB` | `RETURN` |
|
| `GOSUB` | `RETURN` |
|
||||||
| A call to a multi-line user function | that function's `RETURN` |
|
| A call to a multi-line user function | that function's `RETURN` |
|
||||||
| An interrupt firing | the handler's `RETURN` |
|
| An interrupt firing | the handler's `RETURN` |
|
||||||
|
| `FOR EACH`/`DO EACH` — the loop's own scope, during parsing | the `NEXT`/`LOOP` that finds the generator exhausted, or an `EXIT` |
|
||||||
|
| A `FOR EACH`/`DO EACH` invoking a `GEN` | `END GEN` reached for real, or the loop's own abandonment |
|
||||||
|
|
||||||
That `FOR` entry is not a typo. `akbasic_parse_for()` pushes the new environment while
|
That `FOR` entry is not a typo. `akbasic_parse_for()` pushes the new environment while
|
||||||
parsing the line, parks `TO` and `STEP` in it as unevaluated leaves, and makes it active
|
parsing the line, parks `TO` and `STEP` in it as unevaluated leaves, and makes it active
|
||||||
@@ -454,6 +456,59 @@ Three consequences follow, and all three are things people report as bugs:
|
|||||||
the value correctly inside the loop and gets `0` immediately after it, with nothing
|
the value correctly inside the loop and gets `0` immediately after it, with nothing
|
||||||
raised anywhere.
|
raised anywhere.
|
||||||
|
|
||||||
|
### Generators: a scope that outlives the verb that pushed it
|
||||||
|
|
||||||
|
Everything above pops a scope by *releasing* it — `akbasic_runtime_prev_environment()`
|
||||||
|
gives its variables and its own pool slot back in the same motion that hands control to
|
||||||
|
its parent. A `GEN` needed a third option, because `EMIT` has to survive being
|
||||||
|
"returned" from: the next value comes from resuming exactly where the last `EMIT` left
|
||||||
|
off, not from starting over.
|
||||||
|
|
||||||
|
`akbasic_runtime_prev_environment()` is now built from two smaller pieces:
|
||||||
|
|
||||||
|
- `akbasic_runtime_detach_environment()` — moves `obj->environment` to the parent,
|
||||||
|
*without* releasing anything.
|
||||||
|
- `akbasic_runtime_release_environment()` — gives a scope's variables and pool slot
|
||||||
|
back, callable on a scope that is not necessarily the active one.
|
||||||
|
|
||||||
|
A `FOR EACH`/`DO EACH` invocation therefore holds **two** environments at once, for as
|
||||||
|
long as the loop is running:
|
||||||
|
|
||||||
|
```text
|
||||||
|
loop environment (isEachLoop) <- pushed like a plain FOR/DO's, at parse time
|
||||||
|
forGeneratorEnv -----------> generator environment (isGenerator)
|
||||||
|
<- pushed once, by akbasic_runtime_generator_invoke(),
|
||||||
|
and never released until the generator is
|
||||||
|
exhausted or abandoned
|
||||||
|
```
|
||||||
|
|
||||||
|
`EMIT` finds its generator environment by walking *up* from wherever it is actually
|
||||||
|
standing — a `GEN` body is ordinary BASIC and may nest its own `FOR`, `DO` or `GOSUB`
|
||||||
|
around an `EMIT`, each pushing scopes of its own — to the nearest ancestor with
|
||||||
|
`isGenerator` set. It assigns the emitted value into the loop's `forNextVariable`,
|
||||||
|
records *exactly* where it is standing (which may be several environments below the
|
||||||
|
generator's own call frame) as `forGeneratorEnv`, and moves `obj->environment` straight
|
||||||
|
to the loop environment — detaching, not popping, so every environment between the two
|
||||||
|
survives untouched.
|
||||||
|
|
||||||
|
`NEXT`/`LOOP` reactivate a suspended generator by setting `obj->environment` back to
|
||||||
|
`forGeneratorEnv` and driving the step loop (`akbasic_runtime_pump_generator()`) until
|
||||||
|
either another `EMIT` detaches it again or `END GEN` is reached for real — meaning
|
||||||
|
`isGenerator` is set and nothing is skipping forward to it, exactly the same test
|
||||||
|
`RETURN` makes for a multi-line `DEF`. Real exhaustion releases the generator
|
||||||
|
environment (`akbasic_runtime_prev_environment()`, same as anything else that pops) and
|
||||||
|
clears `forGeneratorEnv`, which is what tells the loop apart from one still waiting to
|
||||||
|
resume.
|
||||||
|
|
||||||
|
**Abandoning a live generator has to release it explicitly.** `EXIT` out of a `FOR
|
||||||
|
EACH`/`DO EACH` pops the loop environment the same way it always has, but a generator
|
||||||
|
paused mid-run is not on that direct parent chain from the loop back to the root — it
|
||||||
|
hangs off `forGeneratorEnv` instead, possibly several environments deep if `EMIT` last
|
||||||
|
ran inside a nested `FOR`/`DO` in the `GEN`'s own body. `akbasic_runtime_release_generator()`
|
||||||
|
is what walks that chain and releases all of it; every place that pops a `FOR EACH`/`DO
|
||||||
|
EACH` loop out from under a live generator calls it first, or the pool leaks one
|
||||||
|
generator at a time.
|
||||||
|
|
||||||
## Values
|
## Values
|
||||||
|
|
||||||
`akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct
|
`akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct
|
||||||
|
|||||||
@@ -72,6 +72,37 @@ typedef struct akbasic_Environment
|
|||||||
*/
|
*/
|
||||||
bool exiting;
|
bool exiting;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generator state (GEN / EMIT / FOR EACH / DO EACH).
|
||||||
|
*
|
||||||
|
* `isGenerator` is set on the environment a GEN call pushes -- the one
|
||||||
|
* whose body is actually running the GEN's lines, as opposed to the loop's
|
||||||
|
* own environment. `generatorFn` records *which* GEN it is running, so a
|
||||||
|
* FOR EACH/DO EACH that would invoke a GEN currently running higher up the
|
||||||
|
* parent chain (self-recursion) can be told apart from one invoking it
|
||||||
|
* fresh, or invoking a sibling instance of the same GEN sitting detached in
|
||||||
|
* someone else's `forGeneratorEnv`. It carries an akbasic_FunctionDef *, kept
|
||||||
|
* as void * for the same reason akbasic_environment_get_function() does:
|
||||||
|
* runtime.h includes this header, not the other way around.
|
||||||
|
*/
|
||||||
|
bool isGenerator;
|
||||||
|
void *generatorFn;
|
||||||
|
/**
|
||||||
|
* Set on a FOR EACH or DO EACH loop's own environment, distinguishing it
|
||||||
|
* from a plain FOR/DO for verbs that need to know which kind of loop this
|
||||||
|
* is -- EXIT, NEXT and LOOP all read it.
|
||||||
|
*/
|
||||||
|
bool isEachLoop;
|
||||||
|
/**
|
||||||
|
* The generator environment a FOR EACH/DO EACH loop is suspended on
|
||||||
|
* between iterations -- alive, detached from the step loop, but not
|
||||||
|
* released, so its own `nextline` still says where to resume. NULL means
|
||||||
|
* either "not an EACH loop" or "the generator is exhausted": both leave
|
||||||
|
* nothing to resume, and by the time either becomes true the loop
|
||||||
|
* environment itself is on its way out too.
|
||||||
|
*/
|
||||||
|
struct akbasic_Environment *forGeneratorEnv;
|
||||||
|
|
||||||
int64_t gosubReturnLine;
|
int64_t gosubReturnLine;
|
||||||
|
|
||||||
/* READ state. The identifier leaves are deep copies, so they need storage. */
|
/* READ state. The identifier leaves are deep copies, so they need storage. */
|
||||||
|
|||||||
@@ -102,6 +102,15 @@ typedef struct
|
|||||||
akbasic_ASTLeaf *arglist;
|
akbasic_ASTLeaf *arglist;
|
||||||
akbasic_ASTLeaf *expression;
|
akbasic_ASTLeaf *expression;
|
||||||
int64_t lineno;
|
int64_t lineno;
|
||||||
|
/*
|
||||||
|
* Set by akbasic_parse_gen(), left false by akbasic_parse_def(). GEN and
|
||||||
|
* DEF share this table (TODO.md's namespace decision for generators), so
|
||||||
|
* this is what lets akbasic_runtime_call_function() refuse to run a GEN
|
||||||
|
* called like an ordinary function -- cleanly, before anything is pushed,
|
||||||
|
* rather than relying on EMIT to fail deep inside a call whose BASIC-level
|
||||||
|
* error a caller driving its own step loop would not see raised.
|
||||||
|
*/
|
||||||
|
bool isGenerator;
|
||||||
/*
|
/*
|
||||||
* There is deliberately no environment here. It used to be owned by the
|
* There is deliberately no environment here. It used to be owned by the
|
||||||
* funcdef and reset on every call, which made a function not re-entrant --
|
* funcdef and reset on every call, which made a function not re-entrant --
|
||||||
@@ -595,6 +604,115 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_environment(akbasic_Runti
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_prev_environment(akbasic_Runtime *obj);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_prev_environment(akbasic_Runtime *obj);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return control to the active scope's parent, without releasing it.
|
||||||
|
*
|
||||||
|
* The half of akbasic_runtime_prev_environment() that pops; the other half,
|
||||||
|
* akbasic_runtime_release_environment(), gives the scope's variables and its
|
||||||
|
* pool slot back. Split for EMIT: a generator suspended between iterations
|
||||||
|
* has to keep existing -- its own `nextline` is where NEXT resumes it -- so
|
||||||
|
* detaching without releasing is what lets `obj->environment` move on to the
|
||||||
|
* loop while the generator's scope stays alive, reachable through
|
||||||
|
* `forGeneratorEnv`.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKBASIC_ERR_ENVIRONMENT When the active scope is the root.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_detach_environment(akbasic_Runtime *obj);
|
||||||
|
/**
|
||||||
|
* @brief Give a scope's variables and its pool slot back.
|
||||||
|
*
|
||||||
|
* The other half of akbasic_runtime_prev_environment(): unlike that function,
|
||||||
|
* @p env need not be `obj->environment` -- a generator environment sitting
|
||||||
|
* detached in some loop's `forGeneratorEnv` is released this way once it is
|
||||||
|
* exhausted or abandoned, without disturbing whatever scope is active now.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param env The scope to release; must not be NULL and must not be the root.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When `env` is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_release_environment(akbasic_Runtime *obj, akbasic_Environment *env);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Pop and release every scope from the active one up to @p target.
|
||||||
|
*
|
||||||
|
* The shared teardown for every path that abandons part of the environment
|
||||||
|
* chain at once instead of popping it a verb at a time: the error unwinds in
|
||||||
|
* akbasic_runtime_pump_generator() and akbasic_runtime_call_function(). Each
|
||||||
|
* popped scope's suspended generator (`forGeneratorEnv`), if it still holds
|
||||||
|
* one, is released through akbasic_runtime_release_generator() -- a suspended
|
||||||
|
* generator is a *child* of its loop scope, so no walk up the parent chain
|
||||||
|
* would ever reach it.
|
||||||
|
*
|
||||||
|
* Stops without error at the root if @p target is not on the chain: callers
|
||||||
|
* are already cleaning up after a failure, and releasing everything is the
|
||||||
|
* least-wrong answer to a target that has gone missing.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param target The scope to stop at; it is left active and untouched.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When @p target is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_unwind_to_environment(akbasic_Runtime *obj, akbasic_Environment *target);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Push a GEN's environment, bind its parameters and run it to its first EMIT.
|
||||||
|
*
|
||||||
|
* Shared by the EACH branches of `FOR` and `DO`: both look up the same kind of
|
||||||
|
* call expression (an `AKBASIC_LEAF_FUNCTION` leaf naming a GEN), guard
|
||||||
|
* against invoking a GEN that is already running higher up this same parent
|
||||||
|
* chain, evaluate the call's arguments in the caller's scope, bind them into a
|
||||||
|
* fresh environment the way a function call does, and run that environment
|
||||||
|
* until EMIT detaches it or END GEN ends it with nothing emitted.
|
||||||
|
*
|
||||||
|
* @p loopenv is left in the state a caller checks afterwards:
|
||||||
|
* `loopenv->forGeneratorEnv` is the live generator environment when
|
||||||
|
* something was emitted, or NULL when the GEN produced nothing at all.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param loopenv The FOR EACH/DO EACH loop's own environment; becomes the new
|
||||||
|
* generator environment's parent.
|
||||||
|
* @param callexpr The generator call, e.g. `ROOMOBJECTS(CURROOM%)`.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKBASIC_ERR_STATE When the named GEN is already running higher up
|
||||||
|
* this same parent chain.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_generator_invoke(akbasic_Runtime *obj, akbasic_Environment *loopenv, akbasic_ASTLeaf *callexpr);
|
||||||
|
/**
|
||||||
|
* @brief Run a suspended generator until it next detaches.
|
||||||
|
*
|
||||||
|
* Shared by the EACH branches of `NEXT` and `LOOP`: `obj->environment` is
|
||||||
|
* expected to already be the generator environment to resume (a caller sets
|
||||||
|
* that from `loopenv->forGeneratorEnv` before calling), and this drives the
|
||||||
|
* step loop until either EMIT detaches it back to @p loopenv with another
|
||||||
|
* value, or END GEN really ends it -- in which case it releases the
|
||||||
|
* generator environment itself and clears `loopenv->forGeneratorEnv`.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param loopenv The FOR EACH/DO EACH loop's own environment, and the pump's
|
||||||
|
* stopping point.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_pump_generator(akbasic_Runtime *obj, akbasic_Environment *loopenv);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release a live, abandoned generator, wherever EMIT left it suspended.
|
||||||
|
*
|
||||||
|
* `env` is expected to be a loop's `forGeneratorEnv` -- the resume point, not
|
||||||
|
* necessarily the GEN's own call frame, since EMIT may have run several
|
||||||
|
* levels below it inside a FOR/DO/GOSUB the body wrote. Walks up from there,
|
||||||
|
* releasing every environment through the call frame itself inclusive, so
|
||||||
|
* nothing above the resume point is left behind.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param env The suspended resume point to release.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When `env` is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_release_generator(akbasic_Runtime *obj, akbasic_Environment *env);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Report a BASIC error on the current line, in the reference's format.
|
* @brief Report a BASIC error on the current line, in the reference's format.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ akerr_ErrorContext *akbasic_environment_init(akbasic_Environment *obj, akbasic_R
|
|||||||
obj->doConditionLeaf = NULL;
|
obj->doConditionLeaf = NULL;
|
||||||
obj->doConditionKind = AKBASIC_LOOPCOND_NONE;
|
obj->doConditionKind = AKBASIC_LOOPCOND_NONE;
|
||||||
obj->isDoLoop = false;
|
obj->isDoLoop = false;
|
||||||
|
obj->isGenerator = false;
|
||||||
|
obj->generatorFn = NULL;
|
||||||
|
obj->isEachLoop = false;
|
||||||
|
obj->forGeneratorEnv = NULL;
|
||||||
obj->gosubReturnLine = 0;
|
obj->gosubReturnLine = 0;
|
||||||
obj->readReturnLine = 0;
|
obj->readReturnLine = 0;
|
||||||
obj->readIdentifierIdx = 0;
|
obj->readIdentifierIdx = 0;
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
|
|
||||||
#include "verbs.h"
|
#include "verbs.h"
|
||||||
|
|
||||||
|
/* Shared by akbasic_parse_for() and akbasic_parse_do(); defined after akbasic_parse_def(). */
|
||||||
|
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr);
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_parse_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
akerr_ErrorContext *akbasic_parse_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -309,8 +312,58 @@ akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **d
|
|||||||
akbasic_Environment *newenv = NULL;
|
akbasic_Environment *newenv = NULL;
|
||||||
akbasic_ASTLeaf *expr = NULL;
|
akbasic_ASTLeaf *expr = NULL;
|
||||||
akbasic_ASTLeaf *condition = NULL;
|
akbasic_ASTLeaf *condition = NULL;
|
||||||
|
akbasic_Token *peeked = NULL;
|
||||||
int kind = AKBASIC_LOOPCOND_NONE;
|
int kind = AKBASIC_LOOPCOND_NONE;
|
||||||
int64_t firstline = parent->lineno + 1;
|
int64_t firstline = parent->lineno + 1;
|
||||||
|
int cmp = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DO EACH <variable> IN <generator call> ... LOOP. Mutually exclusive with
|
||||||
|
* DO WHILE/UNTIL on the same DO, so this is checked first and returns
|
||||||
|
* before any of the WHILE/UNTIL machinery runs.
|
||||||
|
*/
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||||
|
PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
|
||||||
|
if ( cmp == 0 ) {
|
||||||
|
akbasic_ASTLeaf *var = NULL;
|
||||||
|
akbasic_ASTLeaf *callexpr = NULL;
|
||||||
|
|
||||||
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||||
|
newenv = runtime->environment;
|
||||||
|
runtime->environment = parent;
|
||||||
|
|
||||||
|
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Same guard as akbasic_parse_for()'s EACH branch, with the likely
|
||||||
|
* mistake named: a condition belongs on the LOOP, where it is
|
||||||
|
* checked against each emitted value, not here on the DO.
|
||||||
|
*/
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
FAIL_NONZERO_RETURN(errctx,
|
||||||
|
(peeked != NULL &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_COLON),
|
||||||
|
AKBASIC_ERR_SYNTAX,
|
||||||
|
"DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here");
|
||||||
|
|
||||||
|
newenv->isDoLoop = true;
|
||||||
|
newenv->isEachLoop = true;
|
||||||
|
newenv->loopFirstLine = firstline;
|
||||||
|
/* See akbasic_parse_for()'s EACH branch for why this is not cloned. */
|
||||||
|
newenv->forToLeaf = callexpr;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(expr, "DO", var));
|
||||||
|
|
||||||
|
runtime->environment = newenv;
|
||||||
|
*dest = expr;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||||
newenv = runtime->environment;
|
newenv = runtime->environment;
|
||||||
@@ -883,6 +936,134 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GEN NAME(parameters) ... END GEN
|
||||||
|
*
|
||||||
|
* A DEF that yields more than once, in the same shape a multi-line DEF is:
|
||||||
|
* the header is parsed, the parameter list is read with parse_def_parameters()
|
||||||
|
* (GEN and DEF share the functions table -- TODO.md's namespace decision for
|
||||||
|
* this feature -- so a name cannot be both), and the body is skipped on this,
|
||||||
|
* the definitional pass, by arming akbasic_environment_wait_for_command() for
|
||||||
|
* "END GEN" instead of "RETURN". It only really executes when a FOR EACH/DO
|
||||||
|
* EACH invokes it through akbasic_runtime_generator_invoke(), which sets
|
||||||
|
* `nextline` to `fndef->lineno` directly and never runs this line again.
|
||||||
|
*
|
||||||
|
* There is no single-expression form: a GEN with nothing to loop over is just
|
||||||
|
* a DEF, and EMIT already needs a body to sit in.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *akbasic_parse_gen(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Runtime *runtime = parser->runtime;
|
||||||
|
akbasic_ASTLeaf *identifier = NULL;
|
||||||
|
akbasic_ASTLeaf *arglist = NULL;
|
||||||
|
akbasic_ASTLeaf *command = NULL;
|
||||||
|
akbasic_FunctionDef *fndef = NULL;
|
||||||
|
size_t namelen = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (identifier->leaftype == AKBASIC_LEAF_IDENTIFIER),
|
||||||
|
AKBASIC_ERR_SYNTAX, "Expected identifier");
|
||||||
|
|
||||||
|
PASS(errctx, parse_def_parameters(parser, &arglist));
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_new_function(runtime, &fndef));
|
||||||
|
|
||||||
|
/* Uppercase the name: verbs, functions and generators are all case-insensitive. */
|
||||||
|
PASS(errctx, aksl_strlen(identifier->identifier, &namelen));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (namelen < sizeof(fndef->name)),
|
||||||
|
AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier);
|
||||||
|
for ( i = 0; i < namelen; i++ ) {
|
||||||
|
char c = identifier->identifier[i];
|
||||||
|
fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
|
||||||
|
}
|
||||||
|
fndef->name[namelen] = '\0';
|
||||||
|
|
||||||
|
fndef->expression = NULL;
|
||||||
|
fndef->isGenerator = true;
|
||||||
|
PASS(errctx, akbasic_environment_wait_for_command(runtime->environment, "END GEN"));
|
||||||
|
PASS(errctx, akbasic_leaf_clone(arglist, &fndef->leafpool, &fndef->arglist));
|
||||||
|
fndef->lineno = runtime->environment->lineno + 1;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_symtab_set(&runtime->environment->functions, fndef->name, fndef, 0));
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(command, "GEN", NULL));
|
||||||
|
*dest = command;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* END [GEN]
|
||||||
|
*
|
||||||
|
* Bare END finishes the program, the default command path's job before this
|
||||||
|
* handler existed. `END GEN` is a GEN body's own closing verb, and it is
|
||||||
|
* never scanned as one token -- GEN follows END as an ordinary COMMAND token
|
||||||
|
* on the same line -- so this is what tells the two apart and builds the
|
||||||
|
* compound leaf akbasic_cmd_end_gen dispatches on, the same trick
|
||||||
|
* akbasic_parse_print() uses for `PRINT #`.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *akbasic_parse_end(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_ASTLeaf *expr = NULL;
|
||||||
|
akbasic_ASTLeaf *right = NULL;
|
||||||
|
akbasic_Token *peeked = NULL;
|
||||||
|
int cmp = 0;
|
||||||
|
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||||
|
PASS(errctx, aksl_strcmp(peeked->lexeme, "GEN", &cmp));
|
||||||
|
if ( cmp == 0 ) {
|
||||||
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(expr, "END GEN", NULL));
|
||||||
|
*dest = expr;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A plain END, matching what the default command path used to do. */
|
||||||
|
if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_COLON ) {
|
||||||
|
PASS(errctx, akbasic_parser_expression(parser, &right));
|
||||||
|
}
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(expr, "END", right));
|
||||||
|
*dest = expr;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* EACH <variable> IN <generator call>
|
||||||
|
*
|
||||||
|
* Shared by akbasic_parse_for() and akbasic_parse_do(), both of which consume
|
||||||
|
* the leading EACH themselves (it is what tells them this is an EACH loop
|
||||||
|
* rather than their ordinary form) before calling this for the rest.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Token *word = NULL;
|
||||||
|
int cmp = 0;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_expression(parser, var));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (*var != NULL && akbasic_leaf_is_identifier(*var)), AKBASIC_ERR_SYNTAX,
|
||||||
|
"Expected EACH (variable) IN (generator call)");
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX,
|
||||||
|
"Expected IN after EACH (variable)");
|
||||||
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
||||||
|
PASS(errctx, aksl_strcmp(word->lexeme, "IN", &cmp));
|
||||||
|
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected IN after EACH (variable)");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_expression(parser, callexpr));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (*callexpr != NULL && (*callexpr)->leaftype == AKBASIC_LEAF_FUNCTION),
|
||||||
|
AKBASIC_ERR_SYNTAX, "Expected a generator call after IN");
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* FOR ... TO .... [STEP ...]
|
* FOR ... TO .... [STEP ...]
|
||||||
* COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION]
|
* COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION]
|
||||||
@@ -898,11 +1079,72 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **
|
|||||||
akbasic_ASTLeaf *assignment = NULL;
|
akbasic_ASTLeaf *assignment = NULL;
|
||||||
akbasic_ASTLeaf *expr = NULL;
|
akbasic_ASTLeaf *expr = NULL;
|
||||||
akbasic_Token *operator_ = NULL;
|
akbasic_Token *operator_ = NULL;
|
||||||
|
akbasic_Token *peeked = NULL;
|
||||||
akbasic_Environment *parent = runtime->environment;
|
akbasic_Environment *parent = runtime->environment;
|
||||||
akbasic_Environment *newenv = NULL;
|
akbasic_Environment *newenv = NULL;
|
||||||
int64_t firstline = 0;
|
int64_t firstline = 0;
|
||||||
int cmp = 0;
|
int cmp = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FOR EACH <variable> IN <generator call>. Checked before the leaf right of
|
||||||
|
* FOR is required to be an assignment, because EACH is the one other thing
|
||||||
|
* that leaf is allowed to be.
|
||||||
|
*/
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||||
|
PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
|
||||||
|
if ( cmp == 0 ) {
|
||||||
|
akbasic_ASTLeaf *var = NULL;
|
||||||
|
akbasic_ASTLeaf *callexpr = NULL;
|
||||||
|
|
||||||
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||||
|
firstline = parent->lineno + 1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pushed the same way a plain FOR's environment is: while the
|
||||||
|
* parent is still active, so the call expression's identifiers
|
||||||
|
* resolve in the caller's scope rather than the loop's own.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||||
|
newenv = runtime->environment;
|
||||||
|
runtime->environment = parent;
|
||||||
|
|
||||||
|
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Nothing may follow the generator call but another statement.
|
||||||
|
* Without this, a stray clause sits unparsed on the line and only
|
||||||
|
* blows up after the whole loop has run, when the parent scope
|
||||||
|
* resumes the line mid-statement -- an error at the loop's end
|
||||||
|
* pointing at its beginning.
|
||||||
|
*/
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
FAIL_NONZERO_RETURN(errctx,
|
||||||
|
(peeked != NULL &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_COLON),
|
||||||
|
AKBASIC_ERR_SYNTAX,
|
||||||
|
"FOR EACH takes nothing after the generator call");
|
||||||
|
|
||||||
|
newenv->isEachLoop = true;
|
||||||
|
newenv->loopFirstLine = firstline;
|
||||||
|
/*
|
||||||
|
* Stashed on forToLeaf rather than cloned into a leaf pool: unlike
|
||||||
|
* DO's condition, this expression is evaluated exactly once, by
|
||||||
|
* akbasic_cmd_for() on this same pass before the per-line leaf
|
||||||
|
* storage it lives in is reused -- see akbasic_runtime_generator_invoke().
|
||||||
|
*/
|
||||||
|
newenv->forToLeaf = callexpr;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(expr, "FOR", var));
|
||||||
|
|
||||||
|
runtime->environment = newenv;
|
||||||
|
*dest = expr;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PASS(errctx, akbasic_parser_assignment(parser, &assignment));
|
PASS(errctx, akbasic_parser_assignment(parser, &assignment));
|
||||||
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
||||||
AKBASIC_ERR_SYNTAX,
|
AKBASIC_ERR_SYNTAX,
|
||||||
|
|||||||
@@ -143,17 +143,24 @@ akerr_ErrorContext *akbasic_runtime_new_environment(akbasic_Runtime *obj)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
akerr_ErrorContext *akbasic_runtime_detach_environment(akbasic_Runtime *obj)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
akbasic_Environment *popped = NULL;
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in prev_environment");
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in detach_environment");
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
"No previous environment to return to");
|
"No previous environment to return to");
|
||||||
popped = obj->environment;
|
obj->environment = obj->environment->parent;
|
||||||
obj->environment = popped->parent;
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_release_environment(akbasic_Runtime *obj, akbasic_Environment *env)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && env != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in release_environment");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Give back the variables this scope created, as well as the scope.
|
* Give back the variables this scope created, as well as the scope.
|
||||||
@@ -173,9 +180,9 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
|||||||
* times exhausted the 128-slot pool and reported "Maximum runtime variables
|
* times exhausted the 128-slot pool and reported "Maximum runtime variables
|
||||||
* reached" on a four-line program.
|
* reached" on a four-line program.
|
||||||
*/
|
*/
|
||||||
for ( i = 0; i < popped->variables.capacity; i++ ) {
|
for ( i = 0; i < env->variables.capacity; i++ ) {
|
||||||
akbasic_Variable *variable = (akbasic_Variable *)popped->variables.slots[i].value;
|
akbasic_Variable *variable = (akbasic_Variable *)env->variables.slots[i].value;
|
||||||
if ( popped->variables.slots[i].used && variable != NULL ) {
|
if ( env->variables.slots[i].used && variable != NULL ) {
|
||||||
variable->used = false;
|
variable->used = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +192,51 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
|||||||
* here the pool is finite, so an unreleased environment is a bug that shows
|
* here the pool is finite, so an unreleased environment is a bug that shows
|
||||||
* up as exhaustion a few thousand GOSUBs later.
|
* up as exhaustion a few thousand GOSUBs later.
|
||||||
*/
|
*/
|
||||||
popped->used = false;
|
env->used = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *popped = NULL;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in prev_environment");
|
||||||
|
popped = obj->environment;
|
||||||
|
PASS(errctx, akbasic_runtime_detach_environment(obj));
|
||||||
|
PASS(errctx, akbasic_runtime_release_environment(obj, popped));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_unwind_to_environment(akbasic_Runtime *obj, akbasic_Environment *target)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *popped = NULL;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && target != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in unwind_to_environment");
|
||||||
|
/*
|
||||||
|
* Stops early at the root rather than failing on it: every caller is an
|
||||||
|
* error-unwind path, where "release whatever there is" beats raising a
|
||||||
|
* second failure on top of the one being cleaned up after.
|
||||||
|
*/
|
||||||
|
while ( obj->environment != target && obj->environment->parent != NULL ) {
|
||||||
|
popped = obj->environment;
|
||||||
|
obj->environment = popped->parent;
|
||||||
|
/*
|
||||||
|
* An EACH loop scope on its way out takes its suspended generator with
|
||||||
|
* it -- the generator is a *child* of the scope, off the parent chain,
|
||||||
|
* and this walk is the only thing that will ever see it again. Guarded
|
||||||
|
* on `used` because a generator that was being pumped when the failure
|
||||||
|
* hit is *on* the chain being unwound, already released by the time
|
||||||
|
* the walk reaches the loop scope that references it.
|
||||||
|
*/
|
||||||
|
if ( popped->forGeneratorEnv != NULL && popped->forGeneratorEnv->used ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, popped->forGeneratorEnv));
|
||||||
|
}
|
||||||
|
popped->forGeneratorEnv = NULL;
|
||||||
|
PASS(errctx, akbasic_runtime_release_environment(obj, popped));
|
||||||
|
}
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1028,6 +1079,18 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch
|
|||||||
|
|
||||||
PASS(errctx, akbasic_environment_get_function(obj->environment, name, &fnptr));
|
PASS(errctx, akbasic_environment_get_function(obj->environment, name, &fnptr));
|
||||||
fndef = (akbasic_FunctionDef *)fnptr;
|
fndef = (akbasic_FunctionDef *)fnptr;
|
||||||
|
/*
|
||||||
|
* A GEN is not a function: it yields more than once, through EMIT, and
|
||||||
|
* nothing about an ordinary call ever resumes it for a second value.
|
||||||
|
* Refused here, before anything is pushed, rather than left to fail
|
||||||
|
* inside the call -- a BASIC-level error down in EMIT is swallowed by
|
||||||
|
* process_line_run() the same way any statement's is, so a caller
|
||||||
|
* driving its own step loop would see this "succeed" with whatever
|
||||||
|
* garbage was left in the return slot instead of failing at all.
|
||||||
|
*/
|
||||||
|
FAIL_NONZERO_RETURN(errctx, fndef->isGenerator, AKBASIC_ERR_STATE,
|
||||||
|
"%s is a GEN; call it with FOR EACH or DO EACH, not as a function",
|
||||||
|
fndef->name);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* **One environment per call, from the pool -- exactly as GOSUB does.**
|
* **One environment per call, from the pool -- exactly as GOSUB does.**
|
||||||
@@ -1135,10 +1198,11 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch
|
|||||||
* Give them back, or a host absorbing script errors drains the
|
* Give them back, or a host absorbing script errors drains the
|
||||||
* twelve-slot environment pool after twelve dead calls and every
|
* twelve-slot environment pool after twelve dead calls and every
|
||||||
* call after that fails for a reason nobody can see in the script.
|
* call after that fails for a reason nobody can see in the script.
|
||||||
|
* The unwind, not a bare prev_environment() loop, because a body that
|
||||||
|
* died inside a FOR EACH leaves a suspended generator hanging off the
|
||||||
|
* loop scope, and only the unwind knows to take it down too.
|
||||||
*/
|
*/
|
||||||
while ( obj->environment != targetenv && obj->environment->parent != NULL ) {
|
IGNORE(akbasic_runtime_unwind_to_environment(obj, targetenv));
|
||||||
IGNORE(akbasic_runtime_prev_environment(obj));
|
|
||||||
}
|
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
PASS(errctx, akbasic_environment_new_value(targetenv, &out));
|
PASS(errctx, akbasic_environment_new_value(targetenv, &out));
|
||||||
|
|||||||
@@ -161,8 +161,22 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
|
|||||||
SUCCEED_TRUE(obj, dest);
|
SUCCEED_TRUE(obj, dest);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* A GEN is a function at heart, and RETURN ends it the way it ends a DEF
|
||||||
|
* or a GOSUB: early, cleanly, from its own frame. What a generator's
|
||||||
|
* RETURN cannot do is carry a value -- values leave a GEN one at a time,
|
||||||
|
* through EMIT, and there is no caller waiting on a return slot.
|
||||||
|
*/
|
||||||
|
if ( obj->environment->isGenerator ) {
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"A GEN yields values through EMIT; RETURN here takes none");
|
||||||
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
|
||||||
"RETURN outside the context of GOSUB");
|
"RETURN outside the context of GOSUB, DEF, or GEN");
|
||||||
|
|
||||||
if ( expr != NULL && expr->right != NULL ) {
|
if ( expr != NULL && expr->right != NULL ) {
|
||||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
|
||||||
@@ -840,6 +854,31 @@ akerr_ErrorContext *akbasic_cmd_for(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
|||||||
bool met = false;
|
bool met = false;
|
||||||
|
|
||||||
(void)lval; (void)rval;
|
(void)lval; (void)rval;
|
||||||
|
if ( obj->environment->isEachLoop ) {
|
||||||
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && akbasic_leaf_is_identifier(expr->right)),
|
||||||
|
AKBASIC_ERR_SYNTAX, "Expected FOR EACH (variable) IN (generator call)");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->forToLeaf != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"Expected FOR EACH (variable) IN (generator call)");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_environment_get(loopenv, expr->right->identifier,
|
||||||
|
&loopenv->forNextVariable));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->forNextVariable != NULL), AKBASIC_ERR_UNDEFINED,
|
||||||
|
"Unable to get loop variable %s", expr->right->identifier);
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_generator_invoke(obj, loopenv, loopenv->forToLeaf));
|
||||||
|
loopenv->forToLeaf = NULL;
|
||||||
|
|
||||||
|
if ( loopenv->forGeneratorEnv == NULL ) {
|
||||||
|
/* The generator produced nothing: skip the body by waiting for NEXT,
|
||||||
|
exactly as a zero-iteration plain FOR does. */
|
||||||
|
PASS(errctx, akbasic_environment_wait_for_command(loopenv, "NEXT"));
|
||||||
|
}
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->forToLeaf != NULL && expr != NULL && expr->right != NULL),
|
FAIL_ZERO_RETURN(errctx, (obj->environment->forToLeaf != NULL && expr != NULL && expr->right != NULL),
|
||||||
AKBASIC_ERR_STATE, "Expected FOR ... TO [STEP ...]");
|
AKBASIC_ERR_STATE, "Expected FOR ... TO [STEP ...]");
|
||||||
FAIL_ZERO_RETURN(errctx,
|
FAIL_ZERO_RETURN(errctx,
|
||||||
@@ -891,10 +930,16 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
"NEXT outside the context of FOR");
|
"NEXT outside the context of FOR");
|
||||||
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
||||||
"Expected NEXT IDENTIFIER");
|
"Expected NEXT IDENTIFIER");
|
||||||
FAIL_ZERO_RETURN(errctx,
|
if ( obj->environment->isEachLoop ) {
|
||||||
(expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
|
/* EACH accepts any emitted type; the numeric-only check is for plain FOR. */
|
||||||
expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT),
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(expr->right), AKBASIC_ERR_SYNTAX,
|
||||||
AKBASIC_ERR_TYPE, "FOR ... NEXT only valid over INT and FLOAT types");
|
"Expected NEXT IDENTIFIER");
|
||||||
|
} else {
|
||||||
|
FAIL_ZERO_RETURN(errctx,
|
||||||
|
(expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
|
||||||
|
expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT),
|
||||||
|
AKBASIC_ERR_TYPE, "FOR ... NEXT only valid over INT and FLOAT types");
|
||||||
|
}
|
||||||
|
|
||||||
obj->environment->loopExitLine = obj->environment->lineno + 1;
|
obj->environment->loopExitLine = obj->environment->lineno + 1;
|
||||||
|
|
||||||
@@ -909,6 +954,14 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
"NEXT in an orphaned environment");
|
"NEXT in an orphaned environment");
|
||||||
|
/*
|
||||||
|
* A live generator abandoned mid-run: release it too, or it never comes
|
||||||
|
* back to the pool. See MAINTENANCE.md's note on abandoned generators.
|
||||||
|
*/
|
||||||
|
if ( obj->environment->forGeneratorEnv != NULL ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
obj->environment->parent->nextline = obj->environment->loopExitLine;
|
obj->environment->parent->nextline = obj->environment->loopExitLine;
|
||||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
*dest = &obj->staticFalseValue;
|
*dest = &obj->staticFalseValue;
|
||||||
@@ -924,12 +977,37 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
if ( cmp != 0 ) {
|
if ( cmp != 0 ) {
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
"NEXT in an orphaned environment");
|
"NEXT in an orphaned environment");
|
||||||
|
if ( obj->environment->forGeneratorEnv != NULL ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
obj->environment->parent->nextline = obj->environment->nextline;
|
obj->environment->parent->nextline = obj->environment->nextline;
|
||||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
*dest = &obj->staticFalseValue;
|
*dest = &obj->staticFalseValue;
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( obj->environment->isEachLoop ) {
|
||||||
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
||||||
|
if ( loopenv->forGeneratorEnv != NULL ) {
|
||||||
|
obj->environment = loopenv->forGeneratorEnv;
|
||||||
|
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||||
|
}
|
||||||
|
if ( loopenv->forGeneratorEnv == NULL ) {
|
||||||
|
/* Exhausted: pop the loop, same landing NEXT always uses when done. */
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
|
"NEXT in an orphaned environment");
|
||||||
|
loopenv->parent->nextline = loopenv->loopExitLine;
|
||||||
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
|
*dest = &obj->staticFalseValue;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->identifier, &nextvar));
|
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->identifier, &nextvar));
|
||||||
FAIL_ZERO_RETURN(errctx, (nextvar != NULL), AKBASIC_ERR_UNDEFINED,
|
FAIL_ZERO_RETURN(errctx, (nextvar != NULL), AKBASIC_ERR_UNDEFINED,
|
||||||
"Unable to get loop variable %s", expr->right->identifier);
|
"Unable to get loop variable %s", expr->right->identifier);
|
||||||
@@ -972,7 +1050,7 @@ akerr_ErrorContext *akbasic_cmd_exit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
*/
|
*/
|
||||||
FAIL_NONZERO_RETURN(errctx,
|
FAIL_NONZERO_RETURN(errctx,
|
||||||
(obj->environment->forToValue.valuetype == AKBASIC_TYPE_UNDEFINED &&
|
(obj->environment->forToValue.valuetype == AKBASIC_TYPE_UNDEFINED &&
|
||||||
!obj->environment->isDoLoop),
|
!obj->environment->isDoLoop && !obj->environment->isEachLoop),
|
||||||
AKBASIC_ERR_STATE, "EXIT outside the context of FOR or DO");
|
AKBASIC_ERR_STATE, "EXIT outside the context of FOR or DO");
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
"EXIT in an orphaned environment");
|
"EXIT in an orphaned environment");
|
||||||
|
|||||||
307
src/runtime_generator.c
Normal file
307
src/runtime_generator.c
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
/**
|
||||||
|
* @file runtime_generator.c
|
||||||
|
* @brief GEN, EMIT and END GEN, plus the machinery FOR EACH/DO EACH share.
|
||||||
|
*
|
||||||
|
* A GEN is a DEF that yields more than once. Its body is skipped on the
|
||||||
|
* definitional pass exactly as a multi-line DEF's is -- `akbasic_parse_gen()`
|
||||||
|
* arms `akbasic_environment_wait_for_command(env, "END GEN")` the same way
|
||||||
|
* `akbasic_parse_def()` arms one for `RETURN` -- and it only ever really runs
|
||||||
|
* when a `FOR EACH`/`DO EACH` invokes it.
|
||||||
|
*
|
||||||
|
* That invocation pushes one environment for the whole lifetime of the loop,
|
||||||
|
* exactly as a GOSUB or a function call does, except that `EMIT` does not pop
|
||||||
|
* it: it hands control back to the loop without releasing anything, so the
|
||||||
|
* environment EMIT actually ran in -- which may be nested several levels
|
||||||
|
* below the GEN's own call frame, inside a FOR/DO/GOSUB the body wrote --
|
||||||
|
* still says exactly where to resume when `NEXT`/`LOOP` calls back into
|
||||||
|
* akbasic_runtime_pump_generator(). Only `END GEN` reached for real --
|
||||||
|
* meaning `obj->environment->isGenerator` is true and nothing is skipping
|
||||||
|
* forward to it -- actually releases the call frame, the way `RETURN`
|
||||||
|
* releases a DEF's call environment.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
#include <akbasic/scanner.h>
|
||||||
|
|
||||||
|
#include "verbs.h"
|
||||||
|
|
||||||
|
/* Most verbs answer "did something happen"; this is that answer. */
|
||||||
|
#define SUCCEED_TRUE(__obj, __dest) \
|
||||||
|
do { \
|
||||||
|
*(__dest) = &(__obj)->staticTrueValue; \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_pump_generator(akbasic_Runtime *obj, akbasic_Environment *loopenv)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && loopenv != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in pump_generator");
|
||||||
|
/*
|
||||||
|
* The same per-line prologue akbasic_runtime_step() and
|
||||||
|
* akbasic_runtime_call_function() run, driving process_line_run() directly
|
||||||
|
* rather than through the ordinary step loop: a generator body is not the
|
||||||
|
* top-level program and nothing else is going to advance it.
|
||||||
|
*/
|
||||||
|
ATTEMPT {
|
||||||
|
while ( obj->environment != loopenv && obj->mode == AKBASIC_MODE_RUN ) {
|
||||||
|
CATCH(errctx, akbasic_runtime_zero(obj));
|
||||||
|
CATCH(errctx, akbasic_scanner_zero(obj));
|
||||||
|
CATCH(errctx, akbasic_runtime_process_line_run(obj));
|
||||||
|
}
|
||||||
|
} CLEANUP {
|
||||||
|
/*
|
||||||
|
* CLEANUP runs unconditionally -- it is not a `catch` -- so it is
|
||||||
|
* guarded on the one thing that tells success and failure apart here:
|
||||||
|
* whether `obj->environment` is still `loopenv`. On the ordinary
|
||||||
|
* success path it already is (that is the ATTEMPT loop's own exit
|
||||||
|
* condition), so this is a no-op there, exactly as it is meant to be.
|
||||||
|
* Only a genuine C-level failure -- the scanner or parser raised, or a
|
||||||
|
* runtime error escaped the swallow process_line_run() ordinarily does
|
||||||
|
* for a BASIC-level one -- leaves scopes active between here and
|
||||||
|
* loopenv, and only then does this force them back, taking
|
||||||
|
* `forGeneratorEnv` down with them since whatever it pointed at is
|
||||||
|
* among the scopes just released.
|
||||||
|
*/
|
||||||
|
if ( obj->environment != loopenv ) {
|
||||||
|
IGNORE(akbasic_runtime_unwind_to_environment(obj, loopenv));
|
||||||
|
loopenv->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release a live generator, however deep EMIT left it suspended.
|
||||||
|
*
|
||||||
|
* `loopenv->forGeneratorEnv` is the *resume point*, not necessarily the GEN's
|
||||||
|
* own call frame -- EMIT may have run several levels down, inside a FOR/DO/
|
||||||
|
* GOSUB the body wrote around it. Abandoning it (EXIT) has to give back every
|
||||||
|
* environment from there up through the call frame itself, or everything
|
||||||
|
* above the resume point leaks.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param env The suspended resume point; walks up through its own parents.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *akbasic_runtime_release_generator(akbasic_Runtime *obj, akbasic_Environment *env)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *walk = env;
|
||||||
|
akbasic_Environment *next = NULL;
|
||||||
|
bool isgen = false;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && env != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in release_generator");
|
||||||
|
while ( walk != NULL ) {
|
||||||
|
isgen = walk->isGenerator;
|
||||||
|
next = walk->parent;
|
||||||
|
/*
|
||||||
|
* A scope between the resume point and the call frame may be an EACH
|
||||||
|
* loop with its *own* generator suspended off to the side. Releasing
|
||||||
|
* the loop scope without releasing that generator strands it in the
|
||||||
|
* pool -- the walk goes through parents and a suspended generator is a
|
||||||
|
* child. Guarded on `used` so a generator already released as part of
|
||||||
|
* some enclosing teardown is not released twice.
|
||||||
|
*/
|
||||||
|
if ( walk->forGeneratorEnv != NULL && walk->forGeneratorEnv->used ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, walk->forGeneratorEnv));
|
||||||
|
}
|
||||||
|
walk->forGeneratorEnv = NULL;
|
||||||
|
PASS(errctx, akbasic_runtime_release_environment(obj, walk));
|
||||||
|
if ( isgen ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
walk = next;
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_generator_invoke(akbasic_Runtime *obj, akbasic_Environment *loopenv, akbasic_ASTLeaf *callexpr)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *callenv = NULL;
|
||||||
|
akbasic_Environment *walk = NULL;
|
||||||
|
akbasic_FunctionDef *fndef = NULL;
|
||||||
|
akbasic_ASTLeaf *fnarg = NULL;
|
||||||
|
akbasic_ASTLeaf *paramleaf = NULL;
|
||||||
|
akbasic_Value *argvals[AKBASIC_MAX_CALL_ARGUMENTS];
|
||||||
|
akbasic_Value *unused = NULL;
|
||||||
|
void *fnptr = NULL;
|
||||||
|
int nargs = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && loopenv != NULL && callexpr != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in generator_invoke");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (callexpr->leaftype == AKBASIC_LEAF_FUNCTION), AKBASIC_ERR_SYNTAX,
|
||||||
|
"Expected a generator call after IN");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GEN and DEF share the functions table (TODO.md's namespace decision for
|
||||||
|
* this feature), so this is the same lookup akbasic_runtime_call_function()
|
||||||
|
* does. The parser already proved the name resolves and the arity matches
|
||||||
|
* when it parsed `callexpr` -- akbasic_parser_expression() would not have
|
||||||
|
* produced an AKBASIC_LEAF_FUNCTION leaf otherwise -- so a miss here would
|
||||||
|
* mean the function table changed out from under a leaf built against it,
|
||||||
|
* which is not a case this needs its own message for.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_environment_get_function(loopenv, callexpr->identifier, &fnptr));
|
||||||
|
fndef = (akbasic_FunctionDef *)fnptr;
|
||||||
|
FAIL_ZERO_RETURN(errctx, fndef->isGenerator, AKBASIC_ERR_STATE,
|
||||||
|
"%s is a DEF, not a GEN -- FOR EACH/DO EACH needs a generator",
|
||||||
|
fndef->name);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Self-recursion: walk the *parent* chain, not the pool. An environment
|
||||||
|
* reachable only through some other loop's `forGeneratorEnv` is a sibling
|
||||||
|
* invocation sitting detached between its own iterations, not an ancestor
|
||||||
|
* of this call -- nothing points from here to it via `parent`, so it never
|
||||||
|
* matches and independent or nested FOR EACH/DO EACH over the same GEN
|
||||||
|
* (even the same GEN with different arguments) is unaffected.
|
||||||
|
*/
|
||||||
|
for ( walk = loopenv; walk != NULL; walk = walk->parent ) {
|
||||||
|
if ( walk->isGenerator && walk->generatorFn == (void *)fndef ) {
|
||||||
|
FAIL_RETURN(errctx, AKBASIC_ERR_STATE,
|
||||||
|
"GEN %s cannot FOR EACH/DO EACH over itself from its own body",
|
||||||
|
fndef->name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Evaluated in the caller's own scope, before anything is pushed -- the
|
||||||
|
* same reason akbasic_runtime_user_function() evaluates every argument
|
||||||
|
* before binding the first one: a later argument must not see an earlier
|
||||||
|
* one already sitting in the callee's scope.
|
||||||
|
*/
|
||||||
|
fnarg = akbasic_leaf_first_argument(callexpr);
|
||||||
|
for ( ; fnarg != NULL; fnarg = fnarg->next ) {
|
||||||
|
FAIL_ZERO_RETURN(errctx, (nargs < AKBASIC_MAX_CALL_ARGUMENTS), AKBASIC_ERR_BOUNDS,
|
||||||
|
"%s was called with more than %d arguments",
|
||||||
|
callexpr->identifier, AKBASIC_MAX_CALL_ARGUMENTS);
|
||||||
|
PASS(errctx, akbasic_runtime_evaluate(obj, fnarg, &argvals[nargs]));
|
||||||
|
nargs += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One environment for the whole lifetime of the loop, exactly as GOSUB and
|
||||||
|
* a function call take one from the pool -- and unlike either, this one
|
||||||
|
* survives past the verb that pushed it, held alive by `loopenv`'s own
|
||||||
|
* reference until NEXT/LOOP exhausts or EXIT abandons it.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_runtime_new_environment(obj));
|
||||||
|
callenv = obj->environment;
|
||||||
|
callenv->isGenerator = true;
|
||||||
|
callenv->generatorFn = (void *)fndef;
|
||||||
|
callenv->nextline = fndef->lineno;
|
||||||
|
loopenv->forGeneratorEnv = callenv;
|
||||||
|
|
||||||
|
paramleaf = (fndef->arglist != NULL ? fndef->arglist->right : NULL);
|
||||||
|
for ( i = 0; i < nargs && paramleaf != NULL; i++ ) {
|
||||||
|
PASS(errctx, akbasic_environment_assign(callenv, paramleaf, argvals[i], &unused));
|
||||||
|
paramleaf = paramleaf->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_cmd_gen(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
(void)expr; (void)lval; (void)rval;
|
||||||
|
/* The parse handler already installed the generator, exactly as DEF's does. */
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_cmd_emit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *genenv = NULL;
|
||||||
|
akbasic_Environment *loopenv = NULL;
|
||||||
|
akbasic_Value *value = NULL;
|
||||||
|
int64_t zerosubscript[1] = { 0 };
|
||||||
|
|
||||||
|
(void)lval; (void)rval;
|
||||||
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
||||||
|
"Expected EMIT (expression)");
|
||||||
|
/*
|
||||||
|
* EMIT is not necessarily standing directly in the environment
|
||||||
|
* akbasic_runtime_generator_invoke() pushed: a GEN body is ordinary BASIC
|
||||||
|
* and may nest its own FOR, DO or GOSUB around an EMIT, each of which
|
||||||
|
* pushes an environment of its own -- exactly what the issue's own
|
||||||
|
* ROOMOBJECTS example does. Walk up to the nearest one that really is a
|
||||||
|
* GEN's own call frame.
|
||||||
|
*/
|
||||||
|
for ( genenv = obj->environment; genenv != NULL && !genenv->isGenerator; genenv = genenv->parent ) {
|
||||||
|
}
|
||||||
|
FAIL_ZERO_RETURN(errctx, (genenv != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"EMIT outside the context of a GEN body");
|
||||||
|
loopenv = genenv->parent;
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
|
"EMIT from an orphaned environment");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &value));
|
||||||
|
/*
|
||||||
|
* Straight into the loop variable's storage, bypassing the arithmetic
|
||||||
|
* akbasic_environment_assign() and evaluate_for_condition() carry for a
|
||||||
|
* plain FOR: an EACH variable takes whatever type the GEN emits, string or
|
||||||
|
* structure element included, and there is no TO/STEP to compare it
|
||||||
|
* against.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_variable_set_subscript(loopenv->forNextVariable, value, zerosubscript, 1));
|
||||||
|
loopenv->nextline = loopenv->loopFirstLine;
|
||||||
|
/*
|
||||||
|
* The resume point, which may be several levels below `genenv` -- whatever
|
||||||
|
* nested FOR/DO/GOSUB environment this EMIT actually ran in. NEXT/LOOP
|
||||||
|
* reactivates exactly this one, so the nested structure picks up exactly
|
||||||
|
* where it left off rather than restarting at the top of the GEN body.
|
||||||
|
*/
|
||||||
|
loopenv->forGeneratorEnv = obj->environment;
|
||||||
|
/*
|
||||||
|
* Not a pop, and not a single-level detach either: everything between here
|
||||||
|
* and `loopenv` -- `genenv` and any of its own descendants -- has to
|
||||||
|
* survive untouched to be resumed, so control moves to `loopenv` directly
|
||||||
|
* rather than walking the chain one release at a time.
|
||||||
|
*/
|
||||||
|
obj->environment = loopenv;
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_cmd_end_gen(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool waiting = false;
|
||||||
|
|
||||||
|
(void)expr; (void)lval; (void)rval;
|
||||||
|
/*
|
||||||
|
* A END GEN reached while skipping forward to one is the end of a GEN
|
||||||
|
* body's *definition*, not the end of a call -- the same distinction
|
||||||
|
* RETURN draws for DEF.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "END GEN", &waiting));
|
||||||
|
if ( waiting ) {
|
||||||
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "END GEN"));
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj->environment->isGenerator), AKBASIC_ERR_STATE,
|
||||||
|
"END GEN outside the context of a generator invocation");
|
||||||
|
/*
|
||||||
|
* Real exhaustion: release this environment and detach in the same
|
||||||
|
* motion prev_environment() always does, then clear the parent's
|
||||||
|
* reference to it so a caller pumping this loop can tell "still alive"
|
||||||
|
* apart from "nothing left to resume".
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
@@ -80,6 +80,31 @@ akerr_ErrorContext *akbasic_cmd_do(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
|||||||
FAIL_ZERO_RETURN(errctx, obj->environment->isDoLoop, AKBASIC_ERR_STATE,
|
FAIL_ZERO_RETURN(errctx, obj->environment->isDoLoop, AKBASIC_ERR_STATE,
|
||||||
"DO did not establish its own scope");
|
"DO did not establish its own scope");
|
||||||
|
|
||||||
|
if ( obj->environment->isEachLoop ) {
|
||||||
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
akbasic_ASTLeaf *var = (expr != NULL ? expr->right : NULL);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (var != NULL && akbasic_leaf_is_identifier(var)),
|
||||||
|
AKBASIC_ERR_SYNTAX, "Expected DO EACH (variable) IN (generator call)");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->forToLeaf != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"Expected DO EACH (variable) IN (generator call)");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_environment_get(loopenv, var->identifier, &loopenv->forNextVariable));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->forNextVariable != NULL), AKBASIC_ERR_UNDEFINED,
|
||||||
|
"Unable to get loop variable %s", var->identifier);
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_generator_invoke(obj, loopenv, loopenv->forToLeaf));
|
||||||
|
loopenv->forToLeaf = NULL;
|
||||||
|
|
||||||
|
if ( loopenv->forGeneratorEnv == NULL ) {
|
||||||
|
/* The generator produced nothing: skip the body, same as DO WHILE
|
||||||
|
false does. */
|
||||||
|
PASS(errctx, akbasic_environment_wait_for_command(loopenv, "LOOP"));
|
||||||
|
}
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
PASS(errctx, loop_continues(obj, obj->environment->doConditionLeaf,
|
PASS(errctx, loop_continues(obj, obj->environment->doConditionLeaf,
|
||||||
obj->environment->doConditionKind, &enter));
|
obj->environment->doConditionKind, &enter));
|
||||||
if ( !enter ) {
|
if ( !enter ) {
|
||||||
@@ -114,7 +139,37 @@ akerr_ErrorContext *akbasic_cmd_loop(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
if ( obj->environment->exiting ) {
|
if ( obj->environment->exiting ) {
|
||||||
obj->environment->exiting = false;
|
obj->environment->exiting = false;
|
||||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||||
|
/* A live generator abandoned mid-run: release it too. */
|
||||||
|
if ( obj->environment->forGeneratorEnv != NULL ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
again = false;
|
again = false;
|
||||||
|
} else if ( obj->environment->isEachLoop ) {
|
||||||
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||||
|
/*
|
||||||
|
* A condition on the LOOP composes with EACH: it is checked after each
|
||||||
|
* trip through the body, with the loop variable still holding that
|
||||||
|
* trip's value, before the generator is pumped for the next one. A
|
||||||
|
* condition that says stop abandons the generator exactly as EXIT does.
|
||||||
|
*/
|
||||||
|
again = true;
|
||||||
|
arg = (expr != NULL ? expr->right : NULL);
|
||||||
|
if ( arg != NULL ) {
|
||||||
|
kind = (int)arg->literal_int;
|
||||||
|
PASS(errctx, loop_continues(obj, arg->left, kind, &again));
|
||||||
|
}
|
||||||
|
if ( !again && loopenv->forGeneratorEnv != NULL ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, loopenv->forGeneratorEnv));
|
||||||
|
loopenv->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
|
if ( again && loopenv->forGeneratorEnv != NULL ) {
|
||||||
|
obj->environment = loopenv->forGeneratorEnv;
|
||||||
|
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||||
|
}
|
||||||
|
again = (again && loopenv->forGeneratorEnv != NULL);
|
||||||
} else {
|
} else {
|
||||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||||
/*
|
/*
|
||||||
|
|||||||
23
src/verbs.c
23
src/verbs.c
@@ -74,14 +74,32 @@ static const akbasic_Verb VERBS[] = {
|
|||||||
{ "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw },
|
{ "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw },
|
||||||
{ "DSAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave },
|
{ "DSAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave },
|
||||||
{ "DVERIFY", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_arglist, akbasic_cmd_dverify },
|
{ "DVERIFY", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_arglist, akbasic_cmd_dverify },
|
||||||
|
/*
|
||||||
|
* EACH is never dispatched on its own -- akbasic_parse_for() and
|
||||||
|
* akbasic_parse_do() consume it directly, the same way TO, STEP, WHILE and
|
||||||
|
* UNTIL are. It exists here only so the scanner gives it a COMMAND token
|
||||||
|
* rather than letting it scan as a plain identifier.
|
||||||
|
*/
|
||||||
|
{ "EACH", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||||
{ "ELSE", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
{ "ELSE", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||||
{ "END", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_end },
|
{ "EMIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_emit },
|
||||||
|
{ "END", AKBASIC_TOK_COMMAND, -1, akbasic_parse_end, akbasic_cmd_end },
|
||||||
|
/*
|
||||||
|
* `END GEN` is never scanned as one token -- END and GEN are ordinary
|
||||||
|
* COMMAND tokens on the same line -- so this row is reached only from
|
||||||
|
* akbasic_parse_end(), which builds a leaf carrying this exact name after
|
||||||
|
* it sees GEN follow END. It still has to be here, and in order, because
|
||||||
|
* dispatch is a bsearch on the leaf's name; the same reason INPUT# and
|
||||||
|
* PRINT# are.
|
||||||
|
*/
|
||||||
|
{ "END GEN", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_end_gen },
|
||||||
{ "ENVELOPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_envelope },
|
{ "ENVELOPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_envelope },
|
||||||
{ "ERR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_err },
|
{ "ERR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_err },
|
||||||
{ "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit },
|
{ "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit },
|
||||||
{ "FETCH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_fetch },
|
{ "FETCH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_fetch },
|
||||||
{ "FILTER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_filter },
|
{ "FILTER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_filter },
|
||||||
{ "FOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_for, akbasic_cmd_for },
|
{ "FOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_for, akbasic_cmd_for },
|
||||||
|
{ "GEN", AKBASIC_TOK_COMMAND, -1, akbasic_parse_gen, akbasic_cmd_gen },
|
||||||
{ "GET", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_get },
|
{ "GET", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_get },
|
||||||
{ "GETKEY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_getkey },
|
{ "GETKEY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_getkey },
|
||||||
{ "GETMENU", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_getmenu },
|
{ "GETMENU", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_getmenu },
|
||||||
@@ -94,6 +112,9 @@ static const akbasic_Verb VERBS[] = {
|
|||||||
{ "HEX", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_hex },
|
{ "HEX", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_hex },
|
||||||
{ "HUD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_hud },
|
{ "HUD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_hud },
|
||||||
{ "IF", AKBASIC_TOK_COMMAND, -1, akbasic_parse_if, akbasic_cmd_if },
|
{ "IF", AKBASIC_TOK_COMMAND, -1, akbasic_parse_if, akbasic_cmd_if },
|
||||||
|
/* IN is consumed directly by akbasic_parse_for()/akbasic_parse_do()'s EACH
|
||||||
|
clause, the same way EACH itself is. */
|
||||||
|
{ "IN", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||||
{ "INPUT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_input, akbasic_cmd_input },
|
{ "INPUT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_input, akbasic_cmd_input },
|
||||||
/*
|
/*
|
||||||
* `INPUT#` and `PRINT#` are never scanned as verb names -- the scanner reads
|
* `INPUT#` and `PRINT#` are never scanned as verb names -- the scanner reads
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_data(struct akbasic_Parser *par
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_graphic(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_graphic(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_draw(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_draw(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_def(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_def(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_gen(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_end(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_dim(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_dim(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_for(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_for(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_if(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_if(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
@@ -111,6 +113,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_swap(struct akbasic_Runtime *obj,
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_troff(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_troff(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_tron(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_tron(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
|
||||||
|
/* Group L generator verbs -- src/runtime_generator.c */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_gen(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_emit(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_end_gen(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
|
||||||
/* Verb handlers -- src/runtime_commands.c */
|
/* Verb handlers -- src/runtime_commands.c */
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_auto(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_auto(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_data(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_data(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
|||||||
360
tests/generators.c
Normal file
360
tests/generators.c
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* @file generators.c
|
||||||
|
* @brief Generators: GEN/EMIT/END GEN and the FOR EACH/DO EACH loops over them.
|
||||||
|
*
|
||||||
|
* The golden corpus (tests/language/flowcontrol/generators_*.bas) covers the
|
||||||
|
* ordinary shapes: the issue's own ROOMOBJECTS example in both loop forms, an
|
||||||
|
* empty generator, a non-numeric EMIT and nested/interleaved invocations. This
|
||||||
|
* file covers what a byte-compared program cannot: that abandoning a
|
||||||
|
* generator with EXIT gives its environment back to the pool rather than
|
||||||
|
* leaking it, and that misusing a GEN fails cleanly rather than corrupting the
|
||||||
|
* environment stack.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
|
||||||
|
#include "harness.h"
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
|
/** @brief Run a program to completion under an explicit step budget. */
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program_bounded(const char *source, int64_t steps)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, harness_start(NULL));
|
||||||
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
||||||
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||||
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, steps));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Run a program to completion, bounded so a hang fails rather than waits. */
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, run_program_bounded(source, 20000));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief The issue's own example, as a sanity check independent of the golden corpus. */
|
||||||
|
static void test_room_objects_smoke(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 DIM OBJ#(3)\n"
|
||||||
|
"20 OBJ#(0) = 100\n"
|
||||||
|
"30 OBJ#(1) = 200\n"
|
||||||
|
"40 OBJ#(2) = 300\n"
|
||||||
|
"50 GEN ROOMOBJECTS(R#)\n"
|
||||||
|
"60 FOR I# = 0 TO 2\n"
|
||||||
|
"70 IF I# <> 1 THEN EMIT OBJ#(I#)\n"
|
||||||
|
"80 NEXT I#\n"
|
||||||
|
"90 END GEN\n"
|
||||||
|
"100 FOR EACH O# IN ROOMOBJECTS(0)\n"
|
||||||
|
"110 PRINT O#\n"
|
||||||
|
"120 NEXT O#\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "100\n300\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief EXIT out of a FOR EACH loop releases its generator, not just the loop.
|
||||||
|
*
|
||||||
|
* Run more FOR EACH/EXIT constructs than AKBASIC_MAX_ENVIRONMENTS -- each one
|
||||||
|
* takes two environments (the loop's own and the generator's) -- in a single
|
||||||
|
* program. If EXIT abandoned the generator environment instead of releasing
|
||||||
|
* it, this exhausts the pool partway through and the run reports "Environment
|
||||||
|
* pool exhausted" instead of finishing.
|
||||||
|
*/
|
||||||
|
static void test_exit_releases_generator_for_each(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 END GEN\n"
|
||||||
|
"40 FOR K# = 1 TO 13\n"
|
||||||
|
"50 FOR EACH V# IN ONE(0)\n"
|
||||||
|
"60 EXIT\n"
|
||||||
|
"70 NEXT V#\n"
|
||||||
|
"80 NEXT K#\n"
|
||||||
|
"90 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief The same leak check for DO EACH/EXIT. */
|
||||||
|
static void test_exit_releases_generator_do_each(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 END GEN\n"
|
||||||
|
"40 FOR K# = 1 TO 13\n"
|
||||||
|
"50 DO EACH V# IN ONE(0)\n"
|
||||||
|
"60 EXIT\n"
|
||||||
|
"70 LOOP\n"
|
||||||
|
"80 NEXT K#\n"
|
||||||
|
"90 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief EXIT partway through, with more of the generator left to run, still
|
||||||
|
* frees the environment for the next construct that needs one.
|
||||||
|
*/
|
||||||
|
static void test_exit_partway_through(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||||
|
"20 FOR I# = 1 TO N#\n"
|
||||||
|
"30 EMIT I#\n"
|
||||||
|
"40 NEXT I#\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 FOR EACH V# IN COUNTUP(10)\n"
|
||||||
|
"70 PRINT V#\n"
|
||||||
|
"80 IF V# = 2 THEN EXIT\n"
|
||||||
|
"90 NEXT V#\n"
|
||||||
|
"100 PRINT \"AFTER\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\nAFTER\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Abandoning a generator that is itself suspended inside a FOR EACH
|
||||||
|
* over another generator releases the inner generator too.
|
||||||
|
*
|
||||||
|
* The inner generator's environment hangs off the *loop* scope inside OUTER's
|
||||||
|
* body as a child, off the parent chain -- the one place a bare parent walk
|
||||||
|
* never looks. Before akbasic_runtime_release_generator() recursed into
|
||||||
|
* `forGeneratorEnv`, every trip through this loop stranded one pool slot and
|
||||||
|
* the 13th trip died with "Environment pool exhausted".
|
||||||
|
*/
|
||||||
|
static void test_exit_releases_nested_generators(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN INNER(N#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 EMIT 2\n"
|
||||||
|
"40 END GEN\n"
|
||||||
|
"50 GEN OUTER(N#)\n"
|
||||||
|
"60 FOR EACH I# IN INNER(0)\n"
|
||||||
|
"70 EMIT I#\n"
|
||||||
|
"80 NEXT I#\n"
|
||||||
|
"90 END GEN\n"
|
||||||
|
"100 FOR K# = 1 TO 40\n"
|
||||||
|
"110 FOR EACH V# IN OUTER(0)\n"
|
||||||
|
"120 EXIT\n"
|
||||||
|
"130 NEXT V#\n"
|
||||||
|
"140 NEXT K#\n"
|
||||||
|
"150 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A LOOP UNTIL that stops a DO EACH early releases the generator it
|
||||||
|
* abandons, every time.
|
||||||
|
*/
|
||||||
|
static void test_loop_condition_releases_generator(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||||
|
"20 FOR I# = 1 TO N#\n"
|
||||||
|
"30 EMIT I#\n"
|
||||||
|
"40 NEXT I#\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 FOR K# = 1 TO 40\n"
|
||||||
|
"70 DO EACH V# IN COUNTUP(10)\n"
|
||||||
|
"80 LOOP UNTIL V# = 2\n"
|
||||||
|
"90 NEXT K#\n"
|
||||||
|
"100 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RETURN standing in a GEN's own frame ends the generator early,
|
||||||
|
* exactly as END GEN would -- a GEN is a function at heart.
|
||||||
|
*/
|
||||||
|
static void test_return_ends_generator(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN G(N#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 RETURN\n"
|
||||||
|
"40 EMIT 2\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 FOR EACH V# IN G(0)\n"
|
||||||
|
"70 PRINT V#\n"
|
||||||
|
"80 NEXT V#\n"
|
||||||
|
"90 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\nDONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RETURN with a value inside a GEN is refused: values leave a GEN one
|
||||||
|
* at a time, through EMIT, and there is no return slot waiting.
|
||||||
|
*/
|
||||||
|
static void test_return_value_in_generator_refused(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program_bounded("10 GEN G(N#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 RETURN 99\n"
|
||||||
|
"40 END GEN\n"
|
||||||
|
"50 FOR EACH V# IN G(0)\n"
|
||||||
|
"60 PRINT V#\n"
|
||||||
|
"70 NEXT V#\n"
|
||||||
|
"80 PRINT \"UNREACHABLE\"\n", 2000));
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "1\n") != NULL,
|
||||||
|
"expected the first EMIT in \"%s\"", HARNESS_OUTPUT);
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
||||||
|
"RETURN with a value must stop the run, got \"%s\"", HARNESS_OUTPUT);
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The unwind primitive releases a popped scope's suspended generator.
|
||||||
|
*
|
||||||
|
* Built by hand rather than through BASIC because the paths that need this --
|
||||||
|
* the error unwinds in pump_generator() and call_function() -- only trigger
|
||||||
|
* on C-level failures a program cannot politely ask for. The shape is the
|
||||||
|
* one EMIT leaves behind: a loop scope holding a detached generator child,
|
||||||
|
* with a further scope active above it.
|
||||||
|
*/
|
||||||
|
static void test_unwind_releases_suspended_generators(void)
|
||||||
|
{
|
||||||
|
akbasic_Environment *root = NULL;
|
||||||
|
akbasic_Environment *loopenv = NULL;
|
||||||
|
akbasic_Environment *genenv = NULL;
|
||||||
|
akbasic_Environment *forenv = NULL;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
root = HARNESS_RUNTIME.environment;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||||
|
loopenv = HARNESS_RUNTIME.environment;
|
||||||
|
loopenv->isEachLoop = true;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||||
|
genenv = HARNESS_RUNTIME.environment;
|
||||||
|
genenv->isGenerator = true;
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_detach_environment(&HARNESS_RUNTIME));
|
||||||
|
loopenv->forGeneratorEnv = genenv;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||||
|
forenv = HARNESS_RUNTIME.environment;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_unwind_to_environment(&HARNESS_RUNTIME, root));
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.environment == root,
|
||||||
|
"unwind must land on the target scope");
|
||||||
|
TEST_REQUIRE(!forenv->used && !loopenv->used && !genenv->used,
|
||||||
|
"unwind must release the chain and the suspended generator");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A GEN invoked like an ordinary function, rather than through FOR
|
||||||
|
* EACH/DO EACH, fails cleanly.
|
||||||
|
*
|
||||||
|
* EMIT requires `isGenerator`, which only a FOR EACH/DO EACH invocation sets
|
||||||
|
* -- an ordinary call pushes a plain environment, exactly as a DEF's does --
|
||||||
|
* so the first EMIT the call reaches is where this is refused.
|
||||||
|
*/
|
||||||
|
static void test_called_like_a_function(void)
|
||||||
|
{
|
||||||
|
akbasic_Value *args[1];
|
||||||
|
akbasic_Value argvalue;
|
||||||
|
akbasic_Value *out = NULL;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||||||
|
"10 GEN ONE(N#)\n"
|
||||||
|
"20 EMIT N#\n"
|
||||||
|
"30 END GEN\n"));
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 100));
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_value_zero(&argvalue));
|
||||||
|
argvalue.valuetype = AKBASIC_TYPE_INTEGER;
|
||||||
|
argvalue.intval = 5;
|
||||||
|
args[0] = &argvalue;
|
||||||
|
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_call_function(&HARNESS_RUNTIME, "ONE", args, 1, &out));
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief EMIT reached with no enclosing GEN invocation is refused. */
|
||||||
|
static void test_emit_outside_gen(void)
|
||||||
|
{
|
||||||
|
akbasic_ASTLeaf *leaf = NULL;
|
||||||
|
akbasic_Value *out = NULL;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||||||
|
TEST_REQUIRE_OK(harness_parse("EMIT 5", &leaf));
|
||||||
|
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A GEN invoking itself, directly, is refused rather than recursing.
|
||||||
|
*
|
||||||
|
* Bounded tightly: a program that recursed forever would hang the whole
|
||||||
|
* suite, and this is exactly the case that must not.
|
||||||
|
*/
|
||||||
|
static void test_self_recursion_refused(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program_bounded("10 GEN RECURSIVE(N#)\n"
|
||||||
|
"20 FOR EACH X# IN RECURSIVE(N# + 1)\n"
|
||||||
|
"30 EMIT X#\n"
|
||||||
|
"40 NEXT X#\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 PRINT \"BEFORE\"\n"
|
||||||
|
"70 FOR EACH R# IN RECURSIVE(1)\n"
|
||||||
|
"80 PRINT R#\n"
|
||||||
|
"90 NEXT R#\n"
|
||||||
|
"100 PRINT \"UNREACHABLE\"\n", 2000));
|
||||||
|
/* Whatever else happened, the line before the recursive call ran and the
|
||||||
|
one two lines after invoking it -- which would only print after the
|
||||||
|
loop completed successfully -- did not. */
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "BEFORE\n") != NULL,
|
||||||
|
"expected \"BEFORE\" in \"%s\"", HARNESS_OUTPUT);
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
||||||
|
"self-recursion must not reach \"UNREACHABLE\", got \"%s\"", HARNESS_OUTPUT);
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Independent (sibling) FOR EACH invocations of the same GEN are not
|
||||||
|
* self-recursion, even nested.
|
||||||
|
*/
|
||||||
|
static void test_sibling_invocations_allowed(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||||
|
"20 FOR I# = 1 TO N#\n"
|
||||||
|
"30 EMIT I#\n"
|
||||||
|
"40 NEXT I#\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 FOR EACH A# IN COUNTUP(2)\n"
|
||||||
|
"70 FOR EACH B# IN COUNTUP(2)\n"
|
||||||
|
"80 PRINT A# * 10 + B#\n"
|
||||||
|
"90 NEXT B#\n"
|
||||||
|
"100 NEXT A#\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "11\n12\n21\n22\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_room_objects_smoke();
|
||||||
|
test_exit_releases_generator_for_each();
|
||||||
|
test_exit_releases_generator_do_each();
|
||||||
|
test_exit_partway_through();
|
||||||
|
test_exit_releases_nested_generators();
|
||||||
|
test_loop_condition_releases_generator();
|
||||||
|
test_return_ends_generator();
|
||||||
|
test_return_value_in_generator_refused();
|
||||||
|
test_unwind_releases_suspended_generators();
|
||||||
|
test_called_like_a_function();
|
||||||
|
test_emit_outside_gen();
|
||||||
|
test_self_recursion_refused();
|
||||||
|
test_sibling_invocations_allowed();
|
||||||
|
return akbasic_test_failures;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
10 REM A GEN invoked like an ordinary function is refused, not silently run.
|
||||||
|
20 GEN ONE(N#)
|
||||||
|
30 EMIT N#
|
||||||
|
40 END GEN
|
||||||
|
50 X# = ONE(5)
|
||||||
|
60 PRINT "UNREACHABLE"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
? 50 : RUNTIME ERROR ONE is a GEN; call it with FOR EACH or DO EACH, not as a function
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
10 REM A DO EACH takes its condition on the LOOP, not on the DO line.
|
||||||
|
20 GEN ONE(N#)
|
||||||
|
30 EMIT N#
|
||||||
|
40 END GEN
|
||||||
|
50 DO EACH V# IN ONE(1) WHILE V# < 9
|
||||||
|
60 PRINT V#
|
||||||
|
70 LOOP
|
||||||
|
80 PRINT "UNREACHABLE"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
? 50 : PARSE ERROR DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here
|
||||||
|
|
||||||
14
tests/language/flowcontrol/generators_doeach.bas
Normal file
14
tests/language/flowcontrol/generators_doeach.bas
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
10 REM Same generator as generators_foreach.bas, via DO EACH ... LOOP.
|
||||||
|
20 DIM OBJ#(3)
|
||||||
|
30 OBJ#(0) = 100
|
||||||
|
40 OBJ#(1) = 200
|
||||||
|
50 OBJ#(2) = 300
|
||||||
|
60 GEN ROOMOBJECTS(R#)
|
||||||
|
70 FOR I# = 0 TO 2
|
||||||
|
80 IF I# <> 1 THEN EMIT OBJ#(I#)
|
||||||
|
90 NEXT I#
|
||||||
|
100 END GEN
|
||||||
|
110 DO EACH O# IN ROOMOBJECTS(0)
|
||||||
|
120 PRINT O#
|
||||||
|
130 LOOP
|
||||||
|
140 PRINT "DONE"
|
||||||
3
tests/language/flowcontrol/generators_doeach.txt
Normal file
3
tests/language/flowcontrol/generators_doeach.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
100
|
||||||
|
300
|
||||||
|
DONE
|
||||||
3
tests/language/flowcontrol/generators_emit_outside.bas
Normal file
3
tests/language/flowcontrol/generators_emit_outside.bas
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
10 REM EMIT outside any GEN invocation is refused cleanly.
|
||||||
|
20 EMIT 5
|
||||||
|
30 PRINT "UNREACHABLE"
|
||||||
2
tests/language/flowcontrol/generators_emit_outside.txt
Normal file
2
tests/language/flowcontrol/generators_emit_outside.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
? 20 : RUNTIME ERROR EMIT outside the context of a GEN body
|
||||||
|
|
||||||
11
tests/language/flowcontrol/generators_empty.bas
Normal file
11
tests/language/flowcontrol/generators_empty.bas
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
10 REM A GEN with no EMIT at all runs its FOR EACH/DO EACH body zero times.
|
||||||
|
20 GEN NOTHING(N#)
|
||||||
|
30 END GEN
|
||||||
|
40 FOR EACH X# IN NOTHING(0)
|
||||||
|
50 PRINT "NEVER FOR"
|
||||||
|
60 NEXT X#
|
||||||
|
70 PRINT "AFTER FOR"
|
||||||
|
80 DO EACH Y# IN NOTHING(0)
|
||||||
|
90 PRINT "NEVER DO"
|
||||||
|
100 LOOP
|
||||||
|
110 PRINT "AFTER DO"
|
||||||
2
tests/language/flowcontrol/generators_empty.txt
Normal file
2
tests/language/flowcontrol/generators_empty.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
AFTER FOR
|
||||||
|
AFTER DO
|
||||||
15
tests/language/flowcontrol/generators_foreach.bas
Normal file
15
tests/language/flowcontrol/generators_foreach.bas
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
10 REM The GEN/FOR EACH example from issue #57, with real arrays standing in
|
||||||
|
20 REM for OBJCOUNT%/VISIBLE/OBJ%.
|
||||||
|
30 DIM OBJ#(3)
|
||||||
|
40 OBJ#(0) = 100
|
||||||
|
50 OBJ#(1) = 200
|
||||||
|
60 OBJ#(2) = 300
|
||||||
|
70 GEN ROOMOBJECTS(R#)
|
||||||
|
80 FOR I# = 0 TO 2
|
||||||
|
90 IF I# <> 1 THEN EMIT OBJ#(I#)
|
||||||
|
100 NEXT I#
|
||||||
|
110 END GEN
|
||||||
|
120 FOR EACH O# IN ROOMOBJECTS(0)
|
||||||
|
130 PRINT O#
|
||||||
|
140 NEXT O#
|
||||||
|
150 PRINT "DONE"
|
||||||
3
tests/language/flowcontrol/generators_foreach.txt
Normal file
3
tests/language/flowcontrol/generators_foreach.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
100
|
||||||
|
300
|
||||||
|
DONE
|
||||||
15
tests/language/flowcontrol/generators_loop_condition.bas
Normal file
15
tests/language/flowcontrol/generators_loop_condition.bas
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
10 REM A WHILE or UNTIL on the LOOP composes with DO EACH: it is checked
|
||||||
|
20 REM after each trip through the body, with the loop variable still
|
||||||
|
30 REM holding that trip's value. Stopping abandons the generator cleanly.
|
||||||
|
40 GEN COUNTUP(N#)
|
||||||
|
50 FOR I# = 1 TO N#
|
||||||
|
60 EMIT I#
|
||||||
|
70 NEXT I#
|
||||||
|
80 END GEN
|
||||||
|
90 DO EACH V# IN COUNTUP(10)
|
||||||
|
100 PRINT V#
|
||||||
|
110 LOOP UNTIL V# = 3
|
||||||
|
120 DO EACH W# IN COUNTUP(4)
|
||||||
|
130 PRINT W# * 10
|
||||||
|
140 LOOP WHILE W# < 3
|
||||||
|
150 PRINT "DONE"
|
||||||
7
tests/language/flowcontrol/generators_loop_condition.txt
Normal file
7
tests/language/flowcontrol/generators_loop_condition.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
10
|
||||||
|
20
|
||||||
|
30
|
||||||
|
DONE
|
||||||
10
tests/language/flowcontrol/generators_mismatched_next.bas
Normal file
10
tests/language/flowcontrol/generators_mismatched_next.bas
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
10 REM A stray NEXT with nothing open behaves like the plain-FOR case: the
|
||||||
|
20 REM FOR EACH's own NEXT closes it cleanly, and a second one errors.
|
||||||
|
30 GEN ONE(N#)
|
||||||
|
40 EMIT N#
|
||||||
|
50 END GEN
|
||||||
|
60 FOR EACH O# IN ONE(1)
|
||||||
|
70 PRINT O#
|
||||||
|
80 NEXT O#
|
||||||
|
90 NEXT O#
|
||||||
|
100 PRINT "UNREACHABLE"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
1
|
||||||
|
? 90 : RUNTIME ERROR NEXT outside the context of FOR
|
||||||
|
|
||||||
22
tests/language/flowcontrol/generators_nested.bas
Normal file
22
tests/language/flowcontrol/generators_nested.bas
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
10 REM Nested FOR EACH/DO EACH over the same GEN with different arguments,
|
||||||
|
20 REM in every combination of the two loop shapes.
|
||||||
|
30 GEN COUNTUP(N#)
|
||||||
|
40 FOR I# = 1 TO N#
|
||||||
|
50 EMIT I#
|
||||||
|
60 NEXT I#
|
||||||
|
70 END GEN
|
||||||
|
80 FOR EACH A# IN COUNTUP(2)
|
||||||
|
90 FOR EACH B# IN COUNTUP(3)
|
||||||
|
100 PRINT A# * 10 + B#
|
||||||
|
110 NEXT B#
|
||||||
|
120 NEXT A#
|
||||||
|
130 DO EACH C# IN COUNTUP(2)
|
||||||
|
140 DO EACH D# IN COUNTUP(2)
|
||||||
|
150 PRINT C# * 100 + D#
|
||||||
|
160 LOOP
|
||||||
|
170 LOOP
|
||||||
|
180 FOR EACH E# IN COUNTUP(2)
|
||||||
|
190 DO EACH F# IN COUNTUP(2)
|
||||||
|
200 PRINT E# * 1000 + F#
|
||||||
|
210 LOOP
|
||||||
|
220 NEXT E#
|
||||||
14
tests/language/flowcontrol/generators_nested.txt
Normal file
14
tests/language/flowcontrol/generators_nested.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
11
|
||||||
|
12
|
||||||
|
13
|
||||||
|
21
|
||||||
|
22
|
||||||
|
23
|
||||||
|
101
|
||||||
|
102
|
||||||
|
201
|
||||||
|
202
|
||||||
|
1001
|
||||||
|
1002
|
||||||
|
2001
|
||||||
|
2002
|
||||||
15
tests/language/flowcontrol/generators_return.bas
Normal file
15
tests/language/flowcontrol/generators_return.bas
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
10 REM RETURN ends a GEN early, exactly as END GEN would: a GEN is a
|
||||||
|
20 REM function at heart, and only EMIT is different about it. Like a
|
||||||
|
30 REM GOSUB's or DEF's RETURN, it must stand in the GEN's own scope,
|
||||||
|
40 REM not inside a FOR or DO the body opened.
|
||||||
|
50 GEN FIRSTFEW(N#)
|
||||||
|
60 EMIT 1
|
||||||
|
70 IF N# < 2 THEN RETURN
|
||||||
|
80 EMIT 2
|
||||||
|
90 IF N# < 3 THEN RETURN
|
||||||
|
100 EMIT 3
|
||||||
|
110 END GEN
|
||||||
|
120 FOR EACH V# IN FIRSTFEW(2)
|
||||||
|
130 PRINT V#
|
||||||
|
140 NEXT V#
|
||||||
|
150 PRINT "DONE"
|
||||||
3
tests/language/flowcontrol/generators_return.txt
Normal file
3
tests/language/flowcontrol/generators_return.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
1
|
||||||
|
2
|
||||||
|
DONE
|
||||||
11
tests/language/flowcontrol/generators_string.bas
Normal file
11
tests/language/flowcontrol/generators_string.bas
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
10 REM FOR EACH/DO EACH accept any emitted type, not just numeric.
|
||||||
|
20 GEN WORDS(N#)
|
||||||
|
30 EMIT "HELLO"
|
||||||
|
40 EMIT "WORLD"
|
||||||
|
50 END GEN
|
||||||
|
60 FOR EACH W$ IN WORDS(0)
|
||||||
|
70 PRINT W$
|
||||||
|
80 NEXT W$
|
||||||
|
90 DO EACH V$ IN WORDS(0)
|
||||||
|
100 PRINT V$
|
||||||
|
110 LOOP
|
||||||
4
tests/language/flowcontrol/generators_string.txt
Normal file
4
tests/language/flowcontrol/generators_string.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
HELLO
|
||||||
|
WORLD
|
||||||
|
HELLO
|
||||||
|
WORLD
|
||||||
@@ -66,15 +66,18 @@ int main(void)
|
|||||||
/*
|
/*
|
||||||
* These rows have no exec handler because another verb's parse path consumes
|
* These rows have no exec handler because another verb's parse path consumes
|
||||||
* them; they are never evaluated on their own. `WHILE` and `UNTIL` belong to
|
* them; they are never evaluated on their own. `WHILE` and `UNTIL` belong to
|
||||||
* DO and LOOP the way `TO` and `STEP` belong to FOR. Any other missing
|
* DO and LOOP the way `TO` and `STEP` belong to FOR, and `EACH`/`IN` belong
|
||||||
* handler is a verb that would report "Unknown command" at runtime.
|
* to FOR EACH and DO EACH the same way. Any other missing handler is a verb
|
||||||
|
* that would report "Unknown command" at runtime.
|
||||||
*/
|
*/
|
||||||
for ( i = 0; i < count; i++ ) {
|
for ( i = 0; i < count; i++ ) {
|
||||||
if ( table[i].exec != NULL ) {
|
if ( table[i].exec != NULL ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
TEST_REQUIRE(strcmp(table[i].name, "AND") == 0 ||
|
TEST_REQUIRE(strcmp(table[i].name, "AND") == 0 ||
|
||||||
|
strcmp(table[i].name, "EACH") == 0 ||
|
||||||
strcmp(table[i].name, "ELSE") == 0 ||
|
strcmp(table[i].name, "ELSE") == 0 ||
|
||||||
|
strcmp(table[i].name, "IN") == 0 ||
|
||||||
strcmp(table[i].name, "NOT") == 0 ||
|
strcmp(table[i].name, "NOT") == 0 ||
|
||||||
strcmp(table[i].name, "OR") == 0 ||
|
strcmp(table[i].name, "OR") == 0 ||
|
||||||
strcmp(table[i].name, "REM") == 0 ||
|
strcmp(table[i].name, "REM") == 0 ||
|
||||||
|
|||||||
Reference in New Issue
Block a user