Deliver GALAGA tutorial and akbasic enemy scripting example #39
@@ -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);
|
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.
|
* @brief Evaluate one AST leaf, drawing scratch values from the environment.
|
||||||
* @param obj Object to initialize, inspect, or modify.
|
* @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
|
* answering wrongly -- which is worse. The fix wants the REPL's own line
|
||||||
* cycle, and that is a larger change than a condition.
|
* cycle, and that is a larger change than a condition.
|
||||||
*/
|
*/
|
||||||
|
ATTEMPT {
|
||||||
while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) {
|
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_environment_new_value(targetenv, &out));
|
||||||
PASS(errctx, akbasic_value_clone(&targetenv->returnValue, out));
|
PASS(errctx, akbasic_value_clone(&targetenv->returnValue, out));
|
||||||
*dest = out;
|
*dest = out;
|
||||||
@@ -1918,6 +1942,15 @@ akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj)
|
|||||||
SUCCEED_RETURN(errctx);
|
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)
|
akerr_ErrorContext *akbasic_runtime_run(akbasic_Runtime *obj, int maxsteps)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|||||||
@@ -337,6 +337,113 @@ static void test_call_from_c(void)
|
|||||||
harness_stop();
|
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)
|
int main(void)
|
||||||
{
|
{
|
||||||
TEST_REQUIRE_OK(akbasic_error_register());
|
TEST_REQUIRE_OK(akbasic_error_register());
|
||||||
@@ -351,6 +458,8 @@ int main(void)
|
|||||||
test_call_scopes_are_reclaimed();
|
test_call_scopes_are_reclaimed();
|
||||||
test_calls_do_not_leak_value_slots();
|
test_calls_do_not_leak_value_slots();
|
||||||
test_call_from_c();
|
test_call_from_c();
|
||||||
|
test_long_body_called_from_c();
|
||||||
|
test_dead_body_releases_environments();
|
||||||
|
|
||||||
return akbasic_test_failures;
|
return akbasic_test_failures;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user