A GALAGA tutorial: C/libakgl engine with akbasic embedded as the enemy-behavior engine #37
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
ATTEMPT {
|
||||
while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) {
|
||||
PASS(errctx, akbasic_runtime_process_line_run(obj));
|
||||
/*
|
||||
* 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);
|
||||
|
||||
@@ -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