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

TP3-BOOLREAD: a boolean read is refused with TP3's own error 66

A `readln (b)` of a boolean used to be accepted, and `rdbool` then stored a
word into the one-byte variable - bug 37's overflow (the second byte lands on
the variable AFTER b), unclaimed only because no fixture read a boolean.

TP3 refuses the whole class. TPSRC8 `prdlp2` ends at `prderrtp` (error 66,
`B $42` = "I/O not allowed here"), and its runtime has no `xrdbool` at all -
`xrdreal`, `xrdchar`, `xrdint`, `xrdbyte` exist, a boolean reader does not.
So the fix is to do what TP3 does rather than invent the missing entry.

- Fixture `t38_readbool.pas` (new): `readln (b)` of a boolean. The expected
  row pins `ERR 66` at position 659, the `)` after `b` - the point the atom
  parser's `Skip` has already run to, exactly where TP3's `varptr2` check
  stands. The row was red before the fix: the accepting compiler answered
  `OK 43 5` (a word store plus the `readln` tail) instead of `ERR 66 659`.
- `Compiler.mod`: `IoCall` now refuses a boolean argument to `read` /
  `readln` with the new `EIoHere = 66`. `rdbool` stays a runtime entry that
  `rt_exec` still probes directly; the compiler simply never emits it.
- `tests/nonvacuity.sh`: one new case (66 -> 67). It reverts the refusing
  arm to the accepting dispatch (`ent := TU_RdBool`) and demands that the
  COMPILE MATRIX go red on `t38` - an `ERR` fixture is never executed, so
  nothing that runs programs can see a refusal. Count asserted before the
  splice; probed red in `tmp/t38_probe.sh` first.
- `summary.md`: milestone row, bug 38, counts 38 -> 39 / 66 -> 67, and the
  honest-limitations and next-steps lists brought up to date.
Eric Streit 11 часов назад
Родитель
Сommit
c9e0e8d05c
5 измененных файлов с 145 добавлено и 27 удалено
  1. 58 26
      SUMMARY.md
  2. 9 1
      shell/Compiler.mod
  3. 13 0
      shell/tests/fixtures/expected.tsv
  4. 16 0
      shell/tests/fixtures/t38_readbool.pas
  5. 49 0
      shell/tests/nonvacuity.sh

+ 58 - 26
SUMMARY.md

@@ -28,6 +28,7 @@ manual, not guessed.
 | Image layout in one file; a check's sweep region measured, not restated | `v-TP3-IMAGELAYOUT` | done, **every reader of a linked `.COM` imports `tests/comimage.py`, and the region `check_framedisp` sweeps is two readings of the file required to agree** |
 | Image layout in one file; a check's sweep region measured, not restated | `v-TP3-IMAGELAYOUT` | done, **every reader of a linked `.COM` imports `tests/comimage.py`, and the region `check_framedisp` sweeps is two readings of the file required to agree** |
 | Arguments pushed as they are parsed; the parameter frame read from BP+4 backwards | `v-TP3-ARGORDER` | done, **`t36_argclobber` runs: a computed argument survives the parse of the next one, and the callee's slots match the order they were pushed in** |
 | Arguments pushed as they are parsed; the parameter frame read from BP+4 backwards | `v-TP3-ARGORDER` | done, **`t36_argclobber` runs: a computed argument survives the parse of the next one, and the callee's slots match the order they were pushed in** |
 | A `byte` reader of its own, and bug 36's four sites proved re-derivable | `v-TP3-NONVACUITY-RDBYTE` | done, **`readln (b)` of a `byte` prints the byte and leaves the variable beside it alone; reverting any one of the argument-order fix's four sites turns `t36_argclobber` red** |
 | A `byte` reader of its own, and bug 36's four sites proved re-derivable | `v-TP3-NONVACUITY-RDBYTE` | done, **`readln (b)` of a `byte` prints the byte and leaves the variable beside it alone; reverting any one of the argument-order fix's four sites turns `t36_argclobber` red** |
+| A `BOOLEAN` read refused with TP3's own error 66 | `v-TP3-BOOLREAD` | done, **`readln (b)` of a boolean is an error, not a word store into a one-byte variable — exactly TPSRC8 `prderrtp`; the compile matrix demands `ERR 66` at the character after the variable, and the accepting dispatch is one of the in-suite reverts** |
 
 
 Every row that names a tag has one, and every tag points at a commit on
 Every row that names a tag has one, and every tag points at a commit on
 `master`; verified by diffing the rows against `git tag -l`, which is how
 `master`; verified by diffing the rows against `git tag -l`, which is how
