/** * @file hostvars.c * @brief Tests the host-facing variable exchange, from the host's side. * * TODO.md section 6 item 17: a host could not reliably *create* a script * variable while a script was suspended, and neither way of trying failed * loudly. Through akbasic_environment_get() the variable landed in whichever * scope happened to be active -- usually a `FOR` or `GOSUB` body -- and died * when that body popped, so the script read it correctly right up until it * stopped. Reaching for the root by hand returned NULL through `dest` without * raising, because that function only auto-creates in the active environment. * * akbasic_runtime_global() is the answer to both, and what is asserted here is * the part a host actually cares about: a value it set mid-run is still there * after the loop it was set from has finished. */ #include "harness.h" /** * @brief Step a program until it is suspended inside a nested scope. * * One step at a time until a child environment is active, rather than a fixed * budget: a step processes one *source line index*, and the interpreter files * lines under their BASIC line numbers, so `10 FOR` is eleven steps in and any * count written here would be a hostage to the line numbers in the fixture. * * Bounded so a program that never nests fails the assertion below rather than * hanging the suite. */ static akerr_ErrorContext AKERR_NOIGNORE *run_until_nested(const char *source) { PREPARE_ERROR(errctx); int steps = 0; PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source)); PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); while ( steps < AKBASIC_MAX_SOURCE_LINES && HARNESS_RUNTIME.mode != AKBASIC_MODE_QUIT && HARNESS_RUNTIME.environment->parent == NULL ) { PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1)); steps += 1; } SUCCEED_RETURN(errctx); } /** @brief Set a global to an integer, the way an embedding host would. */ static akerr_ErrorContext AKERR_NOIGNORE *set_global(const char *name, int64_t value) { PREPARE_ERROR(errctx); akbasic_Variable *variable = NULL; int64_t zerosubscript[1] = { 0 }; PASS(errctx, akbasic_runtime_global(&HARNESS_RUNTIME, name, &variable)); FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED, "runtime_global returned NULL for %s", name); PASS(errctx, akbasic_variable_set_integer(variable, value, zerosubscript, 1)); SUCCEED_RETURN(errctx); } /** * @brief A global created while suspended inside a FOR body outlives the loop. * * The case that motivated the whole entry. `H#` is created from outside while * the script sits in the loop, and line 40 -- which runs after `NEXT` has popped * the loop's scope -- has to still see it. */ static akerr_ErrorContext AKERR_NOIGNORE *test_global_survives_a_for_body(void) { PREPARE_ERROR(errctx); TEST_REQUIRE_OK(harness_start(NULL)); PASS(errctx, run_until_nested("10 FOR I# = 1 TO 3\n" "20 PRINT I#\n" "30 NEXT I#\n" "40 PRINT H#\n")); /* Suspended below the root, which is the state that used to break this. */ TEST_REQUIRE(HARNESS_RUNTIME.environment != NULL, "the runtime must have a scope"); TEST_REQUIRE(HARNESS_RUNTIME.environment->parent != NULL, "the script should be suspended inside the FOR body"); PASS(errctx, set_global("H#", 42)); PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0)); /* 1 2 3 from the loop, then 42 from after it. The 42 is the assertion. */ TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\n3\n42\n"); harness_stop(); SUCCEED_RETURN(errctx); } /** @brief The same from inside a GOSUB, which is the other nesting a host meets. */ static akerr_ErrorContext AKERR_NOIGNORE *test_global_survives_a_gosub(void) { PREPARE_ERROR(errctx); TEST_REQUIRE_OK(harness_start(NULL)); /* * The QUIT is not decoration. Without a terminator the program runs off the * end of line 20 into the subroutine again, and its RETURN then has no GOSUB * to return to -- which is ordinary BASIC and would only obscure what is * being asserted here. */ PASS(errctx, run_until_nested("10 GOSUB 100\n" "20 PRINT H#\n" "30 QUIT\n" "100 PRINT \"IN\"\n" "110 RETURN\n")); TEST_REQUIRE(HARNESS_RUNTIME.environment->parent != NULL, "the script should be suspended inside the subroutine"); PASS(errctx, set_global("H#", 7)); PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0)); TEST_REQUIRE_STR(HARNESS_OUTPUT, "IN\n7\n"); harness_stop(); SUCCEED_RETURN(errctx); } /** * @brief The NULL-without-error path is gone: this always creates or raises. * * akbasic_environment_get() against a non-active environment is the shape that * used to bite, and it is asserted here as well -- unchanged, because it is * still the right behaviour for *that* function and it is what makes * runtime_global() necessary rather than merely convenient. */ static akerr_ErrorContext AKERR_NOIGNORE *test_global_never_returns_null(void) { PREPARE_ERROR(errctx); akbasic_Environment *root = NULL; akbasic_Variable *variable = NULL; TEST_REQUIRE_OK(harness_start(NULL)); root = HARNESS_RUNTIME.environment; PASS(errctx, akbasic_runtime_new_environment(&HARNESS_RUNTIME)); /* The old way, with a child active: NULL, and nothing raised. */ variable = NULL; TEST_REQUIRE_OK(akbasic_environment_get(root, "NEW#", &variable)); TEST_REQUIRE(variable == NULL, "environment_get must still decline to create in a non-active scope"); /* The new way: created, in the root, every time. */ variable = NULL; PASS(errctx, akbasic_runtime_global(&HARNESS_RUNTIME, "NEW#", &variable)); TEST_REQUIRE(variable != NULL, "runtime_global must create rather than return NULL"); /* And it is genuinely in the root, visible from the child. */ TEST_REQUIRE_OK(akbasic_environment_get(HARNESS_RUNTIME.environment, "NEW#", &variable)); TEST_REQUIRE(variable != NULL, "the child must see the global the host created"); /* Twice is the same variable, not a second one shadowing the first. */ { akbasic_Variable *again = NULL; PASS(errctx, akbasic_runtime_global(&HARNESS_RUNTIME, "NEW#", &again)); TEST_REQUIRE(again == variable, "a repeat call must find the variable it created"); } TEST_REQUIRE_STATUS(akbasic_runtime_global(NULL, "A#", &variable), AKERR_NULLPOINTER); TEST_REQUIRE_STATUS(akbasic_runtime_global(&HARNESS_RUNTIME, NULL, &variable), AKERR_NULLPOINTER); TEST_REQUIRE_STATUS(akbasic_runtime_global(&HARNESS_RUNTIME, "A#", NULL), AKERR_NULLPOINTER); TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME)); harness_stop(); SUCCEED_RETURN(errctx); } /** @brief Seeding before the script starts, and reading after it stops, still work. */ static akerr_ErrorContext AKERR_NOIGNORE *test_seed_and_read_back(void) { PREPARE_ERROR(errctx); akbasic_Variable *variable = NULL; akbasic_Value *value = NULL; int64_t zerosubscript[1] = { 0 }; TEST_REQUIRE_OK(harness_start(NULL)); PASS(errctx, set_global("SEED#", 10)); PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, "10 OUT# = SEED# * 2\n" "20 PRINT OUT#\n")); PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0)); TEST_REQUIRE_STR(HARNESS_OUTPUT, "20\n"); PASS(errctx, akbasic_runtime_global(&HARNESS_RUNTIME, "OUT#", &variable)); PASS(errctx, akbasic_variable_get_subscript(variable, zerosubscript, 1, &value)); TEST_REQUIRE_INT(value->intval, 20); harness_stop(); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, test_global_survives_a_for_body()); CATCH(errctx, test_global_survives_a_gosub()); CATCH(errctx, test_global_never_returns_null()); CATCH(errctx, test_seed_and_read_back()); } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "host variable test failed"); akbasic_test_failures += 1; } FINISH_NORETURN(errctx); return akbasic_test_failures; }