Files
akbasic/tests/generators.c

361 lines
12 KiB
C
Raw Normal View History

Implement generators: GEN, EMIT, END GEN, FOR EACH and DO EACH (issue 57) Adds generator support per the plan in issue 57: - environment.h: isGenerator/generatorFn on a GEN call's own environment, isEachLoop and forGeneratorEnv on a FOR EACH/DO EACH loop's own environment. - runtime.c: splits akbasic_runtime_prev_environment() into akbasic_runtime_detach_environment() (return to parent without releasing) and akbasic_runtime_release_environment() (give variables and the pool slot back, on any environment); prev_environment() is now the two in sequence. akbasic_runtime_call_function() refuses to call a GEN like an ordinary function. - verbs.c/verbs.h/scanner: new keywords GEN, EMIT, EACH, IN and the compound verb "END GEN" (built by a new akbasic_parse_end(), the same trick akbasic_parse_print() uses for PRINT #). - parser_commands.c: akbasic_parse_gen() (modelled on multi-line DEF), akbasic_parse_end(), and EACH branches in akbasic_parse_for()/ akbasic_parse_do(). - runtime_generator.c (new): akbasic_cmd_gen, akbasic_cmd_emit, akbasic_cmd_end_gen, and the invoke/pump/release machinery FOR EACH, DO EACH, NEXT and LOOP share. EMIT walks up to the nearest isGenerator ancestor rather than assuming it is standing directly in the GEN's own call frame, because a GEN body may nest its own FOR/DO/GOSUB around an EMIT -- the issue's own ROOMOBJECTS example does exactly that. - runtime_commands.c/runtime_structure.c: EACH branches in cmd_for/cmd_do, matching EACH branches in cmd_next/cmd_loop, and forGeneratorEnv release on every path that can abandon a live generator (EXIT, a NEXT that pops for a mismatched loop variable). Deviates from the plan in one place: FunctionDef gained an isGenerator flag (not in the plan's field list) because refusing a GEN called like a function has to happen before anything is pushed. Relying on EMIT's own isGenerator check for that case doesn't work: akbasic_runtime_call_function() drives its own step loop the same way akbasic_runtime_pump_generator() does, and a BASIC-level error inside that loop is swallowed by process_line_run() as reported-but-not-propagated, so the call would silently "succeed" with a meaningless return value instead of failing. Also: a zero-argument parameter list is not supported by the DEF/GEN parameter parser this reuses (a pre-existing limitation, not generator-specific); every generator in the tests takes at least one parameter as a result. Tests: tests/generators.c (pool exhaustion under repeated EXIT, calling a GEN like a function, EMIT outside a GEN, self-recursion, sibling/nested invocations) and tests/language/flowcontrol/generators_*.bas -- the issue's own ROOMOBJECTS example in both loop shapes, an empty generator, non-numeric EMIT, nested/interleaved invocations, and three error-path golden cases. Docs: control-flow chapter 4 gets a GEN/EMIT/FOR EACH/DO EACH section, the verb reference gets GEN/EMIT/END GEN entries and updated FOR/DO/NEXT/LOOP/EXIT rows, and architecture chapter 14 documents the detach/release split and the two-environment generator invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 10:02:43 -04:00
/**
* @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();
}
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>
2026-08-06 11:29:17 -04:00
/**
* @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();
}
Implement generators: GEN, EMIT, END GEN, FOR EACH and DO EACH (issue 57) Adds generator support per the plan in issue 57: - environment.h: isGenerator/generatorFn on a GEN call's own environment, isEachLoop and forGeneratorEnv on a FOR EACH/DO EACH loop's own environment. - runtime.c: splits akbasic_runtime_prev_environment() into akbasic_runtime_detach_environment() (return to parent without releasing) and akbasic_runtime_release_environment() (give variables and the pool slot back, on any environment); prev_environment() is now the two in sequence. akbasic_runtime_call_function() refuses to call a GEN like an ordinary function. - verbs.c/verbs.h/scanner: new keywords GEN, EMIT, EACH, IN and the compound verb "END GEN" (built by a new akbasic_parse_end(), the same trick akbasic_parse_print() uses for PRINT #). - parser_commands.c: akbasic_parse_gen() (modelled on multi-line DEF), akbasic_parse_end(), and EACH branches in akbasic_parse_for()/ akbasic_parse_do(). - runtime_generator.c (new): akbasic_cmd_gen, akbasic_cmd_emit, akbasic_cmd_end_gen, and the invoke/pump/release machinery FOR EACH, DO EACH, NEXT and LOOP share. EMIT walks up to the nearest isGenerator ancestor rather than assuming it is standing directly in the GEN's own call frame, because a GEN body may nest its own FOR/DO/GOSUB around an EMIT -- the issue's own ROOMOBJECTS example does exactly that. - runtime_commands.c/runtime_structure.c: EACH branches in cmd_for/cmd_do, matching EACH branches in cmd_next/cmd_loop, and forGeneratorEnv release on every path that can abandon a live generator (EXIT, a NEXT that pops for a mismatched loop variable). Deviates from the plan in one place: FunctionDef gained an isGenerator flag (not in the plan's field list) because refusing a GEN called like a function has to happen before anything is pushed. Relying on EMIT's own isGenerator check for that case doesn't work: akbasic_runtime_call_function() drives its own step loop the same way akbasic_runtime_pump_generator() does, and a BASIC-level error inside that loop is swallowed by process_line_run() as reported-but-not-propagated, so the call would silently "succeed" with a meaningless return value instead of failing. Also: a zero-argument parameter list is not supported by the DEF/GEN parameter parser this reuses (a pre-existing limitation, not generator-specific); every generator in the tests takes at least one parameter as a result. Tests: tests/generators.c (pool exhaustion under repeated EXIT, calling a GEN like a function, EMIT outside a GEN, self-recursion, sibling/nested invocations) and tests/language/flowcontrol/generators_*.bas -- the issue's own ROOMOBJECTS example in both loop shapes, an empty generator, non-numeric EMIT, nested/interleaved invocations, and three error-path golden cases. Docs: control-flow chapter 4 gets a GEN/EMIT/FOR EACH/DO EACH section, the verb reference gets GEN/EMIT/END GEN entries and updated FOR/DO/NEXT/LOOP/EXIT rows, and architecture chapter 14 documents the detach/release split and the two-environment generator invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 10:02:43 -04:00
/**
* @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();
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>
2026-08-06 11:29:17 -04:00
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();
Implement generators: GEN, EMIT, END GEN, FOR EACH and DO EACH (issue 57) Adds generator support per the plan in issue 57: - environment.h: isGenerator/generatorFn on a GEN call's own environment, isEachLoop and forGeneratorEnv on a FOR EACH/DO EACH loop's own environment. - runtime.c: splits akbasic_runtime_prev_environment() into akbasic_runtime_detach_environment() (return to parent without releasing) and akbasic_runtime_release_environment() (give variables and the pool slot back, on any environment); prev_environment() is now the two in sequence. akbasic_runtime_call_function() refuses to call a GEN like an ordinary function. - verbs.c/verbs.h/scanner: new keywords GEN, EMIT, EACH, IN and the compound verb "END GEN" (built by a new akbasic_parse_end(), the same trick akbasic_parse_print() uses for PRINT #). - parser_commands.c: akbasic_parse_gen() (modelled on multi-line DEF), akbasic_parse_end(), and EACH branches in akbasic_parse_for()/ akbasic_parse_do(). - runtime_generator.c (new): akbasic_cmd_gen, akbasic_cmd_emit, akbasic_cmd_end_gen, and the invoke/pump/release machinery FOR EACH, DO EACH, NEXT and LOOP share. EMIT walks up to the nearest isGenerator ancestor rather than assuming it is standing directly in the GEN's own call frame, because a GEN body may nest its own FOR/DO/GOSUB around an EMIT -- the issue's own ROOMOBJECTS example does exactly that. - runtime_commands.c/runtime_structure.c: EACH branches in cmd_for/cmd_do, matching EACH branches in cmd_next/cmd_loop, and forGeneratorEnv release on every path that can abandon a live generator (EXIT, a NEXT that pops for a mismatched loop variable). Deviates from the plan in one place: FunctionDef gained an isGenerator flag (not in the plan's field list) because refusing a GEN called like a function has to happen before anything is pushed. Relying on EMIT's own isGenerator check for that case doesn't work: akbasic_runtime_call_function() drives its own step loop the same way akbasic_runtime_pump_generator() does, and a BASIC-level error inside that loop is swallowed by process_line_run() as reported-but-not-propagated, so the call would silently "succeed" with a meaningless return value instead of failing. Also: a zero-argument parameter list is not supported by the DEF/GEN parameter parser this reuses (a pre-existing limitation, not generator-specific); every generator in the tests takes at least one parameter as a result. Tests: tests/generators.c (pool exhaustion under repeated EXIT, calling a GEN like a function, EMIT outside a GEN, self-recursion, sibling/nested invocations) and tests/language/flowcontrol/generators_*.bas -- the issue's own ROOMOBJECTS example in both loop shapes, an empty generator, non-numeric EMIT, nested/interleaved invocations, and three error-path golden cases. Docs: control-flow chapter 4 gets a GEN/EMIT/FOR EACH/DO EACH section, the verb reference gets GEN/EMIT/END GEN entries and updated FOR/DO/NEXT/LOOP/EXIT rows, and architecture chapter 14 documents the detach/release split and the two-environment generator invocation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 10:02:43 -04:00
test_called_like_a_function();
test_emit_outside_gen();
test_self_recursion_refused();
test_sibling_invocations_allowed();
return akbasic_test_failures;
}