Przeglądaj źródła

compiler: record types, and subrange sizing (CALC Stage 7, first half)

The ladder moves again: CALC.PAS went from ERR 102 at 8362 to ERR 1 at 8705.
`CellRec = record ... end' is now a type, and the compiler reaches the
TWO-DIMENSIONAL array `Cells = array['A'..'G',1..FYMax] of CellRec'.

  - ParseType grows the `record' arm: fields are non-compacted and juxtaposed
    (RESUME-TP3.md §2), and each name in an `a, b : T' group is its own field at
    the running offset.
  - Fields live in a table of their own (FldRec, MaxFields) keyed per record,
    because a field name is scoped to its record and must not have to be
    globally unique.  A type carries its field range in SymEntry.fldFirst/
    fldLast (new).
  - SymEntry also grows lo/hi, so an ordinal type can carry its bounds.
  - SUBRANGE SIZING, which records forced: a subrange is ONE byte when both
    bounds are in 0..255 (RESUME-TP3.md §2), not the two this compiler gave it.
    That is what makes CellRec 80 (1 set + 71 string + 6 real + 1 + 1) and
    Sheet 147*80; with two-byte subranges CellRec would be 82.

Fixture t63_record declares CellRec and asserts data=84 - CellRec 80 plus the
two @@T temporaries - so a one-byte error in ANY field moves the row.  It runs
under qemu and Exec86.

Red proof: forcing subranges to two bytes turns the compile matrix's `OK 57 84'
into `OK 57 86'.  nonvacuity.sh gains that case (84 ok, 0 failed).

NOT done yet, and loudly so rather than silently: field SELECTION (`c.DEC',
`Sheet[FX,FY].CellStatus') and `with'.  A field's width is not its variable's,
so the expression record needs to carry one; today `.field' is an error, not a
wrong address.  Records as ARRAY ELEMENTS come with the 2-D array, which
`Sheet[FX,FY].CellStatus' is both of at once.

run_all.sh: ALL PASS - compile matrix 64/64, CALC ladder 1/1 at ERR 1 8705,
54 .COM linked and 54 executed under both qemu and Exec86.
Eric Streit 14 godzin temu
rodzic
commit
7be2276645

+ 36 - 15
CALC-COMPILE.md

@@ -95,7 +95,7 @@ Current rung (measured today, normalised position):
 
 | position | blocker |
 |---|---|
-| **8362** | `CellRec = record` — a **record** type (`ParseType` `TkRecord`, `ERR 102`) |
+| **8705** | `Cells = array['A'..'G',1..FYMax] of CellRec` — a **2-D array** (`ERR 1`) |
 
 Rungs behind it (now passed):
 
@@ -104,14 +104,15 @@ Rungs behind it (now passed):
 | 7791 | `FXMax : Char = 'G'` — scalar typed constant | Stage 4 (scalar) |
 | 7791 | `EofLine = ^M` — caret character constant | Stage 3 |
 | 7972 | `Numbers : set of Char = ['0'..'9']` — set typed constant | sets, increment 1 |
-| 8098 | `Attributes = (Constant,…)` — enumeration; 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) |
 
 Rungs ahead:
 
 | position | blocker |
 |---|---|
-| past 8362 | the record body, then `Cells = array['A'..'G',1..FYMax] of CellRec` (2-D, Stage 8) |
-| then | the `var` block, `{$I}` at 9404, and everything in `CALC.INC` |
+| 8705 | the 2-D array, `Sheet : Cells`, `XPOS` (array typed constant), the `var` block |
+| then | `{$I}` at 9404, and everything in `CALC.INC` |
 
 Corrections the measurements force, worth stating plainly because the stage list
 below was written before them:
