Parcourir la source

compiler: LowVideo/NormVideo/Delay, and the repeat-body parser bug

LowVideo and NormVideo set a one-byte text attribute that ClrScr and
ClrEol load at run time.  The console writer is INT 21h teletype, which
has no attribute channel, but the clear operations do, so the setting
is observable rather than a silent no-op.  The byte is allocated for
every program and initialised to 7 by the prologue (initmem zeroes the
data area first).  Delay(n) is TP3's calibrated busy LOOP (delaycnt 110
per millisecond), with no INT, so it behaves identically under qemu and
Exec86.

Behind those sat a real parser bug: a REPEAT body was parsed as ONE
statement, then UNTIL was demanded, so

    repeat a; b until c

was ENoSemi at the first ';'.  t12_repeat's two bodies are single
statements and never saw it; CALC.INC's Flash is
GotoXY; Blink := not Blink; if ...; Write; Delay.  The body now parses
a statement sequence, stopping at UNTIL, exactly as Compound stops at
END.  t71_repeat_seq is the red-before/green-after fixture.

Exec86 gains opcode C6 (MOV r/m8,imm8) and C7: the prologue uses it,
and without it t71 died with "unknown opcode 0x00C6" while qemu ran it
fine.  The CRT sequence also uses C6 for the attribute stores.

Every fixture grows by 5 bytes of prologue and one data byte, so the
compile expectations, the ladder and nonvacuity's size anchors move
with it; the deltas were asserted to be exactly (+5,+1) before the
update.  The ladder moves 12083 -> 12672, now KeyPressed.
Eric Streit il y a 2 jours
Parent
commit
89c6ca68d4

+ 17 - 7
CALC-COMPILE.md

@@ -97,29 +97,29 @@ Current rung (measured today, normalised position):
 
 | position | blocker |
 |---|---|
-| **12083** | `NormVideo` in `CALC.INC` — a CRT identifier still refused (`ERR 102`) |
+| **12672** | `KeyPressed` in `CALC.INC`'s `Flash` — a boolean FUNCTION (`ERR 41`) |
 
 Rungs behind it (now passed):
 
 | was | blocker | done by |
 |---|---|---|
 | 7791 | `FXMax : Char = 'G'` — scalar typed constant | Stage 4 (scalar) |
-| 7791 | `EofLine = ^M` — caret character constant | Stage 3 |
 | 7972 | `Numbers : set of Char = ['0'..'9']` — set typed constant | sets, increment 1 |
-| 8098 | `Attributes = (Constant,…)` — enum; and `SetOfAttri = Set of Attributes` | Stage 5 (enums) |
+| 8098 | `Attributes = (Constant,…)` — enum | Stage 5 (enums) |
 | 8362 | `CellRec = record … end` — record type | Stage 7 (record types) |
 | 8705 | `Cells = array['A'..'G',1..FYMax]`, `Sheet : Cells`, `XPOS` | Stage 8 (declarations) |
-| 9202 | `MCFile : file of CellRec` — file type, whole `var` block, `{$I}` expansion | Stage 13 (file types) |
+| 9202 | `MCFile : file of CellRec` — file type; the whole `var` block; `{$I}` | 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) |
 | 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 |
 
 Rungs ahead:
 
 | position | blocker |
 |---|---|
-| 12083 | `NormVideo`/`LowVideo`/`Delay`, `KeyPressed`, then the rest of `CALC.INC` (file operations, indexing, `with`, pointers, `var` params, reals, string builtins, recursion) |
+| 12672 | `KeyPressed`, then the rest of `CALC.INC` |
 
 Corrections the measurements force, worth stating plainly because the stage list
 below was written before them:
@@ -482,8 +482,18 @@ Exec86):
   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`.
+- **`NormVideo`/`LowVideo`/`Delay` are landed, and a parser bug behind them was
+  fixed.**  LowVideo/NormVideo set a one-byte attribute that `ClrScr`/`ClrEol`
+  load at run time (the console writer is INT 21h teletype and has no attribute
+  channel; the clears do, so the setting is observable, not a no-op).  `Delay`
+  is TP3's calibrated busy `LOOP`.  Along the way: a **`repeat` body was one
+  statement, not a sequence** — `repeat a; b until c` was ENoSemi at the first
+  `;`; `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.
 - **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

