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:
53
TODO.md
53
TODO.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user