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 |
 |---|---|
-| **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):
 
@@ -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) |
 | 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) |
 
 Rungs ahead:
 
 | 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
 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 |
 | heap | `new` 1, `dispose` 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) |
 
 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
   from the string's length byte (no runtime entry).  Fixture `t68_length`
   (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
   nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the
   `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
   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 **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).
 - **`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

+ 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,
 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 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`;
-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
-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
    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 *)
    BI_Write   = 0 ;  BI_WriteLn = 1 ;  BI_Read  = 2 ;
    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 *)
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
@@ -979,6 +986,11 @@ BEGIN
    Ebyte (5EH)                                   (* POP SI *)
 END EmPopSi ;
 
+PROCEDURE EmPopAx () ;
+BEGIN
+   Ebyte (58H)                                   (* POP AX *)
+END EmPopAx ;
+
 (* 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
    commonest, shape: it pushes the left, loads the right, then pops the
@@ -3613,6 +3625,40 @@ BEGIN
    END
 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) ;
 (* WRITE / WRITELN / READ / READLN / HALT.
 
@@ -3640,6 +3686,43 @@ BEGIN
       DropC (EmCall (TU_Halt)) ;
       RETURN
    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) ;
    nArgs := 0 ;
    IF MatchDelim ('(') THEN
@@ -5115,7 +5198,22 @@ BEGIN
    symtab [i].goPos := TU_Halt ;
    i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;
    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 ;
 
 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
    `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
-   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 SYSTEM IMPORT ADR ;
@@ -67,6 +72,7 @@ VAR
    faultIP  : CARDINAL ;
    loadHi   : CARDINAL ;      (* one past the highest address poked *)
    stepCnt  : LONGCARD ;
+   curRow, curCol : CARDINAL ; (* INT 10h's cursor, for AH=02/AH=03 *)
 
    (* filled in by DoModRM *)
    eaAddr   : CARDINAL ;
@@ -549,6 +555,38 @@ BEGIN
 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 *)
 
 PROCEDURE DoInt21 ;
@@ -990,8 +1028,10 @@ BEGIN
       n := Fetch8 () ;
       IF n = 21H THEN
          DoInt21 ()
+      ELSIF n = 10H THEN
+         DoInt10 ()
       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
 
    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.
         "StrBlob": 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,

+ 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
 # 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
 #                   is 5/3/0 (a variable, a literal, the empty string).
 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()
 old = ('   i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;\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)
 open(p, 'w').write(s.replace(old, ''))
 PYX
@@ -1698,6 +1698,31 @@ else
 fi
 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 python3 tests/run_com_exec.py >/dev/null 2>&1; then
         echo "  ok: every executed fixture passes on the restored sources"