瀏覽代碼

compiler: Read(Kbd, Ch) and ClrEol (CALC file args + CRT)

The ladder moves: CALC.PAS went from ERR 41 at 10927 to ERR 102 at 12083.
`Read(Kbd, Ch)' compiles, `ClrEol' is real, and the next blocker is
`NormVideo' - one of the CRT identifiers still refused.

  - KBD is a predefined KVar of class TText, flagged SymEntry.isKbd.
  - IoCall accepts a LEADING FILE argument.  When it is KBD and the call is a
    READ, the file is dropped: this runtime's read entries already read the
    console, so `Read(Kbd, Ch)' is `Read(Ch)'.  A real file variable there is a
    loud ENoLib, not a silent read into the file variable's own bytes.
  - ClrEol is a real INT 10h call: AH=03 reads the cursor, CX := it, DL := 79,
    AH=06 blanks that window.  Added to EmCrtSeq (CrtEol), so it shares the
    documented COMP exclusion.

Fixtures: t70_kbd (`Read(Kbd, Ch)', .in `Z', .out `Z'+CRLF), executed under
qemu and Exec86; t69_crt extended with ClrEol (OK 119 4, rewritten row).
60 fixtures execute under both oracles.

nonvacuity.sh: a KBD-registration case, and the CLRSCR case's expected size
updated (adding ClrEol moved t69 from OK 102 4 to OK 119 4).  90 ok, 0 failed.

run_all.sh: ALL PASS - matrix 71/71, ladder 1/1 at ERR 102 12083, 62 .COM
linked, 60 executed under both oracles; helper audit PASS.
Eric Streit 2 天之前
父節點
當前提交
4c165e1bcf

+ 13 - 6
CALC-COMPILE.md

@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 | position | blocker |
 |---|---|
-| **10927** | `Read(Kbd, Ch)` in `CALC.INC` — the CRT keyboard file as a READ argument (`ERR 41`) |
+| **12083** | `NormVideo` in `CALC.INC` — a CRT identifier still refused (`ERR 102`) |
 
 Rungs behind it (now passed):
 
@@ -113,12 +113,13 @@ Rungs behind it (now passed):
 | 10350 | `procedure Center(S : AnyString)` — a **string value parameter** | calling convention, steps 1-3 |
 | 10405 | `Length(S)` — the first string builtin | `LENGTH`, inline |
 | 10753 | `ClrScr` — CRT identifiers begin | `ClrScr` + `GotoXY` (real INT 10h) |
+| 10927 | `Read(Kbd, Ch)` — a READ with a leading file | `Kbd` + `ClrEol` |
 
 Rungs ahead:
 
 | position | blocker |
 |---|---|
-| 10927 | `Kbd` / `Read` with a file argument, then `KeyPressed`, `ClrEol`, `LowVideo`/`NormVideo`, `Delay`, the remaining string builtins and the rest of `CALC.INC` |
+| 12083 | `NormVideo`/`LowVideo`/`Delay`, `KeyPressed`, then the rest of `CALC.INC` (file operations, indexing, `with`, pointers, `var` params, reals, string builtins, recursion) |
 
 Corrections the measurements force, worth stating plainly because the stage list
 below was written before them:
@@ -475,8 +476,14 @@ Exec86):
   `INT 10h`** (cursor tracked, screen calls no-ops, everything else faults), so
   `t69_crt` runs under BOTH oracles; removing that facility would have left the
   CRT group with only one execution oracle.  The rung moved 10753 → **10927**.
-- **The ladder now stops at `ERR 41` at `10927`** — `Read(Kbd, Ch)`, the CRT
-  keyboard file as a READ argument.  That is the file-operation + `Kbd` work.
+- **`Read(Kbd, Ch)` and `ClrEol` are landed.**  `KBD` is a predefined console
+  TEXT file; a READ's leading file argument is dropped when it is `Kbd` (the
+  runtime's read entries already read the console) and is a loud `ENoLib` for a
+  real file variable.  `ClrEol` is a real `INT 10h` call (AH=03 reads the
+  cursor, AH=06 blanks to column 79).  Fixtures `t70_kbd` (executed with a
+  `.in`), `t69_crt` extended.  The rung moved 10927 → **12083**.
+- **The ladder now stops at `ERR 102` at `12083`** — `NormVideo` (the
+  `LowVideo`/`NormVideo`/`Delay` group), then `KeyPressed`.
 - **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
@@ -486,8 +493,8 @@ Exec86):
   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 **70/70**, CALC ladder **1/1** at
-  `ERR 41 10927`, 61 `.COM` linked and 59 executed under both qemu and Exec86
+  **OVERALL: ALL PASS**, compile matrix **71/71**, CALC ladder **1/1** at
+  `ERR 102 12083`, 62 `.COM` linked and 60 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

+ 9 - 9
SUMMARY.md

@@ -2037,16 +2037,16 @@ 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 first CRT identifiers** are landed: the ladder
+features through **`Read(Kbd, Ch)` and `ClrEol`** 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 10927, `Read(Kbd, Ch)` inside `CALC.INC`),
-the `{$I}` include, `^X`/`#nn`, scalar typed constants, sets, enums, records
-(CellRec = 80), subranges, arrays, element/field access, file types, string
-value parameters, `Length`, and `ClrScr`/`GotoXY` (fixtures through `t69_crt`).
-The next rungs are `Kbd`/file operations, the remaining CRT identifiers, the
-remaining string builtins, variable-index subscripts + `with`, pointers, `var`
-params, reals, recursion, and the layout change. Items 1-4 below are
-independent of both.
+CALC.PAS now stops at `ERR 102` at pos 12083, `NormVideo`), the `{$I}` include,
+`^X`/`#nn`, scalar typed constants, sets, enums, records (CellRec = 80),
+subranges, arrays, element/field access, file types, string value parameters,
+`Length`, `ClrScr`/`GotoXY`/`ClrEol`, `Kbd`, and `INT 10h` in Exec86 (fixtures
+through `t70_kbd`). The next rungs are `NormVideo`/`LowVideo`/`Delay`,
+`KeyPressed`, 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.
 
 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)`

+ 53 - 6
shell/Compiler.mod

@@ -121,7 +121,7 @@ CONST
    BI_ClrScr  = 6 ;  BI_GotoXY = 7 ;  BI_LowVideo = 8 ;
    BI_NormVideo = 9 ; BI_ClrEol = 10 ; BI_Delay = 11 ;
    (* the sub-codes EmCrtSeq takes *)
-   CrtClr = 0 ; CrtDl = 1 ; CrtDh = 2 ; CrtCaret = 3 ;
+   CrtClr = 0 ; CrtDl = 1 ; CrtDh = 2 ; CrtCaret = 3 ; CrtEol = 4 ;
 
    (* keyword tokens *)
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
@@ -179,6 +179,7 @@ TYPE
          arrIdx : CARDINAL ;        (* an ARRAY type's shape in ArrRec (0 = no) *)
          parmFirst, parmCount : CARDINAL ;  (* a PROCEDURE/FUNCTION's parameters:
                                        symtab indices, in declaration order *)
+         isKbd  : BOOLEAN ;          (* the predefined KBD file: reads are console *)
          level  : CARDINAL ;
          local  : BOOLEAN ;
          resvar : CARDINAL ;
@@ -1495,6 +1496,7 @@ BEGIN
    e.arrIdx := 0 ;
    e.parmFirst := 0 ;
    e.parmCount := 0 ;
+   e.isKbd := FALSE ;
    e.level := lexnest ;
    e.local := local ;
    e.resvar := 0 ;
@@ -3652,6 +3654,15 @@ BEGIN
    ELSIF which = CrtDh THEN
       Ebyte (48H) ;                                (* DEC AX    *)
       Ebyte (88H) ; Ebyte (0C6H)                   (* MOV DH,AL *)
+   ELSIF which = CrtEol THEN
+      Ebyte (0B4H) ; Ebyte (03H) ;                 (* MOV AH,03h  get cursor *)
+      Ebyte (0B7H) ; Ebyte (00H) ;                 (* MOV BH,00h             *)
+      Ebyte (0CDH) ; Ebyte (10H) ;                 (* INT 10h  -> DH,DL      *)
+      Ebyte (89H) ; Ebyte (0D1H) ;                 (* MOV CX,DX  upper-left   *)
+      Ebyte (0B2H) ; Ebyte (4FH) ;                 (* MOV DL,4Fh  col 79      *)
+      Ebyte (0B8H) ; Ebyte (00H) ; Ebyte (06H) ;   (* MOV AX,0600h blank      *)
+      Ebyte (0B7H) ; Ebyte (07H) ;                 (* MOV BH,07h  attribute   *)
+      Ebyte (0CDH) ; Ebyte (10H)                  (* INT 10h                 *)
    ELSE
       Ebyte (0B4H) ; Ebyte (02H) ;                 (* MOV AH,02h *)
       Ebyte (0B7H) ; Ebyte (00H) ;                 (* MOV BH,00h *)
@@ -3674,6 +3685,7 @@ VAR arg : ERes ;
     nArgs, ent, which, acls : CARDINAL ;
     reading : BOOLEAN ;
     pushed : BOOLEAN ;
+    skipArg : BOOLEAN ;
     n : CARDINAL ;
     dummy : ERes ;
 BEGIN
@@ -3690,6 +3702,13 @@ BEGIN
       EmCrtSeq (CrtClr) ;
       RETURN
    END ;
+   IF which = BI_ClrEol THEN
+      (* clear from the cursor to the end of the line - a real INT 10h call,
+         not a no-op: AH=03 reads the cursor, CX := it, DL := 79, AH=06 blanks
+         that window. *)
+      EmCrtSeq (CrtEol) ;
+      RETURN
+   END ;
    IF which = BI_GotoXY THEN
       (* GotoXY (x, y): 1-based; evaluate x first and park it, then y, so the
          code for y cannot clobber DL. *)
@@ -3715,7 +3734,7 @@ BEGIN
       RETURN
    END ;
    IF (which = BI_LowVideo) OR (which = BI_NormVideo) OR
-      (which = BI_ClrEol) OR (which = BI_Delay) THEN
+      (which = BI_Delay) THEN
       (* Catalogued, and LOUD.  Their screen behaviour is a later question
          (CALC-COMPILE.md Stage 14: a compiling CALC needs them to resolve, a
          RUNNING one needs them right), but a silent no-op would be exactly the
@@ -3757,6 +3776,26 @@ BEGIN
                the whole list first and emitting afterwards printed the first
                argument as the last argument's value, and pushed an argument
                that an earlier inline string's call had already overwritten. *)
+            skipArg := FALSE ;
+            IF (nArgs = 0) AND ((arg.cls = TText) OR (arg.cls = TFile)) THEN
+               (* A leading FILE argument: `Read(Kbd, Ch)'.  KBD is the console
+                  keyboard and this runtime's read entries already read the
+                  console, so the file is dropped and the rest of the argument
+                  list is read exactly as without it.  A real file is not
+                  implemented yet - loud, rather than silently reading the file
+                  variable as a data argument. *)
+               IF reading AND (arg.kind = 1) AND symtab [arg.idx].isKbd THEN
+                  IF NOT MatchDelim (',') THEN
+                     Err (ENoSemi) ;
+                     RETURN
+                  END ;
+                  skipArg := TRUE
+               ELSE
+                  Err (ENoLib) ;
+                  RETURN
+               END
+            END ;
+            IF NOT skipArg THEN
             pushed := TRUE ;      (* default: value is on the stack -> call + pop *)
             IF reading THEN
                IF (arg.kind # 1) AND (arg.kind # 5) THEN
@@ -3888,9 +3927,12 @@ BEGIN
                DropC (EmCall (ent)) ;
                EmAddSp (2)                         (* one 16-bit argument *)
             END ;
-            INC (nArgs) ;
-            IF NOT MatchDelim (',') THEN
-               EXIT
+               INC (nArgs)
+            END ;
+            IF NOT skipArg THEN
+               IF NOT MatchDelim (',') THEN
+                  EXIT
+               END
             END
          END ;
          IF NOT MatchDelim (')') THEN
@@ -5213,7 +5255,12 @@ BEGIN
    i := NewSym ("CLREOL" , KBuiltin, BI_ClrEol  , 0, 0, 0, 0, FALSE) ;
    symtab [i].defnd := TRUE ;
    i := NewSym ("DELAY"  , KBuiltin, BI_Delay   , 0, 0, 0, 0, FALSE) ;
-   symtab [i].defnd := TRUE
+   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. *)
+   i := NewSym ("KBD", KVar, TText, FileVarSize, 1, 0, 0, FALSE) ;
+   symtab [i].isKbd := TRUE
 END DefBuiltins ;
 
 PROCEDURE Inittur () ;

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

