A GALAGA tutorial: C/libakgl engine with akbasic embedded as the enemy-behavior engine #37

Open
tachikoma wants to merge 14 commits from galaga-tutorial into main
3 changed files with 166 additions and 3 deletions
Showing only changes of commit 743e610f8f - Show all commits

View File

@@ -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.

View File

@@ -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);

View File

@@ -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;
}