@@ -147,9 +148,9 @@ stripped.
 | `set of <enum>` | `CALC.PAS:121,127` | **sizing done** (enum bounds → 1 byte for Attributes) |
 | set constructor `[a..b, c]`, `in` (compile-time) | `t61_setconst` | **done** — folding only |
 | set `in` (run-time), `+`, `*`, `=`/`<>`, assignment, set *variables* | 47 × `in`, 6 × `+ [`, 2 × `*` | `ENoLib` — next sets increment |
-| `record` + field select `Sheet[FX,FY].CellStatus` | `CALC.PAS:126` | `ERR 102` |
-| `with` | 13 sites | `ERR 102` (via `record`) |
-| 2-D array `array['A'..'G',1..FYMax] of CellRec` | `CALC.PAS:133` | one range only |
+| `record` + field select `Sheet[FX,FY].CellStatus` | `CALC.PAS:126` | **type done (Stage 7)** — CellRec = 80 bytes; field SELECT is next |
+| `with` | 13 sites | after field selection |
+| 2-D array `array['A'..'G',1..FYMax] of CellRec` | `CALC.PAS:133` | **the current rung** — one range only |
 | 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** |
@@ -417,9 +418,15 @@ The ladder row now sits on **sets**.
   bounds carried so `set of Attributes` sizes to one byte. Fixture `t62_enum`.
   `RdConst` now resolves a named constant, which set literals and `case`
   labels need.
-- **The ladder now stops at `ERR 102` at `8362`** — `CellRec = record`. So
-  records are the next rung, then the 2-D array `Cells = array['A'..'G',
-  1..FYMax] of CellRec`, then the `var` block and the `{$I}` at 9404.
+- **Record TYPES are landed (Stage 7, first half)** — non-compacted fields, a
+  per-record field table, `CellRec` = 80 bytes, and the subrange sizing that
+  makes it so. Fixture `t63_record`. **Field selection** (`c.DEC`,
+  `Sheet[FX,FY].CellStatus`) and `with` are the next increment; they need the
+  expression record to carry a width, and today they error loudly.
+- **The ladder now stops at `ERR 1` at `8705`** — `Cells =
+  array['A'..'G',1..FYMax] of CellRec`, a **2-D array**. So the next rung is
+  Stage 8 (two ranges, last index varying fastest, records as elements), then
+  `XPOS`, the `var` block, and the `{$I}` at 9404.
 - **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
@@ -429,8 +436,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 **63/63**, CALC ladder **1/1** at
-  `ERR 102 8362`, 53 `.COM` linked and 53 executed under both qemu and Exec86.
+  **OVERALL: ALL PASS**, compile matrix **64/64**, CALC ladder **1/1** at
+  `ERR 1 8705`, 54 `.COM` linked and 54 executed under both qemu and Exec86.
 - **`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
   the default `run_all.sh` and must be run **manually** after touching any check.
@@ -618,9 +625,23 @@ runtime opcode — so it informs the *algorithms* but not the representation.
 TP3's hybrid bytes remain the ground truth here, per the repo rule.
 Ladder: 8060 → 8237 (now `8098` → the enum).
 
-**Stage 7 — records.** Non-compacted field layout (`RESUME-TP3.md` §2), field
-selection as address arithmetic, `with` (13 sites), records as array elements.
-Ladder: past 8237, into the `var` block, then to the include.
+**Stage 7 — records. Record TYPES done; field selection is the next increment.**
+
+Done: the `record … end` arm of `ParseType`.  Fields are non-compacted and
+juxtaposed (`RESUME-TP3.md` §2); each name in an `a, b : T` group is its own
+field at the running offset.  Fields live in a table of their own (`FldRec`,
+keyed per record) because a field name is scoped to its record, not global.  A
+type carries its field range in `SymEntry.fldFirst/fldLast`.  `CellRec` comes
+out at exactly **80 bytes**, which is what §3 measured `Sheet` against, so the
+layout is pinned by `t63_record`'s `data=84`.  This also required **subrange
+sizing**: `0..20` is one byte (both bounds in 0..255, `RESUME-TP3.md` §2), not
+the two this compiler used to give it.
+
+Not yet: **field selection** (`c.DEC`, `Sheet[FX,FY].CellStatus`) and `with`.
+A field access needs the expression record to carry a width (a field's size is
+not its variable's), which is a small `ERes` change plus the addressing; today
+it is a **loud error, not a wrong address**.  Records as **array elements** come
+with the 2-D array in Stage 8 — `Sheet[FX,FY].CellStatus` is both.
 
 **Stage 8 — arrays at their point of use.** Two ranges `array['A'..'G',
 1..FYMax] of T`, row-major with **the last index varying fastest**

+ 6 - 5
SUMMARY.md

@@ -2037,13 +2037,14 @@ 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 **enumerations** are landed: the ladder
+features through **record types** 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 8362, a record), the `{$I}` include, the
+CALC.PAS now stops at `ERR 1` at pos 8705, a 2-D array), the `{$I}` include, the
 lexical `^X`/`#nn` character constants, scalar typed constants, the hybrid set