@@ -121,11 +122,11 @@ needing a squint. It was checked for vacuousness by reverting the string
 scanner fix: the suite went red with exit 1, and came back green only when the
 scanner fix: the suite went red with exit 1, and came back green only when the
 fix was restored.
 fix was restored.
 
 
-**35 of 38 fixtures compile clean**, up from 1 (the empty program) when the
+**35 of 39 fixtures compile clean**, up from 1 (the empty program) when the
 direct harness was first built.
 direct harness was first built.
 
 
 ```
 ```
-compile matrix: 38 passed, 0 failed (of 38)
+compile matrix: 39 passed, 0 failed (of 39)
 ```
 ```
 
 
 Compiling: `t01` minimal · `t04` var+assign+`writeln` · `t06` two args ·
 Compiling: `t01` minimal · `t04` var+assign+`writeln` · `t06` two args ·
@@ -146,8 +147,10 @@ arguments are computed in various positions, each one keeping its own value. ·
 
 
 Failing, all deliberately: `t14` `array [1..5] of integer` at its point of use
 Failing, all deliberately: `t14` `array [1..5] of integer` at its point of use
 and `t25` a string literal used as a *value* (`s := 'hi'`) both → `ENoLib`
 and `t25` a string literal used as a *value* (`s := 'hi'`) both → `ENoLib`
-(102), the original's "not implemented" path; `uierror` is a deliberate syntax
-error (41) used by the UI test.
+(102), the original's "not implemented" path; `t38` `readln (b)` of a
+`boolean` → error 66, TP3's "I/O not allowed here" (TPSRC8 `prderrtp`), which
+refuses the whole class because the runtime has no boolean reader at all; and
+`uierror` is a deliberate syntax error (41) used by the UI test.
 
 
 `run_com_tests.sh` additionally links **all 35 that compile** to a real `.COM`
 `run_com_tests.sh` additionally links **all 35 that compile** to a real `.COM`
 and re-verifies the bytes with an independent Python checker that *measures*
 and re-verifies the bytes with an independent Python checker that *measures*
@@ -440,16 +443,22 @@ goes red before a single instruction has run.
 
 
 ### Non-vacuity — `tests/nonvacuity.sh`
 ### Non-vacuity — `tests/nonvacuity.sh`
 
 
-Every assertion in this file is proved able to fail: **66 deliberate
+Every assertion in this file is proved able to fail: **67 deliberate
 breakages, each asserted to turn exactly one named check red for the stated
 breakages, each asserted to turn exactly one named check red for the stated
-reason, then restored and re-asserted green** — `non-vacuity: 66 ok, 0 failed`.
+reason, then restored and re-asserted green** — `non-vacuity: 67 ok, 0 failed`.
 They cover the runtime's emitter audit and its restored source, the mod=11
 They cover the runtime's emitter audit and its restored source, the mod=11
 table (including restoring the exact wrong table this project once shipped),
 table (including restoring the exact wrong table this project once shipped),
 the `[BP+off]` rule, the behavioural bugs, the emitter-name audit of
 the `[BP+off]` rule, the behavioural bugs, the emitter-name audit of
 `Compiler.mod`, the BP contract, the `.COM` layout checker, the image-layout
 `Compiler.mod`, the BP contract, the `.COM` layout checker, the image-layout
 helper and the three checks that read it, the 8086 lowering, the interpreter
 helper and the three checks that read it, the 8086 lowering, the interpreter
 itself, the `R` key, and — since `v-TP3-NONVACUITY-RDBYTE` — the four
 itself, the `R` key, and — since `v-TP3-NONVACUITY-RDBYTE` — the four
-independent sites of the argument-order fix.
+independent sites of the argument-order fix. Since `v-TP3-BOOLREAD` there is
+also one whose checker is neither an execution nor a disassembly but the
+**compile matrix** itself: `IoCall`'s refusing arm for a boolean read is
+reverted to the accepting dispatch it replaced, and `run_compile_tests.sh`
+must come back red on `t38`'s `ERR 66` row. Nothing that runs programs could
+see that one — an `ERR` fixture is never executed, so the refusal has to be
+asserted where the refusal happens.
 
 
 Those four replace a proof that could only be quoted. `t36_argclobber`'s
 Those four replace a proof that could only be quoted. `t36_argclobber`'s
 `.out` had been compiled and run against the compiler as it stood **before**
 `.out` had been compiled and run against the compiler as it stood **before**