+ 8 - 7
SUMMARY.md

@@ -2037,16 +2037,17 @@ 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 **`Read(Kbd, Ch)` and `ClrEol`** are landed: the ladder
+features through **the CRT attribute/Delay group** are landed: the ladder
 (`tests/calc_ladder.sh` + `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`;
-CALC.PAS now stops at `ERR 102` at pos 12083, `NormVideo`), the `{$I}` include,
+CALC.PAS now stops at `ERR 41` at pos 12672, `KeyPressed`), 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.
+`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
+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)`

+ 84 - 12
shell/Compiler.mod

@@ -122,6 +122,7 @@ CONST
    BI_NormVideo = 9 ; BI_ClrEol = 10 ; BI_Delay = 11 ;
    (* the sub-codes EmCrtSeq takes *)
    CrtClr = 0 ; CrtDl = 1 ; CrtDh = 2 ; CrtCaret = 3 ; CrtEol = 4 ;
+   CrtLow = 5 ; CrtNorm = 6 ; CrtDelay = 7 ;
 
    (* keyword tokens *)
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
@@ -384,6 +385,8 @@ VAR
    dirs     : DirRec ;
 
    tmpA, tmpB : CARDINAL ;     (* global scratch word addresses *)
+   crtAttrOff : CARDINAL ;     (* image offset of the text attribute byte that
+                                  LowVideo/NormVideo set and ClrScr/ClrEol use *)
 
    hdrFlag, hdrCS, hdrDS, hdrHeap, hdrMax : CARDINAL ;
 
@@ -3640,7 +3643,7 @@ PROCEDURE EmCrtSeq (which : CARDINAL) ;
 BEGIN
    IF which = CrtClr THEN
       Ebyte (0B8H) ; Ebyte (00H) ; Ebyte (06H) ;   (* MOV AX,0600h *)
-      Ebyte (0B7H) ; Ebyte (07H) ;                 (* MOV BH,07h   *)
+      Ebyte (8AH) ; Ebyte (3EH) ; Eword (crtAttrOff) ; (* MOV BH,[crtAttr] *)
       Ebyte (0B9H) ; Ebyte (00H) ; Ebyte (00H) ;   (* MOV CX,0000h *)
       Ebyte (0BAH) ; Ebyte (4FH) ; Ebyte (18H) ;   (* MOV DX,184Fh *)
       Ebyte (0CDH) ; Ebyte (10H) ;                 (* INT 10h      *)
@@ -3661,8 +3664,29 @@ BEGIN
       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 (8AH) ; Ebyte (3EH) ; Eword (crtAttrOff) ; (* MOV BH,[crtAttr] *)
       Ebyte (0CDH) ; Ebyte (10H)                  (* INT 10h                 *)
