/** * @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 #include #include #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 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_called_like_a_function(); test_emit_outside_gen(); test_self_recursion_refused(); test_sibling_invocations_allowed(); return akbasic_test_failures; }