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

`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:
2026-08-02 11:06:45 -04:00
parent a9600c3fcc
commit fdb3421b2a
5 changed files with 205 additions and 20 deletions

View File

@@ -944,21 +944,28 @@ static akerr_ErrorContext *bind_structure_parameter(akbasic_Runtime *obj, akbasi
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const char *name,
akbasic_Value **args, int nargs,
akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_FunctionDef *fndef = NULL;
akbasic_Environment *targetenv = obj->environment;
akbasic_ASTLeaf *leafptr = NULL;
akbasic_Environment *targetenv = NULL;
akbasic_ASTLeaf *argptr = NULL;
akbasic_Value *argvalue = NULL;
akbasic_Value *unused = NULL;
akbasic_Value *result = NULL;
akbasic_Value *out = NULL;
akbasic_Environment *callenv = NULL;
void *fnptr = NULL;
int i = 0;
PASS(errctx, akbasic_environment_get_function(obj->environment, expr->identifier, &fnptr));
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in call_function");
FAIL_ZERO_RETURN(errctx, (nargs == 0 || args != NULL), AKERR_NULLPOINTER,
"call_function was given %d arguments and no array", nargs);
targetenv = obj->environment;
PASS(errctx, akbasic_environment_get_function(obj->environment, name, &fnptr));
fndef = (akbasic_FunctionDef *)fnptr;
/*
@@ -985,22 +992,25 @@ akerr_ErrorContext *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_
obj->environment = targetenv;
/*
* Bind arguments into the call's scope, evaluating each one in the caller's.
* Passing is by value: assignment is what copies, so a structure argument
* deep-copies and a pointer argument copies its reference.
* Bind the values into the call's scope. Passing is by value: assignment is
* what copies, so a structure argument deep-copies and a pointer argument
* copies its reference.
*
* The values arrive already evaluated, which is the whole difference between
* this and what it was. Evaluating a *leaf* here is what tied a call to
* having been parsed from a call site, and therefore to being reachable only
* from an expression -- a verb wanting to hand a function four numbers had
* nowhere to start.
*/
leafptr = (expr->right != NULL ? expr->right->right : NULL);
argptr = (fndef->arglist != NULL ? fndef->arglist->right : NULL);
while ( leafptr != NULL && argptr != NULL ) {
PASS(errctx, akbasic_runtime_evaluate(obj, leafptr, &argvalue));
for ( i = 0; i < nargs && argptr != NULL; i++ ) {
obj->environment = callenv;
if ( argptr->leaftype == AKBASIC_LEAF_IDENTIFIER_STRUCT ) {
PASS(errctx, bind_structure_parameter(obj, callenv, argptr, argvalue));
PASS(errctx, bind_structure_parameter(obj, callenv, argptr, args[i]));
} else {
PASS(errctx, akbasic_environment_assign(callenv, argptr, argvalue, &unused));
PASS(errctx, akbasic_environment_assign(callenv, argptr, args[i], &unused));
}
obj->environment = targetenv;
leafptr = leafptr->next;
argptr = argptr->next;
}
@@ -1030,6 +1040,19 @@ akerr_ErrorContext *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_
*/
callenv->gosubReturnLine = callenv->lineno + 1;
callenv->nextline = fndef->lineno;
/*
* **The body only runs in RUN mode, and that is a defect rather than a
* rule.** A multi-line function called at the REPL falls straight past this
* loop and returns whatever is in the caller's return slot -- zero -- so
* `PRINT TRIPLE(14)` answers "(UNDEFINED STRING REPRESENTATION FOR 0)" with
* no error and no diagnostic. Recorded as TODO.md section 6 item 41.
*
* Widening it to `mode != AKBASIC_MODE_QUIT` is the obvious fix and is
* *wrong*: akbasic_runtime_process_line_run() does not advance a REPL-mode
* runtime the way this loop assumes, so the interpreter hangs instead of
* 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));
}
@@ -1039,6 +1062,41 @@ akerr_ErrorContext *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_
SUCCEED_RETURN(errctx);
}
/**
* @brief Call a user-defined function from a parsed call site.
*
* Evaluates the arguments in the caller's scope and hands the values to
* akbasic_runtime_call_function(). The split is what lets a verb call a BASIC
* function too: everything below the evaluation is shared.
*/
akerr_ErrorContext *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *args[AKBASIC_MAX_CALL_ARGUMENTS];
akbasic_ASTLeaf *leafptr = NULL;
int nargs = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in user_function");
/*
* All of them first, then the call. Evaluating as each one is bound would
* mean a later argument could see an earlier one already in the callee's
* scope, which is not what by-value passing means anywhere else.
*/
leafptr = (expr->right != NULL ? expr->right->right : NULL);
while ( leafptr != NULL ) {
FAIL_ZERO_RETURN(errctx, (nargs < AKBASIC_MAX_CALL_ARGUMENTS), AKBASIC_ERR_BOUNDS,
"%s was called with more than %d arguments",
expr->identifier, AKBASIC_MAX_CALL_ARGUMENTS);
PASS(errctx, akbasic_runtime_evaluate(obj, leafptr, &args[nargs]));
nargs += 1;
leafptr = leafptr->next;
}
PASS(errctx, akbasic_runtime_call_function(obj, expr->identifier, args, nargs, dest));
SUCCEED_RETURN(errctx);
}
/* ------------------------------------------------------------ line cycle -- */
int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj)