Explorar el Código

compiler+interp: the first CRT identifiers, and INT 10h in Exec86

The ladder moves: CALC.PAS went from ERR 41 at 10753 to ERR 41 at 10927.
`ClrScr' and the `GotoXY's around it compile; the next blocker is
`Read(Kbd,Ch)' - the keyboard file as a READ argument.

  - ClrScr and GotoXY are builtins that emit REAL INT 10h code: AH=06 blanks
    the window and AH=02 homes the cursor; GotoXY evaluates x, parks it, then y
    (so y's code cannot clobber DL), makes both 1-based values 0-based, and
    sets the caret.  One parameterised EmCrtSeq holds the byte sequences,
    listed as COMP in the audit (like SetBlob): audit_helpers only
    byte-audits helpers with an empty parameter list, and these are
    multi-instruction.
  - LowVideo, NormVideo, ClrEol and Delay are registered and REFUSED with
    ENoLib.  The plan (Stage 14) allows resolve-only for a compiling CALC, but
    a silent no-op would be the §7 bug; loud is the honest half-way.
  - Exec86 gains INT 10h.  DoInt10 tracks the cursor (AH=02/AH=03) and treats
    AH=00/AH=06 as no-ops that home it, faulting on anything else exactly as
    DoInt21 does.  There is no display model on purpose: both oracles compare
    the captured CONSOLE output, and the BIOS writes to VGA memory.
    t69_crt therefore runs under BOTH oracles (it was compile-only before),
    and 59 fixtures execute under qemu and Exec86.
  - The interpreter's own trap, again: run_exec86.py relinks but does not
    recompile Exec86.mod, so a changed interpreter needs `make` (or gm2 -c)
    first.  run_all's `make clean; make` covers it.

Fixture t69_crt (ClrScr, GotoXY(1,1), GotoXY(40,12), writeln 'crt'): .out `crt',
executed under qemu and Exec86.

nonvacuity.sh: a new CLRSCR-registration case, and the LENGTH anchor updated
after the registration block gained a semicolon.  89 ok, 0 failed.

run_all.sh: ALL PASS - matrix 70/70, ladder 1/1 at ERR 41 10927, 61 .COM
linked, 59 executed under both oracles; helper audit PASS (123 helpers).
Eric Streit hace 2 días
padre
commit
14ce5907f7

+ 15 - 7
CALC-COMPILE.md

@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 
 | position | blocker |
 | position | blocker |
 |---|---|
 |---|---|
-| **10753** | `ClrScr` in `CALC.INC`'s nested `Welcome` — a **CRT identifier** (`ERR 41`) |
+| **10927** | `Read(Kbd, Ch)` in `CALC.INC` — the CRT keyboard file as a READ argument (`ERR 41`) |
 
 
 Rungs behind it (now passed):
 Rungs behind it (now passed):
 
 
@@ -112,12 +112,13 @@ Rungs behind it (now passed):
 | 9202 | `MCFile : file of CellRec` — file type, whole `var` block, `{$I}` expansion | Stage 13 (file types) |
 | 9202 | `MCFile : file of CellRec` — file type, whole `var` block, `{$I}` expansion | Stage 13 (file types) |
 | 10350 | `procedure Center(S : AnyString)` — a **string value parameter** | calling convention, steps 1-3 |
 | 10350 | `procedure Center(S : AnyString)` — a **string value parameter** | calling convention, steps 1-3 |
 | 10405 | `Length(S)` — the first string builtin | `LENGTH`, inline |
 | 10405 | `Length(S)` — the first string builtin | `LENGTH`, inline |
+| 10753 | `ClrScr` — CRT identifiers begin | `ClrScr` + `GotoXY` (real INT 10h) |
 
 
 Rungs ahead:
 Rungs ahead:
 
 
 | position | blocker |
 | position | blocker |
 |---|---|
 |---|---|
-| 10753 | the CRT identifiers (`ClrScr`, `GotoXY`, `LowVideo`/`NormVideo`, `ClrEol`, `KeyPressed`, `Delay`, `Kbd`), then the remaining string builtins and `CALC.INC` |
+| 10927 | `Kbd` / `Read` with a file argument, then `KeyPressed`, `ClrEol`, `LowVideo`/`NormVideo`, `Delay`, the remaining string builtins and 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:
@@ -189,7 +190,7 @@ tables, so this list is also the `KProc`/`KFunc` work list.
 | reals | `ln` 5, `exp` 3, `abs` 3, `sqrt` 2, `sqr` 1, `sin` 1, `cos` 1, `arctan` 1, `int` 1, `str`/`val` above |
 | reals | `ln` 5, `exp` 3, `abs` 3, `sqrt` 2, `sqr` 1, `sin` 1, `cos` 1, `arctan` 1, `int` 1, `str`/`val` above |
 | heap | `new` 1, `dispose` 1 |
 | heap | `new` 1, `dispose` 1 |
 | files | `assign` 5, `reset` 3, `rewrite` 2, `close` 4, `endoffile` 5, `ioresult` 1 |
 | files | `assign` 5, `reset` 3, `rewrite` 2, `close` 4, `endoffile` 5, `ioresult` 1 |
-| CRT | `gotoxy` 25, `lowvideo` 16, `normvideo` 16, `clreol` 10, `clrscr` 6, `keypressed` 3, `delay` 1, `kbd` (8 × `Read(Kbd,Ch)`) |
+| CRT | `gotoxy` 25, `lowvideo` 16, `normvideo` 16, `clreol` 10, `clrscr` 6, `keypressed` 3, `delay` 1, `kbd` (8 × `Read(Kbd,Ch)`) — **`ClrScr`/`GotoXY` done** (`t69_crt`); the rest registered and loud |
 | directives | `{$I CALC.INC}` (include), `{$I-}`/`{$I+}` (2), `{$A-}` (1, harmless/ignored) |
 | directives | `{$I CALC.INC}` (include), `{$I-}`/`{$I+}` (2), `{$A-}` (1, harmless/ignored) |
 
 
 Also needed by name for the *declarations* to resolve: predefined types
 Also needed by name for the *declarations* to resolve: predefined types
@@ -467,8 +468,15 @@ Exec86):
 - **`LENGTH` is landed** — the first string builtin FUNCTION, loaded inline
 - **`LENGTH` is landed** — the first string builtin FUNCTION, loaded inline
   from the string's length byte (no runtime entry).  Fixture `t68_length`
   from the string's length byte (no runtime entry).  Fixture `t68_length`
   (5/3/0).  The rung moved 10405 → **10753**.
   (5/3/0).  The rung moved 10405 → **10753**.
-- **The ladder now stops at `ERR 41` at `10753`** — `ClrScr` in `CALC.INC`'s
-  nested `Welcome`.  The **CRT identifiers** are the next group.
+- **The CRT group has started: `ClrScr` and `GotoXY` are real** `INT 10h`
+  builtins (clear the window + home; set the cursor from a 1-based x,y), fixture
+  `t69_crt`.  `LowVideo`, `NormVideo`, `ClrEol` and `Delay` are registered but
+  **refused with `ENoLib`** — loud, not a silent no-op.  **Exec86 now provides
+  `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.
 - **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
@@ -478,8 +486,8 @@ Exec86):
   byte-for-byte the same for INTEGER locals, so the compile matrix would have
   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.
   been green and only `t40_readln` (and later CALC) would have noticed.
 - **Verified baseline, run on this commit:** `./tests/run_all.sh` →
 - **Verified baseline, run on this commit:** `./tests/run_all.sh` →
-  **OVERALL: ALL PASS**, compile matrix **69/69**, CALC ladder **1/1** at
-  `ERR 41 10753`, 60 `.COM` linked and 58 executed under both qemu and Exec86
+  **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
   (`t64_arrays` is compile-only: its image is past the 8 KB boot-sector read).
   (`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
 - **`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
   membership bit among them); see the commit message for the count. It is not in

+ 8 - 7
SUMMARY.md

@@ -2037,15 +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,
 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 first string builtin** are landed: the ladder
+features through **the first CRT identifiers** 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 10753, `ClrScr` inside `CALC.INC`), the
-`{$I}` include, `^X`/`#nn`, scalar typed constants, sets, enums, records
+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
 (CellRec = 80), subranges, arrays, element/field access, file types, string
