Give each DEF call its own environment, so recursion returns

The function's environment was owned by the funcdef and re-initialised on every
call, which made a function not re-entrant and cost two silent defects:

    DEF DBL(N#) = N# * 2
    PRINT DBL(10) + DBL(1)      was 4, should be 22

The result was a pointer into the funcdef's own environment, so the second call
overwrote the first before the operator saw it -- both operands became the last
call's answer. Two *different* functions in one expression were fine, which is
most of why it was invisible.

    DEF FACT(N#)
    IF N# <= 1 THEN RETURN 1
    RETURN N# * FACT(N# - 1)
    PRINT FACT(5)               never returned

The recursive call re-initialised the environment the outer call was still
using, so the loop waiting for control to come back could not see it. No error,
no bound, no diagnostic -- the one place in this interpreter that looped forever
rather than raising.

A call takes an environment from the pool now, exactly as GOSUB does. The result
is copied into a caller-scope scratch before that environment goes back, because
handing back a pointer into the callee is what made two calls collide and would
now be a pointer into a released slot as well. RETURN parks its result on the
*parent* rather than on the environment it is about to release, so nothing reads
a freed slot to find it.

Recursion depth answers to AKBASIC_MAX_ENVIRONMENTS like every other nesting, so
too deep is "Environment pool exhausted" -- a diagnosis where there was none.

akbasic_FunctionDef.environment goes with it, as dead state.

One thing this exposed but did not cause, measured against a stashed build and
recorded rather than fixed: a statement containing a failed multi-line DEF call
still completes and prints a junk value. It is visible more often now only
because runaway recursion reaches it where it used to hang.
tests/language/functions/recursion.bas deliberately does not pin that answer.

Chapter 16 loses its "walk a list with a loop, not a recursive DEF" caveat and
gains the one that is still true: a function cannot take a structure parameter
yet, so it reaches a record by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 12:29:35 -04:00
parent 6bac929901
commit 00daa17a47
10 changed files with 332 additions and 31 deletions

View File

@@ -186,9 +186,11 @@ Note line 130: assigning one *pointer* to another copies the reference, not the
That is the one place assignment does not deep-copy, and it is why pointers are declared
separately rather than being a mode a structure can be in.
**Walk a list with a loop, not with a recursive `DEF`.** A recursive multi-line `DEF`
does not return in this interpreter; it is a known defect recorded in `TODO.md`, and it
is not specific to structures.
A recursive `DEF` works too, and is bounded by the scope pool at 32 deep — the same
bound `GOSUB` has. What a function cannot yet do is take a structure *parameter*:
`DEF F(B@ AS NODE)` is not implemented, and a bare `DEF F(B@)` is refused because `@`
alone does not say which type. Until it is, a function reaches a record by name, which
the scoping already allows — a call can see the caller's variables.
`PRINT` follows pointers, so a cycle would not come back — it stops after four levels and
prints `(...)`.