@@ -25,9 +25,8 @@
 # positions shift — which is exactly why this must be a measured number, not a
 # remembered one.
 #
-# Current rung (2026-10-20): `Read(Kbd, Ch)` in CALC.INC - the CRT keyboard
-# file `Kbd` as a READ argument (`ERR 41').  The rung moved from 10753 (ClrScr)
-# when the CRT group's ClrScr and GotoXY landed as real INT 10h builtins; the
-# other four CRT identifiers are registered but refused loudly until their
-# screen behaviour is implemented.
-CALC.PAS	ERR	41	10927
+# Current rung (2026-10-20): `NormVideo' in CALC.INC - one of the CRT
+# identifiers still refused with ENoLib.  The rung moved from 10927
+# (`Read(Kbd,Ch)') when the leading-file READ argument and ClrEol landed
+# (`Kbd' is the console, and ClrEol is a real INT 10h call).
+CALC.PAS	ERR	102	12083

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

@@ -543,4 +543,11 @@ t68_length	OK	122	25
 #                   calls no-ops), so the same image must print `crt' there and
 #                   under qemu.  code=102 pins the sequences, data=4 the @@T
 #                   temporaries.
-t69_crt	OK	102	4
+t69_crt	OK	119	4
+#   t70_kbd                `Read(Kbd, Ch)' - a READ with a leading FILE
+#                   argument.  Kbd is the console keyboard; the runtime's read
+#                   entries already read the console, so the file is dropped and
+#                   the rest is `Read(Ch)'.  A real file there is a loud
+#                   ENoLib.  .in `Z', .out `Z' + CRLF.  data=5 = ch (1) + the
+#                   @@T temporaries.
+t70_kbd	OK	55	5