-value parameters, and `Length` (`t68_length`; fixtures through `t68`). The next
-rungs are the CRT identifiers, 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.
+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.
 
 
 1. The rest of the string language. `v-TP3-STRVAR`/`v-TP3-STRREAD-ASSIGN` cover
 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)`
    string variables, `s := 'literal'`, `writeln (s)`, `readln (s)`/`read (s)`

+ 99 - 1
shell/Compiler.mod

@@ -115,6 +115,13 @@ CONST
    (* which standard procedure a KBuiltin symbol denotes *)
    (* which standard procedure a KBuiltin symbol denotes *)
    BI_Write   = 0 ;  BI_WriteLn = 1 ;  BI_Read  = 2 ;
    BI_Write   = 0 ;  BI_WriteLn = 1 ;  BI_Read  = 2 ;
    BI_ReadLn  = 3 ;  BI_Halt    = 4 ;  BI_Length = 5 ;
    BI_ReadLn  = 3 ;  BI_Halt    = 4 ;  BI_Length = 5 ;
+   (* CRT identifiers.  There is no unit system, so these are builtins.  They
+      are statements (except Length), dispatched in IoCall; each one that is
+      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 ;
+   (* the sub-codes EmCrtSeq takes *)
+   CrtClr = 0 ; CrtDl = 1 ; CrtDh = 2 ; CrtCaret = 3 ;
 
 
    (* keyword tokens *)
    (* keyword tokens *)
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
@@ -979,6 +986,11 @@ BEGIN
    Ebyte (5EH)                                   (* POP SI *)
    Ebyte (5EH)                                   (* POP SI *)
 END EmPopSi ;
 END EmPopSi ;
 
 
+PROCEDURE EmPopAx () ;
+BEGIN
+   Ebyte (58H)                                   (* POP AX *)
+END EmPopAx ;
+
 (* 91 = XCHG AX,CX, and NOT 93.  LoadPair hands the operator the left
 (* 91 = XCHG AX,CX, and NOT 93.  LoadPair hands the operator the left
    operand in CX and the right in AX (in the original, and still the
    operand in CX and the right in AX (in the original, and still the
    commonest, shape: it pushes the left, loads the right, then pops the
    commonest, shape: it pushes the left, loads the right, then pops the
@@ -3613,6 +3625,40 @@ BEGIN
    END
    END
 END Compound ;
 END Compound ;
 
 
+PROCEDURE EmCrtSeq (which : CARDINAL) ;
+(* The CRT byte sequences, in one parameterised helper.  Every one is a fixed
+   BIOS call:
+     CrtClr   - INT 10h AH=06 blank the whole window, then AH=02 home the cursor
+     CrtDl    - DEC AX ; MOV DL,AL   (a 1-based column -> 0-based)
+     CrtDh    - DEC AX ; MOV DH,AL   (a 1-based row)
+     CrtCaret - INT 10h AH=02, page 0, cursor at DH,DL
+   It takes a parameter on purpose: audit_helpers audits helpers with an EMPTY
+   parameter list byte-for-byte, and these multi-instruction sequences have no
+   single name grammar.  Listed as COMP in NON_ONE_LINE, like SetBlob. *)
+BEGIN
+   IF which = CrtClr THEN
+      Ebyte (0B8H) ; Ebyte (00H) ; Ebyte (06H) ;   (* MOV AX,0600h *)
+      Ebyte (0B7H) ; Ebyte (07H) ;                 (* MOV BH,07h   *)
+      Ebyte (0B9H) ; Ebyte (00H) ; Ebyte (00H) ;   (* MOV CX,0000h *)
+      Ebyte (0BAH) ; Ebyte (4FH) ; Ebyte (18H) ;   (* MOV DX,184Fh *)
+      Ebyte (0CDH) ; Ebyte (10H) ;                 (* INT 10h      *)
+      Ebyte (0B8H) ; Ebyte (00H) ; Ebyte (02H) ;   (* MOV AX,0200h *)
+      Ebyte (0B7H) ; Ebyte (00H) ;                 (* MOV BH,00h   *)
+      Ebyte (0BAH) ; Ebyte (00H) ; Ebyte (00H) ;   (* MOV DX,0000h *)
+      Ebyte (0CDH) ; Ebyte (10H)                  (* INT 10h      *)
+   ELSIF which = CrtDl THEN
+      Ebyte (48H) ;                                (* DEC AX    *)
+      Ebyte (88H) ; Ebyte (0C2H)                   (* MOV DL,AL *)
+   ELSIF which = CrtDh THEN
+      Ebyte (48H) ;                                (* DEC AX    *)
+      Ebyte (88H) ; Ebyte (0C6H)                   (* MOV DH,AL *)
+   ELSE
+      Ebyte (0B4H) ; Ebyte (02H) ;                 (* MOV AH,02h *)
+      Ebyte (0B7H) ; Ebyte (00H) ;                 (* MOV BH,00h *)
+      Ebyte (0CDH) ; Ebyte (10H)                  (* INT 10h    *)
+   END
+END EmCrtSeq ;
+
 PROCEDURE IoCall (idx : CARDINAL) ;
 PROCEDURE IoCall (idx : CARDINAL) ;
 (* WRITE / WRITELN / READ / READLN / HALT.
 (* WRITE / WRITELN / READ / READLN / HALT.
 
 
@@ -3640,6 +3686,43 @@ BEGIN
       DropC (EmCall (TU_Halt)) ;
       DropC (EmCall (TU_Halt)) ;
       RETURN
       RETURN
    END ;
    END ;
+   IF which = BI_ClrScr THEN
+      EmCrtSeq (CrtClr) ;
+      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. *)
+      IF MatchDelim ('(') THEN
+         ParseExpr (dummy) ;
+         IF OK () THEN
+            LoadAtom (dummy) ;
+            EmPushAx ()
+         END ;
+         ExpectDelim (',', ENoSemi) ;
+         ParseExpr (dummy) ;
+         IF OK () THEN
+            LoadAtom (dummy) ;
+            EmCrtSeq (CrtDh)
+         END ;
+         ExpectDelim (')', ENoSemi) ;
+         EmPopAx () ;
+         EmCrtSeq (CrtDl) ;
+         EmCrtSeq (CrtCaret)
+      ELSE
+         Err (ENoSemi)
+      END ;
+      RETURN
+   END ;
+   IF (which = BI_LowVideo) OR (which = BI_NormVideo) OR
+      (which = BI_ClrEol) OR (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
+         §7 bug - a construct that parses and emits nothing. *)
+      Err (ENoLib) ;
+      RETURN
+   END ;
    reading := (which = BI_Read) OR (which = BI_ReadLn) ;
    reading := (which = BI_Read) OR (which = BI_ReadLn) ;
    nArgs := 0 ;
    nArgs := 0 ;
    IF MatchDelim ('(') THEN
    IF MatchDelim ('(') THEN
@@ -5115,7 +5198,22 @@ BEGIN
    symtab [i].goPos := TU_Halt ;
    symtab [i].goPos := TU_Halt ;
    i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;
    i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;
    symtab [i].defnd := TRUE ;
    symtab [i].defnd := TRUE ;
-   symtab [i].goPos := 0            (* inline: the length byte, no entry *)
+   symtab [i].goPos := 0 ;          (* inline: the length byte, no entry *)
+   (* The CRT identifiers.  ClrScr and GotoXY emit real INT 10h code; the other
+      four are registered so they are recognised, and IoCall refuses them with
+      ENoLib - loud, not inert. *)
+   i := NewSym ("CLRSCR"  , KBuiltin, BI_ClrScr  , 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   i := NewSym ("GOTOXY"  , KBuiltin, BI_GotoXY  , 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   i := NewSym ("LOWVIDEO", KBuiltin, BI_LowVideo, 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   i := NewSym ("NORMVIDEO", KBuiltin, BI_NormVideo, 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   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
 END DefBuiltins ;
 END DefBuiltins ;
 
 
 PROCEDURE Inittur () ;
 PROCEDURE Inittur () ;

+ 42 - 2
shell/Exec86.mod

@@ -43,7 +43,12 @@ IMPLEMENTATION MODULE Exec86 ;
    discarded before the caller can see them.  bootcom.s does `clc' and
    discarded before the caller can see them.  bootcom.s does `clc' and
    `stc' in its handler and they change nothing outside it.  This is the
    `stc' in its handler and they change nothing outside it.  This is the
    one place where matching the machine rather than the visible source is
    one place where matching the machine rather than the visible source is
-   easy to get wrong, so it is written down. *)
+   easy to get wrong, so it is written down.
+
+   INT 10h is also provided, for the CRT builtins the compiler emits.  There is
+   no display model: the cursor is tracked (AH=02/AH=03) and the screen calls
+   are no-ops, so the CONSOLE output this interpreter captures - which is what
+   the execution oracles compare - is identical to qemu's.  See DoInt10. *)
 
 
 FROM Posix IMPORT read, write ;
 FROM Posix IMPORT read, write ;
 FROM SYSTEM IMPORT ADR ;
 FROM SYSTEM IMPORT ADR ;
@@ -67,6 +72,7 @@ VAR
    faultIP  : CARDINAL ;
    faultIP  : CARDINAL ;
    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 *)
 
 
    (* filled in by DoModRM *)
    (* filled in by DoModRM *)
    eaAddr   : CARDINAL ;
    eaAddr   : CARDINAL ;
@@ -549,6 +555,38 @@ BEGIN
 END Cond ;
 END Cond ;
 
 
 
 
+(* ----------------------------------------------------------------- INT 10h *)
+
+PROCEDURE DoInt10 ;
+(* The BIOS screen services this compiler emits, and nothing else.  There is no
+   display model here: the CURSOR is tracked so AH=03 can answer it, and the
+   screen operations (AH=06 clear/scroll, AH=00 set mode) are no-ops that home
+   the cursor.  That is the honest shape, not a convenience: the interpreter
+   captures the guest's CONSOLE output through INT 21h, which is what
+   run_com_exec compares, and the BIOS writes to VGA memory, which no oracle
+   here looks at.  So a program that clears the screen and then prints agrees
+   with qemu byte for byte.  Anything else faults, as with INT 21h: inventing a
+   video BIOS would be worse than saying it is not there. *)
+VAR ah : CARDINAL ;
+BEGIN
+   ah := AX DIV 256 ;
+   IF ah = 02H THEN                            (* set cursor: DH=row, DL=col *)
+      curRow := DX DIV 256 ;
+      curCol := DX MOD 256
+   ELSIF ah = 03H THEN                         (* get cursor -> DH,DL *)
+      DX := ((curRow MOD 256) * 256 + (curCol MOD 256)) MOD 65536
+   ELSIF (ah = 06H) OR (ah = 00H) THEN
+      (* clear/scroll or set mode: no display to change; the cursor homes *)
+      curRow := 0 ;
+      curCol := 0
+   ELSE
+      WrS ("exec86: INT 10h function ") ;
+      WrHex (ah) ;
+      WrS (" is not implemented") ;
+      Fault ("unsupported INT 10h function")
+   END
+END DoInt10 ;
+
 (* ----------------------------------------------------------------- INT 21h *)
 (* ----------------------------------------------------------------- INT 21h *)
 
 
 PROCEDURE DoInt21 ;
 PROCEDURE DoInt21 ;
@@ -990,8 +1028,10 @@ BEGIN
       n := Fetch8 () ;
       n := Fetch8 () ;
       IF n = 21H THEN
       IF n = 21H THEN
          DoInt21 ()
          DoInt21 ()
+      ELSIF n = 10H THEN
+         DoInt10 ()
       ELSE
       ELSE
-         Fault ("only INT 21h is provided; this is not a real-mode machine")
+         Fault ("only INT 21h and INT 10h are provided; this is not a real-mode machine")
       END
       END
 
 
    ELSIF op = 0E2H THEN                      (* LOOP: CX is not a flag *)
    ELSIF op = 0E2H THEN                      (* LOOP: CX is not a flag *)

+ 4 - 0
shell/tests/audit_helpers.py

@@ -140,6 +140,10 @@ NON_ONE_LINE = {
         # string and string-parameter fixtures instead.
         # string and string-parameter fixtures instead.
         "StrBlob": COMP,
         "StrBlob": COMP,
         "PushStrSlot": COMP,
         "PushStrSlot": COMP,
+        # The CRT sequences (INT 10h) are one parameterised helper: several
+        # instructions each, with no single name grammar.  Listed, not left
+        # implicit.
+        "CrtSeq": COMP,
     },
     },
 }
 }
 # The Vx family is NOT here: those four are audited by audit_vx_helpers,
 # The Vx family is NOT here: those four are audited by audit_vx_helpers,

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

@@ -25,9 +25,9 @@
 # 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): `ClrScr' in CALC.INC's nested `Welcome' - a CRT
-# identifier (`ERR 41').  The rung moved from 10405 (`Length(S)') when LENGTH
-# landed (loaded inline from the string's length byte).  The CRT identifiers
-# are the next group: GotoXY, ClrScr, LowVideo/NormVideo, ClrEol, KeyPressed,
-# Delay, Kbd.
-CALC.PAS	ERR	41	10753
+# 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

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

