Files
akbasic/tests/user_functions.c
Tachikoma fc122e5503 Let C call a BASIC function with values it already has
`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
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 11:06:45 -04:00

357 lines
13 KiB
C

/**
* @file user_functions.c
* @brief Tests that a `DEF` call is re-entrant.
*
* Both of the defects here came from one cause: the function's environment used
* to be owned by the funcdef and reset on every call, so a second call trampled
* the first. A call takes one from the pool now, exactly as `GOSUB` does.
*
* Neither failure was visible in an ordinary program. The aliasing one gave a
* wrong number rather than an error, and *only* when the same function appeared
* twice in one expression -- two different functions were fine, which is what
* made it hard to see at all. The recursion one produced no output whatsoever.
* So both are pinned here against the language rather than left to a listing
* somebody might not write.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include "harness.h"
#include "testutil.h"
/** @brief Run a program to completion under an explicit step budget. */
static akerr_ErrorContext AKERR_NOIGNORE *run_program_bounded(const char *source, int64_t steps)
{
PREPARE_ERROR(errctx);
PASS(errctx, harness_start(NULL));
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, steps));
SUCCEED_RETURN(errctx);
}
/**
* @brief Run a program to completion, bounded so a hang fails rather than waits.
*
* 20,000 steps, which is generous for every case here except the two that call a
* function eight thousand times. Those name their own budget rather than raising
* this one, because a bound loose enough for them would stop the recursion case
* from failing quickly.
*/
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
{
PREPARE_ERROR(errctx);
PASS(errctx, run_program_bounded(source, 20000));
SUCCEED_RETURN(errctx);
}
/**
* @brief Two calls to one function in one expression do not share a slot.
*
* The result used to be a pointer into the funcdef's own environment, so the
* second call overwrote the first before the operator saw it: `DBL(10) + DBL(1)`
* came out as 4 rather than 22, both operands having become the last call's
* answer. A silent wrong number from a two-line program.
*/
static void test_two_calls_in_one_expression(void)
{
TEST_REQUIRE_OK(run_program("10 DEF DBL(N#) = N# * 2\n"
"20 DEF TPL(N#) = N# * 3\n"
"30 PRINT DBL(10) + DBL(1)\n"
"40 PRINT DBL(1) + DBL(10) + DBL(100)\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "22\n222\n");
harness_stop();
/*
* Two *different* functions were always correct, because each funcdef owned
* its own environment. Asserted so a future fix that reintroduces sharing
* cannot pass by getting this case right.
*/
TEST_REQUIRE_OK(run_program("10 DEF DBL(N#) = N# * 2\n"
"20 DEF TPL(N#) = N# * 3\n"
"30 PRINT DBL(1) + TPL(10)\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "32\n");
harness_stop();
}
/**
* @brief A recursive multi-line `DEF` returns.
*
* It used to hang: the recursive call re-initialised the environment the outer
* call was still using, so the loop waiting for control to come back never saw
* it. No error, no bound, no diagnostic -- the one place in this interpreter
* that looped forever rather than raising.
*/
static void test_recursion_returns(void)
{
TEST_REQUIRE_OK(run_program("10 DEF FACT(N#)\n"
"20 IF N# <= 1 THEN RETURN 1\n"
"30 RETURN N# * FACT(N# - 1)\n"
"40 PRINT FACT(5)\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "120\n");
harness_stop();
}
/**
* @brief Each call gets its own arguments, which is what recursion needs.
*
* Distinct from the test above: a function could return the right answer for
* `FACT` by luck if the argument happened to be re-read before being clobbered.
* This one unwinds and uses the argument *after* the inner call has returned, so
* a shared parameter slot gives the wrong answer rather than no answer.
*/
static void test_arguments_are_per_call(void)
{
TEST_REQUIRE_OK(run_program("10 DEF SUMTO(N#)\n"
"20 IF N# <= 0 THEN RETURN 0\n"
"30 RETURN N# + SUMTO(N# - 1)\n"
"40 PRINT SUMTO(4)\n"));
/* 4+3+2+1 = 10, and only if each frame kept its own N#. */
TEST_REQUIRE_STR(HARNESS_OUTPUT, "10\n");
harness_stop();
}
/**
* @brief Runaway recursion is reported, not hung.
*
* Depth now answers to the environment pool like every other nesting, so too
* deep is the same diagnosis `GOSUB` already gave. That is the whole change in
* one sentence: a bound where there was none.
*/
static void test_runaway_recursion_is_diagnosed(void)
{
TEST_REQUIRE_OK(run_program("10 DEF INF(N#)\n"
"20 RETURN INF(N# + 1)\n"
"30 PRINT INF(1)\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "Environment pool exhausted") != NULL,
"unbounded recursion should report the pool bound, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief A single-expression function still works, and still nests. */
static void test_single_expression_form(void)
{
TEST_REQUIRE_OK(run_program("10 DEF SQ(N#) = N# * N#\n"
"20 PRINT SQ(4)\n"
"30 PRINT SQ(SQ(2))\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "16\n16\n");
harness_stop();
}
/**
* @brief A structure parameter is passed by value, and a pointer one is not.
*
* Neither is a special rule: a parameter is bound by assignment like any other
* variable, and assignment copies a structure and copies a pointer's reference.
* The two halves of the feature seen from a function's side.
*/
static void test_structure_parameters(void)
{
TEST_REQUIRE_OK(run_program("10 TYPE CRATE\n"
"20 W#\n"
"30 END TYPE\n"
"40 DIM A@ AS CRATE\n"
"50 A@.W# = 5\n"
"60 DEF WIDEN(B@ AS CRATE)\n"
"70 B@.W# = 99\n"
"80 RETURN B@.W#\n"
"90 PRINT WIDEN(A@)\n"
"100 PRINT A@.W#\n"))