summary_step4.2.md 3.5 KB

V3 step 4.2 — nested procedures: static-link display (done 2026-09-22)

Suite 81/81 (39 run — incl. 4 trap-aborts — + 42 reject). LL(1)-clean (only the known-benign TypeBlock advisory). Nested/recursive/module-level all lower; methods stay 230.

What lowers now

  • Nested procedures (any depth) with full up-level access: reads, writes, addresses, calls, recursion. Lexical scoping is honest: inner scopes are entered/exited per procedure, and name lookup walks the scope chain outward.
  • Sibling procedures sharing a parent's locals (both B and D mutate A's s), three-level nesting (A > B > C), and nested recursion (Down calls itself through the display).

How the display works (Chappel / classic Wirth)

  • Every procedure takes a hidden leading static-link parameter l. Its entry allocates an 8-byte-per-slot link record (alloc8 520, 64 slots + parent link) and stores the incoming link at [0].
  • LocAdd (locals + params) stores each slot address into the record: [1..]. AddrOf of a same-scope var returns the slot; of an up-level var walks the chain.
  • Up-level access (LocFindUp returns levels + flat index): walk loadl levels times from the current link, then load the link cell. For composites the cell holds the address; for scalars the cell holds the slot address (so indexing/ deref compose unchanged).
  • Call sites resolve the link at CallBegin time (the caller context can't change mid-call): module callers pass 0; others walk funcDepth - calleeDepth links. Depth comes from SymTab.ProcDepthOf (lexical, recorded at heading time).

Backend problem and fix (the section's hard-won lesson)

QBE rejects a function definition nested inside a function body, but the parser reaches Inner's BEGIN while still inside Outer's body. First fix: buffer nested-function text and emit it after main. That failed for 3 levels: a single shared buffer preserved parse order, so C (parsed inside B) landed textually inside B's emitted body → "label expected". Cure: one buffer per nesting depth (nestBufs[0..15]), so each function's own text stays contiguous regardless of how the parse interleaves; EndModule flushes them in depth order. outSel = funcDepth is derived, not counted (AbortFunc and EndFunc both restore it), which removed the stale-flag class of bug entirely.

Grammar notes

  • ProcDecl no longer sets NoEmit for nesting (methods still do); the EndFuncHeader flush is at the Block boundary so FORWARD/DefUnit paths discard cleanly via AbortFunc.
  • Bare P; calls (parameterless proper procedures) are now legal (AssOrCall third alternative); they were a syntax error before. Inner; in Modula-2 style finally parses. F(); (function-as-statement) and P(args); with wrong arity stay 233.
  • LocFull guard → 233 past 64 locals per scope.

SymTab (additive)

fdep per procedure (lexical depth at heading), ProcDepthOf. Nothing else changed — the scope tree from step 1.1 already modelled nesting, so name lookup needed no work.

Tests

t_nestedproc → 20 (inner reads+writes parent locals, called twice), t_nestdeep → 211 (three levels + siblings sharing the parent, proven by the output), t_nestrec → 15 (nested recursion); t_nestedproc promoted from 230-reject to a real run test. Total 81.

Deferred: methods still 230 (class lowering), procedure-type variables, separate compilation + opaque completion + HIGH/ LEN (4.3), >64 locals, F()^ result suffixes.