diff --git a/CMakeLists.txt b/CMakeLists.txt index c65261a..3d95e93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,6 +143,7 @@ set(AKBASIC_SOURCES src/runtime_disk.c src/runtime_format.c src/runtime_functions.c + src/runtime_generator.c src/runtime_graphics.c src/runtime_housekeeping.c src/runtime_machine.c @@ -352,6 +353,7 @@ set(AKBASIC_TESTS error_codes for_next format_verbs + generators grammar_leaves graphics_verbs hoststruct diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 3ccf604..3a7ea64 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -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 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 diff --git a/TODO.md b/TODO.md index fb96df9..018b00e 100644 --- a/TODO.md +++ b/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 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** diff --git a/docs/04-control-flow.md b/docs/04-control-flow.md index 21c4d14..190f428 100644 --- a/docs/04-control-flow.md +++ b/docs/04-control-flow.md @@ -146,6 +146,143 @@ condition at all loops forever until an `EXIT` or a `GOTO` leaves it. `EXIT` works here too. +## GEN ... EMIT and FOR EACH / DO EACH + +A `GEN` is a subroutine that hands back more than one value, one at a time, instead of +returning once. It looks like a multi-line `DEF`, except its body runs `EMIT` where a +function would `RETURN`, and it ends in `END GEN` rather than `RETURN`: + +```basic +10 GEN COUNTUP(N#) +20 FOR I# = 1 TO N# +30 EMIT I# +40 NEXT I# +50 END GEN +60 FOR EACH V# IN COUNTUP(3) +70 PRINT V# +80 NEXT V# +``` + +```output +1 +2 +3 +``` + +`FOR EACH` is what runs a `GEN`: it calls `COUNTUP(3)` the way `FOR EACH V# IN` says, +and each `EMIT` inside the generator's body becomes one trip through the loop, with `V#` +holding whatever was emitted. When the generator's body reaches `END GEN` with nothing +left to emit, the loop ends -- there is no separate "no more values" check to write. + +`DO EACH ... LOOP` does the same thing: + +```basic +10 GEN COUNTUP(N#) +20 FOR I# = 1 TO N# +30 EMIT I# +40 NEXT I# +50 END GEN +60 DO EACH V# IN COUNTUP(3) +70 PRINT V# +80 LOOP +``` + +```output +1 +2 +3 +``` + +A `GEN`'s body is ordinary BASIC: it may hold its own `FOR`, `DO`, `IF` or `GOSUB` +around the `EMIT`s, and even invoke another `GEN` with its own `FOR EACH`/`DO EACH` -- +the same generator invoked with different arguments, nested or side by side, is not +recursion. A `GEN` invoking *itself* from within its own currently-running body is +refused: unlike a function call, which runs to completion and returns, the outer +invocation is suspended mid-body waiting on the same loop, and there is no answer to +"which EMIT feeds which loop" that is not a surprise. + +`EXIT` leaves a `FOR EACH`/`DO EACH` loop early, exactly as it does a plain `FOR` or +`DO`, and the generator it was consuming stops there -- nothing forces the rest of it to +run just because the loop started it: + +```basic +10 GEN COUNTUP(N#) +20 FOR I# = 1 TO N# +30 EMIT I# +40 NEXT I# +50 END GEN +60 FOR EACH V# IN COUNTUP(100) +70 PRINT V# +80 IF V# = 3 THEN EXIT +90 NEXT V# +100 PRINT "STOPPED" +``` + +```output +1 +2 +3 +STOPPED +``` + +`RETURN` ends a `GEN` from the inside, exactly as it ends a multi-line `DEF` or a +`GOSUB`: the generator is done, the loop consuming it ends, and the program carries on +after the loop. What a generator's `RETURN` cannot do is carry a value -- values leave +a `GEN` one at a time, through `EMIT`, and `RETURN 99` inside one is an error. Like a +`GOSUB`'s or a `DEF`'s, the `RETURN` must stand in the `GEN`'s own scope: from inside +a `FOR` or `DO` the body opened, it is an error, though `IF ... THEN RETURN` is fine +because `IF` opens no scope of its own. + +```basic +10 GEN FIRSTFEW(N#) +20 EMIT 1 +30 IF N# < 2 THEN RETURN +40 EMIT 2 +50 IF N# < 3 THEN RETURN +60 EMIT 3 +70 END GEN +80 FOR EACH V# IN FIRSTFEW(2) +90 PRINT V# +100 NEXT V# +110 PRINT "DONE" +``` + +```output +1 +2 +DONE +``` + +A `DO EACH`'s `LOOP` may carry a `WHILE` or `UNTIL`, and the two compose: the condition +is checked after each trip through the body, with the loop variable still holding that +trip's value, and a condition that says stop abandons the rest of the generator exactly +as `EXIT` does. The condition belongs on the `LOOP` -- putting it on the `DO` line is +an error, since the `DO` line already says what the loop consumes. + +```basic +10 GEN COUNTUP(N#) +20 FOR I# = 1 TO N# +30 EMIT I# +40 NEXT I# +50 END GEN +60 DO EACH V# IN COUNTUP(10) +70 PRINT V# +80 LOOP UNTIL V# = 3 +90 PRINT "STOPPED" +``` + +```output +1 +2 +3 +STOPPED +``` + +A `GEN` shares its namespace with `DEF` -- the same name cannot be both -- but it is not +a function and cannot be called like one: `X# = COUNTUP(3)` is refused, because nothing +about an ordinary call means "resume where the last `EMIT` left off." `FOR EACH`/`DO +EACH` is the only thing that consumes a `GEN`. + ## GOTO and GOSUB ```basic diff --git a/docs/11-verb-reference.md b/docs/11-verb-reference.md index 084d91d..ea7db72 100644 --- a/docs/11-verb-reference.md +++ b/docs/11-verb-reference.md @@ -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. | | `DIRECTORY` | `DIRECTORY` | **Refused.** Not written yet; the standard-library wrapper it waited on has landed. | | `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. | | `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. | | `DSAVE` | `DSAVE "name"` | Save the program to a file. | | `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 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. | -| `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. | | `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. | | `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. | @@ -67,11 +70,11 @@ for the reasoning in each case. | `LIST` | `LIST [n][-n]` | List the program, or part of it. | | `LOAD` | `LOAD "name"` | The other name for `DLOAD`. | | `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. | | `MOVSPR` | `MOVSPR n, ...` | Move a sprite. Four forms; see Chapter 8. | | `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. | | `PAINT` | `PAINT src, x, y` | Flood-fill the region containing a point. | | `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. | | `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. | | `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. | | `SAVE` | `SAVE "name"` | The other name for `DSAVE`. | | `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. | diff --git a/docs/14-architecture.md b/docs/14-architecture.md index e1b39a0..e04a129 100644 --- a/docs/14-architecture.md +++ b/docs/14-architecture.md @@ -407,6 +407,8 @@ block structure is executing: the `FOR` bounds and step, the `DO`/`LOOP` conditi | `GOSUB` | `RETURN` | | A call to a multi-line user function | that function'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 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 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 `akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct diff --git a/include/akbasic/environment.h b/include/akbasic/environment.h index debbb2d..d070bcf 100644 --- a/include/akbasic/environment.h +++ b/include/akbasic/environment.h @@ -72,6 +72,37 @@ typedef struct akbasic_Environment */ 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; /* READ state. The identifier leaves are deep copies, so they need storage. */ diff --git a/include/akbasic/runtime.h b/include/akbasic/runtime.h index 81d35c4..27f3562 100644 --- a/include/akbasic/runtime.h +++ b/include/akbasic/runtime.h @@ -102,6 +102,15 @@ typedef struct akbasic_ASTLeaf *arglist; akbasic_ASTLeaf *expression; 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 * 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); +/** + * @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. * diff --git a/src/environment.c b/src/environment.c index 77a184a..84964b2 100644 --- a/src/environment.c +++ b/src/environment.c @@ -35,6 +35,10 @@ akerr_ErrorContext *akbasic_environment_init(akbasic_Environment *obj, akbasic_R obj->doConditionLeaf = NULL; obj->doConditionKind = AKBASIC_LOOPCOND_NONE; obj->isDoLoop = false; + obj->isGenerator = false; + obj->generatorFn = NULL; + obj->isEachLoop = false; + obj->forGeneratorEnv = NULL; obj->gosubReturnLine = 0; obj->readReturnLine = 0; obj->readIdentifierIdx = 0; diff --git a/src/parser_commands.c b/src/parser_commands.c index e823a5d..44ee9ff 100644 --- a/src/parser_commands.c +++ b/src/parser_commands.c @@ -20,6 +20,9 @@ #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) { PREPARE_ERROR(errctx); @@ -309,8 +312,58 @@ akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **d akbasic_Environment *newenv = NULL; akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *condition = NULL; + akbasic_Token *peeked = NULL; int kind = AKBASIC_LOOPCOND_NONE; int64_t firstline = parent->lineno + 1; + int cmp = 0; + + /* + * DO EACH IN ... 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)); newenv = runtime->environment; @@ -883,6 +936,134 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf ** 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 IN + * + * 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 ...] * 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 *expr = NULL; akbasic_Token *operator_ = NULL; + akbasic_Token *peeked = NULL; akbasic_Environment *parent = runtime->environment; akbasic_Environment *newenv = NULL; int64_t firstline = 0; int cmp = 0; + /* + * FOR EACH IN . 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)); FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX, diff --git a/src/runtime.c b/src/runtime.c index 55550bc..78e2645 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -143,17 +143,24 @@ akerr_ErrorContext *akbasic_runtime_new_environment(akbasic_Runtime *obj) SUCCEED_RETURN(errctx); } -akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj) +akerr_ErrorContext *akbasic_runtime_detach_environment(akbasic_Runtime *obj) { 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, "No previous environment to return to"); - popped = obj->environment; - obj->environment = popped->parent; + obj->environment = obj->environment->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. @@ -173,9 +180,9 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj) * times exhausted the 128-slot pool and reported "Maximum runtime variables * reached" on a four-line program. */ - for ( i = 0; i < popped->variables.capacity; i++ ) { - akbasic_Variable *variable = (akbasic_Variable *)popped->variables.slots[i].value; - if ( popped->variables.slots[i].used && variable != NULL ) { + for ( i = 0; i < env->variables.capacity; i++ ) { + akbasic_Variable *variable = (akbasic_Variable *)env->variables.slots[i].value; + if ( env->variables.slots[i].used && variable != NULL ) { 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 * 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); } @@ -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)); 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.** @@ -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 * twelve-slot environment pool after twelve dead calls and every * 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_prev_environment(obj)); - } + IGNORE(akbasic_runtime_unwind_to_environment(obj, targetenv)); } PROCESS(errctx) { } FINISH(errctx, true); PASS(errctx, akbasic_environment_new_value(targetenv, &out)); diff --git a/src/runtime_commands.c b/src/runtime_commands.c index f9fb06d..fa3f892 100644 --- a/src/runtime_commands.c +++ b/src/runtime_commands.c @@ -161,8 +161,22 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex SUCCEED_TRUE(obj, dest); 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, - "RETURN outside the context of GOSUB"); + "RETURN outside the context of GOSUB, DEF, or GEN"); if ( expr != NULL && expr->right != NULL ) { 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; (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), AKBASIC_ERR_STATE, "Expected FOR ... TO [STEP ...]"); 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"); FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX, "Expected NEXT IDENTIFIER"); - 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"); + if ( obj->environment->isEachLoop ) { + /* EACH accepts any emitted type; the numeric-only check is for plain FOR. */ + FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(expr->right), AKBASIC_ERR_SYNTAX, + "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; @@ -909,6 +954,14 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT")); FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_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; PASS(errctx, akbasic_runtime_prev_environment(obj)); *dest = &obj->staticFalseValue; @@ -924,12 +977,37 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr if ( cmp != 0 ) { FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_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; PASS(errctx, akbasic_runtime_prev_environment(obj)); *dest = &obj->staticFalseValue; 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)); FAIL_ZERO_RETURN(errctx, (nextvar != NULL), AKBASIC_ERR_UNDEFINED, "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, (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"); FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT, "EXIT in an orphaned environment"); diff --git a/src/runtime_generator.c b/src/runtime_generator.c new file mode 100644 index 0000000..da49f5b --- /dev/null +++ b/src/runtime_generator.c @@ -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 +#include + +#include +#include +#include + +#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); +} diff --git a/src/runtime_structure.c b/src/runtime_structure.c index 3f72484..2d2a7f2 100644 --- a/src/runtime_structure.c +++ b/src/runtime_structure.c @@ -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, "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, obj->environment->doConditionKind, &enter)); if ( !enter ) { @@ -114,7 +139,37 @@ akerr_ErrorContext *akbasic_cmd_loop(akbasic_Runtime *obj, akbasic_ASTLeaf *expr if ( obj->environment->exiting ) { obj->environment->exiting = false; 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; + } 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 { PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP")); /* diff --git a/src/verbs.c b/src/verbs.c index 37ebcae..457ee23 100644 --- a/src/verbs.c +++ b/src/verbs.c @@ -74,14 +74,32 @@ static const akbasic_Verb VERBS[] = { { "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw }, { "DSAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave }, { "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 }, - { "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 }, { "ERR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_err }, { "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit }, { "FETCH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_fetch }, { "FILTER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_filter }, { "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 }, { "GETKEY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_getkey }, { "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 }, { "HUD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_hud }, { "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#` and `PRINT#` are never scanned as verb names -- the scanner reads diff --git a/src/verbs.h b/src/verbs.h index 8ad61fe..d7987e5 100644 --- a/src/verbs.h +++ b/src/verbs.h @@ -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_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_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_for(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_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 */ 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); diff --git a/tests/generators.c b/tests/generators.c new file mode 100644 index 0000000..fc3db06 --- /dev/null +++ b/tests/generators.c @@ -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 + +#include +#include + +#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; +} diff --git a/tests/language/flowcontrol/generators_call_as_function.bas b/tests/language/flowcontrol/generators_call_as_function.bas new file mode 100644 index 0000000..8cb6072 --- /dev/null +++ b/tests/language/flowcontrol/generators_call_as_function.bas @@ -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" diff --git a/tests/language/flowcontrol/generators_call_as_function.txt b/tests/language/flowcontrol/generators_call_as_function.txt new file mode 100644 index 0000000..a1e60eb --- /dev/null +++ b/tests/language/flowcontrol/generators_call_as_function.txt @@ -0,0 +1,2 @@ +? 50 : RUNTIME ERROR ONE is a GEN; call it with FOR EACH or DO EACH, not as a function + diff --git a/tests/language/flowcontrol/generators_do_condition_misplaced.bas b/tests/language/flowcontrol/generators_do_condition_misplaced.bas new file mode 100644 index 0000000..5d338c3 --- /dev/null +++ b/tests/language/flowcontrol/generators_do_condition_misplaced.bas @@ -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" diff --git a/tests/language/flowcontrol/generators_do_condition_misplaced.txt b/tests/language/flowcontrol/generators_do_condition_misplaced.txt new file mode 100644 index 0000000..6f0d6ee --- /dev/null +++ b/tests/language/flowcontrol/generators_do_condition_misplaced.txt @@ -0,0 +1,2 @@ +? 50 : PARSE ERROR DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here + diff --git a/tests/language/flowcontrol/generators_doeach.bas b/tests/language/flowcontrol/generators_doeach.bas new file mode 100644 index 0000000..7aa3567 --- /dev/null +++ b/tests/language/flowcontrol/generators_doeach.bas @@ -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" diff --git a/tests/language/flowcontrol/generators_doeach.txt b/tests/language/flowcontrol/generators_doeach.txt new file mode 100644 index 0000000..feb9f41 --- /dev/null +++ b/tests/language/flowcontrol/generators_doeach.txt @@ -0,0 +1,3 @@ +100 +300 +DONE diff --git a/tests/language/flowcontrol/generators_emit_outside.bas b/tests/language/flowcontrol/generators_emit_outside.bas new file mode 100644 index 0000000..ca89012 --- /dev/null +++ b/tests/language/flowcontrol/generators_emit_outside.bas @@ -0,0 +1,3 @@ +10 REM EMIT outside any GEN invocation is refused cleanly. +20 EMIT 5 +30 PRINT "UNREACHABLE" diff --git a/tests/language/flowcontrol/generators_emit_outside.txt b/tests/language/flowcontrol/generators_emit_outside.txt new file mode 100644 index 0000000..8e2933a --- /dev/null +++ b/tests/language/flowcontrol/generators_emit_outside.txt @@ -0,0 +1,2 @@ +? 20 : RUNTIME ERROR EMIT outside the context of a GEN body + diff --git a/tests/language/flowcontrol/generators_empty.bas b/tests/language/flowcontrol/generators_empty.bas new file mode 100644 index 0000000..f8e4787 --- /dev/null +++ b/tests/language/flowcontrol/generators_empty.bas @@ -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" diff --git a/tests/language/flowcontrol/generators_empty.txt b/tests/language/flowcontrol/generators_empty.txt new file mode 100644 index 0000000..f39da24 --- /dev/null +++ b/tests/language/flowcontrol/generators_empty.txt @@ -0,0 +1,2 @@ +AFTER FOR +AFTER DO diff --git a/tests/language/flowcontrol/generators_foreach.bas b/tests/language/flowcontrol/generators_foreach.bas new file mode 100644 index 0000000..e0900b1 --- /dev/null +++ b/tests/language/flowcontrol/generators_foreach.bas @@ -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" diff --git a/tests/language/flowcontrol/generators_foreach.txt b/tests/language/flowcontrol/generators_foreach.txt new file mode 100644 index 0000000..feb9f41 --- /dev/null +++ b/tests/language/flowcontrol/generators_foreach.txt @@ -0,0 +1,3 @@ +100 +300 +DONE diff --git a/tests/language/flowcontrol/generators_loop_condition.bas b/tests/language/flowcontrol/generators_loop_condition.bas new file mode 100644 index 0000000..9f21e95 --- /dev/null +++ b/tests/language/flowcontrol/generators_loop_condition.bas @@ -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" diff --git a/tests/language/flowcontrol/generators_loop_condition.txt b/tests/language/flowcontrol/generators_loop_condition.txt new file mode 100644 index 0000000..7dddada --- /dev/null +++ b/tests/language/flowcontrol/generators_loop_condition.txt @@ -0,0 +1,7 @@ +1 +2 +3 +10 +20 +30 +DONE diff --git a/tests/language/flowcontrol/generators_mismatched_next.bas b/tests/language/flowcontrol/generators_mismatched_next.bas new file mode 100644 index 0000000..270c1e3 --- /dev/null +++ b/tests/language/flowcontrol/generators_mismatched_next.bas @@ -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" diff --git a/tests/language/flowcontrol/generators_mismatched_next.txt b/tests/language/flowcontrol/generators_mismatched_next.txt new file mode 100644 index 0000000..26a04ee --- /dev/null +++ b/tests/language/flowcontrol/generators_mismatched_next.txt @@ -0,0 +1,3 @@ +1 +? 90 : RUNTIME ERROR NEXT outside the context of FOR + diff --git a/tests/language/flowcontrol/generators_nested.bas b/tests/language/flowcontrol/generators_nested.bas new file mode 100644 index 0000000..d1f86c6 --- /dev/null +++ b/tests/language/flowcontrol/generators_nested.bas @@ -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# diff --git a/tests/language/flowcontrol/generators_nested.txt b/tests/language/flowcontrol/generators_nested.txt new file mode 100644 index 0000000..db1e767 --- /dev/null +++ b/tests/language/flowcontrol/generators_nested.txt @@ -0,0 +1,14 @@ +11 +12 +13 +21 +22 +23 +101 +102 +201 +202 +1001 +1002 +2001 +2002 diff --git a/tests/language/flowcontrol/generators_return.bas b/tests/language/flowcontrol/generators_return.bas new file mode 100644 index 0000000..cf65a2d --- /dev/null +++ b/tests/language/flowcontrol/generators_return.bas @@ -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" diff --git a/tests/language/flowcontrol/generators_return.txt b/tests/language/flowcontrol/generators_return.txt new file mode 100644 index 0000000..9b29285 --- /dev/null +++ b/tests/language/flowcontrol/generators_return.txt @@ -0,0 +1,3 @@ +1 +2 +DONE diff --git a/tests/language/flowcontrol/generators_string.bas b/tests/language/flowcontrol/generators_string.bas new file mode 100644 index 0000000..6608ac4 --- /dev/null +++ b/tests/language/flowcontrol/generators_string.bas @@ -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 diff --git a/tests/language/flowcontrol/generators_string.txt b/tests/language/flowcontrol/generators_string.txt new file mode 100644 index 0000000..4e74312 --- /dev/null +++ b/tests/language/flowcontrol/generators_string.txt @@ -0,0 +1,4 @@ +HELLO +WORLD +HELLO +WORLD diff --git a/tests/verbs_table.c b/tests/verbs_table.c index 5993e2e..75bb671 100644 --- a/tests/verbs_table.c +++ b/tests/verbs_table.c @@ -66,15 +66,18 @@ int main(void) /* * 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 - * DO and LOOP the way `TO` and `STEP` belong to FOR. Any other missing - * handler is a verb that would report "Unknown command" at runtime. + * DO and LOOP the way `TO` and `STEP` belong to FOR, and `EACH`/`IN` belong + * 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++ ) { if ( table[i].exec != NULL ) { continue; } TEST_REQUIRE(strcmp(table[i].name, "AND") == 0 || + strcmp(table[i].name, "EACH") == 0 || strcmp(table[i].name, "ELSE") == 0 || + strcmp(table[i].name, "IN") == 0 || strcmp(table[i].name, "NOT") == 0 || strcmp(table[i].name, "OR") == 0 || strcmp(table[i].name, "REM") == 0 ||