/** * @file value_pool.c * @brief Tests that creating a name inside a scope costs the value pool nothing. * * This is the defect a Breakout written against the interpreter died of after * twenty-five seconds, and nothing in either corpus could have found it: the * pool holds 4096 values, so it takes *thousands* of scope entries to see, and * nothing else here runs that long. * * The cause was that scope exit returns the variable *slot* and not its storage. * akbasic_runtime_prev_environment() marks the variable unused, * akbasic_runtime_new_variable() memsets the slot it hands back -- clearing * `values` -- and akbasic_variable_init() therefore drew fresh slots for a * variable whose old ones were still counted. Every GOSUB that created a local * leaked, with no diagnostic until the pool ran dry on whichever line happened * to be unlucky. * * A scalar now lives in the variable itself, so the pool is not involved at all. * Two of the counts below are past the old 4096 ceiling on purpose -- under it, * they would pass against the broken interpreter too. The third takes the * opposite approach and asks for the whole pool after only a few scopes, which * is cheaper and sharper: one leaked slot and it has nowhere to go. */ #include #include #include #include "harness.h" #include "testutil.h" /** @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, 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, 2000000)); SUCCEED_RETURN(errctx); } /** * @brief 6,000 GOSUBs, each creating a local, cost nothing. * * TODO.md section 6 item 30's reduction, unchanged. It used to fail at * `LOC# = 1` on the 4091st call with "Array of 1 elements does not fit in the * 0 remaining value slots" -- naming the line that was unlucky rather than the * line that was wrong, which is most of why it cost an evening to find. */ static void test_scoped_scalar_costs_nothing(void) { TEST_REQUIRE_OK(run_program("10 X# = 0\n" "20 FOR T# = 1 TO 6000\n" "30 GOSUB SUBA\n" "40 NEXT T#\n" "50 PRINT \"OK \" + X#\n" "60 END\n" "70 LABEL SUBA\n" "80 LOC# = 1\n" "90 X# = X# + LOC#\n" "100 RETURN\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK 6000\n"); harness_stop(); } /** * @brief A `FOR` counter is a name like any other, and costs nothing either. * * Worth its own case: the counter is created by the loop rather than by an * assignment the author wrote, so it was the half of this defect that a program * could hit without appearing to create anything at all. * * `TO 2` rather than `TO 1` because equal bounds run the body zero times in this * dialect -- see docs/13-differences.md. A body that never runs would pass this * test for the wrong reason. */ static void test_scoped_loop_counter_costs_nothing(void) { TEST_REQUIRE_OK(run_program("10 X# = 0\n" "20 FOR T# = 1 TO 6000\n" "30 GOSUB SUBA\n" "40 NEXT T#\n" "50 PRINT \"OK \" + X#\n" "60 END\n" "70 LABEL SUBA\n" "80 FOR INNER# = 1 TO 2\n" "90 X# = X# + 1\n" "100 NEXT INNER#\n" "110 RETURN\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK 12000\n"); harness_stop(); } /** * @brief The pool is genuinely untouched, not merely large enough. * * Two hundred scope entries and then the pool's *entire* width -- four arrays, * because AKBASIC_MAX_ARRAY_ELEMENTS caps any one of them at 1024 while the pool * holds 4096. One leaked slot and the fourth `DIM` has nowhere to go, which * makes this the sharpest of the three and by far the cheapest: the two above * prove a program finishes, this proves nothing at all was spent. The long ones * stay because they are the shape the defect was found in. */ static void test_pool_is_untouched_by_scopes(void) { TEST_REQUIRE_OK(run_program("10 FOR T# = 1 TO 200\n" "20 GOSUB SUBA\n" "30 NEXT T#\n" "40 DIM A#(1024)\n" "50 DIM B#(1024)\n" "60 DIM C#(1024)\n" "70 DIM D#(1024)\n" "80 D#(1023) = 7\n" "90 PRINT D#(1023)\n" "100 END\n" "110 LABEL SUBA\n" "120 LOC# = 1\n" "130 RETURN\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "7\n"); harness_stop(); } /** * @brief A structure keeps pool storage, which is what a pointer relies on. * * The exclusion that makes the fix safe. docs/16-structures.md says nothing is * reclaimed and akbasic_runtime_prev_environment() says a pointer into a record * DIMmed in a scope stays sound after the scope is gone -- so a `@` name must * *not* move into the variable, where the next occupant of the slot would * overwrite it. A single-field TYPE is the case that would slip through a test * written against size alone. */ static void test_structure_storage_stays_in_the_pool(void) { akbasic_Variable *variable = NULL; TEST_REQUIRE_OK(run_program("10 TYPE ONE\n" "20 N#\n" "30 END TYPE\n" "40 DIM R@ AS ONE\n" "50 R@.N# = 5\n" "60 PRINT R@.N#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n"); TEST_REQUIRE_OK(akbasic_environment_get(HARNESS_RUNTIME.environment, "R@", &variable)); TEST_REQUIRE(variable != NULL, "the structure variable must exist"); TEST_REQUIRE(variable->values != &variable->inlinevalue, "a structure must keep pool storage, not inline storage"); harness_stop(); } /** * @brief A scalar's storage is inside the variable, and an array's is not. * * Asserted directly rather than only through a program, so the mechanism is * pinned and not just its consequence. A future change that made arrays inline * would pass every test above and break the pointer guarantee. */ static void test_storage_lands_where_it_should(void) { akbasic_Variable *scalar = NULL; akbasic_Variable *array = NULL; TEST_REQUIRE_OK(run_program("10 S# = 1\n" "20 DIM A#(4)\n")); TEST_REQUIRE_OK(akbasic_environment_get(HARNESS_RUNTIME.environment, "S#", &scalar)); TEST_REQUIRE(scalar != NULL, "the scalar must exist"); TEST_REQUIRE(scalar->values == &scalar->inlinevalue, "a scalar must be stored in the variable"); TEST_REQUIRE_OK(akbasic_environment_get(HARNESS_RUNTIME.environment, "A#", &array)); TEST_REQUIRE(array != NULL, "the array must exist"); TEST_REQUIRE(array->values != &array->inlinevalue, "an array must be stored in the pool"); harness_stop(); } /** * @brief `SWAP` exchanges two scalars, whose storage moves with the record. * * A regression rather than a feature: SWAP copies the whole variable record, so * the `values` pointer that comes over names the *other* variable's inline slot * -- which by then holds this variable's own old value. Left alone each side * reads back what it started with and SWAP silently does nothing, which is * exactly what the housekeeping golden case caught. */ static void test_swap_still_exchanges_scalars(void) { TEST_REQUIRE_OK(run_program("10 A# = 1 : B# = 2\n" "20 SWAP A#, B#\n" "30 PRINT A# : PRINT B#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "2\n1\n"); harness_stop(); } int main(void) { test_scoped_scalar_costs_nothing(); test_scoped_loop_counter_costs_nothing(); test_pool_is_untouched_by_scopes(); test_structure_storage_stays_in_the_pool(); test_storage_lands_where_it_should(); test_swap_still_exchanges_scalars(); return akbasic_test_failures; }