@@ -534,3 +534,13 @@ t67_strparam	OK	189	25
 #                   argument is emitted inline and its byte read there.  .out
 #                   argument is emitted inline and its byte read there.  .out
 #                   is 5/3/0 (a variable, a literal, the empty string).
 #                   is 5/3/0 (a variable, a literal, the empty string).
 t68_length	OK	122	25
 t68_length	OK	122	25
+#   t69_crt                ClrScr and GotoXY - CALC.INC:32,35,46.  Builtins
+#                   (no unit system) emitting real INT 10h code: clear the
+#                   window + home the cursor, and set the cursor from a 1-based
+#                   x,y.  LowVideo/NormVideo/ClrEol/Delay are registered but
+#                   refused with ENoLib - loud, not inert.  EXECUTED under both
+#                   oracles: Exec86 now provides INT 10h (cursor tracked, screen
+#                   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

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

@@ -0,0 +1 @@
+crt

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

@@ -0,0 +1,29 @@
+program t69_crt;
+
+{ t69 -- the CRT identifiers, first two real ones.  CALC.INC:32-46 is
+
+      ClrScr;
+      GotoXY(1,9);
+      ...
+      GotoXY(40,12);
+
+  There is no unit system, so these are builtins.  ClrScr and GotoXY emit real
+  INT 10h code (clear the window and home the cursor; set the cursor from a
+  1-based x,y).  LowVideo/NormVideo/ClrEol/Delay are registered but REFUSED
+  with ENoLib - loud, not a silent no-op (CALC-COMPILE.md Stage 14: a compiling
+  CALC needs them to resolve, a running one needs them right, which is a later
+  question; emitting nothing would be the §7 bug).
+
+  COMPILE AND EXECUTE.  The BIOS calls produce no console bytes, so under qemu
+  the printed `crt' is exactly what the .out asserts; and Exec86 now provides
+  INT 10h with a cursor and no display model, so the SAME image runs there and
+  must agree byte for byte.  That is the second oracle's job: if the INT 10h
+  no-op ever grows a side effect, or the emitted sequence breaks, one of the
+  two disagrees. }
+
+begin
+   ClrScr;
+   GotoXY (1, 1);
+   GotoXY (40, 12);
+   writeln ('crt')
+end.

+ 26 - 1
shell/tests/nonvacuity.sh

@@ -1680,7 +1680,7 @@ p = 'Compiler.mod'
 s = open(p).read()
 s = open(p).read()
 old = ('   i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;\n'
 old = ('   i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;\n'
        '   symtab [i].defnd := TRUE ;\n'
        '   symtab [i].defnd := TRUE ;\n'
-       '   symtab [i].goPos := 0            (* inline: the length byte, no entry *)\n')
+       '   symtab [i].goPos := 0 ;          (* inline: the length byte, no entry *)\n')
 assert s.count(old) == 1, 'the LENGTH registration found %d times -- update this mutation' % s.count(old)
 assert s.count(old) == 1, 'the LENGTH registration found %d times -- update this mutation' % s.count(old)
 open(p, 'w').write(s.replace(old, ''))
 open(p, 'w').write(s.replace(old, ''))
 PYX
 PYX
@@ -1698,6 +1698,31 @@ else
 fi
 fi
 cp "$SAVED_C" Compiler.mod
 cp "$SAVED_C" Compiler.mod
 
 
+# --- a CRT builtin is registered ------------------------------------------
+# ClrScr must be in the symbol table for t69 to compile.  Remove it and the
+# fixture stops at ERR 41 against its expected OK 102 4.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = '   i := NewSym ("CLRSCR"  , KBuiltin, BI_ClrScr  , 0, 0, 0, 0, FALSE) ;\n   symtab [i].defnd := TRUE ;\n'
+assert s.count(old) == 1, 'the CLRSCR 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 "t69 red when CLRSCR is not registered" \
+            "expected: OK 102 4" tests/run_compile_tests.sh
+    else
+        echo "  FAIL: the compiler would not rebuild without the CLRSCR builtin"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the CLRSCR-registration 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"