210 lines
7.5 KiB
C
210 lines
7.5 KiB
C
|
|
/**
|
||
|
|
* @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 <string.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
#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;
|
||
|
|
}
|