+ 1 - 0
shell/tests/fixtures/t69_crt.pas

@@ -25,5 +25,6 @@ begin
    ClrScr;
    GotoXY (1, 1);
    GotoXY (40, 12);
+   ClrEol;
    writeln ('crt')
 end.

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

@@ -0,0 +1 @@
+Z

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

@@ -0,0 +1 @@
+Z

+ 20 - 0
shell/tests/fixtures/t70_kbd.pas

@@ -0,0 +1,20 @@
+program t70_kbd;
+
+{ t70 -- Read(Kbd, Ch): a READ with a leading FILE argument.  CALC.INC is full
+  of `Read(Kbd, Ch)' (8 times) and `KeyPressed'.
+
+  `Kbd' is the predefined console keyboard, modelled as a TEXT file.  This
+  runtime's read entries already read the console, so the leading file is
+  DROPPED and the remaining arguments are read exactly as `Read(Ch)' would be.
+  A real file variable in that position is a loud ENoLib, not a silent read
+  into the file variable's own bytes.
+
+  The .in is `Z'; the .out is `Z' + CRLF.  Read (not ReadLn) leaves the newline
+  pending, and the writeln after it adds the CRLF. }
+
+var
+   ch : char;
+begin
+   Read (Kbd, ch);
+   writeln (ch)
+end.

+ 27 - 1
shell/tests/nonvacuity.sh

@@ -1712,7 +1712,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "t69 red when CLRSCR is not registered" \
-            "expected: OK 102 4" tests/run_compile_tests.sh
+            "expected: OK 119 4" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild without the CLRSCR builtin"
         fail=$((fail + 1))
@@ -1723,6 +1723,32 @@ else
 fi
 cp "$SAVED_C" Compiler.mod
 
+# --- KBD is registered ----------------------------------------------------
+# Without the predefined KBD file, `Read(Kbd, Ch)' is an unknown identifier and
+# t70 stops compiling against its expected OK 55 5.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = ('   i := NewSym ("KBD", KVar, TText, FileVarSize, 1, 0, 0, FALSE) ;\n'
+       '   symtab [i].isKbd := TRUE\n')
+assert s.count(old) == 1, 'the KBD 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 "t70 red when KBD is not registered" \
+            "expected: OK 55 5" tests/run_compile_tests.sh
+    else
+        echo "  FAIL: the compiler would not rebuild without the KBD file"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the KBD-registration 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"