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

53
TODO.md
View File

@@ -2298,12 +2298,17 @@ each is here so the reasoning does not have to be reconstructed.
evaluation, with a scope from the pool, re-entrant since §6 item 26, and with recursion
depth answering to `AKBASIC_MAX_ENVIRONMENTS` as a diagnosis rather than a hang.
What is missing is narrow. `akbasic_runtime_user_function()` takes an `akbasic_ASTLeaf *`
call site and evaluates argument *leaves* at `:993-1006`; split that loop so the AST path
evaluates-then-binds and a new entry point binds values it is handed. Resolving a bare word
to a function rather than a label is `akbasic_environment_get_function()` at `:960`, and
`COLLISION 1, BUMPED` is the precedent for a verb taking a bare word by name
(`src/runtime_sprite.c:543-555`).
~~What is missing is narrow.~~ **Done**, as `akbasic_runtime_call_function()`: the
argument binding is split out of the call-site handling, so the AST path evaluates leaves
and then calls the same code a verb can call with values it already has. Bounded by item
41 -- a multi-line body only runs while a program is running, which is the case a verb is
in and not the case a host with a stopped runtime is in.
What is still not done is the *language* half: no verb takes a function yet, and nothing
resolves a bare word to a function rather than a label. That wants
`akbasic_environment_get_function()` and the precedent of `COLLISION 1, BUMPED` taking a
bare word by name (`src/runtime_sprite.c`) -- and a verb that actually needs it, rather
than being built on speculation.
**One hazard, if it is ever wired to a collision query.** Do not pass the BASIC function
through as an `akgl_CollisionVisitFunc`: it would run inside the partitioner's cell-chain
@@ -2311,6 +2316,42 @@ each is here so the reasoning does not have to be reconstructed.
`partitioner.remove()` rewriting the chain the walk holds a cursor into. Collect the
candidates during the walk, close the walk, then dispatch.
41. **A multi-line `DEF` called outside a running program does not run its body**, and says
nothing about it.
```basic
DEF TRIPLE(N#)
T# = N# * 3
RETURN T#
PRINT TRIPLE(14)
```
At the REPL that prints `(UNDEFINED STRING REPRESENTATION FOR 0)`. In a program run from a
file the same function answers 42. No error either way.
`akbasic_runtime_user_function()` (`src/runtime.c`) runs a multi-line body by handing
control to the call's environment and spinning a line loop until `RETURN` pops back out,
guarded on `mode == AKBASIC_MODE_RUN` -- which is true only of a program running from a
file. In `REPL` mode the loop is skipped entirely and the result is whatever is in the
caller's return slot, which is zero. The single-expression form has no such loop and is
unaffected, which is most of why this has gone unnoticed; and `tests/user_functions.c`
drives every case through `run_program`, so the whole suite is in RUN mode.
**The obvious fix is wrong, and was tried.** Widening the guard to
`mode != AKBASIC_MODE_QUIT` makes the interpreter *hang* rather than answer wrongly:
`akbasic_runtime_process_line_run()` does not advance a REPL-mode runtime the way this
loop assumes, so the environment never comes back. Trading a silent wrong answer for a
lock-up is worse. The fix wants the REPL's own line cycle driving the body, which is a
change to how a call is executed rather than to a condition.
It also bounds item 40: `akbasic_runtime_call_function()` reaches a multi-line body only
while a program is running, which is exactly the case a verb calling a callback is in and
not the case a host poking at a stopped runtime is in.
A test belongs in `tests/user_functions.c` asserting the correct contract -- a multi-line
function answering 42 at the REPL -- registered in `AKBASIC_KNOWN_FAILING_TESTS` until it
does.
## 7. Filing gaps against `libakgl`
When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in