Przeglądaj źródła

compiler: array/record element and field access, constant index (CALC Stage 8)

An element's WIDTH is not its array's, so the expression now carries one
(ERes.sz) and every consumer asks VarSize(r) instead of reading the symbol's
size.  Without that, `b[1] := 2' would store the whole array.

  - VarSize(r): r.sz when the parser recorded one (an element or field), else
    the variable's own size.
  - ParseSub takes a `.field' as well as a `[...]': a field adds its offset and
    takes its size and class; a CONSTANT subscript folds
    (index - lower bound) * element size and takes the ELEMENT's size and
    class (ArrRec now records the element class).  A VARIABLE index is still
    ENoLib - scaled run-time addressing, including the 2nd dimension, is next.
  - LoadAtom, Assignment and IoCall use VarSize throughout.

One real bug, caught by the matrix: Statmnt builds an assignment TARGET
directly rather than through ParseVar, so it never set the new `sz' - and every
assignment inside a while/for body then computed a garbage width.  Six fixtures
went red at once (t10, t12, t14, t30, t48, t49); `t.sz := symtab[t.idx].size'
in Statmnt is the fix.

Fixture t65_array_access: `v[1] := 11', `v[3] := v[1] + v[2]' (reads two,
writes a third), `r.A' / `r.B' - .out is 33 then 12, so a wrong scaling, a wrong
lower-bound subtraction or a wrong store width all change the printed number.
Executed under qemu and Exec86.  t14 RE-BASELINED: `array [..] of <type>' now
compiles and its `b[1] := 2' stores two bytes - ERR 102 109 -> OK 41 15.

nonvacuity.sh: the element width is a new case (drop it and t65 is red while
the matrix stays OK), and two older string-mutation anchors were updated - they
matched the exact `symtab[r.idx].size' text this change replaced, so they had
started reporting BROKEN CASE (loudly, by design, rather than silently passing).

run_all.sh: ALL PASS - compile matrix 66/66, CALC ladder 1/1 at ERR 102 9202,
57 .COM linked and 55 executed under both qemu and Exec86.  nonvacuity 85 ok/0.
Eric Streit 14 godzin temu
rodzic
commit
2a31b357e7

+ 26 - 14
CALC-COMPILE.md

@@ -152,7 +152,8 @@ stripped.
 | `with` | 13 sites | after field selection |
 | 2-D array `array['A'..'G',1..FYMax] of CellRec` | `CALC.PAS:133` | **type + variable done (Stage 8)** — `Sheet` = 11760 |
 | array typed constant `XPOS : array[…] of Integer = (…)` | `CALC.PAS:137` | **done (Stage 8)** — code-resident |
-| array **element** access `Sheet[fx,fy]`, `b[i]` | many | `ENoLib` — needs a width on the expression |
+| array **element** access `b[i]` (constant index), `r.f` | `t14`, `t65` | **done** — width on the expression |
+| 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` |
 | pointers `^Pages`, `New`, `Dispose` | `CALC.INC:579,587,639,690` | `ERR 41` |
 | `var` parameters | 10 routines, 14 occurrences (e.g. `CALC.INC:765,979,995,1036`) | **silently by value** |
@@ -427,10 +428,14 @@ The ladder row now sits on **sets**.
   Fixture `t64_arrays` (compile-only: its ~16 KB image is past the exec
   harness's 8 KB boot-sector read). `t14` re-baselined: the array DECLARES now;
   the refusal moved to element access.
-- **Array/record FIELD/ELEMENT access is the next increment, and is loud until
-  then.** A subscript or a `.field` would need the expression to carry a width
-  (an element's size is not its array's), so `b[1] := 2` cannot store the whole
-  array by accident. The rung is now the `file of` in the `var` block.
+- **Array/record ELEMENT and FIELD access is landed for CONSTANT indices**
+  (constant subscript + `.field`), with the value's width carried on the
+  expression (`ERes.sz`/`VarSize`). Fixture `t65_array_access`; `t14`
+  re-baselined `ERR 102 109` → `OK 41 15`. A VARIABLE index (and `with`) is the
+  next increment and is still loud.
+- **The ladder now stops at `ERR 102` at `9202`** — `MCFile : file of
+  CellRec`, the file type in the `var` block. So the next stage is files, then
+  the `{$I}` at 9404 and the `CALC.INC` body.
 - **One surprise on resuming:** the working tree was found holding the
   nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the
   `locFree := 0FFFEH` / pre-decrement-base revert), i.e. a mutation from a
