Compare commits
3 Commits
57-impleme
...
46
| Author | SHA1 | Date | |
|---|---|---|---|
| 21f360ed1d | |||
|
4b5d97910c
|
|||
|
|
a29c7f34fe
|
@@ -698,6 +698,26 @@ as a commit co-author, and this repository follows the same rule.
|
|||||||
Each dependency carries its own `AGENTS.md` with authoritative per-repo rules. Read the
|
Each dependency carries its own `AGENTS.md` with authoritative per-repo rules. Read the
|
||||||
relevant one before editing a submodule.
|
relevant one before editing a submodule.
|
||||||
|
|
||||||
|
### Abandoned generators
|
||||||
|
|
||||||
|
The one invariant generators (`GEN`/`EMIT`, `FOR EACH`/`DO EACH`) add to the environment
|
||||||
|
pool: **a suspended generator hangs off its loop scope's `forGeneratorEnv` as a *child*,
|
||||||
|
off the parent chain, so no walk up `->parent` ever finds it.** Any path that discards a
|
||||||
|
loop scope — `EXIT`, a mismatched `NEXT`, a `LOOP` condition saying stop, an error unwind
|
||||||
|
through `akbasic_runtime_pump_generator()` or `akbasic_runtime_call_function()` — must
|
||||||
|
release that generator too, or its pool slot is stranded until the next `RUN`; with
|
||||||
|
twelve slots, a loop that abandons a dozen of them kills the program with "Environment
|
||||||
|
pool exhausted" far from the cause.
|
||||||
|
|
||||||
|
Two functions own the invariant, and every discard path goes through one of them:
|
||||||
|
`akbasic_runtime_release_generator()` (walks a detached generator chain up through its
|
||||||
|
call frame, recursing into any `forGeneratorEnv` it passes) and
|
||||||
|
`akbasic_runtime_unwind_to_environment()` (pops the *active* chain down to a target,
|
||||||
|
releasing each popped scope's suspended generator on the way). If you add a new path
|
||||||
|
that pops or discards environments, use one of these — a bare
|
||||||
|
`akbasic_runtime_prev_environment()` loop reintroduces the leak, and
|
||||||
|
`tests/generators.c` holds the pool-exhaustion tests that will say so.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Editing the documentation
|
## Editing the documentation
|
||||||
|
|||||||
30
TODO.md
30
TODO.md
@@ -366,6 +366,36 @@ One caveat survives the upgrade unchanged: `aksl_strhash_djb2` still sign-extend
|
|||||||
high-bit byte hashes differently from the `unsigned char` answer. BASIC identifiers are 7-bit
|
high-bit byte hashes differently from the `unsigned char` answer. BASIC identifiers are 7-bit
|
||||||
ASCII so the symbol tables cannot reach it — see §1.3, which is still accurate.
|
ASCII so the symbol tables cannot reach it — see §1.3, which is still accurate.
|
||||||
|
|
||||||
|
### 1.10 Generators share the `DEF` namespace, and the rest of their v1 semantics
|
||||||
|
|
||||||
|
Decided with issue #57 and its review. `GEN` and `DEF` live in the same functions table —
|
||||||
|
one lookup, one "unknown function" error path, a name cannot be both. What follows from
|
||||||
|
that and from the review of PR #61, all settled:
|
||||||
|
|
||||||
|
- A `GEN` called like a function (`X# = COUNTUP(3)`) is refused at the call, before
|
||||||
|
anything is pushed — `akbasic_FunctionDef.isGenerator` exists for exactly this check.
|
||||||
|
- Bare `RETURN` standing in a `GEN`'s own frame ends the generator exactly as `END GEN`
|
||||||
|
does; `RETURN expr` there is an error, because values leave a `GEN` only through `EMIT`.
|
||||||
|
Both inherit the interpreter-wide restriction that `RETURN` does not unwind nested
|
||||||
|
`FOR`/`DO` scopes — lifting that everywhere (the C64 reference *does* unwind) is filed
|
||||||
|
separately.
|
||||||
|
- `DO EACH ... LOOP WHILE c | UNTIL c` composes: the condition is checked after each trip,
|
||||||
|
with the loop variable still holding that trip's value, and stopping abandons the
|
||||||
|
generator exactly as `EXIT` does. A condition on the `DO EACH` line itself is a parse
|
||||||
|
error.
|
||||||
|
- Self-recursion — a `GEN` reached again down its own parent chain — is refused; sibling
|
||||||
|
and nested invocations of the same `GEN` are each a fresh pool environment and are fine.
|
||||||
|
- Every path that discards a `FOR EACH`/`DO EACH` scope must release the generator
|
||||||
|
suspended off it; see MAINTENANCE.md's "Abandoned generators" note for the invariant
|
||||||
|
and `akbasic_runtime_unwind_to_environment()` for the one primitive that enforces it.
|
||||||
|
|
||||||
|
One known limitation, pre-existing and shared with `DEF`: `parse_def_parameters()` does
|
||||||
|
not accept an empty parameter list, so `GEN NAME()` cannot be written — every generator
|
||||||
|
takes at least one parameter whether it wants one or not. Location:
|
||||||
|
`src/parser_commands.c`, `parse_def_parameters()`. Consequence: pointless parameters in
|
||||||
|
programs. Blast radius: cosmetic, both `DEF` and `GEN` headers. Closure: teach the shared
|
||||||
|
helper to accept `()`, one test each for `DEF` and `GEN`; filed as its own issue.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. What exists — **the core port is complete and green**
|
## 2. What exists — **the core port is complete and green**
|
||||||
|
|||||||
@@ -197,8 +197,9 @@ A `GEN`'s body is ordinary BASIC: it may hold its own `FOR`, `DO`, `IF` or `GOSU
|
|||||||
around the `EMIT`s, and even invoke another `GEN` with its own `FOR EACH`/`DO EACH` --
|
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
|
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
|
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
|
refused: unlike a function call, which runs to completion and returns, the outer
|
||||||
by name from inside its own body without meaning to recurse.
|
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
|
`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
|
`DO`, and the generator it was consuming stops there -- nothing forces the rest of it to
|
||||||
@@ -224,6 +225,59 @@ run just because the loop started it:
|
|||||||
STOPPED
|
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 `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
|
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
|
about an ordinary call means "resume where the last `EMIT` left off." `FOR EACH`/`DO
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ for the reasoning in each case.
|
|||||||
| `DIM` … `AS` | `DIM S@ AS T`, `DIM P@ AS PTR TO T` | Make a structure, or a strict pointer to one. See Chapter 16. |
|
| `DIM` … `AS` | `DIM S@ AS T`, `DIM P@ AS PTR TO T` | Make a structure, or a strict pointer to one. See Chapter 16. |
|
||||||
| `DIRECTORY` | `DIRECTORY` | **Refused.** Not written yet; the standard-library wrapper it waited on has landed. |
|
| `DIRECTORY` | `DIRECTORY` | **Refused.** Not written yet; the standard-library wrapper it waited on has landed. |
|
||||||
| `DLOAD` | `DLOAD "name"` | Load a program from a file. |
|
| `DLOAD` | `DLOAD "name"` | Load a program from a file. |
|
||||||
| `DO` | `DO [WHILE c | UNTIL c]`, `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. |
|
| `DO` | `DO [WHILE c | UNTIL c]`, `DO EACH V IN gen(args)` | Start a loop. The condition may be here, on the `LOOP`, or neither. `EACH` consumes a `GEN` instead, and takes its condition only on the `LOOP`; see Chapter 4. |
|
||||||
| `DOPEN` | `DOPEN n, "name" [,W]` | Open a file on channel `n`. `W` opens it for writing. |
|
| `DOPEN` | `DOPEN n, "name" [,W]` | Open a file on channel `n`. `W` opens it for writing. |
|
||||||
| `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. |
|
| `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. |
|
||||||
| `DSAVE` | `DSAVE "name"` | Save the program to a file. |
|
| `DSAVE` | `DSAVE "name"` | Save the program to a file. |
|
||||||
@@ -90,7 +90,7 @@ for the reasoning in each case.
|
|||||||
| `RENUMBER` | `RENUMBER [start [,step [,from]]]` | Renumber lines, rewriting every branch to match. |
|
| `RENUMBER` | `RENUMBER [start [,step [,from]]]` | Renumber lines, rewriting every branch to match. |
|
||||||
| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. |
|
| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. |
|
||||||
| `RESUME` | `RESUME [NEXT | line]` | Return from a `TRAP` handler. |
|
| `RESUME` | `RESUME [NEXT | line]` | Return from a `TRAP` handler. |
|
||||||
| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. |
|
| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. Inside a `GEN`, a bare `RETURN` ends the generator early; `RETURN expr` there is an error. |
|
||||||
| `RUN` | `RUN [line]` | Run the program, optionally from a line. |
|
| `RUN` | `RUN [line]` | Run the program, optionally from a line. |
|
||||||
| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. |
|
| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. |
|
||||||
| `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. |
|
| `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. |
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ typedef struct
|
|||||||
* Registering before the script is loaded is the normal case. A host type and a
|
* Registering before the script is loaded is the normal case. A host type and a
|
||||||
* `TYPE` the script declares share one namespace, so a script cannot declare a
|
* `TYPE` the script declares share one namespace, so a script cannot declare a
|
||||||
* type the host already registered -- and would be refused if it tried.
|
* type the host already registered -- and would be refused if it tried.
|
||||||
|
* Host type and field names are limited to 31 characters, matching
|
||||||
|
* script-declared types and fields.
|
||||||
*
|
*
|
||||||
* @param obj Object to initialize, inspect, or modify.
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
* @param type The host's description of its own struct.
|
* @param type The host's description of its own struct.
|
||||||
@@ -112,6 +114,7 @@ typedef struct
|
|||||||
* @throws AKERR_NULLPOINTER When either argument is NULL.
|
* @throws AKERR_NULLPOINTER When either argument is NULL.
|
||||||
* @throws AKBASIC_ERR_VALUE When a field name carries no type suffix, a nested
|
* @throws AKBASIC_ERR_VALUE When a field name carries no type suffix, a nested
|
||||||
* type is not registered, or the name is already taken.
|
* type is not registered, or the name is already taken.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS When a type or field name exceeds 31 characters.
|
||||||
* @throws AKBASIC_ERR_BOUNDS When the type table or a field list is full.
|
* @throws AKBASIC_ERR_BOUNDS When the type table or a field list is full.
|
||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_host_register_type(struct akbasic_Runtime *obj, const akbasic_HostType *type);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_host_register_type(struct akbasic_Runtime *obj, const akbasic_HostType *type);
|
||||||
|
|||||||
@@ -635,6 +635,28 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_detach_environment(akbasic_Ru
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_release_environment(akbasic_Runtime *obj, akbasic_Environment *env);
|
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.
|
* @brief Push a GEN's environment, bind its parameters and run it to its first EMIT.
|
||||||
*
|
*
|
||||||
|
|||||||
16
src/host.c
16
src/host.c
@@ -235,16 +235,9 @@ akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbas
|
|||||||
|
|
||||||
dest = &obj->structtypes.types[obj->structtypes.count];
|
dest = &obj->structtypes.types[obj->structtypes.count];
|
||||||
PASS(errctx, aksl_memset(dest, 0, sizeof(*dest)));
|
PASS(errctx, aksl_memset(dest, 0, sizeof(*dest)));
|
||||||
/*
|
/* Host and script registration share the 32-byte-including-NUL limit for
|
||||||
* Raw snprintf, and a latent defect rather than a settled decision: a host
|
both type names and field names. */
|
||||||
* type name over 31 characters truncates silently here, and two that share a
|
PASS(errctx, aksl_strcpy(dest->name, sizeof(dest->name), type->name));
|
||||||
* 31-character prefix then collide in akbasic_structtype_find. scan_names()
|
|
||||||
* in structtype.c already refuses the same case for a script-declared type
|
|
||||||
* with an explicit limit message, so the two paths disagree. Converting this
|
|
||||||
* to aksl_strcpy is the fix and it is a behaviour change on a public
|
|
||||||
* registration call, so it wants its own issue rather than this port.
|
|
||||||
*/
|
|
||||||
snprintf(dest->name, sizeof(dest->name), "%s", type->name);
|
|
||||||
dest->used = true;
|
dest->used = true;
|
||||||
dest->ishost = true;
|
dest->ishost = true;
|
||||||
dest->hostsize = type->size;
|
dest->hostsize = type->size;
|
||||||
@@ -270,8 +263,7 @@ akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbas
|
|||||||
"%s.%s must end in '%c' for the C type it describes",
|
"%s.%s must end in '%c' for the C type it describes",
|
||||||
type->name, src->name, suffix_for(src->kind));
|
type->name, src->name, suffix_for(src->kind));
|
||||||
|
|
||||||
/* Same silent truncation as the type name above, and the same fix. */
|
PASS(errctx, aksl_strcpy(field->name, sizeof(field->name), src->name));
|
||||||
snprintf(field->name, sizeof(field->name), "%s", src->name);
|
|
||||||
field->hostkind = src->kind;
|
field->hostkind = src->kind;
|
||||||
field->hostoffset = src->offset;
|
field->hostoffset = src->offset;
|
||||||
field->hostwidth = src->width;
|
field->hostwidth = src->width;
|
||||||
|
|||||||
@@ -337,6 +337,19 @@ akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **d
|
|||||||
|
|
||||||
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
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->isDoLoop = true;
|
||||||
newenv->isEachLoop = true;
|
newenv->isEachLoop = true;
|
||||||
newenv->loopFirstLine = firstline;
|
newenv->loopFirstLine = firstline;
|
||||||
@@ -1098,6 +1111,21 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **
|
|||||||
|
|
||||||
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
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->isEachLoop = true;
|
||||||
newenv->loopFirstLine = firstline;
|
newenv->loopFirstLine = firstline;
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -208,6 +208,38 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
|||||||
SUCCEED_RETURN(errctx);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------- lifecycle -- */
|
/* ------------------------------------------------------------- lifecycle -- */
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_runtime_zero(akbasic_Runtime *obj)
|
akerr_ErrorContext *akbasic_runtime_zero(akbasic_Runtime *obj)
|
||||||
@@ -1166,10 +1198,11 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch
|
|||||||
* Give them back, or a host absorbing script errors drains the
|
* Give them back, or a host absorbing script errors drains the
|
||||||
* twelve-slot environment pool after twelve dead calls and every
|
* twelve-slot environment pool after twelve dead calls and every
|
||||||
* call after that fails for a reason nobody can see in the script.
|
* call after that fails for a reason nobody can see in the script.
|
||||||
|
* The unwind, not a bare prev_environment() loop, because a body that
|
||||||
|
* died inside a FOR EACH leaves a suspended generator hanging off the
|
||||||
|
* loop scope, and only the unwind knows to take it down too.
|
||||||
*/
|
*/
|
||||||
while ( obj->environment != targetenv && obj->environment->parent != NULL ) {
|
IGNORE(akbasic_runtime_unwind_to_environment(obj, targetenv));
|
||||||
IGNORE(akbasic_runtime_prev_environment(obj));
|
|
||||||
}
|
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
PASS(errctx, akbasic_environment_new_value(targetenv, &out));
|
PASS(errctx, akbasic_environment_new_value(targetenv, &out));
|
||||||
|
|||||||
@@ -161,8 +161,22 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
|
|||||||
SUCCEED_TRUE(obj, dest);
|
SUCCEED_TRUE(obj, dest);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* A GEN is a function at heart, and RETURN ends it the way it ends a DEF
|
||||||
|
* or a GOSUB: early, cleanly, from its own frame. What a generator's
|
||||||
|
* RETURN cannot do is carry a value -- values leave a GEN one at a time,
|
||||||
|
* through EMIT, and there is no caller waiting on a return slot.
|
||||||
|
*/
|
||||||
|
if ( obj->environment->isGenerator ) {
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"A GEN yields values through EMIT; RETURN here takes none");
|
||||||
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
|
||||||
"RETURN outside the context of GOSUB");
|
"RETURN outside the context of GOSUB, DEF, or GEN");
|
||||||
|
|
||||||
if ( expr != NULL && expr->right != NULL ) {
|
if ( expr != NULL && expr->right != NULL ) {
|
||||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ akerr_ErrorContext *akbasic_runtime_pump_generator(akbasic_Runtime *obj, akbasic
|
|||||||
* among the scopes just released.
|
* among the scopes just released.
|
||||||
*/
|
*/
|
||||||
if ( obj->environment != loopenv ) {
|
if ( obj->environment != loopenv ) {
|
||||||
while ( obj->environment != loopenv && obj->environment->parent != NULL ) {
|
IGNORE(akbasic_runtime_unwind_to_environment(obj, loopenv));
|
||||||
IGNORE(akbasic_runtime_prev_environment(obj));
|
|
||||||
}
|
|
||||||
loopenv->forGeneratorEnv = NULL;
|
loopenv->forGeneratorEnv = NULL;
|
||||||
}
|
}
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
@@ -103,6 +101,18 @@ akerr_ErrorContext *akbasic_runtime_release_generator(akbasic_Runtime *obj, akba
|
|||||||
while ( walk != NULL ) {
|
while ( walk != NULL ) {
|
||||||
isgen = walk->isGenerator;
|
isgen = walk->isGenerator;
|
||||||
next = walk->parent;
|
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));
|
PASS(errctx, akbasic_runtime_release_environment(obj, walk));
|
||||||
if ( isgen ) {
|
if ( isgen ) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -149,11 +149,27 @@ akerr_ErrorContext *akbasic_cmd_loop(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
akbasic_Environment *loopenv = obj->environment;
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
|
||||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||||
if ( loopenv->forGeneratorEnv != NULL ) {
|
/*
|
||||||
|
* 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;
|
obj->environment = loopenv->forGeneratorEnv;
|
||||||
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||||
}
|
}
|
||||||
again = (loopenv->forGeneratorEnv != NULL);
|
again = (again && loopenv->forGeneratorEnv != NULL);
|
||||||
} else {
|
} else {
|
||||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -119,6 +119,137 @@ static void test_exit_partway_through(void)
|
|||||||
harness_stop();
|
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
|
* @brief A GEN invoked like an ordinary function, rather than through FOR
|
||||||
* EACH/DO EACH, fails cleanly.
|
* EACH/DO EACH, fails cleanly.
|
||||||
@@ -216,6 +347,11 @@ int main(void)
|
|||||||
test_exit_releases_generator_for_each();
|
test_exit_releases_generator_for_each();
|
||||||
test_exit_releases_generator_do_each();
|
test_exit_releases_generator_do_each();
|
||||||
test_exit_partway_through();
|
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_called_like_a_function();
|
||||||
test_emit_outside_gen();
|
test_emit_outside_gen();
|
||||||
test_self_recursion_refused();
|
test_self_recursion_refused();
|
||||||
|
|||||||
@@ -208,6 +208,72 @@ static void test_suffix_must_match_the_c_type(void)
|
|||||||
harness_stop();
|
harness_stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Host names use the same bounded storage as script-declared names. */
|
||||||
|
static void test_registration_name_limits(void)
|
||||||
|
{
|
||||||
|
static const akbasic_HostField SHORT_FIELD[] = {
|
||||||
|
AKBASIC_HOST_FIELD( test_Enemy, hp, "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234#",
|
||||||
|
AKBASIC_HOSTFIELD_INT32 )
|
||||||
|
};
|
||||||
|
static const akbasic_HostField LONG_FIELD_A[] = {
|
||||||
|
AKBASIC_HOST_FIELD( test_Enemy, hp, "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456A#",
|
||||||
|
AKBASIC_HOSTFIELD_INT32 )
|
||||||
|
};
|
||||||
|
static const akbasic_HostField LONG_FIELD_B[] = {
|
||||||
|
AKBASIC_HOST_FIELD( test_Enemy, hp, "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456B#",
|
||||||
|
AKBASIC_HOSTFIELD_INT32 )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType SHORT_TYPE = {
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ12345", sizeof(test_Enemy), SHORT_FIELD, 1
|
||||||
|
};
|
||||||
|
static const akbasic_HostType LONG_TYPE_A = {
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456A", sizeof(test_Enemy), SHORT_FIELD, 1
|
||||||
|
};
|
||||||
|
static const akbasic_HostType LONG_TYPE_B = {
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456B", sizeof(test_Enemy), SHORT_FIELD, 1
|
||||||
|
};
|
||||||
|
static const akbasic_HostType LONG_FIELD_TYPE_A = {
|
||||||
|
"LONGFIELDA", sizeof(test_Enemy), LONG_FIELD_A, 1
|
||||||
|
};
|
||||||
|
static const akbasic_HostType LONG_FIELD_TYPE_B = {
|
||||||
|
"LONGFIELDB", sizeof(test_Enemy), LONG_FIELD_B, 1
|
||||||
|
};
|
||||||
|
akerr_ErrorContext *raised = NULL;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
TEST_REQUIRE_OK(akbasic_host_register_type(&HARNESS_RUNTIME, &SHORT_TYPE));
|
||||||
|
harness_stop();
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
raised = akbasic_host_register_type(&HARNESS_RUNTIME, &LONG_TYPE_A);
|
||||||
|
TEST_REQUIRE(raised != NULL, "an overlong type name must be refused");
|
||||||
|
TEST_REQUIRE_INT(raised->status, AKERR_OUTOFBOUNDS);
|
||||||
|
test_discard_error(raised);
|
||||||
|
raised = akbasic_host_register_type(&HARNESS_RUNTIME, &LONG_TYPE_B);
|
||||||
|
TEST_REQUIRE(raised != NULL, "a second long type name must fail cleanly");
|
||||||
|
TEST_REQUIRE_INT(raised->status, AKERR_OUTOFBOUNDS);
|
||||||
|
test_discard_error(raised);
|
||||||
|
harness_stop();
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
TEST_REQUIRE_OK(akbasic_host_register_type(&HARNESS_RUNTIME,
|
||||||
|
&(akbasic_HostType){
|
||||||
|
"SHORTFIELDS", sizeof(test_Enemy), SHORT_FIELD, 1
|
||||||
|
}));
|
||||||
|
harness_stop();
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
raised = akbasic_host_register_type(&HARNESS_RUNTIME, &LONG_FIELD_TYPE_A);
|
||||||
|
TEST_REQUIRE(raised != NULL, "an overlong field name must be refused");
|
||||||
|
TEST_REQUIRE_INT(raised->status, AKERR_OUTOFBOUNDS);
|
||||||
|
test_discard_error(raised);
|
||||||
|
raised = akbasic_host_register_type(&HARNESS_RUNTIME, &LONG_FIELD_TYPE_B);
|
||||||
|
TEST_REQUIRE(raised != NULL, "a second long field name must fail cleanly");
|
||||||
|
TEST_REQUIRE_INT(raised->status, AKERR_OUTOFBOUNDS);
|
||||||
|
test_discard_error(raised);
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief After unbinding, the name is refused rather than read.
|
* @brief After unbinding, the name is refused rather than read.
|
||||||
*
|
*
|
||||||
@@ -258,6 +324,7 @@ int main(void)
|
|||||||
test_conversion_refuses_rather_than_truncates();
|
test_conversion_refuses_rather_than_truncates();
|
||||||
test_copy_versus_point();
|
test_copy_versus_point();
|
||||||
test_suffix_must_match_the_c_type();
|
test_suffix_must_match_the_c_type();
|
||||||
|
test_registration_name_limits();
|
||||||
test_unbind_refuses_later_reads();
|
test_unbind_refuses_later_reads();
|
||||||
test_registration_survives_a_rerun();
|
test_registration_survives_a_rerun();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
10 REM A DO EACH takes its condition on the LOOP, not on the DO line.
|
||||||
|
20 GEN ONE(N#)
|
||||||
|
30 EMIT N#
|
||||||
|
40 END GEN
|
||||||
|
50 DO EACH V# IN ONE(1) WHILE V# < 9
|
||||||
|
60 PRINT V#
|
||||||
|
70 LOOP
|
||||||
|
80 PRINT "UNREACHABLE"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
? 50 : PARSE ERROR DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here
|
||||||
|
|
||||||
15
tests/language/flowcontrol/generators_loop_condition.bas
Normal file
15
tests/language/flowcontrol/generators_loop_condition.bas
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
10 REM A WHILE or UNTIL on the LOOP composes with DO EACH: it is checked
|
||||||
|
20 REM after each trip through the body, with the loop variable still
|
||||||
|
30 REM holding that trip's value. Stopping abandons the generator cleanly.
|
||||||
|
40 GEN COUNTUP(N#)
|
||||||
|
50 FOR I# = 1 TO N#
|
||||||
|
60 EMIT I#
|
||||||
|
70 NEXT I#
|
||||||
|
80 END GEN
|
||||||
|
90 DO EACH V# IN COUNTUP(10)
|
||||||
|
100 PRINT V#
|
||||||
|
110 LOOP UNTIL V# = 3
|
||||||
|
120 DO EACH W# IN COUNTUP(4)
|
||||||
|
130 PRINT W# * 10
|
||||||
|
140 LOOP WHILE W# < 3
|
||||||
|
150 PRINT "DONE"
|
||||||
7
tests/language/flowcontrol/generators_loop_condition.txt
Normal file
7
tests/language/flowcontrol/generators_loop_condition.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
10
|
||||||
|
20
|
||||||
|
30
|
||||||
|
DONE
|
||||||
15
tests/language/flowcontrol/generators_return.bas
Normal file
15
tests/language/flowcontrol/generators_return.bas
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
10 REM RETURN ends a GEN early, exactly as END GEN would: a GEN is a
|
||||||
|
20 REM function at heart, and only EMIT is different about it. Like a
|
||||||
|
30 REM GOSUB's or DEF's RETURN, it must stand in the GEN's own scope,
|
||||||
|
40 REM not inside a FOR or DO the body opened.
|
||||||
|
50 GEN FIRSTFEW(N#)
|
||||||
|
60 EMIT 1
|
||||||
|
70 IF N# < 2 THEN RETURN
|
||||||
|
80 EMIT 2
|
||||||
|
90 IF N# < 3 THEN RETURN
|
||||||
|
100 EMIT 3
|
||||||
|
110 END GEN
|
||||||
|
120 FOR EACH V# IN FIRSTFEW(2)
|
||||||
|
130 PRINT V#
|
||||||
|
140 NEXT V#
|
||||||
|
150 PRINT "DONE"
|
||||||
3
tests/language/flowcontrol/generators_return.txt
Normal file
3
tests/language/flowcontrol/generators_return.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
1
|
||||||
|
2
|
||||||
|
DONE
|
||||||
Reference in New Issue
Block a user