diff --git a/include/akbasic/runtime.h b/include/akbasic/runtime.h index 8de41fa..592acc5 100644 --- a/include/akbasic/runtime.h +++ b/include/akbasic/runtime.h @@ -632,6 +632,27 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_println(akbasic_Runtime *obj, */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode); +/** + * @brief Forgive the last BASIC-level error, so a host may call again. + * + * A program run latches its first runtime error and ends -- deliberately, and + * a host cannot un-decide that with akbasic_runtime_set_mode() alone: the + * latch survives the mode change, every later line is skipped, and every + * later akbasic_runtime_call_function() answers a stale value after walking + * the whole source table doing nothing. + * + * A host that absorbed a script error -- reported through the sink, actor + * marked dumb, frame preserved -- calls this beside + * `akbasic_runtime_set_mode(obj, AKBASIC_MODE_RUN)` to put the runtime back + * in service. It is for hosts between calls, not for verbs during a run: a + * running program's first error still ends it, exactly once, with one line. + * + * @param obj Object to initialize, inspect, or modify. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When `obj` is NULL. + */ +akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_clear_error(akbasic_Runtime *obj); + /** * @brief Evaluate one AST leaf, drawing scratch values from the environment. * @param obj Object to initialize, inspect, or modify. diff --git a/src/runtime.c b/src/runtime.c index 0b61e0f..3a34754 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -1114,9 +1114,33 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch * answering wrongly -- which is worse. The fix wants the REPL's own line * cycle, and that is a larger change than a condition. */ - while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) { - PASS(errctx, akbasic_runtime_process_line_run(obj)); - } + ATTEMPT { + while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) { + /* + * The same per-line prologue akbasic_runtime_step() runs. Without + * it the call environment's value scratch accumulates across the + * whole body, and a body of ten real lines dies with "Maximum + * values per line reached" -- a limit that is supposed to be per + * line, not per call. step() cannot do this for us: this loop + * drives process_line_run() directly. + */ + CATCH(errctx, akbasic_runtime_zero(obj)); + CATCH(errctx, akbasic_scanner_zero(obj)); + CATCH(errctx, akbasic_runtime_process_line_run(obj)); + } + } CLEANUP { + /* + * A body that died mid-line -- a runtime error set run_finished_mode, + * or a scanner error escaped (issue #4) -- left its scopes active. + * Give them back, or a host absorbing script errors drains the + * twelve-slot environment pool after twelve dead calls and every + * call after that fails for a reason nobody can see in the script. + */ + while ( obj->environment != targetenv && obj->environment->parent != NULL ) { + IGNORE(akbasic_runtime_prev_environment(obj)); + } + } PROCESS(errctx) { + } FINISH(errctx, true); PASS(errctx, akbasic_environment_new_value(targetenv, &out)); PASS(errctx, akbasic_value_clone(&targetenv->returnValue, out)); *dest = out; @@ -1918,6 +1942,15 @@ akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj) SUCCEED_RETURN(errctx); } +akerr_ErrorContext *akbasic_runtime_clear_error(akbasic_Runtime *obj) +{ + PREPARE_ERROR(errctx); + + FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in clear_error"); + obj->errclass = AKBASIC_ERRCLASS_NONE; + SUCCEED_RETURN(errctx); +} + akerr_ErrorContext *akbasic_runtime_run(akbasic_Runtime *obj, int maxsteps) { PREPARE_ERROR(errctx); diff --git a/tests/user_functions.c b/tests/user_functions.c index d2d60d5..3bc0414 100644 --- a/tests/user_functions.c +++ b/tests/user_functions.c @@ -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; }