-type + constructor + set typed constants + compile-time `in`, and enumeration
-types (`t62_enum`; fixtures up to `t62`). The next rung is a record. Items 1-4
-below are the string/robustness work and are independent of both.
+type + constructor + set typed constants + compile-time `in`, enumeration
+types, and record types with the subrange sizing they require (`CellRec` = 80,
+fixtures through `t63_record`). The next rung is the two-dimensional array.
+Items 1-4 below are the string/robustness work and 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)`

+ 105 - 13
shell/Compiler.mod

@@ -79,6 +79,8 @@ CONST
    MaxSym    = 3000 ;
    MaxPatch  = 2000 ;
    MaxPend   = 400 ;
+   MaxFields = 2000 ;          (* record fields, in a table of their own: a
+                                  field is scoped to its record, not global *)
 
    (* type codes (TP3 vartp) *)
    TNone    = 0 ;  TArray  = 1 ;  TRecord = 2 ;  TSet  = 3 ;
@@ -159,6 +161,8 @@ TYPE
          lo, hi : LONGINT ;         (* an ORDINAL type's bounds: an enumeration
                                        (0..n-1) or a subrange.  Needed so
                                        `set of T' can size its base. *)
+         fldFirst, fldLast : CARDINAL ;  (* a RECORD type's field range in the
+                                       field table (0,0 when not a record) *)
          level  : CARDINAL ;
          local  : BOOLEAN ;
          resvar : CARDINAL ;
@@ -202,6 +206,16 @@ TYPE
 
    DirRec = RECORD  rng, chk : BOOLEAN  END ;
 
+   FldRec =
+      RECORD
+         name   : ARRAY [0..MaxName] OF CHAR ;
+         cls    : CARDINAL ;
+         size   : CARDINAL ;
+         elem   : CARDINAL ;
+         off    : CARDINAL ;        (* byte offset within the record *)
+         lo, hi : LONGINT ;         (* bounds, for a subrange/enum field *)
+      END ;
+
 (* ---------------------------------------------------------------- *)
 (*  state                                                           *)
 (* ---------------------------------------------------------------- *)
@@ -223,6 +237,9 @@ VAR
    incBuf     : ARRAY [0..TextLimit] OF CHAR ;
    enumLo, enumHi : LONGINT ;   (* bounds of the enumeration ParseType just read,
                                    handed to DefType which owns the type NAME *)
+   fields : ARRAY [0..MaxFields - 1] OF FldRec ;
+   fldTop : CARDINAL ;          (* next free field-table slot *)
+   recFldFirst, recFldLast : CARDINAL ;  (* field range ParseType just built *)
 
    wrd  : ARRAY [0..MaxName] OF CHAR ;
 
@@ -1432,6 +1449,8 @@ BEGIN
    e.lval := v ;
    e.lo := 0 ;
    e.hi := 0 ;
+   e.fldFirst := 0 ;
+   e.fldLast := 0 ;
    e.level := lexnest ;
    e.local := local ;
    e.resvar := 0 ;
