Implement generators: GEN, EMIT, END GEN, FOR EACH and DO EACH (issue 57)
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m32s
akbasic CI Build / coverage (push) Successful in 4m7s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / akgl_build (push) Successful in 8m12s
akbasic CI Build / mutation_test (push) Successful in 23m3s
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m32s
akbasic CI Build / coverage (push) Successful in 4m7s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / akgl_build (push) Successful in 8m12s
akbasic CI Build / mutation_test (push) Successful in 23m3s
Adds generator support per the plan in issue 57: - environment.h: isGenerator/generatorFn on a GEN call's own environment, isEachLoop and forGeneratorEnv on a FOR EACH/DO EACH loop's own environment. - runtime.c: splits akbasic_runtime_prev_environment() into akbasic_runtime_detach_environment() (return to parent without releasing) and akbasic_runtime_release_environment() (give variables and the pool slot back, on any environment); prev_environment() is now the two in sequence. akbasic_runtime_call_function() refuses to call a GEN like an ordinary function. - verbs.c/verbs.h/scanner: new keywords GEN, EMIT, EACH, IN and the compound verb "END GEN" (built by a new akbasic_parse_end(), the same trick akbasic_parse_print() uses for PRINT #). - parser_commands.c: akbasic_parse_gen() (modelled on multi-line DEF), akbasic_parse_end(), and EACH branches in akbasic_parse_for()/ akbasic_parse_do(). - runtime_generator.c (new): akbasic_cmd_gen, akbasic_cmd_emit, akbasic_cmd_end_gen, and the invoke/pump/release machinery FOR EACH, DO EACH, NEXT and LOOP share. EMIT walks up to the nearest isGenerator ancestor rather than assuming it is standing directly in the GEN's own call frame, because a GEN body may nest its own FOR/DO/GOSUB around an EMIT -- the issue's own ROOMOBJECTS example does exactly that. - runtime_commands.c/runtime_structure.c: EACH branches in cmd_for/cmd_do, matching EACH branches in cmd_next/cmd_loop, and forGeneratorEnv release on every path that can abandon a live generator (EXIT, a NEXT that pops for a mismatched loop variable). Deviates from the plan in one place: FunctionDef gained an isGenerator flag (not in the plan's field list) because refusing a GEN called like a function has to happen before anything is pushed. Relying on EMIT's own isGenerator check for that case doesn't work: akbasic_runtime_call_function() drives its own step loop the same way akbasic_runtime_pump_generator() does, and a BASIC-level error inside that loop is swallowed by process_line_run() as reported-but-not-propagated, so the call would silently "succeed" with a meaningless return value instead of failing. Also: a zero-argument parameter list is not supported by the DEF/GEN parameter parser this reuses (a pre-existing limitation, not generator-specific); every generator in the tests takes at least one parameter as a result. Tests: tests/generators.c (pool exhaustion under repeated EXIT, calling a GEN like a function, EMIT outside a GEN, self-recursion, sibling/nested invocations) and tests/language/flowcontrol/generators_*.bas -- the issue's own ROOMOBJECTS example in both loop shapes, an empty generator, non-numeric EMIT, nested/interleaved invocations, and three error-path golden cases. Docs: control-flow chapter 4 gets a GEN/EMIT/FOR EACH/DO EACH section, the verb reference gets GEN/EMIT/END GEN entries and updated FOR/DO/NEXT/LOOP/EXIT rows, and architecture chapter 14 documents the detach/release split and the two-environment generator invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -146,6 +146,89 @@ 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, the same way it would be an error to call a function that has not returned yet
|
||||
by name from inside its own body without meaning to recurse.
|
||||
|
||||
`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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -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; 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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,93 @@ 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 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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,45 @@ 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 <variable> IN <generator call> ... LOOP. Mutually exclusive with
|
||||
* DO WHILE/UNTIL on the same DO, so this is checked first and returns
|
||||
* before any of the WHILE/UNTIL machinery runs.
|
||||
*/
|
||||
peeked = akbasic_parser_peek(parser);
|
||||
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||
PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
|
||||
if ( cmp == 0 ) {
|
||||
akbasic_ASTLeaf *var = NULL;
|
||||
akbasic_ASTLeaf *callexpr = NULL;
|
||||
|
||||
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||
|
||||
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||
newenv = runtime->environment;
|
||||
runtime->environment = parent;
|
||||
|
||||
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||
|
||||
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 +923,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 <variable> IN <generator call>
|
||||
*
|
||||
* Shared by akbasic_parse_for() and akbasic_parse_do(), both of which consume
|
||||
* the leading EACH themselves (it is what tells them this is an EACH loop
|
||||
* rather than their ordinary form) before calling this for the rest.
|
||||
*/
|
||||
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Token *word = NULL;
|
||||
int cmp = 0;
|
||||
|
||||
PASS(errctx, akbasic_parser_expression(parser, var));
|
||||
FAIL_ZERO_RETURN(errctx, (*var != NULL && akbasic_leaf_is_identifier(*var)), AKBASIC_ERR_SYNTAX,
|
||||
"Expected EACH (variable) IN (generator call)");
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX,
|
||||
"Expected IN after EACH (variable)");
|
||||
PASS(errctx, akbasic_parser_previous(parser, &word));
|
||||
PASS(errctx, aksl_strcmp(word->lexeme, "IN", &cmp));
|
||||
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected IN after EACH (variable)");
|
||||
|
||||
PASS(errctx, akbasic_parser_expression(parser, callexpr));
|
||||
FAIL_ZERO_RETURN(errctx, (*callexpr != NULL && (*callexpr)->leaftype == AKBASIC_LEAF_FUNCTION),
|
||||
AKBASIC_ERR_SYNTAX, "Expected a generator call after IN");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* FOR ... TO .... [STEP ...]
|
||||
* COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION]
|
||||
@@ -898,11 +1066,57 @@ 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 <variable> IN <generator call>. Checked before the leaf right of
|
||||
* FOR is required to be an assignment, because EACH is the one other thing
|
||||
* that leaf is allowed to be.
|
||||
*/
|
||||
peeked = akbasic_parser_peek(parser);
|
||||
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||
PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
|
||||
if ( cmp == 0 ) {
|
||||
akbasic_ASTLeaf *var = NULL;
|
||||
akbasic_ASTLeaf *callexpr = NULL;
|
||||
|
||||
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||
firstline = parent->lineno + 1;
|
||||
|
||||
/*
|
||||
* Pushed the same way a plain FOR's environment is: while the
|
||||
* parent is still active, so the call expression's identifiers
|
||||
* resolve in the caller's scope rather than the loop's own.
|
||||
*/
|
||||
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||
newenv = runtime->environment;
|
||||
runtime->environment = parent;
|
||||
|
||||
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||
|
||||
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,
|
||||
|
||||
@@ -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,19 @@ 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);
|
||||
}
|
||||
|
||||
@@ -1028,6 +1047,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.**
|
||||
|
||||
@@ -840,6 +840,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 +916,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 +940,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 +963,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 +1036,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");
|
||||
|
||||
297
src/runtime_generator.c
Normal file
297
src/runtime_generator.c
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* @file runtime_generator.c
|
||||
* @brief GEN, EMIT and END GEN, plus the machinery FOR EACH/DO EACH share.
|
||||
*
|
||||
* A GEN is a DEF that yields more than once. Its body is skipped on the
|
||||
* definitional pass exactly as a multi-line DEF's is -- `akbasic_parse_gen()`
|
||||
* arms `akbasic_environment_wait_for_command(env, "END GEN")` the same way
|
||||
* `akbasic_parse_def()` arms one for `RETURN` -- and it only ever really runs
|
||||
* when a `FOR EACH`/`DO EACH` invokes it.
|
||||
*
|
||||
* That invocation pushes one environment for the whole lifetime of the loop,
|
||||
* exactly as a GOSUB or a function call does, except that `EMIT` does not pop
|
||||
* it: it hands control back to the loop without releasing anything, so the
|
||||
* environment EMIT actually ran in -- which may be nested several levels
|
||||
* below the GEN's own call frame, inside a FOR/DO/GOSUB the body wrote --
|
||||
* still says exactly where to resume when `NEXT`/`LOOP` calls back into
|
||||
* akbasic_runtime_pump_generator(). Only `END GEN` reached for real --
|
||||
* meaning `obj->environment->isGenerator` is true and nothing is skipping
|
||||
* forward to it -- actually releases the call frame, the way `RETURN`
|
||||
* releases a DEF's call environment.
|
||||
*/
|
||||
|
||||
#include <akerror.h>
|
||||
#include <akstdlib.h>
|
||||
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.h>
|
||||
#include <akbasic/scanner.h>
|
||||
|
||||
#include "verbs.h"
|
||||
|
||||
/* Most verbs answer "did something happen"; this is that answer. */
|
||||
#define SUCCEED_TRUE(__obj, __dest) \
|
||||
do { \
|
||||
*(__dest) = &(__obj)->staticTrueValue; \
|
||||
} while ( 0 )
|
||||
|
||||
akerr_ErrorContext *akbasic_runtime_pump_generator(akbasic_Runtime *obj, akbasic_Environment *loopenv)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && loopenv != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in pump_generator");
|
||||
/*
|
||||
* The same per-line prologue akbasic_runtime_step() and
|
||||
* akbasic_runtime_call_function() run, driving process_line_run() directly
|
||||
* rather than through the ordinary step loop: a generator body is not the
|
||||
* top-level program and nothing else is going to advance it.
|
||||
*/
|
||||
ATTEMPT {
|
||||
while ( obj->environment != loopenv && obj->mode == AKBASIC_MODE_RUN ) {
|
||||
CATCH(errctx, akbasic_runtime_zero(obj));
|
||||
CATCH(errctx, akbasic_scanner_zero(obj));
|
||||
CATCH(errctx, akbasic_runtime_process_line_run(obj));
|
||||
}
|
||||
} CLEANUP {
|
||||
/*
|
||||
* CLEANUP runs unconditionally -- it is not a `catch` -- so it is
|
||||
* guarded on the one thing that tells success and failure apart here:
|
||||
* whether `obj->environment` is still `loopenv`. On the ordinary
|
||||
* success path it already is (that is the ATTEMPT loop's own exit
|
||||
* condition), so this is a no-op there, exactly as it is meant to be.
|
||||
* Only a genuine C-level failure -- the scanner or parser raised, or a
|
||||
* runtime error escaped the swallow process_line_run() ordinarily does
|
||||
* for a BASIC-level one -- leaves scopes active between here and
|
||||
* loopenv, and only then does this force them back, taking
|
||||
* `forGeneratorEnv` down with them since whatever it pointed at is
|
||||
* among the scopes just released.
|
||||
*/
|
||||
if ( obj->environment != loopenv ) {
|
||||
while ( obj->environment != loopenv && obj->environment->parent != NULL ) {
|
||||
IGNORE(akbasic_runtime_prev_environment(obj));
|
||||
}
|
||||
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;
|
||||
PASS(errctx, akbasic_runtime_release_environment(obj, walk));
|
||||
if ( isgen ) {
|
||||
break;
|
||||
}
|
||||
walk = next;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_runtime_generator_invoke(akbasic_Runtime *obj, akbasic_Environment *loopenv, akbasic_ASTLeaf *callexpr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Environment *callenv = NULL;
|
||||
akbasic_Environment *walk = NULL;
|
||||
akbasic_FunctionDef *fndef = NULL;
|
||||
akbasic_ASTLeaf *fnarg = NULL;
|
||||
akbasic_ASTLeaf *paramleaf = NULL;
|
||||
akbasic_Value *argvals[AKBASIC_MAX_CALL_ARGUMENTS];
|
||||
akbasic_Value *unused = NULL;
|
||||
void *fnptr = NULL;
|
||||
int nargs = 0;
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && loopenv != NULL && callexpr != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in generator_invoke");
|
||||
FAIL_ZERO_RETURN(errctx, (callexpr->leaftype == AKBASIC_LEAF_FUNCTION), AKBASIC_ERR_SYNTAX,
|
||||
"Expected a generator call after IN");
|
||||
|
||||
/*
|
||||
* GEN and DEF share the functions table (TODO.md's namespace decision for
|
||||
* this feature), so this is the same lookup akbasic_runtime_call_function()
|
||||
* does. The parser already proved the name resolves and the arity matches
|
||||
* when it parsed `callexpr` -- akbasic_parser_expression() would not have
|
||||
* produced an AKBASIC_LEAF_FUNCTION leaf otherwise -- so a miss here would
|
||||
* mean the function table changed out from under a leaf built against it,
|
||||
* which is not a case this needs its own message for.
|
||||
*/
|
||||
PASS(errctx, akbasic_environment_get_function(loopenv, callexpr->identifier, &fnptr));
|
||||
fndef = (akbasic_FunctionDef *)fnptr;
|
||||
FAIL_ZERO_RETURN(errctx, fndef->isGenerator, AKBASIC_ERR_STATE,
|
||||
"%s is a DEF, not a GEN -- FOR EACH/DO EACH needs a generator",
|
||||
fndef->name);
|
||||
|
||||
/*
|
||||
* Self-recursion: walk the *parent* chain, not the pool. An environment
|
||||
* reachable only through some other loop's `forGeneratorEnv` is a sibling
|
||||
* invocation sitting detached between its own iterations, not an ancestor
|
||||
* of this call -- nothing points from here to it via `parent`, so it never
|
||||
* matches and independent or nested FOR EACH/DO EACH over the same GEN
|
||||
* (even the same GEN with different arguments) is unaffected.
|
||||
*/
|
||||
for ( walk = loopenv; walk != NULL; walk = walk->parent ) {
|
||||
if ( walk->isGenerator && walk->generatorFn == (void *)fndef ) {
|
||||
FAIL_RETURN(errctx, AKBASIC_ERR_STATE,
|
||||
"GEN %s cannot FOR EACH/DO EACH over itself from its own body",
|
||||
fndef->name);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Evaluated in the caller's own scope, before anything is pushed -- the
|
||||
* same reason akbasic_runtime_user_function() evaluates every argument
|
||||
* before binding the first one: a later argument must not see an earlier
|
||||
* one already sitting in the callee's scope.
|
||||
*/
|
||||
fnarg = akbasic_leaf_first_argument(callexpr);
|
||||
for ( ; fnarg != NULL; fnarg = fnarg->next ) {
|
||||
FAIL_ZERO_RETURN(errctx, (nargs < AKBASIC_MAX_CALL_ARGUMENTS), AKBASIC_ERR_BOUNDS,
|
||||
"%s was called with more than %d arguments",
|
||||
callexpr->identifier, AKBASIC_MAX_CALL_ARGUMENTS);
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, fnarg, &argvals[nargs]));
|
||||
nargs += 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* One environment for the whole lifetime of the loop, exactly as GOSUB and
|
||||
* a function call take one from the pool -- and unlike either, this one
|
||||
* survives past the verb that pushed it, held alive by `loopenv`'s own
|
||||
* reference until NEXT/LOOP exhausts or EXIT abandons it.
|
||||
*/
|
||||
PASS(errctx, akbasic_runtime_new_environment(obj));
|
||||
callenv = obj->environment;
|
||||
callenv->isGenerator = true;
|
||||
callenv->generatorFn = (void *)fndef;
|
||||
callenv->nextline = fndef->lineno;
|
||||
loopenv->forGeneratorEnv = callenv;
|
||||
|
||||
paramleaf = (fndef->arglist != NULL ? fndef->arglist->right : NULL);
|
||||
for ( i = 0; i < nargs && paramleaf != NULL; i++ ) {
|
||||
PASS(errctx, akbasic_environment_assign(callenv, paramleaf, argvals[i], &unused));
|
||||
paramleaf = paramleaf->next;
|
||||
}
|
||||
|
||||
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_gen(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)expr; (void)lval; (void)rval;
|
||||
/* The parse handler already installed the generator, exactly as DEF's does. */
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_emit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Environment *genenv = NULL;
|
||||
akbasic_Environment *loopenv = NULL;
|
||||
akbasic_Value *value = NULL;
|
||||
int64_t zerosubscript[1] = { 0 };
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
||||
"Expected EMIT (expression)");
|
||||
/*
|
||||
* EMIT is not necessarily standing directly in the environment
|
||||
* akbasic_runtime_generator_invoke() pushed: a GEN body is ordinary BASIC
|
||||
* and may nest its own FOR, DO or GOSUB around an EMIT, each of which
|
||||
* pushes an environment of its own -- exactly what the issue's own
|
||||
* ROOMOBJECTS example does. Walk up to the nearest one that really is a
|
||||
* GEN's own call frame.
|
||||
*/
|
||||
for ( genenv = obj->environment; genenv != NULL && !genenv->isGenerator; genenv = genenv->parent ) {
|
||||
}
|
||||
FAIL_ZERO_RETURN(errctx, (genenv != NULL), AKBASIC_ERR_STATE,
|
||||
"EMIT outside the context of a GEN body");
|
||||
loopenv = genenv->parent;
|
||||
FAIL_ZERO_RETURN(errctx, (loopenv != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||
"EMIT from an orphaned environment");
|
||||
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &value));
|
||||
/*
|
||||
* Straight into the loop variable's storage, bypassing the arithmetic
|
||||
* akbasic_environment_assign() and evaluate_for_condition() carry for a
|
||||
* plain FOR: an EACH variable takes whatever type the GEN emits, string or
|
||||
* structure element included, and there is no TO/STEP to compare it
|
||||
* against.
|
||||
*/
|
||||
PASS(errctx, akbasic_variable_set_subscript(loopenv->forNextVariable, value, zerosubscript, 1));
|
||||
loopenv->nextline = loopenv->loopFirstLine;
|
||||
/*
|
||||
* The resume point, which may be several levels below `genenv` -- whatever
|
||||
* nested FOR/DO/GOSUB environment this EMIT actually ran in. NEXT/LOOP
|
||||
* reactivates exactly this one, so the nested structure picks up exactly
|
||||
* where it left off rather than restarting at the top of the GEN body.
|
||||
*/
|
||||
loopenv->forGeneratorEnv = obj->environment;
|
||||
/*
|
||||
* Not a pop, and not a single-level detach either: everything between here
|
||||
* and `loopenv` -- `genenv` and any of its own descendants -- has to
|
||||
* survive untouched to be resumed, so control moves to `loopenv` directly
|
||||
* rather than walking the chain one release at a time.
|
||||
*/
|
||||
obj->environment = loopenv;
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_end_gen(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool waiting = false;
|
||||
|
||||
(void)expr; (void)lval; (void)rval;
|
||||
/*
|
||||
* A END GEN reached while skipping forward to one is the end of a GEN
|
||||
* body's *definition*, not the end of a call -- the same distinction
|
||||
* RETURN draws for DEF.
|
||||
*/
|
||||
PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "END GEN", &waiting));
|
||||
if ( waiting ) {
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "END GEN"));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_ZERO_RETURN(errctx, (obj->environment->isGenerator), AKBASIC_ERR_STATE,
|
||||
"END GEN outside the context of a generator invocation");
|
||||
/*
|
||||
* Real exhaustion: release this environment and detach in the same
|
||||
* motion prev_environment() always does, then clear the parent's
|
||||
* reference to it so a caller pumping this loop can tell "still alive"
|
||||
* apart from "nothing left to resume".
|
||||
*/
|
||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||
obj->environment->forGeneratorEnv = NULL;
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -80,6 +80,31 @@ akerr_ErrorContext *akbasic_cmd_do(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
FAIL_ZERO_RETURN(errctx, obj->environment->isDoLoop, AKBASIC_ERR_STATE,
|
||||
"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,21 @@ 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"));
|
||||
if ( loopenv->forGeneratorEnv != NULL ) {
|
||||
obj->environment = loopenv->forGeneratorEnv;
|
||||
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||
}
|
||||
again = (loopenv->forGeneratorEnv != NULL);
|
||||
} else {
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||
/*
|
||||
|
||||
23
src/verbs.c
23
src/verbs.c
@@ -74,14 +74,32 @@ static const akbasic_Verb VERBS[] = {
|
||||
{ "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw },
|
||||
{ "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
|
||||
|
||||
@@ -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);
|
||||
|
||||
224
tests/generators.c
Normal file
224
tests/generators.c
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* @file generators.c
|
||||
* @brief Generators: GEN/EMIT/END GEN and the FOR EACH/DO EACH loops over them.
|
||||
*
|
||||
* The golden corpus (tests/language/flowcontrol/generators_*.bas) covers the
|
||||
* ordinary shapes: the issue's own ROOMOBJECTS example in both loop forms, an
|
||||
* empty generator, a non-numeric EMIT and nested/interleaved invocations. This
|
||||
* file covers what a byte-compared program cannot: that abandoning a
|
||||
* generator with EXIT gives its environment back to the pool rather than
|
||||
* leaking it, and that misusing a GEN fails cleanly rather than corrupting the
|
||||
* environment stack.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.h>
|
||||
|
||||
#include "harness.h"
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Run a program to completion under an explicit step budget. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *run_program_bounded(const char *source, int64_t steps)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, harness_start(NULL));
|
||||
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
||||
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, steps));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Run a program to completion, bounded so a hang fails rather than waits. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, run_program_bounded(source, 20000));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief The issue's own example, as a sanity check independent of the golden corpus. */
|
||||
static void test_room_objects_smoke(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 DIM OBJ#(3)\n"
|
||||
"20 OBJ#(0) = 100\n"
|
||||
"30 OBJ#(1) = 200\n"
|
||||
"40 OBJ#(2) = 300\n"
|
||||
"50 GEN ROOMOBJECTS(R#)\n"
|
||||
"60 FOR I# = 0 TO 2\n"
|
||||
"70 IF I# <> 1 THEN EMIT OBJ#(I#)\n"
|
||||
"80 NEXT I#\n"
|
||||
"90 END GEN\n"
|
||||
"100 FOR EACH O# IN ROOMOBJECTS(0)\n"
|
||||
"110 PRINT O#\n"
|
||||
"120 NEXT O#\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "100\n300\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief EXIT out of a FOR EACH loop releases its generator, not just the loop.
|
||||
*
|
||||
* Run more FOR EACH/EXIT constructs than AKBASIC_MAX_ENVIRONMENTS -- each one
|
||||
* takes two environments (the loop's own and the generator's) -- in a single
|
||||
* program. If EXIT abandoned the generator environment instead of releasing
|
||||
* it, this exhausts the pool partway through and the run reports "Environment
|
||||
* pool exhausted" instead of finishing.
|
||||
*/
|
||||
static void test_exit_releases_generator_for_each(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 END GEN\n"
|
||||
"40 FOR K# = 1 TO 13\n"
|
||||
"50 FOR EACH V# IN ONE(0)\n"
|
||||
"60 EXIT\n"
|
||||
"70 NEXT V#\n"
|
||||
"80 NEXT K#\n"
|
||||
"90 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/** @brief The same leak check for DO EACH/EXIT. */
|
||||
static void test_exit_releases_generator_do_each(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 END GEN\n"
|
||||
"40 FOR K# = 1 TO 13\n"
|
||||
"50 DO EACH V# IN ONE(0)\n"
|
||||
"60 EXIT\n"
|
||||
"70 LOOP\n"
|
||||
"80 NEXT K#\n"
|
||||
"90 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief EXIT partway through, with more of the generator left to run, still
|
||||
* frees the environment for the next construct that needs one.
|
||||
*/
|
||||
static void test_exit_partway_through(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||
"20 FOR I# = 1 TO N#\n"
|
||||
"30 EMIT I#\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR EACH V# IN COUNTUP(10)\n"
|
||||
"70 PRINT V#\n"
|
||||
"80 IF V# = 2 THEN EXIT\n"
|
||||
"90 NEXT V#\n"
|
||||
"100 PRINT \"AFTER\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\nAFTER\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 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_called_like_a_function();
|
||||
test_emit_outside_gen();
|
||||
test_self_recursion_refused();
|
||||
test_sibling_invocations_allowed();
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
10 REM A GEN invoked like an ordinary function is refused, not silently run.
|
||||
20 GEN ONE(N#)
|
||||
30 EMIT N#
|
||||
40 END GEN
|
||||
50 X# = ONE(5)
|
||||
60 PRINT "UNREACHABLE"
|
||||
@@ -0,0 +1,2 @@
|
||||
? 50 : RUNTIME ERROR ONE is a GEN; call it with FOR EACH or DO EACH, not as a function
|
||||
|
||||
14
tests/language/flowcontrol/generators_doeach.bas
Normal file
14
tests/language/flowcontrol/generators_doeach.bas
Normal file
@@ -0,0 +1,14 @@
|
||||
10 REM Same generator as generators_foreach.bas, via DO EACH ... LOOP.
|
||||
20 DIM OBJ#(3)
|
||||
30 OBJ#(0) = 100
|
||||
40 OBJ#(1) = 200
|
||||
50 OBJ#(2) = 300
|
||||
60 GEN ROOMOBJECTS(R#)
|
||||
70 FOR I# = 0 TO 2
|
||||
80 IF I# <> 1 THEN EMIT OBJ#(I#)
|
||||
90 NEXT I#
|
||||
100 END GEN
|
||||
110 DO EACH O# IN ROOMOBJECTS(0)
|
||||
120 PRINT O#
|
||||
130 LOOP
|
||||
140 PRINT "DONE"
|
||||
3
tests/language/flowcontrol/generators_doeach.txt
Normal file
3
tests/language/flowcontrol/generators_doeach.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
100
|
||||
300
|
||||
DONE
|
||||
3
tests/language/flowcontrol/generators_emit_outside.bas
Normal file
3
tests/language/flowcontrol/generators_emit_outside.bas
Normal file
@@ -0,0 +1,3 @@
|
||||
10 REM EMIT outside any GEN invocation is refused cleanly.
|
||||
20 EMIT 5
|
||||
30 PRINT "UNREACHABLE"
|
||||
2
tests/language/flowcontrol/generators_emit_outside.txt
Normal file
2
tests/language/flowcontrol/generators_emit_outside.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
? 20 : RUNTIME ERROR EMIT outside the context of a GEN body
|
||||
|
||||
11
tests/language/flowcontrol/generators_empty.bas
Normal file
11
tests/language/flowcontrol/generators_empty.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
10 REM A GEN with no EMIT at all runs its FOR EACH/DO EACH body zero times.
|
||||
20 GEN NOTHING(N#)
|
||||
30 END GEN
|
||||
40 FOR EACH X# IN NOTHING(0)
|
||||
50 PRINT "NEVER FOR"
|
||||
60 NEXT X#
|
||||
70 PRINT "AFTER FOR"
|
||||
80 DO EACH Y# IN NOTHING(0)
|
||||
90 PRINT "NEVER DO"
|
||||
100 LOOP
|
||||
110 PRINT "AFTER DO"
|
||||
2
tests/language/flowcontrol/generators_empty.txt
Normal file
2
tests/language/flowcontrol/generators_empty.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
AFTER FOR
|
||||
AFTER DO
|
||||
15
tests/language/flowcontrol/generators_foreach.bas
Normal file
15
tests/language/flowcontrol/generators_foreach.bas
Normal file
@@ -0,0 +1,15 @@
|
||||
10 REM The GEN/FOR EACH example from issue #57, with real arrays standing in
|
||||
20 REM for OBJCOUNT%/VISIBLE/OBJ%.
|
||||
30 DIM OBJ#(3)
|
||||
40 OBJ#(0) = 100
|
||||
50 OBJ#(1) = 200
|
||||
60 OBJ#(2) = 300
|
||||
70 GEN ROOMOBJECTS(R#)
|
||||
80 FOR I# = 0 TO 2
|
||||
90 IF I# <> 1 THEN EMIT OBJ#(I#)
|
||||
100 NEXT I#
|
||||
110 END GEN
|
||||
120 FOR EACH O# IN ROOMOBJECTS(0)
|
||||
130 PRINT O#
|
||||
140 NEXT O#
|
||||
150 PRINT "DONE"
|
||||
3
tests/language/flowcontrol/generators_foreach.txt
Normal file
3
tests/language/flowcontrol/generators_foreach.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
100
|
||||
300
|
||||
DONE
|
||||
10
tests/language/flowcontrol/generators_mismatched_next.bas
Normal file
10
tests/language/flowcontrol/generators_mismatched_next.bas
Normal file
@@ -0,0 +1,10 @@
|
||||
10 REM A stray NEXT with nothing open behaves like the plain-FOR case: the
|
||||
20 REM FOR EACH's own NEXT closes it cleanly, and a second one errors.
|
||||
30 GEN ONE(N#)
|
||||
40 EMIT N#
|
||||
50 END GEN
|
||||
60 FOR EACH O# IN ONE(1)
|
||||
70 PRINT O#
|
||||
80 NEXT O#
|
||||
90 NEXT O#
|
||||
100 PRINT "UNREACHABLE"
|
||||
@@ -0,0 +1,3 @@
|
||||
1
|
||||
? 90 : RUNTIME ERROR NEXT outside the context of FOR
|
||||
|
||||
22
tests/language/flowcontrol/generators_nested.bas
Normal file
22
tests/language/flowcontrol/generators_nested.bas
Normal file
@@ -0,0 +1,22 @@
|
||||
10 REM Nested FOR EACH/DO EACH over the same GEN with different arguments,
|
||||
20 REM in every combination of the two loop shapes.
|
||||
30 GEN COUNTUP(N#)
|
||||
40 FOR I# = 1 TO N#
|
||||
50 EMIT I#
|
||||
60 NEXT I#
|
||||
70 END GEN
|
||||
80 FOR EACH A# IN COUNTUP(2)
|
||||
90 FOR EACH B# IN COUNTUP(3)
|
||||
100 PRINT A# * 10 + B#
|
||||
110 NEXT B#
|
||||
120 NEXT A#
|
||||
130 DO EACH C# IN COUNTUP(2)
|
||||
140 DO EACH D# IN COUNTUP(2)
|
||||
150 PRINT C# * 100 + D#
|
||||
160 LOOP
|
||||
170 LOOP
|
||||
180 FOR EACH E# IN COUNTUP(2)
|
||||
190 DO EACH F# IN COUNTUP(2)
|
||||
200 PRINT E# * 1000 + F#
|
||||
210 LOOP
|
||||
220 NEXT E#
|
||||
14
tests/language/flowcontrol/generators_nested.txt
Normal file
14
tests/language/flowcontrol/generators_nested.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
11
|
||||
12
|
||||
13
|
||||
21
|
||||
22
|
||||
23
|
||||
101
|
||||
102
|
||||
201
|
||||
202
|
||||
1001
|
||||
1002
|
||||
2001
|
||||
2002
|
||||
11
tests/language/flowcontrol/generators_string.bas
Normal file
11
tests/language/flowcontrol/generators_string.bas
Normal file
@@ -0,0 +1,11 @@
|
||||
10 REM FOR EACH/DO EACH accept any emitted type, not just numeric.
|
||||
20 GEN WORDS(N#)
|
||||
30 EMIT "HELLO"
|
||||
40 EMIT "WORLD"
|
||||
50 END GEN
|
||||
60 FOR EACH W$ IN WORDS(0)
|
||||
70 PRINT W$
|
||||
80 NEXT W$
|
||||
90 DO EACH V$ IN WORDS(0)
|
||||
100 PRINT V$
|
||||
110 LOOP
|
||||
4
tests/language/flowcontrol/generators_string.txt
Normal file
4
tests/language/flowcontrol/generators_string.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
HELLO
|
||||
WORLD
|
||||
HELLO
|
||||
WORLD
|
||||
@@ -66,15 +66,18 @@ int main(void)
|
||||
/*
|
||||
* These rows have no exec handler because another verb's parse path consumes
|
||||
* 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 ||
|
||||
|
||||
Reference in New Issue
Block a user