Explorar o código

calc: fix nested-procedure entry and outer-frame access

Nested procedures were emitted between the enclosing prologue and body, so
calling the outer procedure fell straight through the nested bodies; and an
enclosing local was always addressed through the current BP, which is the
nested procedure's frame, not its lexical parent's.

Emit nested bodies before the enclosing prologue, pass the parent frame in DX
on nested calls, save it in a hidden static-link slot, and walk that chain for
outer locals, parameters and VAR slots.  Add t113/t114/t115/t116 for the call
layout, outer scalar reads, and outer string writes.

CALC's code grows to 27,458 bytes, so move the boot input descriptor to 0x9800
and read 77 sectors (loader maximum 38,656).  calc_smoke now enters 1+2 in A1
and checks 3.00, pinning the first cell-entry path.
Eric Streit hai 3 días
pai
achega
2d5d316037

+ 18 - 14
CALC-COMPILE.md

@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 | position | blocker |
 |---|---|
-| **compiles** | `CALC.PAS` reaches its final `.` — code **25,908** B, data **19,694** B |
+| **compiles** | `CALC.PAS` reaches its final `.` — code **27,458** B, data **19,694** B |
 
 Rungs behind it (now passed):
 
@@ -164,7 +164,7 @@ Rungs ahead:
 | 51322 | CASE/ELSE closing `end;` - DONE |
 | 52316 | string vs CHAR comparison (`S <> Chr($FF)`) - DONE |
 | 56540 | code-image ceiling - DONE (DataStart moved to 0xA000) |
-| **OK** | **CALC.PAS compiles: code 25,908, data 19,694** |
+| **OK** | **CALC.PAS compiles: code 27,458, data 19,694** |
 
 Corrections the measurements force, worth stating plainly because the stage list
 below was written before them:
@@ -1263,13 +1263,14 @@ include directory, boots it under qemu and feeds the default welcome/quit keys.
 are available; input is a static boot-time key stream, not a live terminal.
 
 **CALC.PAS compiles and runs.**  The runtime is 9,373 bytes; the generated
-program is 25,908 bytes of code and 19,694 bytes of data, with
+program is 27,458 bytes of code and 19,694 bytes of data, with
 `DataStart=0xA000` and `CodeMax=0xA000`.  The boot harness now loads images at
-segment `0x1000` and puts its input descriptor at `0x9000`, so the larger image
-no longer collides with the boot sector/BIOS areas.  `tests/calc_smoke.py`
-boots CALC.COM under qemu, checks the welcome/grid/command menu, sends `/Q`,
-and requires exit code 0.  The original CALC.COM is 27,901 bytes; ours is
-35,284 bytes because the runtime and code layout differ.
+segment `0x1000` and puts its input descriptor at `0x9800`, so the larger image
+no longer collides with the boot sector/BIOS areas.  The loader reads 77
+sectors and accepts images up to 38,656 bytes.  `tests/calc_smoke.py`
+boots CALC.COM under qemu, enters `1+2` in cell A1, checks the resulting
+`3.00` and the welcome/grid/command menu, sends `/Q`, and requires exit code 0.  The original CALC.COM is 27,901 bytes; ours is
+36,834 bytes because the runtime, static links and code layout differ.
 
 **Stage 14 — the CRT identifiers.** `GotoXY` (25), `LowVideo`/`NormVideo`
 (16 + 16), `ClrEol` (10), `ClrScr` (6), `KeyPressed` (3), `Delay` (1),
@@ -1291,10 +1292,13 @@ a later question and explicitly not this goal.
   only the name and must inherit the forward's parameters, result type and
   level; keep the `var`-ness intact (Stage 10).
 
-Today a self-call is `ERR 1` at the `(` (proved with a probe). Grouping a
-callee's parameters must agree with Stage 10's 2-vs-4-byte accounting, so land
-this after parameters — or land the symbol-visibility half first and gate the
-rest.
+This is now landed: self-recursion, forward definitions and nested procedures
+all work.  A nested procedure receives its lexical parent's frame in DX and
+saves it in a hidden static-link slot; accesses to enclosing locals walk that
+chain.  `t113_nestedcall`, `t114_nested2`, `t115_nestedlocal` and
+`t116_nestedstr` pin the call/prologue layout, outer-local reads and outer
+string writes.  Grouping a callee's parameters still agrees with Stage 10's
+2-vs-4-byte accounting.
 
 **Stage 16 — the layout (§3, the layout track). DONE
 (`v-TP3-CALC-LAYOUT`).** The data area is no longer immediately above the
@@ -1313,8 +1317,8 @@ remaining run-time capacity note.
 | limit | value | CALC needs | verdict |
 |---|---|---|---|
 | `TextLimit` (source buffer) | 62,903 | `CALC.PAS` + `CALC.INC`, normalised = **62,415** | **488 bytes of slack** |
-| `MaxCode` | 0A000h | 25,908 B program + 9,373 B runtime = **35,281 B** | **OK, 5,679 B spare** |
-| code window | `CodeMax = 0A000h`, capped by `MaxCode` | 25,908 B | **OK**; no data gap (§3.2) |
+| `MaxCode` | 0A000h | 27,458 B program + 9,373 B runtime = **36,831 B** | **OK, 4,129 B spare** |
+| code window | `CodeMax = 0A000h`, capped by `MaxCode` | 27,458 B | **OK**; no data gap (§3.2) |
 | data area | fixed image `0A000h`, zeroed by `initmem` | **19,694 B** (generated) | **OK**; CALC boots under qemu |
 | stack | 16,384 min / 655,360 max | 16,384 min | matches the original's `minstk`/`maxstk` |
 | `MaxSym` | 3,000 | 47 routines + locals + params | **inferred** OK, verify |

+ 3 - 2
README.md

@@ -18,14 +18,15 @@ second copy of a status table is a claim about the world that expires silently
 and is never noticed expiring, so there is one now, and this file says where it
 is.
 
-In short, current as of `v-TP3-CALC-CLI`:
+In short, current as of `v-TP3-CALC-CELLS`:
 
 - The shell and the WordStar-style editor are done.
 - The compiler front end and 8086 code generator are done for the language
   subset the fixtures cover, and the emitted `.COM` images **run** under
   `qemu-system-i386` and Exec86.
 - **CALC.PAS compiles and runs.** The generated CALC.COM reaches the welcome
-  screen, grid and command menu, and exits cleanly through `/Q`.
+  screen, grid and command menu, accepts a numeric formula or text cell, and
+  exits cleanly through `/Q`.
 - **The shell's `R` key is still a stub.** There is no in-process 8086
   interpreter, so nothing runs a `.COM` from inside `tpshell` itself; execution
   happens under the test harness or the CALC launcher below.

+ 9 - 6
SUMMARY.md

@@ -1133,7 +1133,7 @@ where DOS puts it.
 **Layout:** the old fixed `rtSz + 1000H` data base is gone. `DataStart = 0A000h`,
 `CodeMax = 0A000h`, and `MaxCode = 0A000h` bound the code image; the `.COM`
 writer emits `ImageBytes()` and no padding. The boot loader defaults to
-`LOAD_SEG=0x1000`, input at `0x9000`, so the larger image is clear of the boot
+`LOAD_SEG=0x1000`, input at `0x9800`, so the larger image is clear of the boot
 sector. This is the flat-segment equivalent of the original's separate CS/DS
 layout (see `CALC-COMPILE.md` §3.2). CALC boots and exits through its own
 command menu; running its full interactive/file features is a separate item.