@@ -1097,9 +1106,10 @@ Details that are deliberate, not incidental:
   prove the *path* exists (compile → poke → run → report → no file written),
   prove the *path* exists (compile → poke → run → report → no file written),
   which it does with eight assertions. The other 34 are covered by
   which it does with eight assertions. The other 34 are covered by
   `run_exec86.py`, which calls the same interpreter directly.
   `run_exec86.py`, which calls the same interpreter directly.
-- **Every fixture that compiles is now also run.** 35 of 38 execute; the 3 that
-  do not are `t14` and `t25` (`ENoLib`, by design) and `uierror` (a deliberate
-  syntax error). The gap this replaces was nine fixtures that compiled and were
+- **Every fixture that compiles is now also run.** 35 of 39 execute; the 4 that
+  do not are `t14` and `t25` (`ENoLib`, by design), `t38` (a boolean read,
+  refused with TP3's error 66, by design) and `uierror` (a deliberate syntax
+  error). The gap this replaces was nine fixtures that compiled and were
   *never executed* — `t08` const, `t09` if/then/else, `t10` while, `t11` for/to,
   *never executed* — `t08` const, `t09` if/then/else, `t10` while, `t11` for/to,
   `t12` repeat/until, `t13` procedure + value param, `t15` label + goto,
   `t12` repeat/until, `t13` procedure + value param, `t15` label + goto,
   `t27` five locals, `t28` the 70-parameter declaration — and all nine
   `t27` five locals, `t28` the 70-parameter declaration — and all nine
@@ -1205,11 +1215,13 @@ Details that are deliberate, not incidental:
   still said OK.
   still said OK.
 - **Comma-separated names are not supported.** `var i, c : integer;` is a
 - **Comma-separated names are not supported.** `var i, c : integer;` is a
   parse error. Pre-existing, unrelated to any of the above, and still open.
   parse error. Pre-existing, unrelated to any of the above, and still open.
