Reset scratch per line and unwind dead scopes in host function calls
akbasic_runtime_call_function()'s body loop drives process_line_run() directly, skipping the per-line prologue akbasic_runtime_step() provides. The call environment's value scratch therefore accumulated across the whole body, and any body past about ten real lines died with 'Maximum values per line reached' -- a limit that is supposed to be per line. The loop now runs the same prologue step() does. A body that died also left its call scopes active: nothing popped them, so a host absorbing script errors drained the twelve-slot environment pool after twelve dead calls. The loop now unwinds to the caller's environment on every exit path. New: akbasic_runtime_clear_error(), the missing half of host revival. A run's first BASIC-level error latches deliberately, and set_mode(RUN) alone cannot un-decide that; a host that absorbed the error calls this beside it. Both defects and the revival dance are pinned in tests/user_functions.c. Co-authored-by: andrew <andrew@aklabs.net> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiGgpHuXUm2mR4Wzndw3dc
This commit is contained in:
@@ -337,6 +337,113 @@ static void test_call_from_c(void)
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A long multi-line body called from C completes.
|
||||
*
|
||||
* The value scratch is a *per line* limit, and the call loop has to reset it
|
||||
* per line the way akbasic_runtime_step() does. It did not: the scratch
|
||||
* accumulated across the whole body, and any body past about ten real lines
|
||||
* died with "Maximum values per line reached" -- silently, from the caller's
|
||||
* point of view, because a BASIC-level error inside the loop reports through
|
||||
* the sink and comes back as a zero (issue #7's shape). Found by the galaga
|
||||
* example, whose enemy functions are all longer than ten lines.
|
||||
*
|
||||
* The set_mode(RUN) between the load and the call is the issue #8 workaround:
|
||||
* the body only runs in RUN mode, and the program has already ended.
|
||||
*/
|
||||
static void test_long_body_called_from_c(void)
|
||||
{
|
||||
akbasic_Value arg;
|
||||
akbasic_Value *argp[1];
|
||||
akbasic_Value *result = NULL;
|
||||
|
||||
TEST_REQUIRE_OK(run_program("10 DEF LONGB(N#)\n"
|
||||
"20 A# = N# + 1\n"
|
||||
"30 B# = A# + 1\n"
|
||||
"40 C# = B# + 1\n"
|
||||
"50 D# = C# + 1\n"
|
||||
"60 E# = D# + 1\n"
|
||||
"70 F# = E# + 1\n"
|
||||
"80 G# = F# + 1\n"
|
||||
"90 H# = G# + 1\n"
|
||||
"100 I# = H# + 1\n"
|
||||
"110 J# = I# + 1\n"
|
||||
"120 K# = J# + 1\n"
|
||||
"130 L# = K# + 1\n"
|
||||
"140 M# = L# + 1\n"
|
||||
"150 P# = M# + 1\n"
|
||||
"160 Q# = P# + 1\n"
|
||||
"170 R# = Q# + 1\n"
|
||||
"180 RETURN R#\n"));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_set_mode(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
|
||||
memset(&arg, 0, sizeof(arg));
|
||||
arg.valuetype = AKBASIC_TYPE_INTEGER;
|
||||
arg.intval = 1;
|
||||
argp[0] = &arg;
|
||||
TEST_REQUIRE_OK(akbasic_runtime_call_function(&HARNESS_RUNTIME, "LONGB", argp, 1, &result));
|
||||
TEST_REQUIRE(result != NULL, "a call should have produced a result");
|
||||
TEST_REQUIRE_INT(result->intval, 17);
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A body that dies leaves the environment stack balanced.
|
||||
*
|
||||
* A runtime error inside a called body reports through the sink and ends the
|
||||
* run -- that part is unchanged, and the caller still gets the zeroed slot
|
||||
* (issue #7 tracks whether it should). What must NOT happen is what did: the
|
||||
* call's environment was never given back, so a host that absorbed script
|
||||
* errors and kept calling drained the twelve-slot pool after twelve dead
|
||||
* calls, and every later call failed with "Environment pool exhausted" -- an
|
||||
* exhaustion nothing in the script explains.
|
||||
*
|
||||
* The revival dance after each death is two calls: clear_error(), because a
|
||||
* run's first error latches and every later line is skipped while it stands,
|
||||
* and set_mode(RUN), the issue #8 workaround the boot already needed --
|
||||
* the error dropped the runtime out of RUN mode.
|
||||
*/
|
||||
static void test_dead_body_releases_environments(void)
|
||||
{
|
||||
akbasic_Value arg;
|
||||
akbasic_Value *argp[1];
|
||||
akbasic_Value *result = NULL;
|
||||
akbasic_Environment *root = NULL;
|
||||
int i = 0;
|
||||
|
||||
TEST_REQUIRE_OK(run_program("10 DEF DIE(N#)\n"
|
||||
"20 X# = NOSUCH(N#)\n"
|
||||
"30 RETURN X#\n"
|
||||
"40 DEF FINE(N#)\n"
|
||||
"50 Y# = N# * 2\n"
|
||||
"60 RETURN Y#\n"));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_set_mode(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
root = HARNESS_RUNTIME.environment;
|
||||
|
||||
memset(&arg, 0, sizeof(arg));
|
||||
arg.valuetype = AKBASIC_TYPE_INTEGER;
|
||||
arg.intval = 7;
|
||||
argp[0] = &arg;
|
||||
|
||||
/* Fifteen deaths: more than the pool holds, so a single leaked scope
|
||||
* fails this loop even if the first twelve limp through. */
|
||||
for ( i = 0; i < 15; i++ ) {
|
||||
TEST_REQUIRE_OK(akbasic_runtime_call_function(&HARNESS_RUNTIME, "DIE", argp, 1, &result));
|
||||
TEST_REQUIRE(HARNESS_RUNTIME.environment == root,
|
||||
"a dead call must unwind back to the caller's environment");
|
||||
TEST_REQUIRE_OK(akbasic_runtime_clear_error(&HARNESS_RUNTIME));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_set_mode(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
}
|
||||
|
||||
/* And the runtime is still whole: a healthy function runs to the right
|
||||
* answer after every one of those deaths. */
|
||||
TEST_REQUIRE_OK(akbasic_runtime_call_function(&HARNESS_RUNTIME, "FINE", argp, 1, &result));
|
||||
TEST_REQUIRE(result != NULL, "a call should have produced a result");
|
||||
TEST_REQUIRE_INT(result->intval, 14);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(akbasic_error_register());
|
||||
@@ -351,6 +458,8 @@ int main(void)
|
||||
test_call_scopes_are_reclaimed();
|
||||
test_calls_do_not_leak_value_slots();
|
||||
test_call_from_c();
|
||||
test_long_body_called_from_c();
|
||||
test_dead_body_releases_environments();
|
||||
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user