+   ELSIF which = CrtLow THEN
+      (* [crtAttr] := 7: the dim attribute ClrScr/ClrEol use from now on.  The
+         runtime's console writer is INT 21h teletype, which has no attribute
+         channel, but the clear operations do - so this is where the setting is
+         observable, not a no-op. *)
+      Ebyte (0C6H) ; Ebyte (06H) ; Eword (crtAttrOff) ; Ebyte (07H)
+   ELSIF which = CrtNorm THEN
+      Ebyte (0C6H) ; Ebyte (06H) ; Eword (crtAttrOff) ; Ebyte (0FH)
+   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
+         nothing. *)
+      (* OR AX,AX, not CMP AX,0: check_8086 treats every `CMP AX,0000' in the
+         program region as one of the IF/WHILE/REPEAT tests and demands
+         Jcc+3;EJMP after it.  This is neither. *)
+      Ebyte (0BH) ; Ebyte (0C0H) ;                (* OR AX,AX            *)
+      Ebyte (74H) ; Ebyte (08H) ;                 (* JZ past the loop    *)
+      Ebyte (0B9H) ; Ebyte (6EH) ; Ebyte (00H) ;  (* MOV CX,110 (1 ms)   *)
+      Ebyte (0E2H) ; Ebyte (0FEH) ;               (* LOOP -> itself      *)
+      Ebyte (48H) ;                               (* DEC AX              *)
+      Ebyte (75H) ; Ebyte (0F8H)                  (* JNZ -> MOV CX       *)
    ELSE
       Ebyte (0B4H) ; Ebyte (02H) ;                 (* MOV AH,02h *)
       Ebyte (0B7H) ; Ebyte (00H) ;                 (* MOV BH,00h *)
@@ -3733,13 +3757,28 @@ BEGIN
       END ;
       RETURN
    END ;
-   IF (which = BI_LowVideo) OR (which = BI_NormVideo) 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) ;
+   IF which = BI_LowVideo THEN
+      (* set the attribute the CLEAR operations use to 7; see EmCrtSeq *)
+      EmCrtSeq (CrtLow) ;
+      RETURN
+   END ;
+   IF which = BI_NormVideo THEN
+      EmCrtSeq (CrtNorm) ;
+      RETURN
+   END ;
+   IF which = BI_Delay THEN
+      (* Delay(ms): a real busy loop (EmCrtSeq's CrtDelay), no INT, so it is
+         identical under qemu and Exec86. *)
+      IF MatchDelim ('(') THEN
+         ParseExpr (dummy) ;
+         IF OK () THEN
+            LoadAtom (dummy) ;
+            EmCrtSeq (CrtDelay)
+         END ;
+         ExpectDelim (')', ENoSemi)
+      ELSE
+         Err (ENoSemi)
+      END ;
       RETURN
    END ;
    reading := (which = BI_Read) OR (which = BI_ReadLn) ;
@@ -4035,9 +4074,31 @@ BEGIN
       brkSave [brkN] := exitCnt ;
       loopTy [brkN] := 1 ;
       INC (brkN) ;
-      Statmnt () ;
-      IF NOT (MatchKey (tok) AND (tok = TkUntil)) THEN
-         Err (ENoSemi)
+      (* The body is a STATEMENT SEQUENCE, not one statement: `repeat a; b
+         until c'.  This used to call Statmnt once and then demand UNTIL, so
+         any body with a second statement was ENoSemi at the first ';'.  t12's
+         two bodies are single statements and never saw it; CALC's `Flash' does
+         (GotoXY; Blink := not Blink; if ...; Write; Delay).  Same shape as
+         Compound, stopping at UNTIL instead of END. *)
+      LOOP
+         PeekKw (tok) ;
+         IF tok = TkUntil THEN
+            DropB (MatchKey (tok)) ;
+            EXIT
+         END ;
+         Statmnt () ;
+         IF NOT OK () THEN
+            RETURN
+         END ;
+         IF NOT MatchDelim (';') THEN
+            PeekKw (tok) ;
+            IF tok = TkUntil THEN
+               DropB (MatchKey (tok)) ;
+               EXIT
+            END ;
+            Err (ENoSemi) ;
+            RETURN
+         END
       END ;
       ParseExpr (t) ;
       LoadAtom (t) ;
@@ -5360,6 +5421,11 @@ BEGIN
    dc := dc + 2 ;
    tmpB := NewSym ("@@T2", KVar, TScalar, 2, 2, dc, 0, FALSE) ;
    dc := dc + 2 ;
