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>
361 lines
12 KiB
C
361 lines
12 KiB
C
/**
|
|
* @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 Abandoning a generator that is itself suspended inside a FOR EACH
|
|
* over another generator releases the inner generator too.
|
|
*
|
|
* The inner generator's environment hangs off the *loop* scope inside OUTER's
|
|
* body as a child, off the parent chain -- the one place a bare parent walk
|
|
* never looks. Before akbasic_runtime_release_generator() recursed into
|
|
* `forGeneratorEnv`, every trip through this loop stranded one pool slot and
|
|
* the 13th trip died with "Environment pool exhausted".
|
|
*/
|
|
static void test_exit_releases_nested_generators(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 GEN INNER(N#)\n"
|
|
"20 EMIT 1\n"
|
|
"30 EMIT 2\n"
|
|
"40 END GEN\n"
|
|
"50 GEN OUTER(N#)\n"
|
|
"60 FOR EACH I# IN INNER(0)\n"
|
|
"70 EMIT I#\n"
|
|
"80 NEXT I#\n"
|
|
"90 END GEN\n"
|
|
"100 FOR K# = 1 TO 40\n"
|
|
"110 FOR EACH V# IN OUTER(0)\n"
|
|
"120 EXIT\n"
|
|
"130 NEXT V#\n"
|
|
"140 NEXT K#\n"
|
|
"150 PRINT \"DONE\"\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A LOOP UNTIL that stops a DO EACH early releases the generator it
|
|
* abandons, every time.
|
|
*/
|
|
static void test_loop_condition_releases_generator(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
|
"20 FOR I# = 1 TO N#\n"
|
|
"30 EMIT I#\n"
|
|
"40 NEXT I#\n"
|
|
"50 END GEN\n"
|
|
"60 FOR K# = 1 TO 40\n"
|
|
"70 DO EACH V# IN COUNTUP(10)\n"
|
|
"80 LOOP UNTIL V# = 2\n"
|
|
"90 NEXT K#\n"
|
|
"100 PRINT \"DONE\"\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief RETURN standing in a GEN's own frame ends the generator early,
|
|
* exactly as END GEN would -- a GEN is a function at heart.
|
|
*/
|
|
static void test_return_ends_generator(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 GEN G(N#)\n"
|
|
"20 EMIT 1\n"
|
|
"30 RETURN\n"
|
|
"40 EMIT 2\n"
|
|
"50 END GEN\n"
|
|
"60 FOR EACH V# IN G(0)\n"
|
|
"70 PRINT V#\n"
|
|
"80 NEXT V#\n"
|
|
"90 PRINT \"DONE\"\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\nDONE\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief RETURN with a value inside a GEN is refused: values leave a GEN one
|
|
* at a time, through EMIT, and there is no return slot waiting.
|
|
*/
|
|
static void test_return_value_in_generator_refused(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program_bounded("10 GEN G(N#)\n"
|
|
"20 EMIT 1\n"
|
|
"30 RETURN 99\n"
|
|
"40 END GEN\n"
|
|
"50 FOR EACH V# IN G(0)\n"
|
|
"60 PRINT V#\n"
|
|
"70 NEXT V#\n"
|
|
"80 PRINT \"UNREACHABLE\"\n", 2000));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "1\n") != NULL,
|
|
"expected the first EMIT in \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
|
"RETURN with a value must stop the run, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief The unwind primitive releases a popped scope's suspended generator.
|
|
*
|
|
* Built by hand rather than through BASIC because the paths that need this --
|
|
* the error unwinds in pump_generator() and call_function() -- only trigger
|
|
* on C-level failures a program cannot politely ask for. The shape is the
|
|
* one EMIT leaves behind: a loop scope holding a detached generator child,
|
|
* with a further scope active above it.
|
|
*/
|
|
static void test_unwind_releases_suspended_generators(void)
|
|
{
|
|
akbasic_Environment *root = NULL;
|
|
akbasic_Environment *loopenv = NULL;
|
|
akbasic_Environment *genenv = NULL;
|
|
akbasic_Environment *forenv = NULL;
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
root = HARNESS_RUNTIME.environment;
|
|
|
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
|
loopenv = HARNESS_RUNTIME.environment;
|
|
loopenv->isEachLoop = true;
|
|
|
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
|
genenv = HARNESS_RUNTIME.environment;
|
|
genenv->isGenerator = true;
|
|
TEST_REQUIRE_OK(akbasic_runtime_detach_environment(&HARNESS_RUNTIME));
|
|
loopenv->forGeneratorEnv = genenv;
|
|
|
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
|
forenv = HARNESS_RUNTIME.environment;
|
|
|
|
TEST_REQUIRE_OK(akbasic_runtime_unwind_to_environment(&HARNESS_RUNTIME, root));
|
|
TEST_REQUIRE(HARNESS_RUNTIME.environment == root,
|
|
"unwind must land on the target scope");
|
|
TEST_REQUIRE(!forenv->used && !loopenv->used && !genenv->used,
|
|
"unwind must release the chain and the suspended generator");
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A GEN invoked like an ordinary function, rather than through FOR
|
|
* EACH/DO EACH, fails cleanly.
|
|
*
|
|
* EMIT requires `isGenerator`, which only a FOR EACH/DO EACH invocation sets
|
|
* -- an ordinary call pushes a plain environment, exactly as a DEF's does --
|
|
* so the first EMIT the call reaches is where this is refused.
|
|
*/
|
|
static void test_called_like_a_function(void)
|
|
{
|
|
akbasic_Value *args[1];
|
|
akbasic_Value argvalue;
|
|
akbasic_Value *out = NULL;
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
|
"10 GEN ONE(N#)\n"
|
|
"20 EMIT N#\n"
|
|
"30 END GEN\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 100));
|
|
|
|
TEST_REQUIRE_OK(akbasic_value_zero(&argvalue));
|
|
argvalue.valuetype = AKBASIC_TYPE_INTEGER;
|
|
argvalue.intval = 5;
|
|
args[0] = &argvalue;
|
|
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_call_function(&HARNESS_RUNTIME, "ONE", args, 1, &out));
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief EMIT reached with no enclosing GEN invocation is refused. */
|
|
static void test_emit_outside_gen(void)
|
|
{
|
|
akbasic_ASTLeaf *leaf = NULL;
|
|
akbasic_Value *out = NULL;
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
|
TEST_REQUIRE_OK(harness_parse("EMIT 5", &leaf));
|
|
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A GEN invoking itself, directly, is refused rather than recursing.
|
|
*
|
|
* Bounded tightly: a program that recursed forever would hang the whole
|
|
* suite, and this is exactly the case that must not.
|
|
*/
|
|
static void test_self_recursion_refused(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program_bounded("10 GEN RECURSIVE(N#)\n"
|
|
"20 FOR EACH X# IN RECURSIVE(N# + 1)\n"
|
|
"30 EMIT X#\n"
|
|
"40 NEXT X#\n"
|
|
"50 END GEN\n"
|
|
"60 PRINT \"BEFORE\"\n"
|
|
"70 FOR EACH R# IN RECURSIVE(1)\n"
|
|
"80 PRINT R#\n"
|
|
"90 NEXT R#\n"
|
|
"100 PRINT \"UNREACHABLE\"\n", 2000));
|
|
/* Whatever else happened, the line before the recursive call ran and the
|
|
one two lines after invoking it -- which would only print after the
|
|
loop completed successfully -- did not. */
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "BEFORE\n") != NULL,
|
|
"expected \"BEFORE\" in \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
|
"self-recursion must not reach \"UNREACHABLE\", got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief Independent (sibling) FOR EACH invocations of the same GEN are not
|
|
* self-recursion, even nested.
|
|
*/
|
|
static void test_sibling_invocations_allowed(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
|
"20 FOR I# = 1 TO N#\n"
|
|
"30 EMIT I#\n"
|
|
"40 NEXT I#\n"
|
|
"50 END GEN\n"
|
|
"60 FOR EACH A# IN COUNTUP(2)\n"
|
|
"70 FOR EACH B# IN COUNTUP(2)\n"
|
|
"80 PRINT A# * 10 + B#\n"
|
|
"90 NEXT B#\n"
|
|
"100 NEXT A#\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "11\n12\n21\n22\n");
|
|
harness_stop();
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_room_objects_smoke();
|
|
test_exit_releases_generator_for_each();
|
|
test_exit_releases_generator_do_each();
|
|
test_exit_partway_through();
|
|
test_exit_releases_nested_generators();
|
|
test_loop_condition_releases_generator();
|
|
test_return_ends_generator();
|
|
test_return_value_in_generator_refused();
|
|
test_unwind_releases_suspended_generators();
|
|
test_called_like_a_function();
|
|
test_emit_outside_gen();
|
|
test_self_recursion_refused();
|
|
test_sibling_invocations_allowed();
|
|
return akbasic_test_failures;
|
|
}
|