@@ -440,8 +445,8 @@ The ladder row now sits on **sets**.
   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.
 - **Verified baseline, run on this commit:** `./tests/run_all.sh` →
-  **OVERALL: ALL PASS**, compile matrix **65/65**, CALC ladder **1/1** at
-  `ERR 102 9202`, 55 `.COM` linked and 54 executed under both qemu and Exec86
+  **OVERALL: ALL PASS**, compile matrix **66/66**, CALC ladder **1/1** at
+  `ERR 102 9202`, 57 `.COM` linked and 55 executed under both qemu and Exec86
   (`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
   membership bit among them); see the commit message for the count. It is not in
@@ -662,13 +667,20 @@ why `t64_arrays`'s `data=11764` is `Sheet` plus the two `@@T` temporaries.
 `t14` is re-baselined: the array declares now, and the refusal moved from the
 declaration (pos 83) to the subscript (pos 109).
 
-**Not yet, and loud:** element access (`b[1]`, `Sheet[fx,fy]`), because an
-element's WIDTH is not its array's - folding the address and leaving the size
-at the array's would make `b[1] := 2` store the whole array.  This needs the
-expression to carry a width (an `ERes.sz`), threaded through `LoadAtom`,
-`Assignment`, `IoCall` and `SaveLeft`, plus scaled/2-D address arithmetic for a
-VARIABLE index; it is the next increment and it is what `Sheet[FX,FY].CellStatus`
-and `with` wait on.
+**Element and field access are DONE for constant indices.**  An expression now
+carries its own WIDTH (`ERes.sz`, via `VarSize`), because an element's size is
+not its array's: a constant subscript folds `(index - lower bound) * element
+size` into the address and hands the element's size and class to the store, and
+`r.f` adds the field's offset and takes the field's size and class.  The width
+is threaded through `LoadAtom`, `Assignment` and `IoCall`, and `Statmnt` (which
+builds an assignment target directly, not through `ParseVar`) had to set it too
+- that omission is what the first cut got wrong.  Fixture `t65_array_access`
+(`v[1] := 11`, `v[3] := v[1] + v[2]`, `r.A`, `r.B`; `.out` 33 then 12), and
+`t14` is re-baselined from `ERR 102 109` to `OK 41 15`.
+
+**Not yet:** a VARIABLE index (`Sheet[FX,FY]`, `XPOS[Ch]`, `b[i]`) - scaled
+run-time addressing, including the second dimension - which is a loud `ENoLib`
+today, and `with`.  Both wait on the same scaled-addressing work.
 
 **Two consequences for the harness, both now recorded:**
 

+ 7 - 7
SUMMARY.md

@@ -2037,15 +2037,15 @@ 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,
 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
-features through **array declarations** are landed: the ladder
+features through **array/field element access** are landed: the ladder
 (`tests/calc_ladder.sh` + `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`;
 CALC.PAS now stops at `ERR 102` at pos 9202, a file type), the `{$I}` include,
-`^X`/`#nn` character constants, scalar typed constants, the hybrid set type +
-constructor + set typed constants + compile-time `in`, enumeration types, record
-types (CellRec = 80), subrange sizing, and array types/variables/typed constants
-(Sheet = 11760, XPOS code-resident; fixtures through `t64_arrays`). The next
-rungs are array/field ELEMENT ACCESS, files, CRT, reals, the string builtins,
-recursion, and the layout change. Items 1-4 below are independent of both.
+`^X`/`#nn`, scalar typed constants, sets, enums, records (CellRec = 80),
+subranges, array types/variables/typed constants, and constant-index element and
+field access (fixtures through `t65_array_access`). The next rungs are a
+variable-index subscript (`Sheet[FX,FY]`) + `with`, files, CRT, reals, the
+string builtins, 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
    string variables, `s := 'literal'`, `writeln (s)`, `readln (s)`/`read (s)`

+ 130 - 61
shell/Compiler.mod

@@ -205,6 +205,8 @@ TYPE
          setbase : CARDINAL ;       (* ... the first byte's first element,
                                        a multiple of 8 (TP3's hybrid origin) *)
          setsize : CARDINAL ;       (* ... and how many bytes there are *)
+         sz      : CARDINAL ;       (* width of a kind-1 value: an element or
+                                       field's size, not its variable's *)
       END ;
 
    DirRec = RECORD  rng, chk : BOOLEAN  END ;
@@ -224,6 +226,7 @@ TYPE
          n      : CARDINAL ;        (* number of ranges (1 for array[1..5]) *)
          lo, hi : ARRAY [0..3] OF LONGINT ;
          elem   : CARDINAL ;        (* element size in bytes *)
+         ecls   : CARDINAL ;        (* element CLASS, for a subscripted value *)
       END ;
 
 (* ---------------------------------------------------------------- *)
@@ -2061,6 +2064,17 @@ PROCEDURE EmCallMost (idx, nk : CARDINAL) ; FORWARD ;
 (*  expressions  (TPSRC9)                                           *)
 (* ---------------------------------------------------------------- *)
 
+PROCEDURE VarSize (r : ERes) : CARDINAL ;
+(* The width of a kind-1 value.  An element (`b[i]') or a field (`r.f') has a
+   size of its own, which is NOT its variable's; when the parser has recorded
+   one, it wins.  Otherwise the variable's declared size is the answer. *)
+BEGIN
+   IF r.sz > 0 THEN
+      RETURN r.sz
+   END ;
+   RETURN symtab [r.idx].size
+END VarSize ;
+
 PROCEDURE LoadAtom (VAR r : ERes) ;
 (* load value of r into AX (folding constants).
 
@@ -2104,13 +2118,13 @@ BEGIN
       EmMovAxi (W16 (r.imm)) ;
       r.kind := 2
    ELSIF r.kind = 1 THEN
-      IF (symtab [r.idx].size > 2) AND NOT
+      IF (VarSize (r) > 2) AND NOT
          ((r.cls = TChar) AND (symtab [r.idx].cls = TString)) THEN
          Err (ENoLib)
       ELSE
          (* s[i] indexes one byte of a string, not the string, so its width is
             one whatever the variable's declared width is. *)
-         emsz := symtab [r.idx].size ;
+         emsz := VarSize (r) ;
          IF (r.cls = TChar) AND (symtab [r.idx].cls = TString) THEN
             emsz := 1
          END ;
@@ -2136,62 +2150,112 @@ PROCEDURE ParseSub (VAR r : ERes) ;
    element-size scaling that a one-byte string element does not, and it is
    item 2. *)
 VAR t : ERes ;
+    ai, fi : CARDINAL ;
+    found : BOOLEAN ;
 BEGIN
    LOOP
       Skip () ;
-      IF CurCh () # '[' THEN
-         RETURN
-      END ;
-      DropCh (GetCh ()) ;
-      ParseExpr (t) ;
-      IF OK () THEN
-         IF symtab [r.idx].cls = TString THEN
-            (* s[i]: a STRING is a length byte at s[0] followed by the
-               characters, so index i selects the one byte at off+i.  A second
-               subscript on a character is not a type. *)
-            IF r.cls = TChar THEN
-               Err (ESimpType) ;
-               RETURN
-            END ;
-            IF t.kind = 0 THEN
-               (* constant index: fold it into boff, as an array's does, and
-                  r.cls drops to TChar so every consumer below - LoadAtom,
-                  Assignment, IoCall - sees a one-byte value instead of the
-                  whole string. *)
-               r.boff := W16 (VAL (LONGINT, r.boff) + t.imm)
-            ELSE
-               (* Variable index.  Compute the element address once, now:
-                  the index is evaluated into AX (LoadAtom is a no-op for a
-                  kind-2 value and re-reads [DI] for a nested subscript), then
-                  DI := base + AX.  This is TP3's indstr/ialoop shape (TPSRC9
-                  1100-1111, 1200-1229), which likewise holds the address in
-                  DI until the byte is loaded or stored; a register is what
-                  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) ;
-               EmAddDiAx () ;
-               r.kind := 5
-            END ;
-            r.cls := TChar
-         ELSIF t.kind # 0 THEN
-            Err (ENoLib) ;
+      IF CurCh () = '.' THEN
+         (* FIELD selection: `r.f'.  The field's offset adds to the record
+            base's, and the value's class and WIDTH become the field's - which
+            is the whole reason a width rides on the expression (VarSize). *)
+         DropCh (GetCh ()) ;
+         Skip () ;
+         IF NOT Alpha (CurCh ()) THEN
+            Err (EUnknown) ;
             RETURN
-         ELSIF symtab [r.idx].cls = TArray THEN
-            (* A constant array subscript.  The element's WIDTH is not the
-               array's, so folding the address here and leaving the size at the
-               array's would make `b[1] := 2' store the whole array.  Element
-               addressing (a width on the expression) is the next increment;
-               until then this is LOUD, not a wrong store. *)
-            Err (ENoLib) ;
+         END ;
+         GetWord () ;
+         IF symtab [r.idx].fldFirst >= symtab [r.idx].fldLast THEN
+            Err (ESimpType) ;               (* not a record *)
             RETURN
-         ELSE
-            Err (ESimpType) ;
+         END ;
+         fi := symtab [r.idx].fldFirst ;
+         found := FALSE ;
+         WHILE (fi < symtab [r.idx].fldLast) AND NOT found DO
+            IF NameEq (fields [fi].name, wrd) THEN
+               found := TRUE
+            ELSE
+               INC (fi)
+            END
+         END ;
+         IF NOT found THEN
+            Err (EUnknown) ;
             RETURN
-         END
-      END ;
-      ExpectDelim (']', ENoSemi)
+         END ;
+         r.boff := W16 (VAL (LONGINT, r.boff)
+                        + VAL (LONGINT, fields [fi].off)) ;
+         r.cls := fields [fi].cls ;
+         r.sz := fields [fi].size
+      ELSIF CurCh () = '[' THEN
+         DropCh (GetCh ()) ;
+         ParseExpr (t) ;
+         IF OK () THEN
+            IF symtab [r.idx].cls = TString THEN
+               (* s[i]: a STRING is a length byte at s[0] followed by the
+                  characters, so index i selects the one byte at off+i.  A second
+                  subscript on a character is not a type. *)
+               IF r.cls = TChar THEN
+                  Err (ESimpType) ;
+                  RETURN
+               END ;
+               IF t.kind = 0 THEN
+                  (* constant index: fold it into boff, as an array's does, and
+                     r.cls drops to TChar so every consumer below - LoadAtom,
+                     Assignment, IoCall - sees a one-byte value instead of the
+                     whole string. *)
+                  r.boff := W16 (VAL (LONGINT, r.boff) + t.imm)
+               ELSE
+                  (* Variable index.  Compute the element address once, now:
+                     the index is evaluated into AX (LoadAtom is a no-op for a
+                     kind-2 value and re-reads [DI] for a nested subscript), then
+                     DI := base + AX.  This is TP3's indstr/ialoop shape (TPSRC9
+                     1100-1111, 1200-1229), which likewise holds the address in
+                     DI until the byte is loaded or stored; a register is what
+                     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) ;
+                  EmAddDiAx () ;
+                  r.kind := 5
+               END ;
+               r.cls := TChar ;
+               r.sz := 1
+            ELSIF symtab [r.idx].cls = TArray THEN
+               (* A constant array subscript.  The element's address is
+                  (index - lower bound) * element size from the array base, and
+                  the value's width and class become the ELEMENT's - so a store
+                  writes two bytes for an `array of Integer', not the whole
+                  array.  A VARIABLE index needs scaled run-time addressing and
+                  is the next increment. *)
+               IF t.kind # 0 THEN
+                  Err (ENoLib) ;
+                  RETURN
+               END ;
+               IF symtab [r.idx].arrIdx > 0 THEN
+                  ai := symtab [r.idx].arrIdx - 1 ;
+                  r.boff := W16 (VAL (LONGINT, r.boff)
+                                 + (t.imm - arrs [ai].lo [0])
+                                   * VAL (LONGINT, arrs [ai].elem)) ;
+                  r.sz := arrs [ai].elem ;
+                  r.cls := arrs [ai].ecls
+               ELSE
+                  Err (ESimpType) ;
+                  RETURN
+               END
+            ELSIF t.kind # 0 THEN
+               Err (ENoLib) ;
+               RETURN
+            ELSE
+               Err (ESimpType) ;
+               RETURN
+            END
+         END ;
+         ExpectDelim (']', ENoSemi)
+      ELSE
+         RETURN
+      END
    END
 END ParseSub ;
 
@@ -2223,6 +2287,7 @@ BEGIN
       r.setbase := symtab [r.idx].elem ;
       r.setsize := symtab [r.idx].size
    END ;
+   r.sz := symtab [r.idx].size ;
    ParseSub (r)
 END ParseVar ;
 
@@ -3270,7 +3335,7 @@ BEGIN
       Err (EUnknown) ;
       RETURN
    END ;
-   IF (symtab [r.idx].cls = TString) AND (r.cls = TString) THEN
+   IF (r.cls = TString) AND (VarSize (r) > 1) THEN
       (* s := <string literal>.  TPSRC8 asn2 sends a string through the scalar
          store path, and estcpl's string arm emits `MOV CL,max_len' (max_len =
          varsize-1) then `CALL strstore', which copies the source - length byte
@@ -3303,14 +3368,14 @@ BEGIN
                         (symtab [src.idx].off + src.boff) MOD 10000H) ;
          EmPushVarAddr (symtab [r.idx].local,
                         (symtab [r.idx].off + r.boff) MOD 10000H) ;
-         EmMovCxi ((symtab [r.idx].size - 1) MOD 10000H) ;
+         EmMovCxi ((VarSize (r) - 1) MOD 10000H) ;
          DropC (EmCall (TU_StrStore)) ;
          EmAddSp (4) ;
          RETURN
       END ;
       n := strLen [src.strx] ;
-      IF n > symtab [r.idx].size - 1 THEN
-         n := symtab [r.idx].size - 1
+      IF n > VarSize (r) - 1 THEN
+         n := VarSize (r) - 1
       END ;
       EmMovAxi (n) ;
       EmStoreVar (symtab [r.idx].local,
@@ -3324,8 +3389,8 @@ BEGIN
       END ;
       RETURN
    END ;
-   IF symtab [r.idx].size > 2 THEN
-      IF (r.cls # TChar) OR (symtab [r.idx].cls # TString) THEN
+   IF (r.kind = 5) OR (VarSize (r) > 2) THEN
+      IF (r.cls # TChar) OR (r.kind = 1) THEN
          Err (ENoLib) ;
          RETURN
       END ;
@@ -3360,7 +3425,7 @@ BEGIN
    LoadAtom (src) ;
    EmStoreVar (symtab [r.idx].local,
                (symtab [r.idx].off + r.boff) MOD 10000H,
-               symtab [r.idx].size)
+               VarSize (r))
 END Assignment ;
 
 PROCEDURE Compound () ;
@@ -3483,7 +3548,7 @@ BEGIN
                      runtime has no byte-register CH/CL setup), and rdstr
                      leaves the terminator pending so the readln wrapper can
                      consume the newline. *)
-                  EmMovCxi ((symtab [arg.idx].size - 1) MOD 10000H) ;
+                  EmMovCxi ((VarSize (arg) - 1) MOD 10000H) ;
                   ent := TU_RdStr
                ELSIF acls = TReal THEN
                   ent := TU_RdInt                   (* real reads: not yet *)
@@ -3506,7 +3571,7 @@ BEGIN
                      is *written* as an integer, as in TP3), but reading one
                      through rdint stored a word - and the second byte of
                      that word is the byte that FOLLOWS the variable. *)
-                  IF symtab [arg.idx].size = 1 THEN
+                  IF VarSize (arg) = 1 THEN
                      ent := TU_RdByte
                   ELSE
                      ent := TU_RdInt
@@ -3956,6 +4021,7 @@ BEGIN
             t.idx := symtab [idx].resvar ;
             t.cls := symtab [t.idx].cls
          END ;
+         t.sz := symtab [t.idx].size ;      (* ParseSub may narrow it further *)
          ParseSub (t) ;
          IF MatchAssign () THEN
             Assignment (t) ;
@@ -3982,6 +4048,7 @@ VAR tok : CARDINAL ;
     consumed : BOOLEAN ;
     running, groupFirst, i, an : CARDINAL ;
     alo, ahi : ARRAY [0..3] OF LONGINT ;
+    ecls2 : CARDINAL ;
 BEGIN
    cls := TNone ; size := 0 ; elem := 0 ;
    consumed := FALSE ;
@@ -4059,6 +4126,7 @@ BEGIN
          RETURN
       END ;
       ParseType (cls, s2, e2) ;
+      ecls2 := cls ;
       cls := TArray ;
       elem := s2 ;
       size := s2 ;
@@ -4073,6 +4141,7 @@ BEGIN
       IF arrTop < MaxArrs THEN
          arrs [arrTop].n := an ;
          arrs [arrTop].elem := s2 ;
+         arrs [arrTop].ecls := ecls2 ;
          i := 0 ;
          WHILE i < an DO
             arrs [arrTop].lo [i] := alo [i] ;

+ 16 - 6
shell/tests/fixtures/expected.tsv

@@ -16,11 +16,13 @@
 #
 # ERR 102 = ENoLib, the original's "not implemented" path.  The remaining ERR
 # rows are unimplemented features, not parser bugs:
-#   t14       'array [..] of <type>' DECLARES now (CALC Stage 8, type half);
-#             what remains unimplemented is ELEMENT access - `b[1]' is a loud
-#             ENoLib, because an element's width is not the array's and a
-#             store would otherwise write the whole array.  The position moved
-#             from the declaration (83) to the subscript (109) as a result.
+#   t14       'array [..] of <type>' DECLARES and its constant-index element
+#             access works now (CALC Stage 8): `b[1] := 2' stores TWO bytes, not
+#             the whole ten-byte array, because a constant subscript folds
+#             (index - lower bound) * element size into the address and the
+#             expression carries the ELEMENT's width.  data=15 = b (10) + a (2)
+#             ... plus the temporaries; the row moved ERR->OK when the width
+#             landed.
 #   t25       a string literal used where a 16-bit word is wanted
 #   uierror   deliberate syntax error, pinned by the editor UI test
 #
@@ -266,7 +268,7 @@ t10_while	OK	151	6
 t11_for	OK	144	6
 t12_repeat	OK	137	6
 t13_proc	OK	126	6
-t14_types	ERR	102	109
+t14_types	OK	41	15
 t15_label	OK	90	6
 t16_str1	OK	42	4
 t17_two_str	OK	45	4
@@ -501,3 +503,11 @@ t63_record	OK	57	84
 #                   only: the image is ~16 KB and the exec harness's boot
 #                   sector reads 8 KB (see the layout/execution stage).
 t64_arrays	OK	67	11764
+#   t65_array_access       array ELEMENT and record FIELD access - the width
+#                   matters.  `v[1] := 11' folds (1 - 1) * 2 into the address
+#                   and writes TWO bytes; `v[3] := v[1] + v[2]' reads two and
+#                   writes a third; `r.A := 5' adds the field's offset and takes
+#                   its width.  .out is 33 then 12, so a wrong scaling, a wrong
+#                   lower-bound subtraction or a wrong store width all show up.
+#                   data=16 = v (8) + r (4) + the two @@T temporaries.
+t65_array_access	OK	101	16

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

@@ -0,0 +1,2 @@
+33
+12

+ 32 - 0
shell/tests/fixtures/t65_array_access.pas

@@ -0,0 +1,32 @@
+program t65_array_access;
+
+{ t65 -- array ELEMENT and record FIELD access, which is where the width
+  matters.  CALC does both at once: `Sheet[FX,FY].CellStatus'.
+
+  The point of this fixture is that an element's WIDTH is not its array's.  A
+  constant subscript folds (index - lower bound) * element size into the
+  address, and the value's width becomes the ELEMENT's - so `v[1] := 11' writes
+  two bytes, not the whole ten-byte array.  A field selection adds the field's
+  offset and takes the field's width and class.
+
+  `v[3] := v[1] + v[2]' reads two elements and writes a third, so a wrong
+  scaling, a wrong lower-bound subtraction or a wrong store width all change
+  the printed number.  .out is 33 then 12. }
+
+type
+   Rec = record
+      A : Integer;
+      B : Integer;
+   end;
+var
+   v : array [1..4] of Integer;
+   r : Rec;
+begin
+   v[1] := 11;
+   v[2] := 22;
+   v[3] := v[1] + v[2];
+   writeln (v[3]);
+   r.A := 5;
+   r.B := 7;
+   writeln (r.A + r.B)
+end.

+ 30 - 3
shell/tests/nonvacuity.sh

@@ -1100,10 +1100,10 @@ cp "$SAVED_C" Compiler.mod
 if python3 - <<'PYX'
 p = 'Compiler.mod'
 s = open(p).read()
-g = '   IF (symtab [r.idx].cls = TString) AND (r.cls = TString) THEN\n'
+g = '   IF (r.cls = TString) AND (VarSize (r) > 1) THEN\n'
 assert s.count(g) == 1, 'the string-assignment guard found %d times' % s.count(g)
 a = s.index(g)
-m = '      RETURN\n   END ;\n   IF symtab [r.idx].size > 2 THEN\n'
+m = '      RETURN\n   END ;\n   IF (r.kind = 5) OR (VarSize (r) > 2) THEN\n'
 i = s.index(m, a)
 new = g + '      Err (ENoLib) ;              (* MUTATION: the arm is refused *)\n      RETURN\n'
 open(p, 'w').write(s[:a] + new + s[i + len('      RETURN\n'):])
@@ -1405,7 +1405,7 @@ cp "$SAVED_C" Compiler.mod
 if python3 - <<'PYX'
 p = 'Compiler.mod'
 s = open(p).read()
-old = ('                  EmMovCxi ((symtab [arg.idx].size - 1) MOD 10000H) ;\n'
+old = ('                  EmMovCxi ((VarSize (arg) - 1) MOD 10000H) ;\n'
        '                  ent := TU_RdStr')
 assert s.count(old) == 1, 'the string-read arm found %d times -- update this mutation' % s.count(old)
 open(p, 'w').write(s.replace(old, old.replace('TU_RdStr', 'TU_RdInt')))
@@ -1589,6 +1589,33 @@ else
 fi
 cp "$SAVED_C" Compiler.mod
 
+# --- an element's width ---------------------------------------------------
+# A constant array subscript must hand the ELEMENT's size to the store, or the
+# array's own size is used and `v[1] := 11' writes the whole array.  Drop the
+# width and the compile matrix stays green (same OK 101 16) while execution
+# sees 0s and a corrupted neighbour - the shape the width exists to prevent.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = 'r.sz := arrs [ai].elem'
+assert s.count(old) == 1, 'the element width assignment found %d times -- update this mutation' % s.count(old)
+open(p, 'w').write(s.replace(old, 'r.sz := 0'))
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t65 red when a subscripted element keeps the array's width" \
+            "t65_array_access" python3 tests/run_com_exec.py t65_array_access
+    else
+        echo "  FAIL: the compiler would not rebuild with the element width dropped"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the element-width mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED_C" Compiler.mod
+
 if rebuild_compiler; then
     if python3 tests/run_com_exec.py >/dev/null 2>&1; then
         echo "  ok: every executed fixture passes on the restored sources"