RETURN should unwind nested scopes to the nearest call frame, as the C64 reference does #62
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during the PR #61 (generators) review. Filed by Ishikawa via Andrew's account; discussed and agreed with Andrew there.
The problem
RETURNin this interpreter only works when standing directly in the environment that carriesgosubReturnLine(or, since #61, a GEN's own frame). From inside any nested scope it errors:The C64 reference does not behave this way. MS-family BASICs keep FOR and GOSUB entries interleaved on one runtime stack, and RETURN discards FOR entries above the topmost GOSUB entry. Our behavior is the deviation. The same restriction limits #61's
RETURN-in-GEN(it must not stand inside aFOR/DOthe body opened) and is called out indocs/04-control-flow.mdand TODO.md §1.10.Semantics (settled during review — do not relitigate, but do record in TODO.md §1)
RETURNwalks the parent chain from the current environment to the nearest frame withgosubReturnLine != 0 || isGenerator. It can never pass through such a frame — the walk stops at the first one, period. This invariant is load-bearing (see "Why the driving loops survive" below).forGeneratorEnv(see MAINTENANCE.md "Abandoned generators").akbasic_runtime_unwind_to_environment()from #61 is the model; this change likely generalizes it or adds a sibling that stops-at-frame instead of stops-at-target.akbasic_cmd_end_gen()'s real-exhaustion path (release, pop to loop env, clearforGeneratorEnv).RETURN exprinto a GEN frame stays an error.akbasic_cmd_return()tail — park the value (cloned) on the target frame's parentreturnValue, set that parent'snextline = target->gosubReturnLine, release everything through the target.obj->environment == obj->handlerenvidentity check must be applied to the target frame, not the environment RETURN was standing in.Implementation steps
src/runtime_commands.cakbasic_cmd_return(): after the is-waiting check, walk the parent chain per rule 1. Keep the current fast path (already standing in the frame) intact for readability if that reads better.akbasic_runtime_release_environment()+akbasic_runtime_release_generator(); look atakbasic_runtime_unwind_to_environment()insrc/runtime.cfirst — the shape is nearly identical and one shared primitive is the goal, not a fourth hand-rolled loop.handlerenvcheck onto the target (rule 5).docs/04-control-flow.md(the GOSUB section and the generators section both state the current restriction — both must change) anddocs/11-verb-reference.md's RETURN row. Remove the now-wrong "must stand in the GEN's own scope" sentences, and fixtests/language/flowcontrol/generators_return.bas's leading REM which states the same.Why the driving loops survive (verified during review — trust but re-verify with the tests below)
akbasic_runtime_pump_generator()spinswhile obj->environment != loopenv; its anchorloopenvsits strictly above the GEN frame where any interior RETURN's walk stops. A generator-frame RETURN lands onloopenvexactly asEND GENdoes.akbasic_runtime_call_function()'s loop anchors ontargetenv(the caller), strictly above the DEF'scallenvwhere the walk stops; the value parks ontargetenv->returnValue, exactly where the loop reads it.NEXTstill cannot pop a call frame (forNextVariableguard incmd_next), so frame integrity is preserved from that side too.Test matrix (add to
tests/generators.cand a newtests/return_unwind.c; golden pairs undertests/language/flowcontrol/)AKBASIC_MAX_ENVIRONMENTS) proving release.GOSUBwith trailing statements on its line, subroutine RETURNs from inside a FOR → trailing statements still run (per-environment token cursors make this work; prove it stays true).Architecture docs: thorough treatment of the interpreter's stack — REQUIRED, not optional
docs/14-architecture.mdgained an environments-and-generators section in #61, but nothing documents the machine as a whole. This issue must leave behind a full treatment covering, at minimum:akbasic_Environment.parentlinks a fixed pool (AKBASIC_MAX_ENVIRONMENTS= 12) into the active chain;obj->environmentis the top.gosubReturnLine != 0orisGenerator). Enumerate every push site by file/function. RETURN targets frames; loop verbs pop scopes; this distinction is what the RETURN walk runs on.environment.h—tokens/nexttoken/curtoken); a parent suspended mid-line resumes exactly where it stopped when its child pops. Walk through10 GOSUB 100 : PRINT "TAIL"step by step — this is the least-obvious mechanism in the interpreter and it took tracing to rediscover during review.nextline/linenoper environment, advanced before dispatch (process_line_run()), and how verbs redirect them (GOTO, EMIT, RETURN).wait_for_command) as per-environment skip state, and how EXIT rides them.prev_environmentvsrelease_generatorvsunwind_to_environment(and this issue's RETURN walk); the abandoned-generators invariant, cross-referencing MAINTENANCE.md.forGeneratorEnvas the one place an environment is referenced from outside the parent chain.Every fenced BASIC example in it runs under the docs checker, so write them as real programs.