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

compiler: KeyPressed (INT 21h AH=0B), in Exec86 and the boot shim too

KeyPressed is a boolean function with no parameter list (CALC's Flash
ends `until KeyPressed').  It asks DOS AH=0Bh whether a character is
waiting, exactly as TP3's pkeypressed does (TPSRC8 1522 -> xkeyp),
masked with AND AX,1.

The mask is AX, not AL, and that is the subtle part: the interrupt
leaves AH = 0Bh, so `AND AL,1' still leaves AX = 0B00h and the boolean
test `CMP AX,0' calls the no-key answer TRUE.  bootcom's handler
happens to return the whole AX from its own code (AH=0), so qemu
could never see this; the Exec86 oracle found it on the first run.

bootcom.s grows AH=0Bh: compare INCUR with INLEN, exactly as its AH=08h
does, and return FFh/0 in AL.  Exec86 grows the same function, backed
by a one-byte PEEK buffer - a status call must not consume the char
the next AH=08h will read; at end of input it reports 0, not "ready".

Fixture t72_keypress runs under both oracles: one byte pending (true),
read it, then empty (false).  Non-vacuity: removing the KEYPRESSED
registration reddens the compile matrix, and changing AND AX,1 back to
AND AL,1 reddens Exec86 only - which is why the fixture is executed by
both.

The ladder moves 12672 -> 12901, now `procedure IBMCh(var Ch: Char)'.
That is the var-parameter rung, and it needs the real thing: `var' is
currently parsed and then silently recorded as a VALUE parameter, which
is the section-7 failure mode.  It is masked only because DupTest
rejects the parameter name for shadowing the global `Ch'.  Implementing
by-reference parameters and the shadowing rule are the next increment;
CALC-COMPILE.md records that they must go together.
Eric Streit 2 дней назад
Родитель
Сommit
033eb8ea12

+ 18 - 5
CALC-COMPILE.md

@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 
 | position | blocker |
 | position | blocker |
 |---|---|
 |---|---|
-| **12672** | `KeyPressed` in `CALC.INC`'s `Flash` — a boolean FUNCTION (`ERR 41`) |
+| **12901** | `procedure IBMCh(var Ch: Char)` — a `var` parameter, and its name shadows a global (`ERR 41`) |
 
 
 Rungs behind it (now passed):
 Rungs behind it (now passed):
 
 
@@ -114,12 +114,13 @@ Rungs behind it (now passed):
 | 10753 | `ClrScr` — CRT identifiers begin | `ClrScr` + `GotoXY` (real INT 10h) |
 | 10753 | `ClrScr` — CRT identifiers begin | `ClrScr` + `GotoXY` (real INT 10h) |
 | 10927 | `Read(Kbd, Ch)` — a READ with a leading file | `Kbd` + `ClrEol` |
 | 10927 | `Read(Kbd, Ch)` — a READ with a leading file | `Kbd` + `ClrEol` |
 | 12083 | `NormVideo`/`LowVideo`/`Delay` | attribute byte + busy loop; and the repeat-body parser bug fixed |
 | 12083 | `NormVideo`/`LowVideo`/`Delay` | attribute byte + busy loop; and the repeat-body parser bug fixed |
+| 12672 | `KeyPressed` | `INT 21h AH=0B`, in Exec86 and bootcom too |
 
 
 Rungs ahead:
 Rungs ahead:
 
 
 | position | blocker |
 | position | blocker |
 |---|---|
 |---|---|
-| 12672 | `KeyPressed`, then the rest of `CALC.INC` |
+| 12901 | `var` parameters (implement by reference, do not record as value), plus parameter shadowing of globals; 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:
@@ -491,9 +492,21 @@ Exec86):
   `;`; `t12_repeat` only ever had single-statement bodies.  Fixed and pinned by
   `;`; `t12_repeat` only ever had single-statement bodies.  Fixed and pinned by
   `t71_repeat_seq`.  Exec86 also gained opcode `C6` (`MOV r/m8,imm8`), which the
   `t71_repeat_seq`.  Exec86 also gained opcode `C6` (`MOV r/m8,imm8`), which the
   prologue now uses.  The rung moved 12083 → **12672**.
   prologue now uses.  The rung moved 12083 → **12672**.
