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:
23
tests/language/functions/recursion.bas
Normal file
23
tests/language/functions/recursion.bas
Normal 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.
|
||||
Reference in New Issue
Block a user