Просмотр исходного кода

compiler: STRING VALUE parameters, whole-slot copy (step 3)

The ladder moves: CALC.PAS went from ERR 102 at 10350 to ERR 41 at 10405.
`procedure Center(S : AnyString)' is accepted, its body compiles, and the next
blocker is `Length(S)' - a string builtin, the first rung that is a standard
FUNCTION rather than a type.

  - ProcFunc now allows size > 2 for a TString parameter; the slot is n+1 bytes
    (TP3's `tstscal': a value parameter's whole size goes on the stack).
  - EmStrBlob is the string-literal blob factored out of EmPushStrAddr, so a
    literal argument can be copied from too.  (Renamed from StrLitBlob because
    audit_helpers strips two characters unconditionally for Compiler.mod and
    reported it as `rLitBlob'.)
  - EmPushStrSlot (r, reserve, copyN) pushes the source address via
    EmPushStrAddr (so a caller's LOCAL, a global and a literal all work),
    POP SI, reserves `reserve' bytes, and copies `copyN' of <len><chars> with a
    DS-relative byte loop.  NOT REP MOVSB: MOVS/STOS address through ES, and
    nothing here establishes ES = DS.
  - Both call sites push the whole slot when ParmSlot > 2 and add it to the
    cleanup total.  `copyN' is at most the slot, so a shorter actual than the
    formal leaves the tail untouched (harmless: the length byte bounds reads).

Fixture t67_strparam proves VALUE semantics, not merely "it compiles":
`Show (t)' prints hello, sets S[1] := 'X', and the caller's `t' still prints
hello; `Show ('world')' copies a literal too.  .out is hello/hello/world/hello,
executed under qemu AND Exec86 (57/57).  A by-address implementation would print
`Xello'.

nonvacuity.sh gains the matching case: collapse SlotSize's fallthrough to 2 and
t67 goes red while the compile matrix stays green.  87 ok, 0 failed.

run_all.sh: ALL PASS - matrix 68/68, ladder 1/1 at ERR 41 10405, 59 .COM
linked, 57 executed under qemu and Exec86; helper audit PASS (122 helpers).
Eric Streit 2 дней назад
Родитель
Сommit
2b90a8decd

+ 29 - 28
CALC-COMPILE.md

@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 
 | position | blocker |
 | position | blocker |
 |---|---|
 |---|---|
-| **10350** | `procedure Center(S : AnyString)` — a **string value parameter**, INSIDE `CALC.INC` |
+| **10405** | `Length(S)` inside `CALC.INC`'s `Center` — a **string builtin** (`ERR 41`), the first rung that is a standard FUNCTION |
 
 
 Rungs behind it (now passed):
 Rungs behind it (now passed):
 
 
@@ -108,14 +108,15 @@ Rungs behind it (now passed):
 | 7972 | `Numbers : set of Char = ['0'..'9']` — set typed constant | sets, increment 1 |
 | 7972 | `Numbers : set of Char = ['0'..'9']` — set typed constant | sets, increment 1 |
 | 8098 | `Attributes = (Constant,…)` — enum; and `SetOfAttri = Set of Attributes` | Stage 5 (enums) |
 | 8098 | `Attributes = (Constant,…)` — enum; and `SetOfAttri = Set of Attributes` | Stage 5 (enums) |
 | 8362 | `CellRec = record … end` — record type | Stage 7 (record types) |
 | 8362 | `CellRec = record … end` — record type | Stage 7 (record types) |
-| 8705 | `Cells = array['A'..'G',1..FYMax]`, `Sheet : Cells`, `XPOS : array[…] = (…)` | Stage 8 (declarations) |
-| 9202 | `MCFile : file of CellRec` — the file type, the whole `var` block, and the `{$I}` expansion | Stage 13 (file types) |
+| 8705 | `Cells = array['A'..'G',1..FYMax]`, `Sheet : Cells`, `XPOS` | Stage 8 (declarations) |
+| 9202 | `MCFile : file of CellRec` — file type, whole `var` block, `{$I}` expansion | Stage 13 (file types) |
+| 10350 | `procedure Center(S : AnyString)` — a **string value parameter** | calling convention, steps 1-3 |
 
 
 Rungs ahead:
 Rungs ahead:
 
 
 | position | blocker |
 | position | blocker |
 |---|---|
 |---|---|
-| 10350 | a string value parameter, then the rest of `CALC.INC`: file operations, indexing, `with`, pointers, `var` params, reals, string builtins, CRT, recursion |
+| 10405 | the string builtins (`Length`, `Copy`, `Delete`, `Insert`, `Str`, `Val`), then the rest of `CALC.INC` |
 
 
 Corrections the measurements force, worth stating plainly because the stage list
 Corrections the measurements force, worth stating plainly because the stage list
 below was written before them:
 below was written before them:
@@ -159,6 +160,7 @@ stripped.
 | array **element** access with a VARIABLE index `Sheet[fx,fy]`, `with` | 13 `with` sites | `ENoLib` — scaled addressing next |
 | array **element** access with a VARIABLE index `Sheet[fx,fy]`, `with` | 13 `with` sites | `ENoLib` — scaled addressing next |
 | array indexed by an **enum type** | `CALC.INC:773,927` | `ERR 41` |
 | array indexed by an **enum type** | `CALC.INC:773,927` | `ERR 41` |
 | pointers `^Pages`, `New`, `Dispose` | `CALC.INC:579,587,639,690` | `ERR 41` |
 | pointers `^Pages`, `New`, `Dispose` | `CALC.INC:579,587,639,690` | `ERR 41` |
+| string **value** parameters (`S : AnyString`) | `CALC.INC:17`, … | **done** — whole-slot copy; `t67_strparam` |
 | `var` parameters | 10 routines, 14 occurrences (e.g. `CALC.INC:765,979,995,1036`) | **silently by value** |
 | `var` parameters | 10 routines, 14 occurrences (e.g. `CALC.INC:765,979,995,1036`) | **silently by value** |
 | self-recursion | `Fact` — `Fact := I * Fact(I - 1)`, `CALC.INC:747,754` | `ERR 1` at the `(` |
 | self-recursion | `Fact` — `Fact := I * Fact(I - 1)`, `CALC.INC:747,754` | `ERR 1` at the `(` |
 | mutual recursion via `forward` | `Expression` fwd `CALC.INC:742` ↔ `Factor` calls it at `820`,`932` | `forward` parses; definition untested |
 | mutual recursion via `forward` | `Expression` fwd `CALC.INC:742` ↔ `Factor` calls it at `820`,`932` | `forward` parses; definition untested |
@@ -455,8 +457,16 @@ Exec86):
   **10350**, where the first blocker is a string VALUE parameter in a nested
   **10350**, where the first blocker is a string VALUE parameter in a nested
   procedure. File OPERATIONS (`Assign`/`Reset`/…, read/write with a file
   procedure. File OPERATIONS (`Assign`/`Reset`/…, read/write with a file
   argument) are the next increment and are loud until then.
   argument) are the next increment and are loud until then.
-- **The ladder now stops at `ERR 102` at `10350`** — `procedure Center(S :
-  AnyString)` in `CALC.INC`, a value parameter wider than a word.
+- **String VALUE parameters are landed** (TP3's whole-slot copy).  Three
+  commits: slot sizes (`v-TP3-CALC-PARAMSLOTS`), the parameter list on the proc
+  symbol (`v-TP3-CALC-PARAMLIST`), and the copy + `size > 2` for strings
+  (`v-TP3-CALC-STRPARAM`).  Fixture `t67_strparam` proves value semantics: the
+  callee mutates `S[1]`, the caller's string is unchanged.  This moved the rung
+  from 10350 to **10405**.
+- **The ladder now stops at `ERR 41` at `10405`** — `Length(S)`, inside
+  `Center`'s body.  The string builtins (`Length`, `Copy`, `Delete`, `Insert`,
+  `Str`, `Val`) are the next increment, and the first rung that is a standard
+  FUNCTION rather than a type.
 - **One surprise on resuming:** the working tree was found holding the
 - **One surprise on resuming:** the working tree was found holding the
   nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the
   nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the
   `locFree := 0FFFEH` / pre-decrement-base revert), i.e. a mutation from a
   `locFree := 0FFFEH` / pre-decrement-base revert), i.e. a mutation from a
@@ -466,8 +476,8 @@ Exec86):
   byte-for-byte the same for INTEGER locals, so the compile matrix would have
   byte-for-byte the same for INTEGER locals, so the compile matrix would have
   been green and only `t40_readln` (and later CALC) would have noticed.
   been green and only `t40_readln` (and later CALC) would have noticed.
 - **Verified baseline, run on this commit:** `./tests/run_all.sh` →
 - **Verified baseline, run on this commit:** `./tests/run_all.sh` →
-  **OVERALL: ALL PASS**, compile matrix **67/67**, CALC ladder **1/1** at
-  `ERR 102 10350`, 58 `.COM` linked and 56 executed under both qemu and Exec86
+  **OVERALL: ALL PASS**, compile matrix **68/68**, CALC ladder **1/1** at
+  `ERR 41 10405`, 59 `.COM` linked and 57 executed under both qemu and Exec86
   (`t64_arrays` is compile-only: its image is past the 8 KB boot-sector read).
   (`t64_arrays` is compile-only: its image is past the 8 KB boot-sector read).
 - **`tests/nonvacuity.sh`:** re-run after this commit's cases (the set
 - **`tests/nonvacuity.sh`:** re-run after this commit's cases (the set
   membership bit among them); see the commit message for the count. It is not in
   membership bit among them); see the commit message for the count. It is not in
@@ -510,26 +520,17 @@ ordered by what the ladder will hit next, not by the original stage numbers
 shape as this session's nine: one focused change, one fixture that is red before
 shape as this session's nine: one focused change, one fixture that is red before
 and green after, an execution proof where the image fits, and a commit.
 and green after, an execution proof where the image fits, and a commit.
 
 
-1. **A string VALUE parameter** — the current rung:
-   `procedure Center(S : AnyString)` (`CALC.INC:17`).  `ProcFunc` refuses a
-   parameter wider than a word; a `string[n]` parameter is `n+1` bytes.  TP3
-   passes a value parameter's whole size on the stack (`TPSRC7` `tstscal`), so
-   the caller must copy the string into the callee's slot.
-   **Step 1 is DONE** (`v-TP3-CALC-PARAMSLOTS`): parameter *slots* are
-   generalised — `SlotSize (cls, size)` is TP3's "scalar-like ⇒ at least two
-   bytes, larger ⇒ its own size", and the callee's reverse layout is the
-   general `off := 8 + frameTotal − off − slot`, which reduces exactly to the
-   old `parmOff + 2 − off` while every slot is 2.  Nothing observable moved
-   (matrix 67/67, ladder unchanged, framedisp PASS, 56/56 executed).
-   **Next:** (3) push a string's whole slot (`SUB SP,size / MOV DI,SP / MOV
-   SI,src / MOV CX,size / CLD / REP MOVSB`, or a `TU_PushStr` entry) and allow
-   `size > 2` for string parameters; (4) a fixture where the callee **mutates**
-   `S` and the caller's variable is unchanged — the value semantics TP3
-   promises.  `var` parameters proper are item 6.
-   **Step 2 is DONE too:** the procedure symbol carries `parmFirst`/`parmCount`,
-   `ParmSlot (idx, k)` sizes each argument from it, and `EmCallMost` cleans up
-   the sum of slot sizes.  Still behaviour-identical (every slot 2), and a
-   slot > 2 is a loud `ENoLib` until step 3 pushes it.
+1. **String VALUE parameters — DONE** (the rung moved 10350 → 10405; the
+   blocker is now `Length(S)`, a builtin).  TP3 puts a value parameter's whole
+   size on the stack (`TPSRC7` `tstscal`), so a `string[n]` parameter is `n+1`
+   bytes.  The three steps landed as separate commits/tags:
+   `v-TP3-CALC-PARAMSLOTS` (slot sizes + reverse layout),
+   `v-TP3-CALC-PARAMLIST` (the procedure symbol carries `parmFirst`/`parmCount`;
+   call sites size each argument), and `v-TP3-CALC-STRPARAM` (the caller copies
+   `<len><chars>` into the slot; `EmStrBlob` + `EmPushStrSlot`, a DS-relative
+   byte loop, no ES dependency).  Fixture `t67_strparam`: the callee mutates
+   `S[1]` and the caller's string is unchanged, so it is a copy and not an
+   address.  `var` parameters proper are item 6.
 2. **The rest of the declaration part**, if the rung is still there: local
 2. **The rest of the declaration part**, if the rung is still there: local
    `type`/`const`/`var` inside a nested function (`CALC.INC:767-778`, the
    `type`/`const`/`var` inside a nested function (`CALC.INC:767-778`, the
    enum-indexed array of strings), `Succ`/`Pred`/`Ord`/`Chr`.
    enum-indexed array of strings), `Succ`/`Pred`/`Ord`/`Chr`.

+ 8 - 7
SUMMARY.md

@@ -2037,15 +2037,16 @@ census of what `CALC.PAS` actually needs and the reference measurements from
 the original `TURBO.COM`. Two tracks run in parallel: the ladder (front-end,
 the original `TURBO.COM`. Two tracks run in parallel: the ladder (front-end,
 verifiable on small fixtures) and the layout (§3, the only thing that stops
 verifiable on small fixtures) and the layout (§3, the only thing that stops
 the compiler *emitting* a 16,560-byte program). The ratchet and front-end
 the compiler *emitting* a 16,560-byte program). The ratchet and front-end
-features through **file types** are landed: the ladder
+features through **string value parameters** are landed: the ladder
 (`tests/calc_ladder.sh` + `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`;
 (`tests/calc_ladder.sh` + `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`;
-CALC.PAS now stops at `ERR 102` at pos 10350, a string value parameter INSIDE
+CALC.PAS now stops at `ERR 41` at pos 10405, the `Length` builtin inside
 `CALC.INC`), the `{$I}` include, `^X`/`#nn`, scalar typed constants, sets,
 `CALC.INC`), the `{$I}` include, `^X`/`#nn`, scalar typed constants, sets,
-enums, records (CellRec = 80), subranges, arrays, element/field access, and file
-types (`text`, `file of T`; fixtures through `t66_files`). The next rungs are
-file operations, a variable-index subscript (`Sheet[FX,FY]`) + `with`, CRT,
-reals, the string builtins, pointers, `var` params, recursion, and the layout
-change. Items 1-4 below are independent of both.
+enums, records (CellRec = 80), subranges, arrays, element/field access, file
+types, and string value parameters (`t67_strparam`; fixtures through `t67`).
+The next rungs are the string builtins (`Length`/`Copy`/`Delete`/`Insert`/
+`Str`/`Val`), file operations, variable-index subscripts + `with`, pointers,
+`var` params, reals, CRT, recursion, and the layout change. Items 1-4 below are
+independent of both.
 
 
 1. The rest of the string language. `v-TP3-STRVAR`/`v-TP3-STRREAD-ASSIGN` cover
 1. The rest of the string language. `v-TP3-STRVAR`/`v-TP3-STRREAD-ASSIGN` cover
    string variables, `s := 'literal'`, `writeln (s)`, `readln (s)`/`read (s)`
    string variables, `s := 'literal'`, `writeln (s)`, `readln (s)`/`read (s)`

+ 124 - 38
shell/Compiler.mod

@@ -974,6 +974,11 @@ BEGIN
    Ebyte (5AH)
    Ebyte (5AH)
 END EmPopDx ;
 END EmPopDx ;
 
 
+PROCEDURE EmPopSi () ;
+BEGIN
+   Ebyte (5EH)                                   (* POP SI *)
+END EmPopSi ;
+
 (* 91 = XCHG AX,CX, and NOT 93.  LoadPair hands the operator the left
 (* 91 = XCHG AX,CX, and NOT 93.  LoadPair hands the operator the left
    operand in CX and the right in AX (in the original, and still the
    operand in CX and the right in AX (in the original, and still the
    commonest, shape: it pushes the left, loads the right, then pops the
    commonest, shape: it pushes the left, loads the right, then pops the
@@ -2528,6 +2533,36 @@ BEGIN
    END
    END
 END LoadPair ;
 END LoadPair ;
 
 
+PROCEDURE EmStrBlob (r : ERes) : CARDINAL ;
+(* Emit a string literal's <len><chars> in the code stream behind a JMP and
+   return the image offset of the LENGTH byte.  Shared by EmPushStrAddr (which
+   pushes that address) and EmPushStrSlot (which copies the bytes onto the
+   stack for a string value parameter). *)
+VAR n, len, here : CARDINAL ;
+BEGIN
+   len := strLen [r.strx] ;
+   IF len > 255 THEN
+      (* As in IoCall: the length is one byte, so a longer literal would wrap
+         and silently carry only its first (length MOD 256) characters.
+         TP3 strings are at most 255 characters. *)
+      Err (EConstRange) ;
+      RETURN 0
+   END ;
+   IF len + 1 <= 127 THEN
+      Ebyte (0EBH) ; Ebyte (VAL (BYTE, len + 1))    (* JMP rel8 over it *)
+   ELSE
+      Ebyte (0E9H) ; Eword (len + 1)                (* JMP rel16 over it *)
+   END ;
+   here := pc ;                 (* the length byte: the literal's address *)
+   Ebyte (VAL (BYTE, len)) ;
+   n := 0 ;
+   WHILE n < len DO
+      Ebyte (VAL (BYTE, ORD (strPool [strOff [r.strx] + n]))) ;
+      INC (n)
+   END ;
+   RETURN here
+END EmStrBlob ;
+
 PROCEDURE EmPushStrAddr (r : ERes) ;
 PROCEDURE EmPushStrAddr (r : ERes) ;
 (* Push the ADDRESS of a whole-string operand - the argument shape cmpstr
 (* Push the ADDRESS of a whole-string operand - the argument shape cmpstr
    takes, one per operand.
    takes, one per operand.
@@ -2561,38 +2596,52 @@ PROCEDURE EmPushStrAddr (r : ERes) ;
    (its character code), so `s = 'a'` compares a string with a character and
    (its character code), so `s = 'a'` compares a string with a character and
    is a type error rather than a length-1 string compare.  That is deliberate
    is a type error rather than a length-1 string compare.  That is deliberate
    - it is the same rule `c := 'a'` relies on - and the caller rejects it. *)
    - it is the same rule `c := 'a'` relies on - and the caller rejects it. *)
-VAR n, len, here : CARDINAL ;
 BEGIN
 BEGIN
    IF r.kind = 1 THEN
    IF r.kind = 1 THEN
       EmPushVarAddr (symtab [r.idx].local,
       EmPushVarAddr (symtab [r.idx].local,
                      (symtab [r.idx].off + r.boff) MOD 10000H)
                      (symtab [r.idx].off + r.boff) MOD 10000H)
    ELSIF r.kind = 3 THEN
    ELSIF r.kind = 3 THEN
-      len := strLen [r.strx] ;
-      IF len > 255 THEN
-         (* As in IoCall: the length is one byte, so a longer literal would
-            wrap and silently compare only its first (length MOD 256)
-            characters.  TP3 strings are at most 255 characters. *)
-         Err (EConstRange) ;
-         RETURN
-      END ;
-      IF len + 1 <= 127 THEN
-         Ebyte (0EBH) ; Ebyte (VAL (BYTE, len + 1))    (* JMP rel8 over it *)
-      ELSE
-         Ebyte (0E9H) ; Eword (len + 1)                (* JMP rel16 over it *)
-      END ;
-      here := pc ;                 (* the length byte: the literal's address *)
-      Ebyte (VAL (BYTE, len)) ;
-      n := 0 ;
-      WHILE n < len DO
-         Ebyte (VAL (BYTE, ORD (strPool [strOff [r.strx] + n]))) ;
-         INC (n)
-      END ;
-      EmPushVarAddr (FALSE, here)  (* LEA AX,[here]; PUSH AX *)
+      EmPushVarAddr (FALSE, EmStrBlob (r))  (* the literal travels inline *)
    ELSE
    ELSE
       Err (ENoLib)                 (* a computed string is not a value *)
       Err (ENoLib)                 (* a computed string is not a value *)
    END
    END
 END EmPushStrAddr ;
 END EmPushStrAddr ;
 
 
+PROCEDURE EmPushStrSlot (r : ERes ; reserve, copyN : CARDINAL) ;
+(* Push a STRING VALUE argument's whole slot: reserve `reserve' bytes on the
+   stack and copy `copyN' of the source string's <len><chars> into them, so
+   that after the CALL the callee's BP+4 is the length byte and the characters
+   follow it.
+
+     <push the source address, then POP SI>   ; local/global/literal alike
+     SUB SP,reserve ; MOV DI,SP ; MOV CX,copyN
+     loop: MOV AL,[SI] ; MOV [DI],AL ; INC SI ; INC DI ; LOOP loop
+
+   The address comes from EmPushStrAddr, which already knows how to form it for
+   a local ([BP+off]), a global (absolute) or a literal (inline blob) - so a
+   caller's LOCAL string can be passed too, not just a global.
+
+   A byte loop, not REP MOVSB, on purpose: MOVS/STOS address through ES, and a
+   .COM's ES happens to equal DS but nothing in this compiler establishes that
+   contract.  Every access here is DS-relative, which is the one segment the
+   image already guarantees.  `copyN' is at most `reserve' (a shorter actual
+   string than the formal leaves the tail of the slot untouched, harmless
+   because the length byte bounds every read). *)
+BEGIN
+   EmPushStrAddr (r) ;                   (* push the source address *)
+   EmPopSi () ;                          (* SI := source *)
+   EmSubSp (reserve) ;
+   Ebyte (89H) ; Ebyte (0E7H) ;          (* MOV DI,SP *)
+   EmMovCxi (copyN) ;
+   IF copyN > 0 THEN
+      Ebyte (8AH) ; Ebyte (04H) ;        (* MOV AL,[SI] *)
+      Ebyte (88H) ; Ebyte (05H) ;        (* MOV [DI],AL *)
+      Ebyte (46H) ;                      (* INC SI *)
+      Ebyte (47H) ;                      (* INC DI *)
+      Ebyte (0E2H) ; Ebyte (0F8H)        (* LOOP -8: back to MOV AL,[SI] *)
+   END
+END EmPushStrSlot ;
+
 (* ---------------------------------------------------------------- *)
 (* ---------------------------------------------------------------- *)
 (*  sets - type sizing, literals, compile-time membership            *)
 (*  sets - type sizing, literals, compile-time membership            *)
 (* ---------------------------------------------------------------- *)
 (* ---------------------------------------------------------------- *)
@@ -3252,7 +3301,7 @@ PROCEDURE ParseCallArgs (idx : CARDINAL) ;
    first argument goes on first and so ends up farthest from BP, and the LAST
    first argument goes on first and so ends up farthest from BP, and the LAST
    declared parameter is the one at BP+4 (RESUME-TP3.md 3.11). *)
    declared parameter is the one at BP+4 (RESUME-TP3.md 3.11). *)
 VAR arg : ERes ;
 VAR arg : ERes ;
-    nArgs, cleanup, slot : CARDINAL ;
+    nArgs, cleanup, slot, nCopy : CARDINAL ;
 BEGIN
 BEGIN
    nArgs := 0 ;
    nArgs := 0 ;
    cleanup := 0 ;
    cleanup := 0 ;
@@ -3267,15 +3316,32 @@ BEGIN
          ParseExpr (arg) ;
          ParseExpr (arg) ;
          slot := ParmSlot (idx, nArgs) ;
          slot := ParmSlot (idx, nArgs) ;
          IF slot > 2 THEN
          IF slot > 2 THEN
-            (* A value parameter wider than a word (a string): its whole slot
-               must be pushed, which is step 3 of the calling-convention work.
-               Loud, because pushing two bytes of a 71-byte argument would be
-               a silent miscompile. *)
-            Err (ENoLib) ;
-            EXIT
+            (* A STRING VALUE parameter: push its whole slot, copied.
+               `copyN' is at most the slot: an actual shorter than the formal
+               leaves the tail untouched, which is harmless because the length
+               byte bounds every read. *)
+            IF arg.cls # TString THEN
+               Err (ETypeErr) ;
+               EXIT
+            END ;
+            nCopy := slot ;
+            IF arg.kind = 1 THEN
+               IF VarSize (arg) < nCopy THEN
+                  nCopy := VarSize (arg)
+               END
+            ELSIF arg.kind = 3 THEN
+               IF strLen [arg.strx] + 1 < nCopy THEN
+                  nCopy := strLen [arg.strx] + 1
+               END
+            ELSE
+               Err (ENoLib) ;          (* a computed string is not a value *)
+               EXIT
+            END ;
+            EmPushStrSlot (arg, slot, nCopy)
+         ELSE
+            LoadAtom (arg) ;
+            EmPushAx ()
          END ;
          END ;
-         LoadAtom (arg) ;
-         EmPushAx () ;
          cleanup := cleanup + slot ;
          cleanup := cleanup + slot ;
          INC (nArgs) ;
          INC (nArgs) ;
          IF NOT MatchDelim (',') THEN
          IF NOT MatchDelim (',') THEN
@@ -3291,7 +3357,7 @@ PROCEDURE ParseCall (idx : CARDINAL) ;
 (* procedure/function call; '(' optional - same parse-then-push as above,
 (* procedure/function call; '(' optional - same parse-then-push as above,
    reached from ParseAtom when a FUNCTION is called in an expression *)
    reached from ParseAtom when a FUNCTION is called in an expression *)
 VAR arg : ERes ;
 VAR arg : ERes ;
-    nArgs, cleanup, slot : CARDINAL ;
+    nArgs, cleanup, slot, nCopy : CARDINAL ;
 BEGIN
 BEGIN
    nArgs := 0 ;
    nArgs := 0 ;
    cleanup := 0 ;
    cleanup := 0 ;
@@ -3305,12 +3371,29 @@ BEGIN
             ParseExpr (arg) ;
             ParseExpr (arg) ;
             slot := ParmSlot (idx, nArgs) ;
             slot := ParmSlot (idx, nArgs) ;
             IF slot > 2 THEN
             IF slot > 2 THEN
-               (* see ParseCallArgs: a string slot needs its whole width *)
-               Err (ENoLib) ;
-               EXIT
+               (* see ParseCallArgs: push the whole string slot *)
+               IF arg.cls # TString THEN
+                  Err (ETypeErr) ;
+                  EXIT
+               END ;
+               nCopy := slot ;
+               IF arg.kind = 1 THEN
+                  IF VarSize (arg) < nCopy THEN
+                     nCopy := VarSize (arg)
+                  END
+               ELSIF arg.kind = 3 THEN
+                  IF strLen [arg.strx] + 1 < nCopy THEN
+                     nCopy := strLen [arg.strx] + 1
+                  END
+               ELSE
+                  Err (ENoLib) ;
+                  EXIT
+               END ;
+               EmPushStrSlot (arg, slot, nCopy)
+            ELSE
+               LoadAtom (arg) ;
+               EmPushAx ()
             END ;
             END ;
-            LoadAtom (arg) ;
-            EmPushAx () ;
             cleanup := cleanup + slot ;
             cleanup := cleanup + slot ;
             INC (nArgs) ;
             INC (nArgs) ;
             IF NOT MatchDelim (',') THEN
             IF NOT MatchDelim (',') THEN
@@ -4732,7 +4815,10 @@ BEGIN
             END ;
             END ;
             ExpectDelim (':', ENoSemi) ;   (* formal is 'name : type' *)
             ExpectDelim (':', ENoSemi) ;   (* formal is 'name : type' *)
             ParseType (cls, size, elem) ;
             ParseType (cls, size, elem) ;
-            IF size > 2 THEN
+            IF (size > 2) AND (cls # TString) THEN
+               (* A string VALUE parameter is a whole-slot copy (n+1 bytes on
+                  the stack); any other type wider than a word - a record, an
+                  array, a set - is still refused. *)
                Err (ENoLib) ;
                Err (ENoLib) ;
                RETURN
                RETURN
             END ;
             END ;

+ 8 - 0
shell/tests/audit_helpers.py

@@ -132,6 +132,14 @@ NON_ONE_LINE = {
         # single sequence to disassemble; the LAYOUT it produces is pinned by
         # single sequence to disassemble; the LAYOUT it produces is pinned by
         # t61_setconst's .out instead.
         # t61_setconst's .out instead.
         "SetBlob": COMP,
         "SetBlob": COMP,
+        # StrBlob is the string-literal blob on its own (factored out of
+        # PushStrAddr so a string value parameter can copy from it); PushStrSlot
+        # reserves a slot and copies a string value into it.  Both emit data
+        # behind a jump / a data-dependent byte loop, so there is no single
+        # sequence to disassemble.  The layouts they produce are pinned by the
+        # string and string-parameter fixtures instead.
+        "StrBlob": COMP,
+        "PushStrSlot": COMP,
     },
     },
 }
 }
 # The Vx family is NOT here: those four are audited by audit_vx_helpers,
 # The Vx family is NOT here: those four are audited by audit_vx_helpers,

+ 6 - 8
shell/tests/fixtures/calc_ladder.tsv

@@ -25,11 +25,9 @@
 # positions shift — which is exactly why this must be a measured number, not a
 # positions shift — which is exactly why this must be a measured number, not a
 # remembered one.
 # remembered one.
 #
 #
-# Current rung (2026-10-19): INSIDE CALC.INC — `procedure Center(S : AnyString)',
-# a STRING VALUE parameter.  The rung moved from 9202 (MCFile : file of
-# CellRec) when FILE TYPES landed (`text' and `file of T'), which let the whole
-# `var' block pass and the `{$I CALC.INC}' at 9404 expand, so the compiler is
-# now compiling the include's body.  This is a big step: past the include is
-# where all of CALC's own code lives.  The immediate blocker is a value
-# parameter whose type is bigger than a word (string[70]).
-CALC.PAS	ERR	102	10350
+# Current rung (2026-10-20): `Length(S)' inside CALC.INC's Center - a string
+# BUILTIN, not a declaration.  The rung moved from 10350 (`procedure Center(S :
+# AnyString)') when STRING VALUE PARAMETERS landed: the whole slot is copied
+# onto the stack, length byte at BP+4, and the callee mutates only its copy.
+# This is the first rung that is a standard FUNCTION rather than a type.
+CALC.PAS	ERR	41	10405

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

@@ -519,3 +519,12 @@ t65_array_access	OK	101	16
 #                   data=12 = two file variables (4 each) + the two @@T
 #                   data=12 = two file variables (4 each) + the two @@T
 #                   temporaries.  File OPERATIONS are the next increment.
 #                   temporaries.  File OPERATIONS are the next increment.
 t66_files	OK	50	12
 t66_files	OK	50	12
+#   t67_strparam           a STRING VALUE parameter - CALC.INC:17.  TP3 puts a
+#                   value parameter's whole size on the stack, so a string[20]
+#                   parameter is 21 bytes and the caller COPIES <len><chars>
+#                   into the callee's slot (length byte at BP+4).  The .out is
+#                   `hello / hello / world / hello': Show mutates S[1], and the
+#                   caller's t still prints `hello', so it is a copy and not an
+#                   address; a literal argument is copied too.  data=25 = t (21)
+#                   + the @@T temporaries; the slot lives on the stack.
+t67_strparam	OK	189	25

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

@@ -0,0 +1,4 @@
+hello
+hello
+world
+hello

+ 35 - 0
shell/tests/fixtures/t67_strparam.pas

@@ -0,0 +1,35 @@
+program t67_strparam;
+
+{ t67 -- a STRING VALUE parameter.  CALC.INC:17 is
+
+      procedure Center(S : AnyString);
+
+  TP3 puts a value parameter's whole SIZE on the stack (TPSRC7 tstscal), so a
+  string[20] parameter is 21 bytes: the caller copies <len><chars> into the
+  callee's slot, with the length byte at the lowest address (BP+4).  That is
+  what makes it a VALUE: the callee has its own copy, and mutating S must not
+  touch the caller's variable.
+
+  The fixture proves both halves:
+    - `Show (t)' then `writeln (t)' prints `hello' again, so S[1] := 'X'
+      changed only the copy;
+    - `Show ('world')' shows a LITERAL argument being copied too.
+  A by-address implementation would print `Xello' after the first call.
+
+  The bytes come from a DS-relative byte loop, not REP MOVSB: MOVS addresses
+  through ES, and nothing here establishes ES = DS. }
+
+var
+   t : string[20];
+procedure Show (S : string[20]);
+begin
+   writeln (S);
+   S[1] := 'X'
+end;
+begin
+   t := 'hello';
+   Show (t);
+   writeln (t);
+   Show ('world');
+   writeln (t)
+end.

+ 27 - 0
shell/tests/nonvacuity.sh

@@ -1643,6 +1643,33 @@ else
 fi
 fi
 cp "$SAVED_C" Compiler.mod
 cp "$SAVED_C" Compiler.mod
 
 
+# --- a string parameter's slot --------------------------------------------
+# A string VALUE parameter's slot is its size (n+1 bytes).  Collapse SlotSize
+# to 2 for it and BOTH sides agree on 2 - so the compiler still compiles and
+# the matrix stays green - but the callee's "string" is just a length byte and
+# garbage; t67 prints something other than `hello'.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = '   RETURN size\nEND SlotSize ;'
+assert s.count(old) == 1, 'the SlotSize fallthrough found %d times -- update this mutation' % s.count(old)
+open(p, 'w').write(s.replace(old, '   RETURN 2\nEND SlotSize ;'))
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t67 red when a string parameter's slot collapses to a word" \
+            "t67_strparam" python3 tests/run_com_exec.py t67_strparam
+    else
+        echo "  FAIL: the compiler would not rebuild with SlotSize collapsed"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the string-slot mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED_C" Compiler.mod
+
 if rebuild_compiler; then
 if rebuild_compiler; then
     if python3 tests/run_com_exec.py >/dev/null 2>&1; then
     if python3 tests/run_com_exec.py >/dev/null 2>&1; then
         echo "  ok: every executed fixture passes on the restored sources"
         echo "  ok: every executed fixture passes on the restored sources"