-- **The ladder now stops at `ERR 41` at `12672`** — `KeyPressed`, a boolean
-  function that asks whether a key is waiting.  That needs an input-status
-  primitive across the runtime, Exec86 and the boot shim.
+- **`KeyPressed` is landed; the rung moved 12672 → 12901.**  It is
+  `INT 21h AH=0B` (AL=FFh/0, masked with `AND AX,1` - not `AND AL,1`: AH is
+  still 0Bh after the interrupt, and the boolean test would read 0B00h).
+  bootcom answers it from its input buffer and Exec86 from a one-byte peek
+  buffer, so a status call never consumes the character the next read wants.
+  Fixture `t72_keypress`, executed under both oracles.
+- **The ladder now stops at `ERR 41` at `12901`** — `procedure IBMCh(var Ch:
+  Char)`.  This is the `var`-parameter rung, and it needs the real thing:
+  today a `var` parameter is **silently recorded as a value parameter**
+  ("recorded as value in this milestone"), which is exactly the §7 failure
+  mode.  It is masked here only because `DupTest` rejects the parameter name
+  `Ch` for shadowing the global `Ch`; ISO Pascal allows that shadowing, so
+  fixing DupTest alone would turn CALC's next rung into a silent miscompile.
+  Do both together: pass addresses, and let inner declarations shadow outer
+  ones (duplicates at the SAME level stay errors).
 - **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

+ 5 - 4
SUMMARY.md

@@ -2037,14 +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,
 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 **the CRT attribute/Delay group** are landed: the ladder
+features through **`KeyPressed`** 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 41` at pos 12672, `KeyPressed`), the `{$I}` include,
+CALC.PAS now stops at `ERR 41` at pos 12901, `IBMCh`'s var parameter), the `{$I}` include,
 `^X`/`#nn`, scalar typed constants, sets, enums, records (CellRec = 80),
 `^X`/`#nn`, scalar typed constants, sets, enums, records (CellRec = 80),
 subranges, arrays, element/field access, file types, string value parameters,
 subranges, arrays, element/field access, file types, string value parameters,
 `Length`, the CRT identifiers (`ClrScr`/`GotoXY`/`ClrEol`/`LowVideo`/
 `Length`, the CRT identifiers (`ClrScr`/`GotoXY`/`ClrEol`/`LowVideo`/
-`NormVideo`/`Delay`), `Kbd`, and `INT 10h`+`C6` in Exec86 (fixtures through
-`t71_repeat_seq`). The next rungs are `KeyPressed`, the remaining string
+`NormVideo`/`Delay`), `Kbd`, `KeyPressed` (`INT 21h AH=0B`), and
+`INT 10h`+`C6` in Exec86 (fixtures through `t72_keypress`). The next rungs are
+`var` parameters (by reference, with the name-shadowing rule), the remaining string
 builtins, file operations, variable-index subscripts + `with`, pointers, `var`
 builtins, file operations, variable-index subscripts + `with`, pointers, `var`
 params, reals, recursion, and the layout change. Items 1-4 below are
 params, reals, recursion, and the layout change. Items 1-4 below are
 independent of both.
 independent of both.

+ 23 - 1
shell/Compiler.mod

@@ -120,9 +120,10 @@ CONST
       not implemented yet is a loud ENoLib, never a silent no-op. *)
       not implemented yet is a loud ENoLib, never a silent no-op. *)
    BI_ClrScr  = 6 ;  BI_GotoXY = 7 ;  BI_LowVideo = 8 ;
    BI_ClrScr  = 6 ;  BI_GotoXY = 7 ;  BI_LowVideo = 8 ;
    BI_NormVideo = 9 ; BI_ClrEol = 10 ; BI_Delay = 11 ;
    BI_NormVideo = 9 ; BI_ClrEol = 10 ; BI_Delay = 11 ;
