Fix generator teardown leaks, add RETURN-in-GEN and LOOP conditions on DO EACH
Review findings and follow-ups from PR #61 review: - runtime_generator.c: akbasic_runtime_release_generator() now releases the forGeneratorEnv of every scope it walks through. Abandoning a generator that was itself suspended inside a FOR EACH over another generator stranded the inner generator's pool slot; a loop doing so exhausted the twelve-slot pool and died far from the cause. - runtime.c/runtime.h: new akbasic_runtime_unwind_to_environment(), the shared teardown for the error unwinds in pump_generator() and call_function() -- both previously bare prev_environment() loops with the same suspended-generator blindness. - runtime_commands.c: bare RETURN standing in a GEN's own frame ends the generator exactly as END GEN does -- a GEN is a function at heart. RETURN with a value there is refused (values leave a GEN only through EMIT). The no-frame error message now says "GOSUB, DEF, or GEN". - runtime_structure.c: LOOP WHILE/UNTIL composes with DO EACH -- checked after each trip with the loop variable still holding that trip's value; a condition that stops the loop abandons the generator exactly as EXIT does. Previously the condition was silently ignored, while the verb reference documented it as working. - parser_commands.c: trailing tokens after the generator call on a FOR EACH/DO EACH line are refused at parse. Previously they sat unparsed and blew up only after the loop completed, when the parent scope resumed the line mid-statement -- an error at the loop's end pointing at its start. - tests/generators.c: pool-exhaustion tests for the nested-abandonment and LOOP-condition paths, RETURN semantics tests, and a direct test of the unwind primitive. Three new golden pairs cover RETURN, LOOP conditions and the misplaced-condition parse error. - docs: RETURN and LOOP-condition semantics in 04-control-flow.md and 11-verb-reference.md; corrected the self-recursion analogy (functions are re-entrant here). TODO.md 1.10 records the generator design decisions the code comments were already citing, plus the zero-arg parameter-list limitation. MAINTENANCE.md gains the abandoned-generators invariant those comments also cited. Co-Authored-By: Andrew Kesterson <andrew@aklabs.net> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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