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

@@ -0,0 +1,23 @@
10 REM A DEF call takes its environment from the pool, one per call, exactly as
20 REM GOSUB does. It used to be owned by the funcdef and reset on every call,
30 REM which cost two silent defects: two calls in one expression shared a slot,
40 REM and recursion never came back at all.
50 DEF FACT(N#)
60 IF N# <= 1 THEN RETURN 1
70 RETURN N# * FACT(N# - 1)
80 PRINT FACT(5)
90 REM Each frame keeps its own argument, which is what makes the unwind work.
100 DEF SUMTO(N#)
110 IF N# <= 0 THEN RETURN 0
120 RETURN N# + SUMTO(N# - 1)
130 PRINT SUMTO(4)
140 REM And two calls to one function in one expression no longer collide.
150 DEF DBL(N#) = N# * 2
160 PRINT DBL(10) + DBL(1)
170 PRINT DBL(1) + DBL(10) + DBL(100)
180 REM Depth answers to the environment pool now, so runaway recursion reports
190 REM "Environment pool exhausted" rather than hanging. It is not exercised here
200 REM because the statement containing a failed call still prints a junk value
210 REM afterwards -- a separate, pre-existing defect recorded in TODO.md, and one
220 REM this golden file would pin if it went in. tests/user_functions.c asserts
230 REM the message instead.

View File

@@ -0,0 +1,4 @@
120
10
22
222