Przeglądaj źródła

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 3 dni temu
rodzic
commit
033eb8ea12

+ 18 - 5
CALC-COMPILE.md

@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 | 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):
 
@@ -114,12 +114,13 @@ Rungs behind it (now passed):
 | 10753 | `ClrScr` — CRT identifiers begin | `ClrScr` + `GotoXY` (real INT 10h) |
 | 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 |
+| 12672 | `KeyPressed` | `INT 21h AH=0B`, in Exec86 and bootcom too |
 
 Rungs ahead:
 
 | 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
 below was written before them:
@@ -491,9 +492,21 @@ Exec86):
   `;`; `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
   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
   nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the
   `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,
 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 **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`;
-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),
 subranges, arrays, element/field access, file types, string value parameters,
 `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`
 params, reals, recursion, and the layout change. Items 1-4 below are
 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. *)
    BI_ClrScr  = 6 ;  BI_GotoXY = 7 ;  BI_LowVideo = 8 ;
    BI_NormVideo = 9 ; BI_ClrEol = 10 ; BI_Delay = 11 ;
+   BI_KeyPressed = 12 ;
    (* the sub-codes EmCrtSeq takes *)
    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 *)
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
@@ -3257,6 +3258,14 @@ BEGIN
          fixture had ever exercised the fold. *)
       ParseVar (r) ;
       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
       (* LENGTH (s): the string's length byte, loaded inline.  No runtime
          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)
    ELSIF which = CrtNorm THEN
       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
       (* 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
@@ -5317,6 +5337,8 @@ BEGIN
    symtab [i].defnd := TRUE ;
    i := NewSym ("DELAY"  , KBuiltin, BI_Delay   , 0, 0, 0, 0, FALSE) ;
    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
       already read the console, so `Read(Kbd, Ch)' drops the file argument and
       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 *)
    stepCnt  : LONGCARD ;
    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 *)
    eaAddr   : CARDINAL ;
@@ -590,7 +593,7 @@ END DoInt10 ;
 (* ----------------------------------------------------------------- INT 21h *)
 
 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.
 
    No flag is touched anywhere in here: IRET restores the caller's FLAGS, so
@@ -626,14 +629,39 @@ BEGIN
       END
 
    | 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)
       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 ;
       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 *)
       halted := TRUE ;
       haltCode := AX MOD 256

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

@@ -17,9 +17,10 @@
 #   AH=02h  display character in AL
 #   AH=09h  display the $-terminated string at DS:DX
 #   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
 #
-# 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
 # MOV AH,0 that loads a digit into AL before an ADD -- an easy thing to
 # misread as a fourth function).
@@ -160,6 +161,8 @@ handler:
 	je	.Lh09
 	cmpb	$0x08, %ah
 	je	.Lh08
+	cmpb	$0x0b, %ah
+	je	.Lh0b
 	cmpb	$0x4c, %ah
 	je	.Lh4c
 	stc				# unknown function
@@ -213,6 +216,19 @@ handler:
 	clc
 	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
 	movw	$0x0501, %dx		# isa-debug-exit
 	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
 # 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
 #                   (ERR 1), green after; .out is 6.
 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
 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 python3 tests/run_com_exec.py >/dev/null 2>&1; then
         echo "  ok: every executed fixture passes on the restored sources"