-- **`readln` of a `BOOLEAN` still stores a word into a one-byte variable.**
-  This is bug 37's shape, unfixed: the BYTE half now has its own `rdbyte`
-  entry, but a boolean read still dispatches to `rdbool`, which stores `CX`.
-  TP3 refuses a boolean read with error 66 rather than implementing it, so the
-  honest options are to refuse it too or to add the one-byte entry.
+- **`readln` of a `BOOLEAN` is refused, exactly as TP3 refuses it.** It used to
+  dispatch to `rdbool`, which stores a word (`CX`) into a one-byte variable —
+  bug 37's shape, unclaimed only because no fixture read a boolean. TPSRC8
+  `prderrtp` errors 66 ("I/O not allowed here") for the whole class, and the
+  original runtime has no `xrdbool` at all, so `IoCall` now does the same and
+  never emits a boolean read. `rdbool` stays a runtime entry that `rt_exec`
+  still probes directly; only the compiler stopped reaching it.
 - **The program-header parameter loop is unguarded against non-advancing
 - **The program-header parameter loop is unguarded against non-advancing
   input.** `program p(1;)` would loop forever. The fix needs a `BOOLEAN` flag
   input.** `program p(1;)` would loop forever. The fix needs a `BOOLEAN` flag
   and **must not** use `EXIT`, which ICEs gm2 in pass 3 (see the pitfalls
   and **must not** use `EXIT`, which ICEs gm2 in pass 3 (see the pitfalls
@@ -1761,6 +1773,31 @@ reason for existing.
     just past the variable and require it to survive, and the fixture's `.out`
     just past the variable and require it to survive, and the fixture's `.out`
     is the same assertion at the level of a whole program.
     is the same assertion at the level of a whole program.
 
 
+### Then a boolean read was refused the way TP3 refuses it
+
+38. **A `readln` of a `BOOLEAN` was accepted, and `rdbool` stored a *word*
+    into a one-byte variable.** The byte half of bug 37 got its own reader,
+    but a boolean read still went through the word-storing `rdbool`, so the
+    only thing between the compiler and the same overflow again was that no
+    fixture read a boolean.
+
+    TP3 refuses the whole class: TPSRC8 `prdlp2` ends at `prderrtp`, which
+    errors 66 (`B $42` is "I/O not allowed here"), and the load-to-type list
+    in TPSRC8's `prdnum` has `xrdreal`, `xrdchar`, `xrdint`, `xrdbyte` and
+    the rest but no `xrdbool` at all — a boolean read is not something the
+    original runtime can answer. So the fix is to do what TP3 does rather
+    than to invent the missing entry: `IoCall` now refuses a boolean argument
+    to `read`/`readln` with `EIoHere = 66`. The new `t38_readbool` fixture
+    pins error 66 at the character after the variable, and the matrix row was
+    red before the fix landed — the accepting compiler answered `OK 43 5`
+    (a word store plus the `readln` tail) instead of `ERR 66 659`.
+
+    The refusal happens after the argument is fully parsed, which is the same
+    point TP3's `varptr2` check stands: the atom parser's `Skip` has already
+    run, so the pinned position is the `)` that closes `readln (b)`.
+    `rdbool` stays in the runtime and `rt_exec` still probes it; the compiler
+    simply no longer reaches it.
+
 ## The bug family, stated once
 ## The bug family, stated once
 
 
 Nine of the bugs recorded here are the *same* bug in different clothes: **loading the
 Nine of the bugs recorded here are the *same* bug in different clothes: **loading the
@@ -1840,30 +1877,25 @@ independently-scanned inventory at all.
 
 
 ## Next steps
 ## Next steps
 
 
-1. **`readln` of a `BOOLEAN`** is bug 37's shape with the fix not yet written:
-   `rdbool` stores a word (`CX`) into a one-byte variable. TP3 itself refuses a
-   boolean read with error 66 rather than implementing it, so the choice is to
-   refuse it the same way or to add the one-byte entry. Fixture first, so the
-   bug is red before the fix.
-2. **String *variables*** — `s : string`, `s := 'hi'`, `writeln(s)`. The
+1. **String *variables*** — `s : string`, `s := 'hi'`, `writeln(s)`. The
    encoding blocker is gone (`EmBpDisp`); what is left is a length word, an
    encoding blocker is gone (`EmBpDisp`); what is left is a length word, an
    assignment path, and a `WrStr` entry (TPSRC4 `xwrtstr`). `IoCall` currently
    assignment path, and a `WrStr` entry (TPSRC4 `xwrtstr`). `IoCall` currently
    refuses with `ENoLib`.
    refuses with `ENoLib`.
-3. Nested procedures / recursion, `var` parameters (the `SEG:OFF` push from
+2. Nested procedures / recursion, `var` parameters (the `SEG:OFF` push from
    RESUME-TP3.md §3.11), range/index checks (`TU_RANGE_CHECK`,
    RESUME-TP3.md §3.11), range/index checks (`TU_RANGE_CHECK`,
    `TU_INDEX_CHECK`), typed constants (RESUME-TP3.md §3.14), `array` at its
    `TU_INDEX_CHECK`), typed constants (RESUME-TP3.md §3.14), `array` at its
    point of use (`t14`), `case` with subrange labels.
    point of use (`t14`), `case` with subrange labels.
-4. Make the 4 KiB code window an enforced limit rather than a documented one:
+3. Make the 4 KiB code window an enforced limit rather than a documented one:
    report an error when `pc` reaches `dc`, instead of writing over the data.
    report an error when `pc` reaches `dc`, instead of writing over the data.
-5. Both spellings of a multi-name declaration. `var i, c : integer;` is
+4. Both spellings of a multi-name declaration. `var i, c : integer;` is
    error 1 at the comma and needs two `var` lines; `procedure f (a : integer;
    error 1 at the comma and needs two `var` lines; `procedure f (a : integer;
    b : integer)` is error 1 at the semicolon and needs a comma. Neither is
    b : integer)` is error 1 at the semicolon and needs a comma. Neither is
    wrong Pascal, so a program that compiles under one compiler may not under
    wrong Pascal, so a program that compiles under one compiler may not under
    another. A parameter may also not shadow a global (`DupTest` rejects any
    another. A parameter may also not shadow a global (`DupTest` rejects any
    name `Search` finds at any level), which Pascal allows.
    name `Search` finds at any level), which Pascal allows.
-6. Harden the program-header parameter loop against non-advancing input
+5. Harden the program-header parameter loop against non-advancing input
    (`program p(1;)`) with a `BOOLEAN` flag — **not** `EXIT`, which ICEs gm2.
    (`program p(1;)`) with a `BOOLEAN` flag — **not** `EXIT`, which ICEs gm2.
-7. FreeDOS (`freedos.qcow2`, FD14-LiveCD) is still untried. Now that `R` runs
+6. FreeDOS (`freedos.qcow2`, FD14-LiveCD) is still untried. Now that `R` runs
    the image in-process, a real DOS is no longer needed for any claim above —
    the image in-process, a real DOS is no longer needed for any claim above —
    but it is still the only way to get a third opinion on the `INT 21h` shim,
    but it is still the only way to get a third opinion on the `INT 21h` shim,
    and `Exec86` deliberately implements only the three functions the runtime
    and `Exec86` deliberately implements only the three functions the runtime

+ 9 - 1
shell/Compiler.mod

@@ -132,6 +132,7 @@ CONST
    ENoSemi    = 1 ;  EPointExp  = 10 ;  ESimpType  = 30 ;
    ENoSemi    = 1 ;  EPointExp  = 10 ;  ESimpType  = 30 ;
    EUnknown   = 41 ; EConstRange = 45 ; EMemOvf    = 98 ;
    EUnknown   = 41 ; EConstRange = 45 ; EMemOvf    = 98 ;
    ECompOvf   = 99 ; ENoLib     = 102 ; ETypeErr   = 56 ;
    ECompOvf   = 99 ; ENoLib     = 102 ; ETypeErr   = 56 ;
+   EIoHere    = 66 ;            (* I/O not allowed here - TPSRC8 prderrtp *)
 
 
    AposC      = 39 ;            (* ORD ("'") - avoids quote-in-quote *)
    AposC      = 39 ;            (* ORD ("'") - avoids quote-in-quote *)
 
 
@@ -2371,7 +2372,14 @@ BEGIN
                IF acls = TReal THEN
                IF acls = TReal THEN
                   ent := TU_RdInt                   (* real reads: not yet *)
                   ent := TU_RdInt                   (* real reads: not yet *)
                ELSIF acls = TBool THEN
                ELSIF acls = TBool THEN
-                  ent := TU_RdBool
+                  (* TP3 refuses boolean I/O: TPSRC8 prderrtp is error 66,
+                     "I/O not allowed here", and the runtime has no xrdbool
+                     at all.  rdbool stores a WORD, so one byte of an
+                     accepted read would land on the variable AFTER b - the
+                     exact overflow t37_readbyte pins for a byte.  TP3's
+                     answer to the whole class is the error; so is ours. *)
+                  Err (EIoHere) ;
+                  RETURN
                ELSIF acls = TChar THEN
                ELSIF acls = TChar THEN
                   ent := TU_RdChar                  (* one byte, not a word *)
                   ent := TU_RdChar                  (* one byte, not a word *)
                ELSIF acls = TScalar THEN
                ELSIF acls = TScalar THEN

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

@@ -198,6 +198,18 @@
 #                 chooses which entry answers without changing how long the
 #                 chooses which entry answers without changing how long the
 #                 call is.  The WIDTH of the store is asserted by execution
 #                 call is.  The WIDTH of the store is asserted by execution
 #                 (run_com_exec.py), not by this row - a size cannot see it.
 #                 (run_com_exec.py), not by this row - a size cannot see it.
+#
+#   t38_readbool   a boolean READ is refused, not misread.  TP3 errors 66
+#                 ("I/O not allowed here", TPSRC8 prderrtp) and its runtime
+#                 has no boolean reader at all - xrdreal, xrdchar, xrdint and
+#                 xrdbyte exist; xrdbool does not.  This compiler used to
+#                 accept it and dispatch to rdbool, which stores a WORD into
+#                 the one-byte variable - the same overflow t37 pins for a
+#                 byte, unobserved only because no fixture read a boolean.
+#                 The position is the ')' after b: IoCall has parsed the
+#                 whole argument, and the atom parser's Skip() has run, so the
+#                 parser stands on the delimiter when IoCall dispatches on
+#                 the variable's class.
 
 
 t01_minimal	OK	29	4
 t01_minimal	OK	29	4
 t02_writeln	OK	38	4
 t02_writeln	OK	38	4
@@ -236,4 +248,5 @@ t34_arith	OK	448	8
 t35_not	OK	224	7
 t35_not	OK	224	7
 t36_argclobber	OK	492	12
 t36_argclobber	OK	492	12
 t37_readbyte	OK	139	7
 t37_readbyte	OK	139	7
+t38_readbool	ERR	66	659
 uierror	ERR	41	331
 uierror	ERR	41	331

+ 16 - 0
shell/tests/fixtures/t38_readbool.pas

@@ -0,0 +1,16 @@
+program t38;
+(* TP3 refuses a boolean I/O with error 66 ("I/O not allowed here",
+   TPSRC8 prderrtp) and its runtime has no boolean reader at all -- xrdreal,
+   xrdchar, xrdint and xrdbyte exist; xrdbool does not.  A BOOLEAN is one
+   byte, so an accepted read would be a two-byte store into it, and the
+   second of those bytes is the byte that follows the variable.
+
+   The position pinned in expected.tsv is the ')' after b: IoCall has parsed
+   the whole argument, and the atom parser has already skipped the whitespace
+   after it, so the parser stands on the ')' when IoCall dispatches on the
+   variable's class. *)
+var
+  b : boolean;
+begin
+  readln (b)
+end.

+ 49 - 0
shell/tests/nonvacuity.sh

@@ -59,6 +59,15 @@
 #                      and a boolean `not' lowered as the integer one.  Every
 #                      and a boolean `not' lowered as the integer one.  Every
 #                      one of them sat in a fixture that COMPILED and was
 #                      one of them sat in a fixture that COMPILED and was
 #                      never RUN, with every byte-level check green.
 #                      never RUN, with every byte-level check green.
+#   run_compile_tests.sh   refusal:     catches a boolean READ slipping
+#                      through to the runtime.  TP3 refuses it with error 66
+#                      (TPSRC8 prderrtp; its runtime has no xrdbool), and
+#                      this compiler used to accept it and let rdbool store
+#                      a WORD into the one-byte variable.  The mutation is
+#                      the one arm of IoCall's type dispatch that refuses it,
+#                      and only the COMPILE MATRIX can see a refusal: an ERR
+#                      fixture is never executed, so nothing that runs
+#                      programs on it can fail the check.
 #   run_exec86.py     behaviour (Exec86):   catches the interpreter's OWN
 #   run_exec86.py     behaviour (Exec86):   catches the interpreter's OWN
 #                      reading of the machine: JE inverted in Exec86's Cond,
 #                      reading of the machine: JE inverted in Exec86's Cond,
 #                      which swaps the two arms of every `=' in every program
 #                      which swaps the two arms of every `=' in every program
@@ -960,6 +969,46 @@ else
 fi
 fi
 cp "$SAVED_C" Compiler.mod
 cp "$SAVED_C" Compiler.mod
 
 
+# The boolean read (t38): TP3 refuses `readln (b)' with error 66 - prderrtp
+# rejects the whole class and the runtime has no xrdbool - and this compiler
+# used to accept it and dispatch to rdbool, which stores a WORD into the
+# one-byte variable, its second byte landing on the variable AFTER b (t37's
+# overflow, unobserved only because no fixture read a boolean).  The fix is
+# one arm of IoCall's type dispatch, so the mutation restores exactly that
+# arm.  The check is the COMPILE MATRIX rather than any run: an ERR fixture
+# is never executed, so nothing that runs programs on it can see a refusal.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = """               ELSIF acls = TBool THEN
+                  (* TP3 refuses boolean I/O: TPSRC8 prderrtp is error 66,
+                     "I/O not allowed here", and the runtime has no xrdbool
+                     at all.  rdbool stores a WORD, so one byte of an
+                     accepted read would land on the variable AFTER b - the
+                     exact overflow t37_readbyte pins for a byte.  TP3's
+                     answer to the whole class is the error; so is ours. *)
+                  Err (EIoHere) ;
+                  RETURN"""
+assert s.count(old) == 1, 'the boolean-read error arm found %d times -- update this mutation' % s.count(old)
+rep = """               ELSIF acls = TBool THEN
+                  ent := TU_RdBool"""
+open(p, 'w').write(s.replace(old, rep))
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t38 red when a boolean read dispatches to TU_RdBool" \
+            "t38_readbool" tests/run_compile_tests.sh
+    else
+        echo "  FAIL: the compiler would not rebuild with the boolean read accepted"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the boolean-read 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
         # No count: the matrix grows every time a fixture is added, and a
         # No count: the matrix grows every time a fixture is added, and a