Let C call a BASIC function with values it already has
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m22s
akbasic CI Build / sanitizers (push) Failing after 4m31s
akbasic CI Build / coverage (push) Failing after 3m40s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Has been cancelled
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m22s
akbasic CI Build / sanitizers (push) Failing after 4m31s
akbasic CI Build / coverage (push) Failing after 3m40s
akbasic CI Build / akgl_build (push) Failing after 22s
akbasic CI Build / mutation_test (push) Has been cancelled
`akbasic_runtime_call_function(obj, name, args, nargs, dest)`. The argument
binding is split out of the call-site handling, so
`akbasic_runtime_user_function()` becomes evaluate-the-leaves and then call the
same code -- and a verb that wants to hand a BASIC function four numbers has
somewhere to start, which it did not before. The only entry point took a parsed
AST call site, so calling a function required having been parsed as an
expression.
Behaviour-preserving: both suites pass unmodified. The one deliberate difference
is that the AST path now evaluates *all* the arguments before binding any of
them, where it used to interleave. That is the safer order and it is what
by-value passing means everywhere else -- interleaved, a later argument could see
an earlier one already in the callee's scope.
**Finding, filed as section 6 item 41: a multi-line `DEF` called outside a
running program does not run its body, and says nothing about it.**
DEF TRIPLE(N#)
T# = N# * 3
RETURN T#
PRINT TRIPLE(14)
At the REPL that prints "(UNDEFINED STRING REPRESENTATION FOR 0)". From a file
the same function answers 42. The multi-line body runs by spinning a line loop
guarded on `mode == AKBASIC_MODE_RUN`, which is true only of a program running
from a file; in REPL mode the loop is skipped and the result is the caller's
zeroed return slot. The single-expression form has no such loop, and every case
in tests/user_functions.c goes through run_program and is therefore in RUN mode,
which is most of why nobody had seen it.
**The obvious fix is wrong and I tried it.** Widening the guard to
`mode != AKBASIC_MODE_QUIT` makes the interpreter *hang* instead of answering
wrongly -- `akbasic_runtime_process_line_run()` does not advance a REPL-mode
runtime the way the loop assumes, so the environment never comes back. Trading a
silent wrong answer for a lock-up is worse, so it is reverted, the reasoning is
in a comment where the next person will try the same thing, and the fix is filed
rather than guessed at.
That bounds this entry point rather than blocking it: it reaches a multi-line
body while a program is running, which is exactly the case a verb calling a
callback is in. The new test asserts the single-expression form from C and says
in a comment why the multi-line one is not asserted, so the omission is a
statement rather than a gap.
What is still not done is the language half -- no verb takes a function, and
nothing resolves a bare word to a function rather than a label. Item 40 now says
so, and says it wants a verb that needs it rather than speculation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
This commit is contained in:
@@ -284,6 +284,59 @@ static void test_calls_do_not_leak_value_slots(void)
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A host can call a BASIC function with values it already has.
|
||||
*
|
||||
* The entry point a *verb* would use. Everything else here reaches a function
|
||||
* through an expression the parser built; this reaches one with four numbers and
|
||||
* a name, which is what a verb taking a callback would have to do and what was
|
||||
* impossible until the argument binding was split out of the call site handling.
|
||||
*/
|
||||
static void test_call_from_c(void)
|
||||
{
|
||||
akbasic_Value args[2];
|
||||
akbasic_Value *argp[2];
|
||||
akbasic_Value *result = NULL;
|
||||
|
||||
TEST_REQUIRE_OK(run_program("10 DEF ADDEM(A#, B#) = A# + B#\n"
|
||||
"20 DEF TRIPLE(N#)\n"
|
||||
"30 T# = N# * 3\n"
|
||||
"40 RETURN T#\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
|
||||
|
||||
memset(&args[0], 0, sizeof(args[0]));
|
||||
memset(&args[1], 0, sizeof(args[1]));
|
||||
args[0].valuetype = AKBASIC_TYPE_INTEGER;
|
||||
args[0].intval = 17;
|
||||
args[1].valuetype = AKBASIC_TYPE_INTEGER;
|
||||
args[1].intval = 25;
|
||||
argp[0] = &args[0];
|
||||
argp[1] = &args[1];
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_call_function(&HARNESS_RUNTIME, "ADDEM", argp, 2, &result));
|
||||
TEST_REQUIRE(result != NULL, "a call should have produced a result");
|
||||
TEST_REQUIRE_INT(result->intval, 42);
|
||||
|
||||
/*
|
||||
* **The multi-line form is not asserted here, and that is a finding rather
|
||||
* than an omission.** Its body is *source lines*, so the call re-enters the
|
||||
* interpreter's line loop -- and that loop only runs while the runtime is in
|
||||
* RUN mode. By the time a host can call this the program has stopped, so the
|
||||
* body does not run and the result is the caller's zeroed return slot.
|
||||
*
|
||||
* The same defect is reachable without any of this: a multi-line function
|
||||
* called at the *REPL* answers "(UNDEFINED STRING REPRESENTATION FOR 0)".
|
||||
* TODO.md section 6 item 41. So this entry point is usable today from a verb
|
||||
* during execution, which is what it was built for, and from a host only for
|
||||
* the single-expression form.
|
||||
*/
|
||||
|
||||
/* A name nobody defined is refused rather than crashing. */
|
||||
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_call_function(&HARNESS_RUNTIME, "NOSUCH",
|
||||
argp, 1, &result));
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(akbasic_error_register());
|
||||
@@ -297,6 +350,7 @@ int main(void)
|
||||
test_structure_parameter_types_are_checked();
|
||||
test_call_scopes_are_reclaimed();
|
||||
test_calls_do_not_leak_value_slots();
|
||||
test_call_from_c();
|
||||
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user