Compare commits
1 Commits
main
...
57-impleme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cb68665d0 |
@@ -698,6 +698,26 @@ as a commit co-author, and this repository follows the same rule.
|
||||
Each dependency carries its own `AGENTS.md` with authoritative per-repo rules. Read the
|
||||
relevant one before editing a submodule.
|
||||
|
||||
### Abandoned generators
|
||||
|
||||
The one invariant generators (`GEN`/`EMIT`, `FOR EACH`/`DO EACH`) add to the environment
|
||||
pool: **a suspended generator hangs off its loop scope's `forGeneratorEnv` as a *child*,
|
||||
off the parent chain, so no walk up `->parent` ever finds it.** Any path that discards a
|
||||
loop scope — `EXIT`, a mismatched `NEXT`, a `LOOP` condition saying stop, an error unwind
|
||||
through `akbasic_runtime_pump_generator()` or `akbasic_runtime_call_function()` — must
|
||||
release that generator too, or its pool slot is stranded until the next `RUN`; with
|
||||
twelve slots, a loop that abandons a dozen of them kills the program with "Environment
|
||||
pool exhausted" far from the cause.
|
||||
|
||||
Two functions own the invariant, and every discard path goes through one of them:
|
||||
`akbasic_runtime_release_generator()` (walks a detached generator chain up through its
|
||||
call frame, recursing into any `forGeneratorEnv` it passes) and
|
||||
`akbasic_runtime_unwind_to_environment()` (pops the *active* chain down to a target,
|
||||
releasing each popped scope's suspended generator on the way). If you add a new path
|
||||
that pops or discards environments, use one of these — a bare
|
||||
`akbasic_runtime_prev_environment()` loop reintroduces the leak, and
|
||||
`tests/generators.c` holds the pool-exhaustion tests that will say so.
|
||||
|
||||
---
|
||||
|
||||
## Editing the documentation
|
||||
|
||||
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
|
||||
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**
|
||||
|
||||
@@ -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` --
|
||||
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.
|
||||
refused: unlike a function call, which runs to completion and returns, the outer
|
||||
invocation is suspended mid-body waiting on the same loop, and there is no answer to
|
||||
"which EMIT feeds which loop" that is not a surprise.
|
||||
|
||||
`EXIT` leaves a `FOR EACH`/`DO EACH` loop early, exactly as it does a plain `FOR` or
|
||||
`DO`, and the generator it was consuming stops there -- nothing forces the rest of it to
|
||||
@@ -224,6 +225,59 @@ run just because the loop started it:
|
||||
STOPPED
|
||||
```
|
||||
|
||||
`RETURN` ends a `GEN` from the inside, exactly as it ends a multi-line `DEF` or a
|
||||
`GOSUB`: the generator is done, the loop consuming it ends, and the program carries on
|
||||
after the loop. What a generator's `RETURN` cannot do is carry a value -- values leave
|
||||
a `GEN` one at a time, through `EMIT`, and `RETURN 99` inside one is an error. Like a
|
||||
`GOSUB`'s or a `DEF`'s, the `RETURN` must stand in the `GEN`'s own scope: from inside
|
||||
a `FOR` or `DO` the body opened, it is an error, though `IF ... THEN RETURN` is fine
|
||||
because `IF` opens no scope of its own.
|
||||
|
||||
```basic
|
||||
10 GEN FIRSTFEW(N#)
|
||||
20 EMIT 1
|
||||
30 IF N# < 2 THEN RETURN
|
||||
40 EMIT 2
|
||||
50 IF N# < 3 THEN RETURN
|
||||
60 EMIT 3
|
||||
70 END GEN
|
||||
80 FOR EACH V# IN FIRSTFEW(2)
|
||||
90 PRINT V#
|
||||
100 NEXT V#
|
||||
110 PRINT "DONE"
|
||||
```
|
||||
|
||||
```output
|
||||
1
|
||||
2
|
||||
DONE
|
||||
```
|
||||
|
||||
A `DO EACH`'s `LOOP` may carry a `WHILE` or `UNTIL`, and the two compose: the condition
|
||||
is checked after each trip through the body, with the loop variable still holding that
|
||||
trip's value, and a condition that says stop abandons the rest of the generator exactly
|
||||
as `EXIT` does. The condition belongs on the `LOOP` -- putting it on the `DO` line is
|
||||
an error, since the `DO` line already says what the loop consumes.
|
||||
|
||||
```basic
|
||||
10 GEN COUNTUP(N#)
|
||||
20 FOR I# = 1 TO N#
|
||||
30 EMIT I#
|
||||
40 NEXT I#
|
||||
50 END GEN
|
||||
60 DO EACH V# IN COUNTUP(10)
|
||||
70 PRINT V#
|
||||
80 LOOP UNTIL V# = 3
|
||||
90 PRINT "STOPPED"
|
||||
```
|
||||
|
||||
```output
|
||||
1
|
||||
2
|
||||
3
|
||||
STOPPED
|
||||
```
|
||||
|
||||
A `GEN` shares its namespace with `DEF` -- the same name cannot be both -- but it is not
|
||||
a function and cannot be called like one: `X# = COUNTUP(3)` is refused, because nothing
|
||||
about an ordinary call means "resume where the last `EMIT` left off." `FOR EACH`/`DO
|
||||
|
||||
@@ -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. |
|
||||
| `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]`, `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. |
|
||||
| `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. |
|
||||
| `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. |
|
||||
| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. |
|
||||
| `RESUME` | `RESUME [NEXT | line]` | Return from a `TRAP` handler. |
|
||||
| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. |
|
||||
| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. Inside a `GEN`, a bare `RETURN` ends the generator early; `RETURN expr` there is an error. |
|
||||
| `RUN` | `RUN [line]` | Run the program, optionally from a line. |
|
||||
| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. |
|
||||
| `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. |
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
|
||||
@@ -337,6 +337,19 @@ akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **d
|
||||
|
||||
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||
|
||||
/*
|
||||
* Same guard as akbasic_parse_for()'s EACH branch, with the likely
|
||||
* mistake named: a condition belongs on the LOOP, where it is
|
||||
* checked against each emitted value, not here on the DO.
|
||||
*/
|
||||
peeked = akbasic_parser_peek(parser);
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(peeked != NULL &&
|
||||
peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||
peeked->tokentype != AKBASIC_TOK_COLON),
|
||||
AKBASIC_ERR_SYNTAX,
|
||||
"DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here");
|
||||
|
||||
newenv->isDoLoop = true;
|
||||
newenv->isEachLoop = true;
|
||||
newenv->loopFirstLine = firstline;
|
||||
@@ -1098,6 +1111,21 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **
|
||||
|
||||
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||
|
||||
/*
|
||||
* Nothing may follow the generator call but another statement.
|
||||
* Without this, a stray clause sits unparsed on the line and only
|
||||
* blows up after the whole loop has run, when the parent scope
|
||||
* resumes the line mid-statement -- an error at the loop's end
|
||||
* pointing at its beginning.
|
||||
*/
|
||||
peeked = akbasic_parser_peek(parser);
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(peeked != NULL &&
|
||||
peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||
peeked->tokentype != AKBASIC_TOK_COLON),
|
||||
AKBASIC_ERR_SYNTAX,
|
||||
"FOR EACH takes nothing after the generator call");
|
||||
|
||||
newenv->isEachLoop = true;
|
||||
newenv->loopFirstLine = firstline;
|
||||
/*
|
||||
|
||||
@@ -208,6 +208,38 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
||||
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 -- */
|
||||
|
||||
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
|
||||
* twelve-slot environment pool after twelve dead calls and every
|
||||
* call after that fails for a reason nobody can see in the script.
|
||||
* The unwind, not a bare prev_environment() loop, because a body that
|
||||
* died inside a FOR EACH leaves a suspended generator hanging off the
|
||||
* loop scope, and only the unwind knows to take it down too.
|
||||
*/
|
||||
while ( obj->environment != targetenv && obj->environment->parent != NULL ) {
|
||||
IGNORE(akbasic_runtime_prev_environment(obj));
|
||||
}
|
||||
IGNORE(akbasic_runtime_unwind_to_environment(obj, targetenv));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
PASS(errctx, akbasic_environment_new_value(targetenv, &out));
|
||||
|
||||
@@ -161,8 +161,22 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
/*
|
||||
* A GEN is a function at heart, and RETURN ends it the way it ends a DEF
|
||||
* or a GOSUB: early, cleanly, from its own frame. What a generator's
|
||||
* RETURN cannot do is carry a value -- values leave a GEN one at a time,
|
||||
* through EMIT, and there is no caller waiting on a return slot.
|
||||
*/
|
||||
if ( obj->environment->isGenerator ) {
|
||||
FAIL_NONZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_STATE,
|
||||
"A GEN yields values through EMIT; RETURN here takes none");
|
||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||
obj->environment->forGeneratorEnv = NULL;
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
|
||||
"RETURN outside the context of GOSUB");
|
||||
"RETURN outside the context of GOSUB, DEF, or GEN");
|
||||
|
||||
if ( expr != NULL && expr->right != NULL ) {
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
|
||||
|
||||
@@ -68,9 +68,7 @@ akerr_ErrorContext *akbasic_runtime_pump_generator(akbasic_Runtime *obj, akbasic
|
||||
* among the scopes just released.
|
||||
*/
|
||||
if ( obj->environment != loopenv ) {
|
||||
while ( obj->environment != loopenv && obj->environment->parent != NULL ) {
|
||||
IGNORE(akbasic_runtime_prev_environment(obj));
|
||||
}
|
||||
IGNORE(akbasic_runtime_unwind_to_environment(obj, loopenv));
|
||||
loopenv->forGeneratorEnv = NULL;
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
@@ -103,6 +101,18 @@ akerr_ErrorContext *akbasic_runtime_release_generator(akbasic_Runtime *obj, akba
|
||||
while ( walk != NULL ) {
|
||||
isgen = walk->isGenerator;
|
||||
next = walk->parent;
|
||||
/*
|
||||
* A scope between the resume point and the call frame may be an EACH
|
||||
* loop with its *own* generator suspended off to the side. Releasing
|
||||
* the loop scope without releasing that generator strands it in the
|
||||
* pool -- the walk goes through parents and a suspended generator is a
|
||||
* child. Guarded on `used` so a generator already released as part of
|
||||
* some enclosing teardown is not released twice.
|
||||
*/
|
||||
if ( walk->forGeneratorEnv != NULL && walk->forGeneratorEnv->used ) {
|
||||
PASS(errctx, akbasic_runtime_release_generator(obj, walk->forGeneratorEnv));
|
||||
}
|
||||
walk->forGeneratorEnv = NULL;
|
||||
PASS(errctx, akbasic_runtime_release_environment(obj, walk));
|
||||
if ( isgen ) {
|
||||
break;
|
||||
|
||||
@@ -149,11 +149,27 @@ akerr_ErrorContext *akbasic_cmd_loop(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
||||
akbasic_Environment *loopenv = obj->environment;
|
||||
|
||||
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;
|
||||
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||
}
|
||||
again = (loopenv->forGeneratorEnv != NULL);
|
||||
again = (again && loopenv->forGeneratorEnv != NULL);
|
||||
} else {
|
||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||
/*
|
||||
|
||||
@@ -119,6 +119,137 @@ static void test_exit_partway_through(void)
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Abandoning a generator that is itself suspended inside a FOR EACH
|
||||
* over another generator releases the inner generator too.
|
||||
*
|
||||
* The inner generator's environment hangs off the *loop* scope inside OUTER's
|
||||
* body as a child, off the parent chain -- the one place a bare parent walk
|
||||
* never looks. Before akbasic_runtime_release_generator() recursed into
|
||||
* `forGeneratorEnv`, every trip through this loop stranded one pool slot and
|
||||
* the 13th trip died with "Environment pool exhausted".
|
||||
*/
|
||||
static void test_exit_releases_nested_generators(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN INNER(N#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 EMIT 2\n"
|
||||
"40 END GEN\n"
|
||||
"50 GEN OUTER(N#)\n"
|
||||
"60 FOR EACH I# IN INNER(0)\n"
|
||||
"70 EMIT I#\n"
|
||||
"80 NEXT I#\n"
|
||||
"90 END GEN\n"
|
||||
"100 FOR K# = 1 TO 40\n"
|
||||
"110 FOR EACH V# IN OUTER(0)\n"
|
||||
"120 EXIT\n"
|
||||
"130 NEXT V#\n"
|
||||
"140 NEXT K#\n"
|
||||
"150 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A LOOP UNTIL that stops a DO EACH early releases the generator it
|
||||
* abandons, every time.
|
||||
*/
|
||||
static void test_loop_condition_releases_generator(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||
"20 FOR I# = 1 TO N#\n"
|
||||
"30 EMIT I#\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR K# = 1 TO 40\n"
|
||||
"70 DO EACH V# IN COUNTUP(10)\n"
|
||||
"80 LOOP UNTIL V# = 2\n"
|
||||
"90 NEXT K#\n"
|
||||
"100 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RETURN standing in a GEN's own frame ends the generator early,
|
||||
* exactly as END GEN would -- a GEN is a function at heart.
|
||||
*/
|
||||
static void test_return_ends_generator(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program("10 GEN G(N#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 RETURN\n"
|
||||
"40 EMIT 2\n"
|
||||
"50 END GEN\n"
|
||||
"60 FOR EACH V# IN G(0)\n"
|
||||
"70 PRINT V#\n"
|
||||
"80 NEXT V#\n"
|
||||
"90 PRINT \"DONE\"\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\nDONE\n");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RETURN with a value inside a GEN is refused: values leave a GEN one
|
||||
* at a time, through EMIT, and there is no return slot waiting.
|
||||
*/
|
||||
static void test_return_value_in_generator_refused(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(run_program_bounded("10 GEN G(N#)\n"
|
||||
"20 EMIT 1\n"
|
||||
"30 RETURN 99\n"
|
||||
"40 END GEN\n"
|
||||
"50 FOR EACH V# IN G(0)\n"
|
||||
"60 PRINT V#\n"
|
||||
"70 NEXT V#\n"
|
||||
"80 PRINT \"UNREACHABLE\"\n", 2000));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "1\n") != NULL,
|
||||
"expected the first EMIT in \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
||||
"RETURN with a value must stop the run, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The unwind primitive releases a popped scope's suspended generator.
|
||||
*
|
||||
* Built by hand rather than through BASIC because the paths that need this --
|
||||
* the error unwinds in pump_generator() and call_function() -- only trigger
|
||||
* on C-level failures a program cannot politely ask for. The shape is the
|
||||
* one EMIT leaves behind: a loop scope holding a detached generator child,
|
||||
* with a further scope active above it.
|
||||
*/
|
||||
static void test_unwind_releases_suspended_generators(void)
|
||||
{
|
||||
akbasic_Environment *root = NULL;
|
||||
akbasic_Environment *loopenv = NULL;
|
||||
akbasic_Environment *genenv = NULL;
|
||||
akbasic_Environment *forenv = NULL;
|
||||
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
root = HARNESS_RUNTIME.environment;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||
loopenv = HARNESS_RUNTIME.environment;
|
||||
loopenv->isEachLoop = true;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||
genenv = HARNESS_RUNTIME.environment;
|
||||
genenv->isGenerator = true;
|
||||
TEST_REQUIRE_OK(akbasic_runtime_detach_environment(&HARNESS_RUNTIME));
|
||||
loopenv->forGeneratorEnv = genenv;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||
forenv = HARNESS_RUNTIME.environment;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_unwind_to_environment(&HARNESS_RUNTIME, root));
|
||||
TEST_REQUIRE(HARNESS_RUNTIME.environment == root,
|
||||
"unwind must land on the target scope");
|
||||
TEST_REQUIRE(!forenv->used && !loopenv->used && !genenv->used,
|
||||
"unwind must release the chain and the suspended generator");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A GEN invoked like an ordinary function, rather than through FOR
|
||||
* EACH/DO EACH, fails cleanly.
|
||||
@@ -216,6 +347,11 @@ int main(void)
|
||||
test_exit_releases_generator_for_each();
|
||||
test_exit_releases_generator_do_each();
|
||||
test_exit_partway_through();
|
||||
test_exit_releases_nested_generators();
|
||||
test_loop_condition_releases_generator();
|
||||
test_return_ends_generator();
|
||||
test_return_value_in_generator_refused();
|
||||
test_unwind_releases_suspended_generators();
|
||||
test_called_like_a_function();
|
||||
test_emit_outside_gen();
|
||||
test_self_recursion_refused();
|
||||
|
||||
@@ -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