+   (* The text attribute LowVideo/NormVideo set (default 7, written by the
+      prologue).  One byte, and its address is baked into the clear sequences. *)
+   crtAttrOff := dc ;
+   DropC (NewSym ("@@ATTR", KVar, TScalar, 1, 1, dc, 0, FALSE)) ;
+   dc := dc + 1 ;
    (* Runtime entry offsets, derived from the blob rather than assumed.  This
       has to happen after RT_Build, since RT_Entry only knows where the code
       landed once the blob is assembled.
@@ -5446,6 +5512,12 @@ BEGIN
       EmMovAxi ((rtSz + LoadBias) MOD 10000H) ;
       DropC (EmCall (TU_InitMem)) ;
       EmMovBpSp () ;
+      (* [@@ATTR] := 7 - the default text attribute, before any LowVideo or
+         NormVideo runs.  The data area is zero-filled, and attribute 0 would
+         make every clear black-on-black.  Through EmCrtSeq, not raw Ebyte:
+         Compile is not a one-line emitter, and the helper audit rightly
+         refuses to let a byte-emitting statement hide inside it. *)
+      EmCrtSeq (CrtLow) ;
       IF MatchKey (tok) AND (tok = TkProgram) THEN
          (* MatchKey stops right after "PROGRAM", so the optional program
             name normally follows blanks.  Skip them before testing for the

+ 18 - 0
shell/Exec86.mod

@@ -974,6 +974,24 @@ BEGIN
          RmWr (GetReg (rmReg, w), w)
       END
 
+   ELSIF (op = 0C6H) OR (op = 0C7H) THEN
+      (* MOV r/m,imm.  The compiler emits C6 06 lo hi imm for the CRT
+         attribute byte; C7 is the 16-bit form.  The ModRM reg field is
+         reserved (must be 0) and a register destination is not this opcode
+         (8A/8B or B0+/B8+ are). *)
+      w := op - 0C6H ;
+      DoModRM (w) ;
+      IF eaIsReg THEN
+         Fault ("MOV r/m,imm needs a memory destination")
+      ELSE
+         IF w = 0 THEN
+            v := Fetch8 ()
+         ELSE
+            v := Fetch16 ()
+         END ;
+         RmWr (v, w)
+      END
+
    ELSIF op = 8DH THEN
       DoModRM (1) ;
       IF eaIsReg THEN

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

@@ -25,8 +25,9 @@
 # positions shift — which is exactly why this must be a measured number, not a
 # remembered one.
 #
-# 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
+# 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

+ 71 - 64
shell/tests/fixtures/expected.tsv

@@ -255,47 +255,47 @@
 #                 stores that used to wrap to [BP+0]/[BP+1] and fit the disp8
 #                 form are now disp16 - two bytes, hence 217.  See t40.
 
-t01_minimal	OK	29	4
-t02_writeln	OK	38	4
-t03_inline_comment	OK	38	4
-t04_var	OK	48	6
-t05_own_line_comment	OK	38	4
-t06_two_args	OK	52	4
-t07_big	OK	102	6
-t08_const	OK	76	7
-t09_if	OK	195	6
-t10_while	OK	151	6
-t11_for	OK	144	6
-t12_repeat	OK	137	6
-t13_proc	OK	126	6
-t14_types	OK	41	15
-t15_label	OK	90	6
-t16_str1	OK	42	4
-t17_two_str	OK	45	4
-t18_writeln_bare	OK	32	4
-t19_int1	OK	42	4
-t20_str3	OK	62	4
-t21_mixed	OK	62	4
-t22_case	OK	94	6
-t23_str_empty	OK	36	4
-t24_str_quote	OK	41	4
+t01_minimal	OK	34	5
+t02_writeln	OK	43	5
+t03_inline_comment	OK	43	5
+t04_var	OK	53	7
+t05_own_line_comment	OK	43	5
+t06_two_args	OK	57	5
+t07_big	OK	107	7
+t08_const	OK	81	8
+t09_if	OK	200	7
+t10_while	OK	156	7
+t11_for	OK	149	7
+t12_repeat	OK	142	7
+t13_proc	OK	131	7
+t14_types	OK	46	16
+t15_label	OK	95	7
+t16_str1	OK	47	5
+t17_two_str	OK	50	5
+t18_writeln_bare	OK	37	5
+t19_int1	OK	47	5
+t20_str3	OK	67	5
+t21_mixed	OK	67	5
+t22_case	OK	99	7
+t23_str_empty	OK	41	5
+t24_str_quote	OK	46	5
 t25_str_as_value	ERR	102	48
-t26_str_mixed_args	OK	58	4
-t27_localvar	OK	256	6
-t28_farparam	OK	153	8
-t29_readln	OK	82	7
-t30_forloop	OK	97	8
-t31_procparam	OK	69	6
-t32_forexit	OK	138	8
-t33_cmpops	OK	404	12
-t34_arith	OK	448	8
-t35_not	OK	224	7
-t36_argclobber	OK	492	12
-t37_readbyte	OK	139	7
+t26_str_mixed_args	OK	63	5
+t27_localvar	OK	261	7
+t28_farparam	OK	158	9
+t29_readln	OK	87	8
+t30_forloop	OK	102	9
+t31_procparam	OK	74	7
+t32_forexit	OK	143	9
+t33_cmpops	OK	409	13
+t34_arith	OK	453	9
+t35_not	OK	229	8
+t36_argclobber	OK	497	13
+t37_readbyte	OK	144	8
 t38_readbool	ERR	66	659
-t39_strvar	OK	217	15
-t40_readln	OK	163	15
-t41_strassign	OK	263	51
+t39_strvar	OK	222	16
+t40_readln	OK	168	16
+t41_strassign	OK	268	52
 #   t42_multidecl   multi-name declarations in both spellings: `var a, b, c :
 #                   integer', `procedure add3 (x, y, z : integer)' and
 #                   `procedure mix (u, v : integer ; w : integer)' (TP3's
@@ -303,7 +303,7 @@ t41_strassign	OK	263	51
 #                   older `p : integer, q : integer' form.  data=12 is the four
 #                   2-byte globals a,b,c,d plus the two @@T temporaries; the
 #                   .out proves each name landed on its own slot.
-t42_multidecl	OK	304	12
+t42_multidecl	OK	309	13
 uierror	ERR	41	331
 #   t43_codewindow  a program whose code exceeds the 4 KiB window between the
 #                   end of the runtime and the data base (runtime + 1000h).  It
@@ -323,13 +323,13 @@ t43_codewindow	ERR	98	4978
 #                   string[10] (11 bytes), the char c (1) and two @@T temps.
 #                   The fix also had to drop ParseAtom's `r.boff := 0` after
 #                   ParseVar, which had been discarding every subscript.
-t44_strindex	OK	258	16
+t44_strindex	OK	263	17
 #   t45_strindex_read  readln (s[i]) reads one character into position i; the
 #                   reader classifies by arg.cls (TChar for an indexed string)
 #                   and pushes off+boff.  data=15 is the global string[10]
 #                   (11 bytes) plus two @@T temps.  t45_strindex_read.in feeds
 #                   'X' then 'Y'.
-t45_strindex_read	OK	134	15
+t45_strindex_read	OK	139	16
 #   t46_strindex_var  s[i] with a VARIABLE index, both directions and in both
 #                   registers.  ParseSub used to accept a constant only (ENoLib
 #                   otherwise, exactly as for an array), because off+i is a
@@ -344,14 +344,14 @@ t45_strindex_read	OK	134	15
 #                   destination is saved across the source.  data=31 is g and h
 #                   (string[10] = 11 each), i and j (4), c (1), and the two @@T
 #                   temporaries (4).
-t46_strindex_var	OK	578	31
+t46_strindex_var	OK	583	32
 #   t47_strindex_var_read  readln (g[i]) with a variable i.  The reader needs
 #                   the computed address, not a folded off+boff, so IoCall
 #                   pushes DI (57) instead of LEA AX,[off] in that case.
 #                   data=15 is the global string[8] (9 bytes) plus i (2) and the
 #                   two @@T temporaries (4).  t47_strindex_var_read.in feeds
 #                   'X' into g[3] then 'Y' into g[6].
-t47_strindex_var_read	OK	188	15
+t47_strindex_var_read	OK	193	16
 #   t48_strcmp             string = <> < <= > >=, both operands string
 #                   VARIABLES, i.e. string-vs-string (a string SUBSCRIPT s[i] is
 #                   a CHAR, so s[i] = c would be a scalar compare).  Five
@@ -366,7 +366,7 @@ t47_strindex_var_read	OK	188	15
 #                   min(len)=0 arm.  code=1905 is 5*6 branch compares plus the
 #                   6 value compares of the final writeln; data=24 is a and b
 #                   (string[8] = 9 each), n (2), and the two @@T temporaries (4).
-t48_strcmp	OK	1905	24
+t48_strcmp	OK	1910	25
 #   t49_strcmp_lit         the same operators with a string LITERAL in one or
 #                   both positions.  A literal has no storage and the image
 #                   carries no initialised data, so PushStrAddr emits it inline
@@ -375,7 +375,7 @@ t48_strcmp	OK	1905	24
 #                   operands literal, including '' on both sides; the last line
 #                   takes the empty literal's relation as a VALUE.  data=15 is s
 #                   (string[8] = 9), n (2), and the two @@T temporaries (4).
-t49_strcmp_lit	OK	1006	15
+t49_strcmp_lit	OK	1011	16
 #   t50_strcmp_type        s = 'a' where s is a string: a one-character literal
 #                   is a CHAR constant in ISO Modula-2, not a string, so this
 #                   mixes TString with TScalar and ParseCmp rejects it.  (''
@@ -392,14 +392,14 @@ t50_strcmp_type	ERR	56	600
 #                   scratches (7*256), the three CHAR operands ('!', and ch in
 #                   a+ch and in ch+a) at 2 bytes each, and the two @@T
 #                   temporaries (4).
-t51_strcat	OK	537	1842
+t51_strcat	OK	542	1843
 #   t52_strcat_nest        nested concatenations - left-associative, so `a+b+'!''
 #                   is two occurrences and two scratches - plus a concatenation
 #                   as a comparison operand and a string ELEMENT as the left
 #                   operand of `+'.  data=1574 is a,b (13 each), the six
 #                   scratches (6*256), the four CHAR operands ('!', '<', '>',
 #                   a[2]) at 2 bytes each, and the two @@T temporaries (4).
-t52_strcat_nest	OK	501	1574
+t52_strcat_nest	OK	506	1575
 #   t53_strcat_type        `'a' + 'b'': two CHARs under `+' is a type error.  A
 #                   CHAR is a string operand, so `'a' + b' concatenates; BOTH
 #                   operands being CHARs, with no STRING on either side, is the
@@ -423,7 +423,7 @@ t54_strcat_strnum	ERR	56	463
 #                   `after' a truncating implementation would have printed.
 #                   data=516 is s (the default string = 256), one scratch
 #                   (256), and the two @@T temporaries (4).
-t55_strcat_overflow	OK	126	516
+t55_strcat_overflow	OK	131	517
 #   t56_include            {$I t56_include.inc} - the include is expanded
 #                   before parsing (ExpandIncludes), so FromInclude, which
 #                   exists only in the .inc, is a real symbol here.  The .inc
@@ -433,7 +433,7 @@ t55_strcat_overflow	OK	126	516
 #                   two @@T temporaries); the .out is `7', which is the
 #                   INCLUDED procedure actually running, so this is not just
 #                   "the name resolved".
-t56_include	OK	56	4
+t56_include	OK	61	5
 #   t57_include_missing    {$I t57_this_file_does_not_exist.inc}: an include
 #                   that cannot be opened is TP3 error 90 ("File not found",
 #                   TPSRC10 cdincl errb) AT THE DIRECTIVE - not a silent
@@ -454,7 +454,7 @@ t58_include_nest	ERR	96	43
 #                   also needs RdConst to read TRUE/FALSE, which it now does.
 #                   A structured typed constant (set/array) is still ENoLib -
 #                   it needs initialised storage this compiler lacks.
-t59_typedconst	OK	65	4
+t59_typedconst	OK	70	5
 #   t60_charcaret          `^M' and `#27' - CALC.PAS:113 is `EofLine = ^M'.  Both
 #                   are unquoted CHARACTER constants, so ParseAtom must accept
 #                   them as literals (not only RdConst in a declaration), or
@@ -462,7 +462,7 @@ t59_typedconst	OK	65	4
 #                   bytes 0D 1B, which is the proof they are characters and not
 #                   the text "13"/"27"; the last two lines check the values
 #                   (EofLine = ^M, Esc = 27).
-t60_charcaret	OK	75	4
+t60_charcaret	OK	80	5
 #   t61_setconst           `Numbers : set of Char = ['0'..'9']' - CALC.PAS:114.
 #                   TP3's HYBRID set layout (coeur-tp-ocr 4.7.2): only the whole
 #                   bytes covering the base range are stored, element e is bit
@@ -476,7 +476,7 @@ t60_charcaret	OK	75	4
 #                   TRUE - the constant, an inline literal, and a two-range
 #                   literal.  data=4 is the @@T temporaries; the set's 32 bytes
 #                   are in the code stream, not the data area.
-t61_setconst	OK	183	4
+t61_setconst	OK	188	5
 #   t62_enum               `Attributes = (Constant, Formula, Txt, ...)' -
 #                   CALC.PAS:120.  Each enumerator is a constant holding its
 #                   ordinal position; the TYPE is one byte and carries its
@@ -484,7 +484,7 @@ t61_setconst	OK	183	4
 #                   (vs 32 for `set of Char').  A named constant may now be a
 #                   set element (RdConst resolved identifiers), which is what
 #                   `[Constant, Formula]' needs.  .out TRUE FALSE TRUE FALSE.
-t62_enum	OK	187	5
+t62_enum	OK	192	6
 #   t63_record             `CellRec = record ... end' - CALC.PAS:126.  Fields
 #                   are non-compacted and juxtaposed; CellRec is 1 (set of
 #                   Attributes) + 71 (String[70]) + 6 (Real) + 1 + 1 (two
@@ -492,7 +492,7 @@ t62_enum	OK	187	5
 #                   data=84 = 80 + the two @@T temporaries, so this row pins
 #                   the layout to the byte.  Subrange sizing is part of it.
 #                   Field SELECTION is still a loud error (the next increment).
-t63_record	OK	57	84
+t63_record	OK	62	85
 #   t64_arrays             the 2-D array TYPE and an array TYPED CONSTANT -
 #                   CALC.PAS:133 and :137.  Ranges are row-major, last fastest,
 #                   both kept in an ArrRec the symbol points at.  Sheet is
@@ -502,7 +502,7 @@ t63_record	OK	57	84
 #                   @@T temporaries, with XPOS in the code stream.  Compile
 #                   only: the image is ~16 KB and the exec harness's boot
 #                   sector reads 8 KB (see the layout/execution stage).
-t64_arrays	OK	67	11764
+t64_arrays	OK	72	11765
 #   t65_array_access       array ELEMENT and record FIELD access - the width
 #                   matters.  `v[1] := 11' folds (1 - 1) * 2 into the address
 #                   and writes TWO bytes; `v[3] := v[1] + v[2]' reads two and
@@ -510,7 +510,7 @@ t64_arrays	OK	67	11764
 #                   its width.  .out is 33 then 12, so a wrong scaling, a wrong
 #                   lower-bound subtraction or a wrong store width all show up.
 #                   data=16 = v (8) + r (4) + the two @@T temporaries.
-t65_array_access	OK	101	16
+t65_array_access	OK	106	17
 #   t66_files              `text' and `file of T' - CALC.PAS:144.  A file
 #                   variable is our control block (FileVarSize = 4: DOS handle
 #                   + mode), because this runtime does unbuffered INT 21h I/O
@@ -518,7 +518,7 @@ t65_array_access	OK	101	16
 #                   element type for the record size a Read/Write moves.
 #                   data=12 = two file variables (4 each) + the two @@T
 #                   temporaries.  File OPERATIONS are the next increment.
-t66_files	OK	50	12
+t66_files	OK	55	13
 #   t67_strparam           a STRING VALUE parameter - CALC.INC:17.  TP3 puts a
 #                   value parameter's whole size on the stack, so a string[20]
 #                   parameter is 21 bytes and the caller COPIES <len><chars>
@@ -527,13 +527,13 @@ t66_files	OK	50	12
 #                   caller's t still prints `hello', so it is a copy and not an
 #                   address; a literal argument is copied too.  data=25 = t (21)
 #                   + the @@T temporaries; the slot lives on the stack.
-t67_strparam	OK	189	25
+t67_strparam	OK	194	26
 #   t68_length             LENGTH(s) - the first string builtin FUNCTION.
 #                   CALC.INC:20 uses it in `(80-Length(S)) div 2'.  Inline:
 #                   the length byte at [addr], no runtime entry.  A literal
 #                   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
+t68_length	OK	127	26
 #   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
@@ -541,13 +541,20 @@ t68_length	OK	122	25
 #                   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	119	4
+#                   under qemu.  NormVideo/LowVideo set the attribute the clears
+#                   use (a tracked byte, +1 data); Delay is a busy loop.
+t69_crt	OK	158	5
 #   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
+t70_kbd	OK	60	6
+#   t71_repeat_seq         a REPEAT body with MORE THAN ONE statement.  The
+#                   parser called Statmnt once and then demanded UNTIL, so
+#                   `repeat a; b until c' was ENoSemi at the first ';'.
+#                   t12_repeat's bodies are single statements, so this stayed
+#                   hidden; CALC.INC's Flash does it.  RED before the fix
+#                   (ERR 1), green after; .out is 6.
+t71_repeat_seq	OK	106	7

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

@@ -26,5 +26,9 @@ begin
    GotoXY (1, 1);
    GotoXY (40, 12);
    ClrEol;
+   NormVideo;
+   LowVideo;
+   NormVideo;
+   Delay (1);
    writeln ('crt')
 end.

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

@@ -0,0 +1 @@
+6

+ 21 - 0
shell/tests/fixtures/t71_repeat_seq.pas

@@ -0,0 +1,21 @@
+program t71_repeat_seq;
+
+{ t71 -- a REPEAT body with more than one statement.  This was a parser bug:
+  the body called Statmnt once and then demanded UNTIL, so
+
+      repeat a; b until c
+
+  was ENoSemi at the first ';'.  t12_repeat's two bodies are single
+  statements, so nothing saw it; CALC.INC's Flash does (GotoXY; Blink := not
+  Blink; if ...; Write; Delay).  Red before the fix (ERR 1), green after. }
+
+var
+   i : integer;
+begin
+   i := 0;
+   repeat
+      i := i + 1;
+      i := i + 1
+   until i >= 6;
+   writeln (i)
+end.

+ 8 - 8
shell/tests/nonvacuity.sh

@@ -1331,7 +1331,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "the compile matrix catches a local string packed over the frame" \
-            "expected: OK 217" tests/run_compile_tests.sh
+            "expected: OK 222" tests/run_compile_tests.sh
         expect_red "t40 red when readln into a local string overwrites its return address" \
             "never halted" python3 tests/run_com_exec.py t40_readln
     else
@@ -1443,7 +1443,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "t56 red when the include is swallowed, not expanded" \
-            "expected: OK 56 4" tests/run_compile_tests.sh
+            "expected: OK 61 5" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild with the include forced off"
         fail=$((fail + 1))
@@ -1498,7 +1498,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "t60 red when ^M / #nn are not character literals" \
-            "expected: OK 75 4" tests/run_compile_tests.sh
+            "expected: OK 80 5" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild with the caret literal test removed"
         fail=$((fail + 1))
@@ -1579,7 +1579,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "t63 red when a small subrange is sized two bytes" \
-            "expected: OK 57 84" tests/run_compile_tests.sh
+            "expected: OK 62 85" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild with subranges forced to two bytes"
         fail=$((fail + 1))
@@ -1632,7 +1632,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "t66 red when the file variable is the wrong size" \
-            "expected: OK 50 12" tests/run_compile_tests.sh
+            "expected: OK 55 13" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild with the file size changed"
         fail=$((fail + 1))
@@ -1687,7 +1687,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "t68 red when LENGTH is not registered" \
-            "expected: OK 122 25" tests/run_compile_tests.sh
+            "expected: OK 127 26" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild without the LENGTH builtin"
         fail=$((fail + 1))
@@ -1712,7 +1712,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "t69 red when CLRSCR is not registered" \
-            "expected: OK 119 4" tests/run_compile_tests.sh
+            "expected: OK 158 5" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild without the CLRSCR builtin"
         fail=$((fail + 1))
@@ -1738,7 +1738,7 @@ PYX
 then
     if rebuild_compiler; then
         expect_red "t70 red when KBD is not registered" \
-            "expected: OK 55 5" tests/run_compile_tests.sh
+            "expected: OK 60 6" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild without the KBD file"
         fail=$((fail + 1))