@@ -2045,7 +2045,7 @@ the compiler *emitting* a 16,560-byte program). The ratchet and front-end
 features through **file control-block operations and the original memory
 layout** are landed: the ladder (`tests/calc_ladder.sh` +
 `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`; CALC.PAS now stops at
-**CALC.PAS compiles and runs** (code 25,908, data 19,694), the `{$I}` include,
+**CALC.PAS compiles and runs** (code 27,458, data 19,694), the `{$I}` include,
 `^X`/`#nn`, scalar typed constants, sets, enums, records (CellRec = 80),
 subranges, arrays, element/field access, file types, string value parameters,
 `Length`, the CRT identifiers (`ClrScr`/`GotoXY`/`ClrEol`/`LowVideo`/
@@ -2071,10 +2071,13 @@ standard functions (`Abs`, `Sqr`, `Sqrt`, `Ln`, `Exp`, `Sin`, `Cos`,
 landed, along with `Delete` and local `set of Char` variables.  The math runtime
 was compacted to guest Horner/square loops, reducing it from 14,063 to 9,203
 bytes.  `Insert`, CASE/ELSE trailing semicolons and string-vs-CHAR comparisons
-are also landed.  **CALC.PAS now compiles to its final `.`** with code 25,908
-and data 19,694.  The boot loader now uses segment 0x1000 with input at
-0x9000; `tests/calc_smoke.py` boots CALC.COM, checks the welcome/grid/menu and
-quits via `/Q` with exit 0.  `tests/calc_cli.py` is the manual launcher
+are also landed.  **CALC.PAS now compiles to its final `.`** with code 27,458
+and data 19,694.  Nested procedures now use a real static link, so a cell can
+be entered and its formula/text edited (the first cell-entry path no longer
+restarts or corrupts the screen).  The boot loader now uses segment 0x1000
+with input at 0x9800 and reads 77 sectors; `tests/calc_smoke.py` boots
+CALC.COM, enters `1+2` in cell A1, checks the resulting `3.00` and the
+welcome/grid/menu, and quits via `/Q` with exit 0.  `tests/calc_cli.py` is the manual launcher
 (`--keys`, `--keys-file`, `--com`, `--raw`, `--timeout`).  The runtime is 9,373 bytes / 56 entries; the
 rt_exec scaffold is at HDR=0x4000 / data 0x5000.  Items 1-4 below are
 independent of both tracks.

+ 264 - 110
shell/Compiler.mod

@@ -211,6 +211,8 @@ TYPE
          isVar  : BOOLEAN ;          (* a VAR parameter: the slot holds an address *)
          level  : CARDINAL ;
          local  : BOOLEAN ;
+         parent : CARDINAL ;       (* lexical parent procedure, 0 at top level *)
+         staticOff : CARDINAL ;    (* nested proc's hidden static-link local *)
          resvar : CARDINAL ;
          goPos  : CARDINAL ;
          defnd  : BOOLEAN ;
@@ -440,6 +442,8 @@ VAR
    txerrPos : CARDINAL ;
 
    lexnest  : CARDINAL ;
+   curProc  : CARDINAL ;       (* symbol of the procedure body being compiled *)
+   curStaticOff : CARDINAL ;   (* its hidden static-link slot, 0 if none *)
    curIsFunc : BOOLEAN ;
    resultVar : CARDINAL ;
    resultIsReal : BOOLEAN ;       (* the CURRENT function returns TReal *)
@@ -1324,6 +1328,55 @@ BEGIN
    END
 END EmBpDisp ;
 
+PROCEDURE EmBxDisp (off, regf : CARDINAL) ;
+(* The [BX+disp] twin of EmBpDisp.  A nested procedure reaches an enclosing
+   frame through the static link in BX; the displacement rules are identical
+   except for the rm field (111 for BX instead of 110 for BP). *)
+BEGIN
+   IF off <= 127 THEN
+      Ebyte (VAL (BYTE, 40H + regf * 8 + 7H)) ; Ebyte (VAL (BYTE, off))
+   ELSE
+      Ebyte (VAL (BYTE, 80H + regf * 8 + 7H)) ; Eword (off)
+   END
+END EmBxDisp ;
+
+PROCEDURE EmMovBxBp () ;
+BEGIN Ebyte (89H) ; Ebyte (0EBH) END EmMovBxBp ;   (* MOV BX,BP *)
+
+PROCEDURE EmMovDxBp () ;
+BEGIN Ebyte (89H) ; Ebyte (0EAH) END EmMovDxBp ;   (* MOV DX,BP *)
+
+PROCEDURE EmMovDxBx () ;
+BEGIN Ebyte (89H) ; Ebyte (0DAH) END EmMovDxBx ;   (* MOV DX,BX *)
+
+PROCEDURE EmMovAxDx () ;
+BEGIN Ebyte (89H) ; Ebyte (0D0H) END EmMovAxDx ;   (* MOV AX,DX *)
+
+PROCEDURE EmLdBxBx (off : CARDINAL) ;
+(* BX := [BX+off] - one step up the static-link chain. *)
+BEGIN Ebyte (8BH) ; EmBxDisp (off, 3) END EmLdBxBx ;
+
+PROCEDURE EmLoadVarBx (off, nbytes : CARDINAL) ;
+BEGIN
+   IF nbytes = 1 THEN
+      Ebyte (8AH) ; EmBxDisp (off, 0)
+   ELSE
+      Ebyte (8BH) ; EmBxDisp (off, 0)
+   END
+END EmLoadVarBx ;
+
+PROCEDURE EmStoreVarBx (off, nbytes : CARDINAL) ;
+BEGIN
+   IF nbytes = 1 THEN
+      Ebyte (88H) ; EmBxDisp (off, 0)
+   ELSE
+      Ebyte (89H) ; EmBxDisp (off, 0)
+   END
+END EmStoreVarBx ;
+
+PROCEDURE EmLeaDiBx (off : CARDINAL) ;
+BEGIN Ebyte (8DH) ; EmBxDisp (off, 7) END EmLeaDiBx ;
+
 PROCEDURE EmLoadVar (local : BOOLEAN ; off, nbytes : CARDINAL) ;
 (* A local is [BP+off] and `off' is already a frame displacement, so it needs
    no bias.  A global is [off] with a DIRECT displacement, i.e. an absolute
@@ -1405,6 +1458,117 @@ BEGIN
    END
 END EmLeaDi ;
 
+PROCEDURE EmMovBxAx () ;
+BEGIN Ebyte (89H) ; Ebyte (0C3H) END EmMovBxAx ;   (* MOV BX,AX *)
+
+PROCEDURE EmitFrameBase (level : CARDINAL) ;
+(* Leave BX pointing at the frame for lexical scope `level', pushing the old
+   BX.  The current frame is at `lexnest'; each step follows the hidden
+   static link stored by the enclosing procedure.  The caller must POP BX
+   after the access. *)
+VAR p, k : CARDINAL ;
+BEGIN
+   EmPushBx () ;
+   EmMovBxBp () ;
+   p := curProc ;
+   k := lexnest ;
+   WHILE k > level DO
+      EmLdBxBx (symtab [p].staticOff) ;
+      p := symtab [p].parent ;
+      DEC (k)
+   END
+END EmitFrameBase ;
+
+PROCEDURE IsOuterLocal (idx : CARDINAL) : BOOLEAN ;
+BEGIN
+   RETURN symtab [idx].local AND (symtab [idx].level < lexnest)
+END IsOuterLocal ;
+
+PROCEDURE LoadSymVar (idx, off, nbytes : CARDINAL) ;
+BEGIN
+   IF IsOuterLocal (idx) THEN
+      EmitFrameBase (symtab [idx].level) ;
+      EmLoadVarBx (off, nbytes) ;
+      EmPopBx ()
+   ELSE
+      EmLoadVar (symtab [idx].local, off, nbytes)
+   END
+END LoadSymVar ;
+
+PROCEDURE StoreSymVar (idx, off, nbytes : CARDINAL) ;
+BEGIN
+   IF IsOuterLocal (idx) THEN
+      EmitFrameBase (symtab [idx].level) ;
+      EmStoreVarBx (off, nbytes) ;
+      EmPopBx ()
+   ELSE
+      EmStoreVar (symtab [idx].local, off, nbytes)
+   END
+END StoreSymVar ;
+
+PROCEDURE EmPushSymAddr (idx, off : CARDINAL) ;
+BEGIN
+   IF IsOuterLocal (idx) THEN
+      EmitFrameBase (symtab [idx].level) ;
+      Ebyte (8DH) ; EmBxDisp (off, 0) ;   (* LEA AX,[BX+off] *)
+      EmPopBx () ;                        (* restore BX before pushing AX *)
+      EmPushAx ()
+   ELSE
+      EmPushVarAddr (symtab [idx].local, off)
+   END
+END EmPushSymAddr ;
+
+PROCEDURE LeaDiSym (idx, off : CARDINAL) ;
+BEGIN
+   IF IsOuterLocal (idx) THEN
+      EmitFrameBase (symtab [idx].level) ;
+      EmLeaDiBx (off) ;
+      EmPopBx ()
+   ELSE
+      EmLeaDi (symtab [idx].local, off)
+   END
+END LeaDiSym ;
+
+PROCEDURE EmLoadVarIndSym (idx, nbytes : CARDINAL) ;
+(* AX := the value reached through a VAR parameter's address slot. *)
+BEGIN
+   LoadSymVar (idx, symtab [idx].off, 2) ;
+   EmMovBxAx () ;
+   IF nbytes = 1 THEN
+      Ebyte (8AH) ; Ebyte (07H) ;
+      EmMovAh0 ()
+   ELSE
+      Ebyte (8BH) ; Ebyte (07H)
+   END
+END EmLoadVarIndSym ;
+
+PROCEDURE StoreVarIndPrepSym (idx : CARDINAL) ;
+(* DI := the address held by a VAR parameter's slot, then PUSH DI. *)
+BEGIN
+   LoadSymVar (idx, symtab [idx].off, 2) ;
+   EmMovDiAx () ;
+   EmPushDi ()
+END StoreVarIndPrepSym ;
+
+PROCEDURE EmitStaticLink (idx : CARDINAL) ;
+(* Pass the lexical parent frame in DX before a call to a nested procedure.
+   The callee saves it in its hidden static-link local.  A top-level procedure
+   has no parent frame and gets nothing. *)
+VAR level : CARDINAL ;
+BEGIN
+   level := symtab [idx].level ;
+   IF level = 0 THEN
+      RETURN
+   END ;
+   IF level = lexnest THEN
+      EmMovDxBp ()
+   ELSE
+      EmitFrameBase (level) ;
+      EmMovDxBx () ;
+      EmPopBx ()
+   END
+END EmitStaticLink ;
+
 PROCEDURE EmAddDiAx () ;
 BEGIN Ebyte (01H) ; Ebyte (0C7H) END EmAddDiAx ;  (* 01 C7: DI := DI + AX *)
 
@@ -1800,6 +1964,8 @@ BEGIN
    e.isVar := FALSE ;
    e.level := lexnest ;
    e.local := local ;
+   e.parent := 0 ;
+   e.staticOff := 0 ;
    e.resvar := 0 ;
    e.goPos := 0 ;
    e.defnd := FALSE ;
@@ -2478,7 +2644,7 @@ BEGIN
             r.kind := 2 ;
             RETURN
          END ;
-         EmLoadVarInd (symtab [r.idx].off, VarSize (r)) ;
+         EmLoadVarIndSym (r.idx, VarSize (r)) ;
          r.kind := 2 ;
          RETURN
       END ;
@@ -2492,9 +2658,7 @@ BEGIN
          IF (r.cls = TChar) AND (symtab [r.idx].cls = TString) THEN
             emsz := 1
          END ;
-         EmLoadVar (symtab [r.idx].local,
-                    (symtab [r.idx].off + r.boff) MOD 10000H,
-                    emsz) ;
+         LoadSymVar (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H, emsz) ;
          IF emsz = 1 THEN
             EmMovAh0 ()
          END ;
@@ -2529,7 +2693,7 @@ BEGIN
             metadata, exactly as in ParseVar. *)
          DropCh (GetCh ()) ;
          p := symtab [r.idx].elem ;
-         EmLoadVar (symtab [r.idx].local, symtab [r.idx].off, 2) ;
+         LoadSymVar (r.idx, symtab [r.idx].off, 2) ;
          EmMovDiAx () ;
          r.idx := p ;
          r.kind := 5 ;
@@ -2604,7 +2768,7 @@ BEGIN
                      both constant and variable indices, so LoadAtom and
                      Assignment see the same kind-5 character address they do
                      for an ordinary string. *)
-                  EmLoadVar (TRUE, symtab [r.idx].off, 2) ;
+                  LoadSymVar (r.idx, symtab [r.idx].off, 2) ;
                   EmMovDiAx () ;
                   IF t.kind = 0 THEN
                      EmAddDiImm ((r.boff + W16 (t.imm)) MOD 10000H)
@@ -2632,8 +2796,7 @@ BEGIN
                      lets a later LoadAtom read [DI] and a later assignment PUSH
                      DI across its right-hand side. *)
                   LoadAtom (t) ;
-                  EmLeaDi (symtab [r.idx].local,
-                           (symtab [r.idx].off + r.boff) MOD 10000H) ;
+                  LeaDiSym (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H) ;
                   EmAddDiAx () ;
                   r.kind := 5
                END ;
@@ -2745,8 +2908,7 @@ BEGIN
                END ;
                r.kind := 5
             ELSIF haveVar THEN
-               EmLeaDi (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff
+               LeaDiSym (r.idx, (symtab [r.idx].off + r.boff
                          + W16 (coff)) MOD 10000H) ;
                EmAddDiAx () ;
                r.kind := 5
@@ -2788,7 +2950,7 @@ BEGIN
             metadata from the symbol, not from the expression. *)
          DropCh (GetCh ()) ;
          p := symtab [idx].elem ;
-         EmLoadVar (symtab [idx].local, symtab [idx].off, 2) ;
+         LoadSymVar (idx, symtab [idx].off, 2) ;
          EmMovDiAx () ;
          r.idx := p ;
          r.kind := 5 ;
@@ -3127,8 +3289,7 @@ BEGIN
    IF r.kind = 5 THEN
       EmPushDi ()
    ELSIF r.kind = 1 THEN
-      EmPushVarAddr (symtab [r.idx].local,
-                     (symtab [r.idx].off + r.boff) MOD 10000H)
+      EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H)
    ELSIF (r.kind = 7) OR (r.kind = 8) THEN
       EmPushVarAddr (FALSE, r.setoff)
    ELSE
@@ -3259,12 +3420,11 @@ BEGIN
          IF r.boff # 0 THEN
             Err (ENoLib)
          ELSE
-            EmLoadVar (TRUE, symtab [r.idx].off, 2) ;
+            LoadSymVar (r.idx, symtab [r.idx].off, 2) ;
             EmPushAx ()
          END
       ELSE
-         EmPushVarAddr (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff) MOD 10000H)
+         EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H)
       END
    ELSIF r.kind = 3 THEN
       EmPushVarAddr (FALSE, EmStrBlob (r))  (* the literal travels inline *)
@@ -3281,11 +3441,10 @@ BEGIN
       EmPushDi ()
    ELSIF r.kind = 1 THEN
       IF symtab [r.idx].isVar THEN
-         EmLoadVar (TRUE, symtab [r.idx].off, 2) ;
+         LoadSymVar (r.idx, symtab [r.idx].off, 2) ;
          EmPushAx ()
       ELSE
-         EmPushVarAddr (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff) MOD 10000H)
+         EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H)
       END
    ELSE
       Err (ENoLib)
@@ -3571,8 +3730,7 @@ BEGIN
                IF right.kind = 5 THEN
                   EmLdAlDiOff (bidx)
                ELSIF right.kind = 1 THEN
-                  EmLoadVar (symtab [right.idx].local,
-                             (right.setoff + right.boff + bidx) MOD 10000H, 1)
+                  LoadSymVar (right.idx, (right.setoff + right.boff + bidx) MOD 10000H, 1)
                ELSE
                   Err (ENoLib) ;
                   RETURN
@@ -4379,8 +4537,7 @@ BEGIN
       IF OK () THEN
          IF (r.kind = 1) AND
             ((r.cls = TText) OR (r.cls = TFile)) THEN
-            EmPushVarAddr (symtab [r.idx].local,
-                           (symtab [r.idx].off + r.boff) MOD 10000H) ;
+            EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H) ;
             DropC (EmCall (TU_FileEof)) ;
             EmAddSp (2) ;
             r.kind := 2 ;
@@ -4618,11 +4775,10 @@ BEGIN
          IF r.boff # 0 THEN
             Err (ENoLib)
          ELSE
-            EmLoadVarInd (symtab [r.idx].off, 1)
+            EmLoadVarIndSym (r.idx, 1)
          END
       ELSIF r.kind = 1 THEN
-         EmLoadVar (symtab [r.idx].local,
-                    (symtab [r.idx].off + r.boff) MOD 10000H, 1)
+         LoadSymVar (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H, 1)
       ELSIF r.kind = 3 THEN
          EmLoadVar (FALSE, EmStrBlob (r), 1)
       ELSE
@@ -4684,6 +4840,7 @@ PROCEDURE EmCallMost (idx, cleanup : CARDINAL) ;
    larger than a word. *)
 VAR p : CARDINAL ;
 BEGIN
+   EmitStaticLink (idx) ;
    IF symtab [idx].defnd THEN
       DropC (EmCall (symtab [idx].goPos))
    ELSE
@@ -4742,11 +4899,10 @@ BEGIN
                   Err (ENoLib) ;
                   EXIT
                END ;
-               EmLoadVar (TRUE, symtab [arg.idx].off, 2) ;
+               LoadSymVar (arg.idx, symtab [arg.idx].off, 2) ;
                EmPushAx ()
             ELSE
-               EmPushVarAddr (symtab [arg.idx].local,
-                              (symtab [arg.idx].off + arg.boff) MOD 10000H)
+               EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H)
             END
          ELSIF slot > 2 THEN
             (* A STRING VALUE parameter: push its whole slot, copied.
@@ -4822,11 +4978,10 @@ BEGIN
                      Err (ENoLib) ;
                      EXIT
                   END ;
-                  EmLoadVar (TRUE, symtab [arg.idx].off, 2) ;
+                  LoadSymVar (arg.idx, symtab [arg.idx].off, 2) ;
                   EmPushAx ()
                ELSE
-                  EmPushVarAddr (symtab [arg.idx].local,
-                                 (symtab [arg.idx].off + arg.boff) MOD 10000H)
+                  EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H)
                END
             ELSIF slot > 2 THEN
                (* see ParseCallArgs: push the whole string slot *)
@@ -5061,7 +5216,7 @@ BEGIN
          END ;
          IF (src.kind = 0) AND (src.cls # TString) AND (src.cls # TSet) THEN
             IntToMbf (src.imm, rbuf) ;
-            EmLoadVar (TRUE, symtab [r.idx].off, 2) ;
+            LoadSymVar (r.idx, symtab [r.idx].off, 2) ;
             EmMovDiAx () ;
             k := 0 ;
             WHILE k < 6 DO
@@ -5073,16 +5228,15 @@ BEGIN
          IF (src.cls = TReal) AND ((src.kind = 1) OR (src.kind = 5)) THEN
             (* Destination pointer first, then the source address; pop SI
                (source), DI (destination), and copy six bytes. *)
-            EmLoadVar (TRUE, symtab [r.idx].off, 2) ;
+            LoadSymVar (r.idx, symtab [r.idx].off, 2) ;
             EmPushAx () ;
             IF src.kind = 5 THEN
                EmPushDi ()
             ELSIF symtab [src.idx].isVar THEN
-               EmLoadVar (TRUE, symtab [src.idx].off, 2) ;
+               LoadSymVar (src.idx, symtab [src.idx].off, 2) ;
                EmPushAx ()
             ELSE
-               EmPushVarAddr (symtab [src.idx].local,
-                              (symtab [src.idx].off + src.boff) MOD 10000H)
+               EmPushSymAddr (src.idx, (symtab [src.idx].off + src.boff) MOD 10000H)
             END ;
             EmPopSi () ;
             EmPopDi () ;
@@ -5115,8 +5269,7 @@ BEGIN
             k := 0 ;
             WHILE k < 6 DO
                EmMovAxi (ORD (rbuf [k])) ;
-               EmStoreVar (symtab [r.idx].local,
-                           (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
+               StoreSymVar (r.idx, (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
                INC (k)
             END
          ELSE
@@ -5130,8 +5283,7 @@ BEGIN
          k := 0 ;
          WHILE k < 6 DO
             EmLdAlSi () ;
-            EmStoreVar (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
+            StoreSymVar (r.idx, (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
             EmIncSi () ;
             INC (k)
          END ;
@@ -5145,18 +5297,15 @@ BEGIN
             END ;
             k := 0 ;
             WHILE k < 6 DO
-               EmLoadVar (symtab [src.idx].local,
-                          (symtab [src.idx].off + src.boff + k) MOD 10000H, 1) ;
+               LoadSymVar (src.idx, (symtab [src.idx].off + src.boff + k) MOD 10000H, 1) ;
                EmStDiAlOff (k) ;
                INC (k)
             END
          ELSIF r.kind = 1 THEN
             k := 0 ;
             WHILE k < 6 DO
-               EmLoadVar (symtab [src.idx].local,
-                          (symtab [src.idx].off + src.boff + k) MOD 10000H, 1) ;
-               EmStoreVar (symtab [r.idx].local,
-                           (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
+               LoadSymVar (src.idx, (symtab [src.idx].off + src.boff + k) MOD 10000H, 1) ;
+               StoreSymVar (r.idx, (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
                INC (k)
             END
          ELSE
@@ -5193,8 +5342,7 @@ BEGIN
             RETURN
          END ;
          IF r.kind = 1 THEN
-            EmPushVarAddr (symtab [r.idx].local,
-                           (symtab [r.idx].off + r.boff) MOD 10000H)
+            EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H)
          ELSIF r.kind # 5 THEN
             Err (ENoLib) ;
             RETURN
@@ -5223,8 +5371,7 @@ BEGIN
          k := 0 ;
          WHILE k < r.setsize DO
             EmMovAxi (ORD (sbuf [k])) ;
-            EmStoreVar (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
+            StoreSymVar (r.idx, (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
             INC (k)
          END
       ELSE
@@ -5254,8 +5401,7 @@ BEGIN
             INC (k)
          END
       ELSIF src.kind = 1 THEN
-         EmPushVarAddr (symtab [src.idx].local,
-                        (symtab [src.idx].off + src.boff) MOD 10000H) ;
+         EmPushSymAddr (src.idx, (symtab [src.idx].off + src.boff) MOD 10000H) ;
          EmPushDi () ;                    (* destination = the field *)
          EmMovCxi (n) ;                   (* strstore's limit *)
          DropC (EmCall (TU_StrStore)) ;
@@ -5281,7 +5427,7 @@ BEGIN
          RETURN
       END ;
       EmPushStrAddr (src) ;
-      EmLoadVar (TRUE, symtab [r.idx].off, 2) ;
+      LoadSymVar (r.idx, symtab [r.idx].off, 2) ;
       EmPushAx () ;
       EmMovCxi ((VarSize (r) - 1) MOD 10000H) ;
       DropC (EmCall (TU_StrStore)) ;
@@ -5296,7 +5442,7 @@ BEGIN
          Err (ENoLib) ;
          RETURN
       END ;
-      EmStoreVarIndPrep (symtab [r.idx].off) ;
+      StoreVarIndPrepSym (r.idx) ;
       ParseExpr (src) ;
       LoadAtom (src) ;
       EmPopDi () ;
@@ -5320,18 +5466,14 @@ BEGIN
             `EXY := FactorCh' before Val. *)
          IF src.kind = 0 THEN
             EmMovAxi (1) ;
-            EmStoreVar (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff) MOD 10000H, 1) ;
+            StoreSymVar (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H, 1) ;
             EmMovAxi (W16 (src.imm)) ;
-            EmStoreVar (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff + 1) MOD 10000H, 1)
+            StoreSymVar (r.idx, (symtab [r.idx].off + r.boff + 1) MOD 10000H, 1)
          ELSE
             LoadAtom (src) ;
-            EmStoreVar (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff + 1) MOD 10000H, 1) ;
+            StoreSymVar (r.idx, (symtab [r.idx].off + r.boff + 1) MOD 10000H, 1) ;
             EmMovAxi (1) ;
-            EmStoreVar (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff) MOD 10000H, 1)
+            StoreSymVar (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H, 1)
          END ;
          RETURN
       END ;
@@ -5349,8 +5491,7 @@ BEGIN
             destination at [BP+4] - the caller pops both, and the limit is a
             full MOV CX,imm16 because this runtime reads it in CX outright. *)
          IF src.kind = 1 THEN