@@ -3940,6 +3959,7 @@ VAR tok : CARDINAL ;
     subcls : CARDINAL ;
     strf : BOOLEAN ;
     consumed : BOOLEAN ;
+    running, groupFirst, i : CARDINAL ;
 BEGIN
    cls := TNone ; size := 0 ; elem := 0 ;
    consumed := FALSE ;
@@ -4029,7 +4049,14 @@ BEGIN
          Err (ENoSemi)
       END
    ELSIF tok = TkRecord THEN
-      Err (ENoLib) ;
+      (* record  fieldList  end, fields NON-COMPACTED and juxtaposed
+         (RESUME-TP3.md §2).  Each name in a `a, b : T' group is a field of its
+         own at the running offset; the field table is where `x.f' looks, keyed
+         per record so field names need not be globally unique.
+         recFldFirst/recFldLast hand the range to DefType, or to the enclosing
+         record's field entry when records nest. *)
+      recFldFirst := fldTop ;
+      running := 0 ;
       LOOP
          PeekKw (tok) ;
          IF tok = TkEnd THEN
@@ -4037,15 +4064,55 @@ BEGIN
             EXIT
          END ;
          IF CurCh () = 0C THEN
+            Err (EUnknown) ;
             EXIT
          END ;
-         Skip () ;
-         IF Alpha (CurCh ()) THEN
-            DropCh (GetCh ())
-         ELSE
-            DropCh (GetCh ())
+         (* the group's names, then ONE `: type' *)
+         groupFirst := fldTop ;
+         LOOP
+            Skip () ;
+            IF NOT Alpha (CurCh ()) THEN
+               Err (EUnknown) ;
+               RETURN
+            END ;
+            GetWord () ;
+            IF fldTop < MaxFields THEN
+               CopyStr (fields [fldTop].name, wrd) ;
+               fields [fldTop].off := 0 ;
+               INC (fldTop)
+            ELSE
+               Err (ECompOvf)
+            END ;
+            IF NOT MatchDelim (',') THEN
+               EXIT
+            END
+         END ;
+         ExpectDelim (':', ENoSemi) ;
+         ParseType (subcls, s2, e2) ;
+         IF NOT OK () THEN
+            RETURN
+         END ;
+         (* every name in the group gets the type and its own slot *)
+         i := groupFirst ;
+         WHILE i < fldTop DO
+            fields [i].cls := subcls ;
+            fields [i].size := s2 ;
+            fields [i].elem := e2 ;
+            fields [i].off := running ;
+            fields [i].lo := enumLo ;
+            fields [i].hi := enumHi ;
+            running := running + s2 ;
+            INC (i)
+         END ;
+         IF NOT MatchDelim (';') THEN
+            Err (ENoSemi) ;
+            RETURN
          END
-      END
+      END ;
+      recFldLast := fldTop ;
+      cls := TRecord ;
+      size := running ;
+      elem := running
    ELSIF (tok = TkFile) OR (tok = TkText) THEN
       cls := TFile ;
       size := 0 ;
@@ -4056,12 +4123,19 @@ BEGIN
          IF Search (wrd, idx) AND (symtab [idx].tag = KType) THEN
             cls := symtab [idx].cls ;
             size := symtab [idx].size ;
-            elem := symtab [idx].size
+            elem := symtab [idx].size ;
+            enumLo := symtab [idx].lo ;
+            enumHi := symtab [idx].hi ;
+            recFldFirst := symtab [idx].fldFirst ;
+            recFldLast := symtab [idx].fldLast
          ELSE
             Err (EUnknown)
          END
       ELSE
-         (* subrange lo .. hi *)
+         (* subrange lo .. hi.  TP3 sizes it by its bounds: ONE byte when both
+            are inside 0..255, else two (RESUME-TP3.md §2).  `0..20' and
+            `'A'..'G'' are one byte - which is what makes CellRec 80 and not
+            82, and `Sheet' 147*80.  The bounds are kept for `set of T'. *)
          RdConst (lo, subcls, strf) ;
          IF NOT MatchRange () THEN
             Err (ESimpType) ;
@@ -4069,8 +4143,14 @@ BEGIN
          END ;
          RdConst (hi, subcls, strf) ;
          cls := TScalar ;
-         size := 2 ;
-         elem := 2
+         IF (lo >= 0) AND (hi <= 255) AND (lo <= hi) THEN
+            size := 1
+         ELSE
+            size := 2
+         END ;
+         elem := size ;
+         enumLo := lo ;
+         enumHi := hi
       END
    END
 END ParseType ;
@@ -4118,7 +4198,7 @@ BEGIN
       ParseType (cls, size, elem) ;
       IF OK () THEN
          IF lexnest = 0 THEN
-            IF (size > 2) AND (cls # TString) THEN
+            IF (size > 2) AND (cls # TString) AND (cls # TRecord) THEN
                (* Still the catch-all for a type whose storage this compiler does
                   not lay out yet (an ARRAY at its point of use, a RECORD, ...).
                   A STRING is the exception now: its layout is length byte +
@@ -4143,6 +4223,8 @@ BEGIN
                WHILE i < symTop DO
                   symtab [i].cls := cls ; symtab [i].size := size ;
                   symtab [i].elem := elem ; symtab [i].off := off ;
+                  symtab [i].fldFirst := recFldFirst ;
+                  symtab [i].fldLast := recFldLast ;
                   INC (i)
                END
             ELSE
@@ -4150,13 +4232,15 @@ BEGIN
                WHILE i < symTop DO
                   symtab [i].cls := cls ; symtab [i].size := size ;
                   symtab [i].elem := elem ; symtab [i].off := dc ;
+                  symtab [i].fldFirst := recFldFirst ;
+                  symtab [i].fldLast := recFldLast ;
                   dc := dc + size ;
                   INC (i)
                END
             END ;
             varspc := varspc + size * (symTop - first)
          ELSE
-            IF (size > 2) AND (cls # TString) THEN
+            IF (size > 2) AND (cls # TString) AND (cls # TRecord) THEN
                Err (ENoLib) ;
                RETURN
             END ;
@@ -4177,6 +4261,8 @@ BEGIN
             WHILE i < symTop DO
                symtab [i].cls := cls ; symtab [i].size := size ;
                symtab [i].elem := elem ;
+               symtab [i].fldFirst := recFldFirst ;
+               symtab [i].fldLast := recFldLast ;
                symtab [i].off := (locFree - size + 1) MOD 10000H ;
                locFree := (locFree - size) MOD 10000H ;
                INC (i)
@@ -4548,6 +4634,12 @@ BEGIN
          symtab [tidx].lo := enumLo ;
          symtab [tidx].hi := enumHi
       END ;
+      IF cls = TRecord THEN
+         (* and a record's fields live in the field table; the type records
+            which range is its own *)
+         symtab [tidx].fldFirst := recFldFirst ;
+         symtab [tidx].fldLast := recFldLast
+      END ;
       DropC (tidx) ;
       IF NOT MatchDelim (';') THEN
          Err (ENoSemi) ;

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

@@ -25,8 +25,9 @@
 # positions shift — which is exactly why this must be a measured number, not a
 # remembered one.
 #
-# Current rung (2026-10-19): CellRec = record — a RECORD type, so ParseType
-# reaches its TkRecord arm and reports ENoLib (102).  The rung moved from 8098
-# (the enumeration `Attributes = (Constant, ...)') when enumerations landed,
-# which also let `SetOfAttri = Set of Attributes' size its set base 0..5.
-CALC.PAS	ERR	102	8362
+# Current rung (2026-10-19): Cells = array['A'..'G',1..FYMax] of CellRec — a
+# TWO-dimensional array.  The array arm accepts one range and then expects ']',
+# so the second range is ENoSemi (1).  The rung moved from 8362 (the record
+# `CellRec = record') when record TYPES landed: field list, non-compacted
+# layout, and the subrange sizing that makes CellRec 80 bytes.
+CALC.PAS	ERR	1	8705

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

@@ -479,3 +479,11 @@ t61_setconst	OK	183	4
 #                   set element (RdConst resolved identifiers), which is what
 #                   `[Constant, Formula]' needs.  .out TRUE FALSE TRUE FALSE.
 t62_enum	OK	187	5
+#   t63_record             `CellRec = record ... end' - CALC.PAS:126.  Fields
+#                   are non-compacted and juxtaposed; CellRec is 1 (set of
+#                   Attributes) + 71 (String[70]) + 6 (Real) + 1 + 1 (two
+#                   0..20 subranges: both bounds in 0..255 -> ONE byte) = 80.
+#                   data=84 = 80 + the two @@T temporaries, so this row pins
+#                   the layout to the byte.  Subrange sizing is part of it.
+#                   Field SELECTION is still a loud error (the next increment).
+t63_record	OK	57	84

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

@@ -0,0 +1 @@
+a CellRec is 80 bytes

+ 36 - 0
shell/tests/fixtures/t63_record.pas

@@ -0,0 +1,36 @@
+program t63_record;
+
+{ t63 -- record TYPES: field list, non-compacted layout, and the size.  CALC's
+  record is CALC.PAS:126:
+
+      CellRec = record
+        CellStatus : set of Attributes;   -- 1:  set of 0..5, one byte
+        Contents   : String[70];          -- 71: length byte + 70
+        Value      : Real;                -- 6
+        DEC, FW    : 0..20;               -- 1+1: a subrange with both bounds
+                                                 in 0..255 is ONE byte
+      end;                                -- = 80
+
+  Section 3 of CALC-COMPILE.md measured CellRec at 80 from the original's data
+  report, so "data=84" here (CellRec 80 + the two @@T temporaries) IS the
+  assertion that the layout is right: a difference of one byte in any field
+  moves it.  Subrange sizing is load-bearing for the same reason: "0..20" must
+  be one byte, per RESUME-TP3.md section 2, or CellRec would be 82 and Sheet
+  240 bytes too big.
+
+  Field SELECTION ("c.DEC", "Sheet[FX,FY].CellStatus") is the next increment;
+  today it is a loud error, not a wrong address. }
+
+type
+   Attributes = (Constant, Formula, Txt, OverWritten, Locked, Calculated);
+   CellRec = record
+      CellStatus : set of Attributes;
+      Contents   : String[70];
+      Value      : Real;
+      DEC, FW    : 0..20;
+   end;
+var
+   c : CellRec;
+begin
+   writeln ('a CellRec is 80 bytes')
+end.

+ 27 - 0
shell/tests/nonvacuity.sh

@@ -1562,6 +1562,33 @@ else
 fi
 cp "$SAVED_C" Compiler.mod
 
+# --- subrange sizing ------------------------------------------------------
+# TP3 sizes a subrange by its bounds: one byte when both are inside 0..255.
+# Force two bytes and CellRec grows from 80 to 82, so the compile matrix's
+# `data=84' becomes `data=86' - this one is visible at compile time, unlike the
+# set/class cases above.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = 'IF (lo >= 0) AND (hi <= 255) AND (lo <= hi) THEN\n            size := 1'
+assert s.count(old) == 1, 'the subrange size test found %d times -- update this mutation' % s.count(old)
+open(p, 'w').write(s.replace(old, old.replace('size := 1', 'size := 2')))
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t63 red when a small subrange is sized two bytes" \
+            "expected: OK 57 84" tests/run_compile_tests.sh
+    else
+        echo "  FAIL: the compiler would not rebuild with subranges forced to two bytes"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the subrange-size 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"