+   BI_KeyPressed = 12 ;
    (* the sub-codes EmCrtSeq takes *)
    (* the sub-codes EmCrtSeq takes *)
    CrtClr = 0 ; CrtDl = 1 ; CrtDh = 2 ; CrtCaret = 3 ; CrtEol = 4 ;
    CrtClr = 0 ; CrtDl = 1 ; CrtDh = 2 ; CrtCaret = 3 ; CrtEol = 4 ;
-   CrtLow = 5 ; CrtNorm = 6 ; CrtDelay = 7 ;
+   CrtLow = 5 ; CrtNorm = 6 ; CrtDelay = 7 ; CrtKey = 8 ;
 
 
    (* keyword tokens *)
    (* keyword tokens *)
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
@@ -3257,6 +3258,14 @@ BEGIN
          fixture had ever exercised the fold. *)
          fixture had ever exercised the fold. *)
       ParseVar (r) ;
       ParseVar (r) ;
       RETURN
       RETURN
+   ELSIF (symtab [idx].tag = KBuiltin) AND (symtab [idx].cls = BI_KeyPressed) THEN
+      (* KeyPressed: a boolean FUNCTION with no argument, and TP3 lets it be
+         written without a parameter list (`until KeyPressed'). *)
+      EmCrtSeq (CrtKey) ;
+      r.kind := 2 ;
+      r.cls := TBool ;
+      r.chr := FALSE ;
+      RETURN
    ELSIF (symtab [idx].tag = KBuiltin) AND (symtab [idx].cls = BI_Length) THEN
    ELSIF (symtab [idx].tag = KBuiltin) AND (symtab [idx].cls = BI_Length) THEN
       (* LENGTH (s): the string's length byte, loaded inline.  No runtime
       (* LENGTH (s): the string's length byte, loaded inline.  No runtime
          entry, and no address is pushed - a string's value already carries its
          entry, and no address is pushed - a string's value already carries its
@@ -3674,6 +3683,17 @@ BEGIN
       Ebyte (0C6H) ; Ebyte (06H) ; Eword (crtAttrOff) ; Ebyte (07H)
       Ebyte (0C6H) ; Ebyte (06H) ; Eword (crtAttrOff) ; Ebyte (07H)
    ELSIF which = CrtNorm THEN
    ELSIF which = CrtNorm THEN
       Ebyte (0C6H) ; Ebyte (06H) ; Eword (crtAttrOff) ; Ebyte (0FH)
       Ebyte (0C6H) ; Ebyte (06H) ; Eword (crtAttrOff) ; Ebyte (0FH)
+   ELSIF which = CrtKey THEN
+      (* KeyPressed: DOS AH=0Bh asks whether a character is waiting.  AL is
+         0FFh when one is, 0 when not; the boolean representation here is 0/1.
+         AND AX,1, not AND AL,1: the interrupt leaves AH = 0Bh, so the
+         comparison that follows (`CMP AX,0') would read 0B00h and call the
+         false case TRUE.  bootcom happened to return AH=0 (it IRETs the
+         handler's whole AX) and hid this; hlt.  No argument, no runtime
+         entry. *)
+      Ebyte (0B4H) ; Ebyte (0BH) ;                 (* MOV AH,0Bh           *)
+      Ebyte (0CDH) ; Ebyte (21H) ;                 (* INT 21h              *)
+      Ebyte (25H) ; Ebyte (01H) ; Ebyte (00H)      (* AND AX,1             *)
    ELSIF which = CrtDelay THEN
    ELSIF which = CrtDelay THEN
       (* Delay(AX) milliseconds: TP3's xdelay, a calibrated busy loop.  The
       (* Delay(AX) milliseconds: TP3's xdelay, a calibrated busy loop.  The
          count 110 is TP3's default `delaycnt' for 1 ms (TPSRC1).  AX = 0 does
          count 110 is TP3's default `delaycnt' for 1 ms (TPSRC1).  AX = 0 does
@@ -5317,6 +5337,8 @@ BEGIN
    symtab [i].defnd := TRUE ;
    symtab [i].defnd := TRUE ;
    i := NewSym ("DELAY"  , KBuiltin, BI_Delay   , 0, 0, 0, 0, FALSE) ;
    i := NewSym ("DELAY"  , KBuiltin, BI_Delay   , 0, 0, 0, 0, FALSE) ;
    symtab [i].defnd := TRUE ;
    symtab [i].defnd := TRUE ;
+   i := NewSym ("KEYPRESSED", KBuiltin, BI_KeyPressed, 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
    (* KBD: the console keyboard as a TEXT file.  This runtime's read entries
    (* KBD: the console keyboard as a TEXT file.  This runtime's read entries
       already read the console, so `Read(Kbd, Ch)' drops the file argument and
       already read the console, so `Read(Kbd, Ch)' drops the file argument and
       reads a character exactly as `Read(Ch)' does. *)
       reads a character exactly as `Read(Ch)' does. *)

+ 32 - 4
shell/Exec86.mod

@@ -73,6 +73,9 @@ VAR
    loadHi   : CARDINAL ;      (* one past the highest address poked *)
    loadHi   : CARDINAL ;      (* one past the highest address poked *)
    stepCnt  : LONGCARD ;
    stepCnt  : LONGCARD ;
    curRow, curCol : CARDINAL ; (* INT 10h's cursor, for AH=02/AH=03 *)
    curRow, curCol : CARDINAL ; (* INT 10h's cursor, for AH=02/AH=03 *)
+   peeked : BOOLEAN ;         (* AH=0Bh looked one byte ahead; keep it for
+                                 AH=08h rather than losing it *)
+   peekCh : CHAR ;
 
 
    (* filled in by DoModRM *)
    (* filled in by DoModRM *)
    eaAddr   : CARDINAL ;
    eaAddr   : CARDINAL ;
@@ -590,7 +593,7 @@ END DoInt10 ;
 (* ----------------------------------------------------------------- INT 21h *)
 (* ----------------------------------------------------------------- INT 21h *)
 
 
 PROCEDURE DoInt21 ;
 PROCEDURE DoInt21 ;
-(* The four services bootcom.s provides and the runtime calls, and nothing
+(* The five services bootcom.s provides and the compiler emits, and nothing
    else.  Anything else faults, because the alternative is inventing DOS.
    else.  Anything else faults, because the alternative is inventing DOS.
 
 
    No flag is touched anywhere in here: IRET restores the caller's FLAGS, so
    No flag is touched anywhere in here: IRET restores the caller's FLAGS, so
@@ -626,14 +629,39 @@ BEGIN
       END
       END
 
 
    | 08H :                                   (* read a character, no echo *)
    | 08H :                                   (* read a character, no echo *)
-      n := read (STDIN, ADR (c), 1) ;
-      IF n = 1 THEN
+      IF peeked THEN
+         c := peekCh ;
+         peeked := FALSE ;
          b := ORD (c)
          b := ORD (c)
       ELSE
       ELSE
-         b := 1AH                            (* end of input, as bootcom does *)
+         n := read (STDIN, ADR (c), 1) ;
+         IF n = 1 THEN
+            b := ORD (c)
+         ELSE
+            b := 1AH                         (* end of input, as bootcom does *)
+         END
       END ;
       END ;
       AX := (AX DIV 256) * 256 + b           (* AL only: AH is the function *)
       AX := (AX DIV 256) * 256 + b           (* AL only: AH is the function *)
 
 
+   | 0BH :                                   (* input status: FFh or 0 *)
+      (* bootcom answers from its in-memory input buffer; here stdin is a
+         pipe that already holds the whole guest input.  Look one byte ahead
+         and KEEP it: a status call must not consume the character the next
+         AH=08h will read.  A read of 0 is end of input, so nothing waits. *)
+      IF peeked THEN
+         b := 0FFH
+      ELSE
+         n := read (STDIN, ADR (c), 1) ;
+         IF n = 1 THEN
+            peekCh := c ;
+            peeked := TRUE ;
+            b := 0FFH
+         ELSE
+            b := 0
+         END
+      END ;
+      AX := (AX DIV 256) * 256 + b
+
    | 04CH :                                  (* terminate *)
    | 04CH :                                  (* terminate *)
       halted := TRUE ;
       halted := TRUE ;
       haltCode := AX MOD 256
       haltCode := AX MOD 256

+ 17 - 1
shell/tests/exec/bootcom.s

@@ -17,9 +17,10 @@
 #   AH=02h  display character in AL
 #   AH=02h  display character in AL
 #   AH=09h  display the $-terminated string at DS:DX
 #   AH=09h  display the $-terminated string at DS:DX
 #   AH=08h  read a character without echo; 1Ah at end of input
 #   AH=08h  read a character without echo; 1Ah at end of input
+#   AH=0Bh  input status: AL=FFh if a character waits, 0 if not
 #   AH=4Ch  terminate
 #   AH=4Ch  terminate
 #
 #
-# Those four are the complete set.  See Runtime.mod: every Int21 in it is one
+# Those five are the complete set.  See Runtime.mod: every Int21 in it is one
 # of them (the "MovAh (0)" in EmitRdInt is not an INT 21h call, it is the
 # of them (the "MovAh (0)" in EmitRdInt is not an INT 21h call, it is the
 # MOV AH,0 that loads a digit into AL before an ADD -- an easy thing to
 # MOV AH,0 that loads a digit into AL before an ADD -- an easy thing to
 # misread as a fourth function).
 # misread as a fourth function).
@@ -160,6 +161,8 @@ handler:
 	je	.Lh09
 	je	.Lh09
 	cmpb	$0x08, %ah
 	cmpb	$0x08, %ah
 	je	.Lh08
 	je	.Lh08
+	cmpb	$0x0b, %ah
+	je	.Lh0b
 	cmpb	$0x4c, %ah
 	cmpb	$0x4c, %ah
 	je	.Lh4c
 	je	.Lh4c
 	stc				# unknown function
 	stc				# unknown function
@@ -213,6 +216,19 @@ handler:
 	clc
 	clc
 	jmp	.Ldone8
 	jmp	.Ldone8
 
 
+.Lh0b:					# input status: AL=FFh if a character waits
+	movw	INLEN, %ax
+	movw	INCUR, %bx
+	cmpw	%ax, %bx		# INCUR vs INLEN: >= is end of input
+	jae	.Lh0b_none
+	movb	$0xff, %al
+	clc
+	jmp	.Ldone8		# AL is the result, as with AH=08h
+.Lh0b_none:
+	xorw	%ax, %ax		# AL = 0: nothing waiting
+	clc
+	jmp	.Ldone8
+
 .Lh4c:					# terminate: exit with AL as the code
 .Lh4c:					# terminate: exit with AL as the code
 	movw	$0x0501, %dx		# isa-debug-exit
 	movw	$0x0501, %dx		# isa-debug-exit
 	outb	%al, %dx
 	outb	%al, %dx

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

@@ -25,9 +25,11 @@
 # 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-20): `KeyPressed` in CALC.INC's Flash - a boolean
-# FUNCTION that asks whether a key is waiting.  The rung moved from 12083
-# (NormVideo) when LowVideo/NormVideo and Delay landed, and it also exposed a
-# PARSER bug on the way: a `repeat' body was one statement, not a sequence
-# (fixed; t71_repeat_seq).
-CALC.PAS	ERR	41	12672
+# Current rung (2026-10-20): `procedure IBMCh(var Ch: Char)' in CALC.INC.
+# KeyPressed landed (INT 21h AH=0B; t72), moving 12672 -> 12901.  The blocker
+# is now a VAR PARAMETER, and it is two things at once: `var' is parsed but
+# then silently recorded as a VALUE parameter today (a section-7 violation
+# that must be IMPLEMENTED, not waved through), and the parameter is named
+# `Ch', which is also a global - DupTest rejects shadowing an outer name,
+# which ISO Pascal allows.  Both must be fixed together.
+CALC.PAS	ERR	41	12901

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

@@ -558,3 +558,9 @@ t70_kbd	OK	60	6
 #                   hidden; CALC.INC's Flash does it.  RED before the fix
 #                   hidden; CALC.INC's Flash does it.  RED before the fix
 #                   (ERR 1), green after; .out is 6.
 #                   (ERR 1), green after; .out is 6.
 t71_repeat_seq	OK	106	7
 t71_repeat_seq	OK	106	7
+#   t72_keypress           KeyPressed: INT 21h AH=0Bh.  With one byte pending
+#                   the answer is true and the byte must survive for the
+#                   following read; at end of input it is false.  AND AX,1,
+#                   not AND AL,1: AH is still 0Bh after the interrupt.  The
+#                   .in is a single 'A'; .out exercises both answers.
+t72_keypress	OK	138	6

+ 1 - 0
shell/tests/fixtures/t72_keypress.in

@@ -0,0 +1 @@
+A

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

@@ -0,0 +1,3 @@
+key
+A
+end

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

@@ -0,0 +1,16 @@
+program t72_keypress;
+
+{ t72 -- KeyPressed.  DOS AH=0Bh asks whether a character is waiting, and
+  TP3 lets the function be written without a parameter list (CALC.INC's Flash
+  ends `until KeyPressed').  The fixture asks with one byte of input pending,
+  reads it, and asks again - so both the true and the false answer are
+  checked, and the call must not consume the byte. }
+
+var
+   ch : char;
+begin
+   if KeyPressed then writeln ('key') else writeln ('none');
+   read (ch);
+   writeln (ch);
+   if KeyPressed then writeln ('more') else writeln ('end')
+end.

+ 44 - 0
shell/tests/nonvacuity.sh

@@ -1749,6 +1749,50 @@ else
 fi
 fi
 cp "$SAVED_C" Compiler.mod
 cp "$SAVED_C" Compiler.mod
 
 
+# --- KEYPRESSED is registered ---------------------------------------------
+# It is a FUNCTION, not a statement builtin, so it is registered like LENGTH
+# and parsed in ParseAtom.  Remove it and t72 stops at ERR 41.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = ('   i := NewSym ("KEYPRESSED", KBuiltin, BI_KeyPressed, 0, 0, 0, 0, FALSE) ;\n'
+       '   symtab [i].defnd := TRUE ;\n')
+assert s.count(old) == 1, 'the KEYPRESSED registration found %d times -- update this mutation' % s.count(old)
+open(p, 'w').write(s.replace(old, ''))
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t72 red when KEYPRESSED is not registered"             "expected: OK 138 6" tests/run_compile_tests.sh
+    else
+        echo "  FAIL: the compiler would not rebuild without the KEYPRESSED builtin"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the KEYPRESSED-registration mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED_C" Compiler.mod
+
+# --- KeyPressed masks AL, not AX ------------------------------------------
+# AH is still 0Bh after INT 21h, so `AND AL,1' leaves AX = 0B00h and the
+# boolean test `CMP AX,0' calls the NO-KEY answer TRUE.  bootcom's handler
+# happens to IRET the whole AX and returns AH=0, so qemu cannot see this;
+# only the Exec86 oracle can, which is why t72 runs under both.
+cp "$SAVED_C" Compiler.mod
+if mutate_compiler 's|   Ebyte (25H) ; Ebyte (01H) ; Ebyte (00H)|   Ebyte (24H) ; Ebyte (01H)|'; then
+    if rebuild_compiler; then
+        expect_red "t72 red when KeyPressed masks AL instead of AX"             "more" python3 tests/run_exec86.py t72_keypress
+    else
+        echo "  FAIL: the compiler would not rebuild with AND AL,1"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the AND AX,1 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"