-            EmPushVarAddr (symtab [src.idx].local,
-                           (symtab [src.idx].off + src.boff) MOD 10000H)
+            EmPushSymAddr (src.idx, (symtab [src.idx].off + src.boff) MOD 10000H)
          ELSIF src.kind = 5 THEN
             (* A string element reached through DI, including a pointer
                dereference: Buffer^[i].  Its address is already in DI. *)
@@ -5359,8 +5500,7 @@ BEGIN
             Err (ENoLib) ;          (* a computed string is not a value *)
             RETURN
          END ;
-         EmPushVarAddr (symtab [r.idx].local,
-                        (symtab [r.idx].off + r.boff) MOD 10000H) ;
+         EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H) ;
          EmMovCxi ((VarSize (r) - 1) MOD 10000H) ;
          DropC (EmCall (TU_StrStore)) ;
          EmAddSp (4) ;
@@ -5371,13 +5511,11 @@ BEGIN
          n := VarSize (r) - 1
       END ;
       EmMovAxi (n) ;
-      EmStoreVar (symtab [r.idx].local,
-                  (symtab [r.idx].off + r.boff) MOD 10000H, 1) ;
+      StoreSymVar (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H, 1) ;
       k := 0 ;
       WHILE k < n DO
          EmMovAxi (ORD (strPool [strOff [src.strx] + k])) ;
-         EmStoreVar (symtab [r.idx].local,
-                     (symtab [r.idx].off + r.boff + 1 + k) MOD 10000H, 1) ;
+         StoreSymVar (r.idx, (symtab [r.idx].off + r.boff + 1 + k) MOD 10000H, 1) ;
          INC (k)
       END ;
       RETURN
@@ -5411,9 +5549,7 @@ BEGIN
    END ;
    ParseExpr (src) ;
    LoadAtom (src) ;
-   EmStoreVar (symtab [r.idx].local,
-               (symtab [r.idx].off + r.boff) MOD 10000H,
-               VarSize (r))
+   StoreSymVar (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H, VarSize (r))
 END Assignment ;
 
 PROCEDURE Compound () ;
@@ -5557,8 +5693,7 @@ BEGIN
             p := symtab [arg.idx].elem ;
             EmMovAxi (symtab [p].size) ;
             EmPushAx () ;                    (* size -> [BP+6] *)
-            EmPushVarAddr (symtab [arg.idx].local,
-                           (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
+            EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
             DropC (EmCall (TU_HeapNew)) ;
             EmAddSp (4)
          ELSIF OK () THEN
@@ -5749,10 +5884,8 @@ BEGIN
          IF OK () AND (dummy.kind = 1) AND (dummy.cls = TScalar)
             AND (VarSize (dummy) = 2) AND (errV.kind = 1)
             AND (errV.cls = TScalar) AND (VarSize (errV) = 2) THEN
-            EmPushVarAddr (symtab [errV.idx].local,
-                           (symtab [errV.idx].off + errV.boff) MOD 10000H) ;
-            EmPushVarAddr (symtab [dummy.idx].local,
-                           (symtab [dummy.idx].off + dummy.boff) MOD 10000H) ;
+            EmPushSymAddr (errV.idx, (symtab [errV.idx].off + errV.boff) MOD 10000H) ;
+            EmPushSymAddr (dummy.idx, (symtab [dummy.idx].off + dummy.boff) MOD 10000H) ;
             EmPushStrAddr (arg) ;
             DropC (EmCall (TU_ValInt)) ;
             EmAddSp (6)
@@ -5760,8 +5893,7 @@ BEGIN
             ((dummy.kind = 1) OR (dummy.kind = 5)) AND
             (errV.kind = 1) AND (errV.cls = TScalar) AND (VarSize (errV) = 2)
          THEN
-            EmPushVarAddr (symtab [errV.idx].local,
-                           (symtab [errV.idx].off + errV.boff) MOD 10000H) ;
+            EmPushSymAddr (errV.idx, (symtab [errV.idx].off + errV.boff) MOD 10000H) ;
             EmPushRealAddr (dummy) ;
             EmPushStrAddr (arg) ;
             DropC (EmCall (TU_ValReal)) ;
@@ -5819,8 +5951,7 @@ BEGIN
          IF arg.kind = 5 THEN
             EmPushDi ()
          ELSE
-            EmPushVarAddr (symtab [arg.idx].local,
-                           (symtab [arg.idx].off + arg.boff) MOD 10000H)
+            EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H)
          END ;
          IF MatchDelim (':') THEN
             ParseExpr (dummy) ;
@@ -5888,8 +6019,7 @@ BEGIN
             IF OK () THEN
                IF dummy.cls = TString THEN
                   EmPushStrAddr (dummy) ;
-                  EmPushVarAddr (symtab [arg.idx].local,
-                                 (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
+                  EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
                   DropC (EmCall (TU_FileAssign)) ;
                   EmAddSp (4)
                ELSE
@@ -5911,8 +6041,7 @@ BEGIN
          IF OK () THEN
             IF (arg.kind = 1) AND
                ((arg.cls = TFile) OR (arg.cls = TText) OR (arg.cls = TUntyp)) THEN
-               EmPushVarAddr (symtab [arg.idx].local,
-                              (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
+               EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
                IF which = BI_Reset THEN
                   DropC (EmCall (TU_FileReset))
                ELSIF which = BI_Rewrite THEN
@@ -5992,8 +6121,7 @@ BEGIN
                      EmMovAxi ((VarSize (dummy) - 1) MOD 10000H) ;
                      EmPushAx () ;
                      EmPushStrAddr (dummy) ;
-                     EmPushVarAddr (symtab [arg.idx].local,
-                                    (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
+                     EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
                      DropC (EmCall (TU_FileReadln)) ;
                      EmAddSp (6)
                   ELSIF OK () THEN
@@ -6123,11 +6251,10 @@ BEGIN
                      Err (ENoLib) ;
                      RETURN
                   END ;
-                  EmLoadVar (TRUE, symtab [arg.idx].off, 2) ;
+                  LoadSymVar (arg.idx, symtab [arg.idx].off, 2) ;
                   EmPushAx ()
                ELSE
-                  EmPushVarAddr (symtab [arg.idx].local,
-                                 (symtab [arg.idx].off + arg.boff) MOD 10000H)
+                  EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H)
                END ;
                IF acls = TString THEN
                   (* readln(s)/read(s) of a string variable: TPSRC8 prdstr
@@ -6219,11 +6346,10 @@ BEGIN
                      ELSIF symtab [arg.idx].isVar THEN
                         (* VAR string: the slot holds the address of the
                            caller's characters; push that, not the slot. *)
-                        EmLoadVar (TRUE, symtab [arg.idx].off, 2) ;
+                        LoadSymVar (arg.idx, symtab [arg.idx].off, 2) ;
                         EmPushAx ()
                      ELSE
-                        EmPushVarAddr (symtab [arg.idx].local,
-                                       (symtab [arg.idx].off + arg.boff) MOD 10000H)
+                        EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H)
                      END ;
                      ent := TU_WrStr
                   ELSE
@@ -6239,8 +6365,7 @@ BEGIN
                         IF arg.kind = 5 THEN
                            EmPushDi ()
                         ELSE
-                           EmPushVarAddr (symtab [arg.idx].local,
-                                          (symtab [arg.idx].off + arg.boff) MOD 10000H)
+                           EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H)
                         END ;
                         IF MatchDelim (':') THEN
                            ParseExpr (dummy) ;
@@ -6492,8 +6617,7 @@ BEGIN
       END ;
       ParseExpr (src) ;
       LoadAtom (src) ;
-      EmStoreVar (symtab [idx].local, symtab [idx].off,
-                  symtab [idx].size) ;
+      StoreSymVar (idx, symtab [idx].off, symtab [idx].size) ;
       IF NOT MatchKey (tok) THEN
          Err (ESimpType) ;
          RETURN
@@ -6528,8 +6652,7 @@ BEGIN
          8086, so EmMovCxSp is POP CX ; PUSH CX, an observational no-op that
          leaves the bound in place. *)
       EmMovCxSp () ;
-      EmLoadVar (symtab [idx].local, symtab [idx].off,
-                 symtab [idx].size) ;
+      LoadSymVar (idx, symtab [idx].off, symtab [idx].size) ;
       EmCmpAxCx () ;
       IF dow THEN
          zj := EmJcc (8CH, 0)          (* JL -> done *)
@@ -6552,15 +6675,13 @@ BEGIN
          A WHILE nested inside the FOR saves and restores its own range and
          leaves this one intact. *)
       (* step *)
-      EmLoadVar (symtab [idx].local, symtab [idx].off,
-                 symtab [idx].size) ;
+      LoadSymVar (idx, symtab [idx].off, symtab [idx].size) ;
       IF dow THEN
          EmDecAx ()
       ELSE
          EmIncAx ()
       END ;
-      EmStoreVar (symtab [idx].local, symtab [idx].off,
-                  symtab [idx].size) ;
+      StoreSymVar (idx, symtab [idx].off, symtab [idx].size) ;
       DropC (EmJmpNear (L1)) ;
       SetPatTgt (zj, pc) ;             (* done: *)
       (* A WHILE loop here, not a FOR over the exit range.  The range is
@@ -7553,7 +7674,7 @@ BEGIN
                INC (k)
             END
          ELSE
-            EmLoadVar (TRUE, symtab [resultVar].off, symtab [resultVar].size)
+            LoadSymVar (resultVar, symtab [resultVar].off, symtab [resultVar].size)
          END
       END
    END
@@ -7593,6 +7714,7 @@ VAR nm : ARRAY [0..MaxName] OF CHAR ;
     isFunc, groupVar : BOOLEAN ;
     saveNest, saveLoc, saveRes, saveF : CARDINAL ;
     saveLB, savePO, saveRP : CARDINAL ;
+    saveProc, saveStatic, staticOff : CARDINAL ;
     saveR : BOOLEAN ;
     realResult : BOOLEAN ;
     reusedFwd : BOOLEAN ;
@@ -7619,6 +7741,10 @@ BEGIN
    IF isFunc THEN
       symtab [old].tag := KFunc
    END ;
+   saveProc := curProc ;
+   saveStatic := curStaticOff ;
+   symtab [old].parent := curProc ;
+   curProc := old ;
    saveNest := lexnest ;
    saveLoc := locFree ;
    saveRes := resultVar ;
@@ -7820,20 +7946,44 @@ BEGIN
       curIsFunc := (saveF # 0) ;
       locBytes := saveLB ;
       parmOff := savePO ;
+      curProc := saveProc ;
+      curStaticOff := saveStatic ;
       RETURN
    END ;
-   (* body *)
+   (* A procedure declared inside another one receives its lexical parent's
+      frame in DX.  Reserve a hidden local for it before ordinary locals, so
+      nested code can walk the static chain.  Top-level procedures have no
+      parent frame and no slot. *)
+   IF symtab [old].level > 0 THEN
+      staticOff := (locFree - 1) MOD 10000H ;
+      locFree := (locFree - 2) MOD 10000H ;
+      locBytes := locBytes + 2 ;
+      symtab [old].staticOff := staticOff ;
+      curStaticOff := staticOff
+   ELSE
+      staticOff := 0 ;
+      curStaticOff := 0
+   END ;
+   (* Body.  Nested procedures are emitted BEFORE the enclosing procedure's
+      prologue: otherwise execution entering the outer procedure falls straight
+      through the nested bodies.  The outer entry point is therefore set only
+      after DefPart has emitted them. *)
+   DefPart () ;                       (* nested declarations; stops at BEGIN *)
    symtab [old].goPos := pc ;
    symtab [old].defnd := TRUE ;
    EmPushBp () ;
    EmMovBpSp () ;
+   IF symtab [old].level > 0 THEN
+      (* The caller passed the parent frame in DX. *)
+      EmMovAxDx () ;
+      EmStoreVar (TRUE, staticOff, 2)
+   END ;
    IF resultIsReal THEN
       (* The caller put the six-byte result destination in DI.  Save it
          before declarations/body can reuse DI. *)
       EmMovAxDi () ;
       EmStoreVar (TRUE, resultPtrOff, 2)
    END ;
-   DefPart () ;                       (* nested declarations; stops at BEGIN *)
    IF locBytes > 0 THEN
       EmSubSp (locBytes)
    END ;
@@ -7864,7 +8014,9 @@ BEGIN
    resultPtrOff := saveRP ;
    curIsFunc := (saveF # 0) ;
    locBytes := saveLB ;
-   parmOff := savePO
+   parmOff := savePO ;
+   curProc := saveProc ;
+   curStaticOff := saveStatic
 END ProcFunc ;
 
 PROCEDURE DefType () ;
@@ -8163,6 +8315,8 @@ BEGIN
    brkN := 0 ;
    caseN := 0 ;
    lexnest := 0 ;
+   curProc := 0 ;
+   curStaticOff := 0 ;
    curIsFunc := FALSE ;
    resultVar := 0 ;
    resultIsReal := FALSE ;

+ 7 - 0
shell/tests/audit_helpers.py

@@ -211,6 +211,13 @@ NON_ONE_LINE = {
         "LoadVarInd": COMP,
         "StoreVarIndPrep": COMP,
         "StoreInd": COMP,
+        # The nested-procedure static-link accessors.  Each chooses a byte or
+        # word load/store, or a LEA, through [BX+disp] after walking the static
+        # chain; the sequence depends on the operand width, so the name grammar
+        # cannot describe it.  t115_nestedlocal (outer scalar read), t116_nestedstr
+        # (outer string write/read) and CALC's cell-entry path pin them.
+        "LdBxBx": COMP, "LoadVarBx": COMP, "StoreVarBx": COMP,
+        "LeaDiBx": COMP, "LoadVarIndSym": COMP, "PushSymAddr": COMP,
         # The WITH/2-D field helpers.  AddDiImm adds a record field's fixed
         # offset to an address already in DI; LdDiGlob and StDiGlob reload and
         # park that address in a data slot across a WITH body.  Each is a fixed

+ 9 - 6
shell/tests/calc_smoke.py

@@ -3,10 +3,13 @@
 
 The compile ladder proves the front-end reaches the final '.'.  This proves the
 linked image is a runnable program: it is loaded above the boot sector, prints
-the MicroCalc welcome/grid, accepts `/Q' in the command menu and exits 0.
+the MicroCalc welcome/grid, accepts a numeric formula in cell A1, reaches the
+command menu, accepts `/Q' and exits 0.  The first cell-entry path used to
+restart CALC (a nested procedure fell through into its enclosing body); feeding
+one formula here keeps that regression visible.
 
 The loader is the high-image variant of bootcom.s (LOAD_SEG=0x1000, input at
-0x9000); the ordinary fixture harness uses the same default now, while
+0x9800); the ordinary fixture harness uses the same default now, while
 rt_exec.py keeps the original low loader for its own driver image.
 """
 import os
@@ -40,18 +43,18 @@ def main():
         if len(com) > r.COM_MAX:
             sys.exit("CALC.COM is %d bytes, past the loader's %d"
                      % (len(com), r.COM_MAX))
-        boot = r.build_boot_sector()
+        boot = r.build_boot_sector(no_keypress=True)
         img = os.path.join(td, "calc.img")
-        open(img, "wb").write(r.build_floppy(boot, com, b"\r\r/Q"))
+        open(img, "wb").write(r.build_floppy(boot, com, b"\r1+2\r/Q"))
         out, rc, note = r.run_qemu(img)
         code = r.exit_code_of(rc)
         text = out.decode("latin-1")
         missing = [s for s in ("Welcome to MicroCalc", "AutoCalc: ON",
-                               "Update, Quit") if s not in text]
+                               "3.00", "Update, Quit") if s not in text]
         if code != 0 or missing:
             sys.exit("CALC did not start/quit cleanly: exit=%r note=%s missing=%r\n%s"
                      % (code, note, missing, r.visible_output(out[:2000])))
-        print("  CALC.COM             PASS  com=%d bytes, exit=0, welcome/grid/menu"
+        print("  CALC.COM             PASS  com=%d bytes, exit=0, cell-entry/formula/menu"
               % len(com))
         return 0
     finally:

+ 6 - 6
shell/tests/exec/bootcom.s

@@ -37,8 +37,8 @@
 #
 #   * The default program segment is 1000h, so the image starts at 1000:0100
 #     and the boot sector/IVT stay untouched.  The input descriptor is at
-#     1000:9000, above the largest current image (CALC's code ends below 8A00h)
-#     and below the data area at 1000:A100.  It is simply part of the disk image
+#     1000:9800, above the largest current image (CALC's image ends below
+#     9000h) and below the data area at 1000:A100.  It is simply part of the disk image
 #     the harness writes.  rt_exec.py overrides LOAD_SEG/INLEN back to 0/7000
 #     for its own driver image.
 #
@@ -387,8 +387,8 @@ ser_put:
 	ret
 
 # --------------------------------------------------------------- layout
-# The defaults are LOAD_SEG=1000h and INLEN=9000h: the image is copied to
-# 1000:0100, the input descriptor is at 1000:9000, and the data area is at
+# The defaults are LOAD_SEG=1000h and INLEN=9800h: the image is copied to
+# 1000:0100, the input descriptor is at 1000:9800, and the data area is at
 # 1000:A100.  The copy and the boot sector are in different segments, so the
 # boot sector at 0000:7C00h is never touched.  The constants are assembly
 # constants and the harness only has to know where they land in the disk image.
@@ -402,7 +402,7 @@ ser_put:
 	.set	LOAD_SEG, 0x1000	# load above the boot sector/BIOS areas
 	.endif
 	.ifndef INLEN
-	.set	INLEN, 0x9000		# word: number of input bytes
+	.set	INLEN, 0x9800		# word: number of input bytes
 	.endif
 	.ifndef SCRATCH_SEG
 	.set	SCRATCH_SEG, LOAD_SEG + 0x1000
@@ -410,7 +410,7 @@ ser_put:
 	.ifndef COPY_BYTES
 	.set	COPY_BYTES, INLEN
 	.endif
-	.set	SECTORS, 72		# 36,864 bytes read; input is inside this
+	.set	SECTORS, 77		# 39,424 bytes read; input is inside this
 	.set	INCUR, INLEN + 2	# word: cursor, starts at INBUF
 	.set	INBUF, INLEN + 4	# the bytes themselves
 	.set	INMAX, 0x00fc		# INLEN+100h - INBUF, the most that fits

+ 1 - 1
shell/tests/fixtures/calc_ladder.tsv

@@ -32,4 +32,4 @@
 # data base.  UpCase, set membership, strings, pointers, files, REAL
 # expressions/functions, recursion, forward definitions, Copy/Val/Delete/
 # Insert, local sets and the compact math runtime are all landed.
-CALC.PAS	OK	25908	19694
+CALC.PAS	OK	27458	19694

+ 9 - 1
shell/tests/fixtures/expected.tsv

@@ -570,7 +570,7 @@ t72_keypress	OK	138	6
 #                   Global char, global integer, local char, a VAR parameter
 #                   relayed to another, and READ into a VAR parameter.  A
 #                   value implementation prints H,H,41,a instead of h,h,42,?.
-t73_varparam	OK	334	8
+t73_varparam	OK	340	8
 #   t74_caselist           comma-separated CASE labels (CALC's 'G','I','O','Q')
 #                   and REAL ranges.  The range emitter used to compare the
 #                   selector with lo AND hi, both demanding equality, so
@@ -732,3 +732,11 @@ t110_insert	OK	405	1051
 t111_caseelse	OK	93	7
 
 t112_strchar	OK	230	784
+
+t113_nestedcall	OK	144	5
+
+t114_nested2	OK	106	5
+
+t115_nestedlocal	OK	104	5
+
+t116_nestedstr	OK	301	263

+ 4 - 0
shell/tests/fixtures/t113_nestedcall.out

@@ -0,0 +1,4 @@
+welcome
+inner
+getcell
+done

+ 6 - 0
shell/tests/fixtures/t113_nestedcall.pas

@@ -0,0 +1,6 @@
+program nc;
+procedure Welcome; begin writeln('welcome') end;
+procedure GetCell(FX:char; FY:integer);
+  procedure Inner; begin writeln('inner') end;
+begin Inner; writeln('getcell') end;
+begin Welcome; GetCell('A',1); writeln('done') end.

+ 3 - 0
shell/tests/fixtures/t114_nested2.out

@@ -0,0 +1,3 @@
+inner
+outer
+done

+ 5 - 0
shell/tests/fixtures/t114_nested2.pas

@@ -0,0 +1,5 @@
+program n2;
+procedure Outer;
+  procedure Inner; begin writeln('inner') end;
+begin Inner; writeln('outer') end;
+begin Outer; writeln('done') end.

+ 1 - 0
shell/tests/fixtures/t115_nestedlocal.out

@@ -0,0 +1 @@
+42

+ 6 - 0
shell/tests/fixtures/t115_nestedlocal.pas

@@ -0,0 +1,6 @@
+program nl;
+procedure Outer;
+var X:integer;
+  procedure Inner; begin writeln(X) end;
+begin X:=42; Inner end;
+begin Outer end.

+ 2 - 0
shell/tests/fixtures/t116_nestedstr.out

@@ -0,0 +1,2 @@
+Hello
+Hello!

+ 15 - 0
shell/tests/fixtures/t116_nestedstr.pas

@@ -0,0 +1,15 @@
+program ns;
+procedure Outer;
+var S : string[70];
+  procedure Inner;
+  begin
+    S := 'Hello';
+    writeln(S);
+    S := S + '!';
+    writeln(S)
+  end;
+begin
+  S := '';
+  Inner
+end;
+begin Outer end.

+ 7 - 12
shell/tests/nonvacuity.sh

@@ -1140,11 +1140,10 @@ old = """                     IF arg.kind = 5 THEN
                      ELSIF symtab [arg.idx].isVar THEN
                         (* VAR string: the slot holds the address of the
                            caller's characters; push that, not the slot. *)
-                        EmLoadVar (TRUE, symtab [arg.idx].off, 2) ;
+                        LoadSymVar (arg.idx, symtab [arg.idx].off, 2) ;
                         EmPushAx ()
                      ELSE
-                        EmPushVarAddr (symtab [arg.idx].local,
-                                       (symtab [arg.idx].off + arg.boff) MOD 10000H)
+                        EmPushSymAddr (arg.idx, (symtab [arg.idx].off + arg.boff) MOD 10000H)
                      END ;
                      ent := TU_WrStr"""
 assert s.count(old) == 1, 'the string-write arm found %d times -- update this mutation' % s.count(old)
@@ -1560,8 +1559,7 @@ if python3 - <<'PYX'
 p = 'Compiler.mod'
 s = open(p).read()
 src = ('         IF src.kind = 1 THEN\n'
-       '            EmPushVarAddr (symtab [src.idx].local,\n'
-       '                           (symtab [src.idx].off + src.boff) MOD 10000H)\n'
+       '            EmPushSymAddr (src.idx, (symtab [src.idx].off + src.boff) MOD 10000H)\n'
        '         ELSIF src.kind = 5 THEN\n'
        '            (* A string element reached through DI, including a pointer\n'
        '               dereference: Buffer^[i].  Its address is already in DI. *)\n'
@@ -1570,11 +1568,9 @@ src = ('         IF src.kind = 1 THEN\n'
        '            Err (ENoLib) ;          (* a computed string is not a value *)\n'
        '            RETURN\n'
        '         END ;\n'
-       '         EmPushVarAddr (symtab [r.idx].local,\n'
-       '                        (symtab [r.idx].off + r.boff) MOD 10000H) ;')
+       '         EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H) ;')
 dst = ('         IF src.kind = 1 THEN\n'
-       '            EmPushVarAddr (symtab [r.idx].local,\n'
-       '                           (symtab [r.idx].off + r.boff) MOD 10000H)\n'
+       '            EmPushSymAddr (r.idx, (symtab [r.idx].off + r.boff) MOD 10000H)\n'
        '         ELSIF src.kind = 5 THEN\n'
        '            (* A string element reached through DI, including a pointer\n'
        '               dereference: Buffer^[i].  Its address is already in DI. *)\n'
@@ -1583,8 +1579,7 @@ dst = ('         IF src.kind = 1 THEN\n'
        '            Err (ENoLib) ;          (* a computed string is not a value *)\n'
        '            RETURN\n'
        '         END ;\n'
-       '         EmPushVarAddr (symtab [src.idx].local,\n'
-       '                        (symtab [src.idx].off + src.boff) MOD 10000H) ;')
+       '         EmPushSymAddr (src.idx, (symtab [src.idx].off + src.boff) MOD 10000H) ;')
 assert s.count(src) == 1, 'the assignment pushes found %d times -- update this mutation' % s.count(src)
 open(p, 'w').write(s.replace(src, dst))
 PYX
@@ -2008,7 +2003,7 @@ cp "$SAVED_C" Compiler.mod
 cp "$SAVED_C" Compiler.mod
 if mutate_compiler 's|      IF symtab \[i\].level = lexnest THEN|      IF TRUE THEN|'; then
     if rebuild_compiler; then
-        expect_red "t73 red when a parameter may not shadow a global"             "expected: OK 334 8" tests/run_compile_tests.sh
+        expect_red "t73 red when a parameter may not shadow a global"             "expected: OK 340 8" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild with the old DupTest"
         fail=$((fail + 1))

+ 4 - 4
shell/tests/run_com_exec.py

@@ -60,10 +60,10 @@ TIMEOUT = 15
 LOAD_SEG = 0x1000        # the .COM's load segment
 LOAD_OFF = 0x0100        # ...and offset; DOS puts a .COM at CS:0100
 FLAT_LOAD = LOAD_SEG * 16 + LOAD_OFF
-INLEN_OFF = 0x9000       # word: how many input bytes follow
-INBUF_OFF = 0x9004       # the input bytes
-INBUF_MAX = 0x00FC       # 7100h - 7004h
-SECTORS = 72             # what bootcom.s reads off the disk
+INLEN_OFF = 0x9800       # word: how many input bytes follow
+INBUF_OFF = 0x9804       # the input bytes
+INBUF_MAX = 0x00FC       # INLEN+100h - INBUF
+SECTORS = 77             # what bootcom.s reads off the disk
 FLOPPY_SECTORS = 2880    # 1.44M, 18 sectors per track
 FLOPPY_BYTES = FLOPPY_SECTORS * 512
 COM_MAX = INLEN_OFF - LOAD_OFF  # the image must end before the input