Przeglądaj źródła

Compiler: wire the runtime in, so a program can finally call it

The runtime blob existed and the TU_* offsets were a hand-written placeholder
ladder, because the compiler did not copy the blob into its code buffer and so
could not call it.  This does that, and the placeholders are gone: the entries
now come from Runtime.RT_Entry, which derives them from where the code actually
lands in the assembled blob.

Layout, as in TPSRC7 copyrt (REPZ MOVSB with SI=DI=0, then pc past the end):

    [runtime][program header][program code]

The runtime goes at offset 0, so every address the compiler emits is already
image-absolute: data symbol offsets, TU_* call targets and rel16 displacements
all need no relocation pass.  pc := RT_Size, dc := RT_Size + 1000H - a fixed
4 KiB above the code so data cannot collide with code in one 64 KiB segment.
LIMITATION, now documented at the code: a program whose code exceeds 4 KiB
overruns its own data area.  TP3 had overlay segments for this; we do not.

Runtime: EmitInitMem read the data base and end at header +2 and +6, but our
header puts them at +4 and +8 (+2 is hdrCS).  So it would have zeroed the
4 KiB gap between code and data and left every global untouched.  Fixed by
adding MovDxSi4 / MovCxSi8 - same instruction length, so no entry offset moves
and the blob is still 385 bytes with the same 14 entries.  Verified: only the
two disp8 bytes changed, 8B 54 02 / 8B 4C 06 -> 8B 54 04 / 8B 4C 08.

Compiler.def gains ImageBytes, ImageByteAt and DataBase, so a test can look at
the image a .COM would actually contain.  CodeBytes stays program-only, which
is what the fixture table has always meant.

expected.tsv re-baselined, +3 code and -256 data on every OK row, both
explained in the file's header:

  +3   the prologue now emits MOV AX,<header offset> before CALL TU_InitMem,
       because InitMem takes the header offset in a REGISTER.  It used to call
       offset 8, which in this layout is the hdrMax word - coherent only
       because nothing was there.

  -256 DataBytes() returned dc, the ABSOLUTE end of the data area, not a size.
       dc started at 100H, so every program reported 256 bytes it never
       allocated: 260 for a program whose only data is the two @@T scratch
       words.  The field is documented as "emitted data size", so 4 is right
       and 260 was the bug.  The 6-byte rows are the fixtures declaring one
       global, which is the independent check that this is a real correction
       and not a fit-to-the-test.

Two bugs found by running, both of which would have been silent:

- My own guard was wrong in a way I had commented on and then written anyway:
  IF (TU_InitMem = 0) OR ... is always true, because initmem legitimately sits
  at offset 0.  Every fixture came back ERR 98 at pos 0.  Caught because the
  matrix asserts; the check now tests wrtinl/rdln/wrint, which can never be 0.

- ALL FOUR header words were written with a NIBBLE shift, not a byte shift:
  (w DIV 16) MOD 100H instead of w MOD 100H, so the data base 1181h was stored
  as 0118h = 280.  It was invisible for as long as nothing read them - which is
  the whole content of "write it inline once and trust it" - and InitMem now
  reads them, so it would have cleared nothing while appearing to work.  Replaced
  with a PatchWord helper, because this was the second time these words were
  written by hand and the second time it was wrong.

CompileTest gains @image: the layout line plus the image hex, so the claims
above are checkable by reading a dump rather than by trusting this message.
Decoding t26 by hand now shows MOV AX,181h / CALL 0 (initmem) and calls
targeting 24h (wrint=36), 8Eh (wrtinl=142), 86h (wrln=134), 1Ch (progend=28)
- the real entry offsets, not the placeholders - with the header reading
flag=1, code end=01BB, data base=1181, data end=1185.

Build: Runtime added to the Makefile, build_tpshell.sh and the compile-test
script.  run_compile_tests.sh also no longer assumes Posix.o is present - it
compiles the C module when missing, so it survives a `make clean` (it used to
die with "cannot find Posix.o").

Verified: make clean && make rc=0, tpshell 158472 bytes, compile matrix 27/27
exit 0, uitest 10/10.

Still not executable: no linker, no .COM writer, CmdRun still a stub.  The next
step is both, and then an 8086 executor to run the result in.
Eric Streit 2 tygodni temu
rodzic
commit
0990ae7cb8

+ 14 - 1
shell/Compiler.def

@@ -23,7 +23,20 @@ PROCEDURE CodeBytes () : CARDINAL ;
 (* emitted code size in bytes *)
 
 PROCEDURE DataBytes () : CARDINAL ;
-(* emitted data size in bytes *)
+(* emitted data size in bytes - the program's own data, not counting the
+   runtime or the fixed 4 KiB gap between code and data *)
+
+PROCEDURE ImageBytes () : CARDINAL ;
+(* total linked image size = runtime + program.  This is what a .COM writer
+   must emit, padded on to DataBase + DataBytes by the linker. *)
+
+PROCEDURE DataBase () : CARDINAL ;
+(* image-absolute offset of the data area, rtSz + 1000H.  The linker places
+   the program's globals here and zero-fills the gap after the code. *)
+
+PROCEDURE ImageByteAt (i : CARDINAL) : BYTE ;
+(* i-th byte of the whole image, runtime included, 0 past the end.  This is
+   what a .COM would actually start with. *)
 
 PROCEDURE CodeByteAt (i : CARDINAL) : BYTE ;
 (* i-th byte of the emitted image (0 past the end), so tests can check the

+ 152 - 37
shell/Compiler.mod

@@ -47,6 +47,11 @@ FROM TextBuf IMPORT Length, CharAt ;
 
 FROM SYSTEM IMPORT BYTE ;
 
+FROM Runtime IMPORT RT_Build, RT_Size, RT_Byte, RT_Entry ;
+(* The runtime is copied to the front of the code buffer and pc/dc start past
+   it, so every emitted address is image-absolute and no relocation pass is
+   needed.  See Inittur. *)
+
 (* ---------------------------------------------------------------- *)
 (*  constants                                                       *)
 (* ---------------------------------------------------------------- *)
@@ -86,28 +91,6 @@ CONST
    TkSet  = 36 ;  TkPacked  = 37 ; TkForward = 38 ; TkExternal = 39 ;
    TkAbsolute = 40 ; TkOverlay = 41 ; TkString = 42 ;
 
-   (* Runtime entry offsets in the emitted image.
-
-      Standard-procedure entries: TP3 does NOT pass a descriptor -
-      TPSRC8 pwriteln/pwrloop inspects each argument's class in CL and emits
-      a *different* call per type, so the type is fixed at compile time and
-      the runtime needs only the value.  Mirrored here.
-
-      These offsets are still PLACEHOLDERS on a fixed ladder.  The real
-      offsets are known - Runtime.RT_Entry derives them from the assembled
-      blob (wrtinl is really at 8EH) - but the compiler does not copy the
-      runtime into its code buffer yet, so it cannot call it, and using the
-      true offsets here would only look like it works.  They all get replaced
-      by RT_Entry in one go when the runtime is wired in. *)
-   TU_InitMem  = 8H ;
-   TU_ProgEnd  = 10H ;
-   TU_StackChk = 18H ;
-   TU_WrInt    = 20H ;  TU_WrChar   = 28H ;  TU_WrBool   = 30H ;
-   TU_WrReal   = 38H ;  TU_WrLn     = 40H ;
-   TU_RdInt    = 48H ;  TU_RdChar   = 50H ;  TU_RdBool   = 58H ;
-   TU_RdLn     = 60H ;  TU_Halt     = 68H ;
-   TU_WrInl    = 70H ;  (* inline string literal; takes NO stack argument *)
-
    (* TP3 error numbers *)
    ENoSemi    = 1 ;  EPointExp  = 10 ;  ESimpType  = 30 ;
    EUnknown   = 41 ; EConstRange = 45 ; EMemOvf    = 98 ;
@@ -214,6 +197,34 @@ VAR
 
    codeSz, dataSz : CARDINAL ;
 
+   (* Image layout, all image-absolute.  rtSz is where the runtime ends and
+      the program header begins; dataBase is where the data area begins
+      (rtSz + 1000H, a fixed 4 KiB above the code).  codeSz and dataSz are
+      PROGRAM sizes - the runtime is excluded - so the numbers the fixture
+      table pins keep meaning what they meant before the runtime was
+      prepended. *)
+   rtSz, dataBase : CARDINAL ;
+
+   (* Runtime entry offsets inside the emitted image, i.e. offsets into the
+      runtime blob, which the linker places at offset 0.  They were a
+      hand-written placeholder ladder until the runtime was wired in; they are
+      now taken from Runtime.RT_Entry, which derives them from where the code
+      actually lands in the assembled blob.  Not a CONST block any more
+      because RT_Entry is a function.
+
+      Standard-procedure entries: TP3 does NOT pass a descriptor -
+      TPSRC8 pwriteln/pwrloop inspects each argument's class in CL and emits
+      a *different* call per type, so the type is fixed at compile time and
+      the runtime needs only the value.  Mirrored here. *)
+   TU_InitMem  : CARDINAL ;
+   TU_ProgEnd  : CARDINAL ;
+   TU_StackChk : CARDINAL ;
+   TU_WrInt    : CARDINAL ;  TU_WrChar : CARDINAL ;  TU_WrBool : CARDINAL ;
+   TU_WrReal   : CARDINAL ;  TU_WrLn   : CARDINAL ;
+   TU_RdInt    : CARDINAL ;  TU_RdChar : CARDINAL ;  TU_RdBool : CARDINAL ;
+   TU_RdLn     : CARDINAL ;  TU_Halt   : CARDINAL ;
+   TU_WrInl    : CARDINAL ;  (* inline string literal; NO stack argument *)
+
    abortFac : BOOLEAN ;
    errNum   : CARDINAL ;        (* NOT "errNo": Compile's formal of that
                                   name would shadow it, and the caller's
@@ -376,6 +387,19 @@ BEGIN
    Ebyte (VAL (BYTE, (w DIV 100H) MOD 100H))
 END Eword ;
 
+PROCEDURE PatchWord (at, w : CARDINAL) ;
+(* Store a 16-bit word into cbuf at an absolute offset.
+
+   A helper, because writing this inline got it wrong in all four header
+   words: the low byte was (w DIV 16) MOD 100H, which is a NIBBLE shift, not
+   the byte shift (w MOD 100H).  So 1181h - the data base - was stored as
+   0118h = 280.  It was invisible for as long as nothing read those words,
+   which is exactly what "write it inline once and trust it" buys you. *)
+BEGIN
+   cbuf [at] := VAL (BYTE, w MOD 100H) ;
+   cbuf [at + 1] := VAL (BYTE, (w DIV 100H) MOD 100H)
+END PatchWord ;
+
 PROCEDURE AddPatch (place, target : CARDINAL) ;
 BEGIN
    IF nPatch < MaxPatch THEN
@@ -2835,14 +2859,46 @@ END DefBuiltins ;
 
 PROCEDURE Inittur () ;
 (* reset compiler state and define the standard types *)
+VAR i, rt : CARDINAL ;
 BEGIN
    abortFac := FALSE ;
    errNum := 0 ;
    txerrPos := 0 ;
    srcPos := 0 ;
    srcLen := Length () ;
-   pc := 0 ;
-   dc := 100H ;
+   (* Copy the runtime to the front of the code buffer and start pc past it,
+      which is what the original does: TPSRC7 "copyrt" runs REPZ MOVSB with
+      SI=DI=0 and then "MOV pc,#$2D7C".  The image is therefore
+
+          [runtime][program header][program code]
+
+      and because the runtime sits at offset 0, every address the compiler
+      emits is already image-absolute - the data symbols' offsets, the TU_*
+      call targets and the rel16 displacements all need no relocation pass.
+      (The base shift would in fact cancel in EmCall's arithmetic, since both
+      sides of a CALL move together; making the offsets absolute just means
+      the linker has nothing to do but copy bytes.)
+
+      dc is put a fixed 4 KiB above the end of the program so that data cannot
+      collide with code in a single 64 KiB .COM segment.  LIMITATION: a
+      program whose code exceeds 4 KiB overruns its own data area.  TP3 had
+      overlay segments for this; we do not, and the check belongs where the
+      limit is documented rather than as a silent truncation. *)
+   RT_Build () ;
+   rt := RT_Size () ;
+   IF rt >= MaxCode THEN
+      Err (EMemOvf) ;                 (* cannot happen: rt is 385 *)
+      RETURN
+   END ;
+   i := 0 ;
+   WHILE i < rt DO
+      cbuf [i] := RT_Byte (i) ;
+      INC (i)
+   END ;
+   pc := rt ;
+   rtSz := rt ;
+   dataBase := rt + 1000H ;
+   dc := dataBase ;
    strTop := 0 ;
    strCnt := 0 ;
    rdStrX := 0 ;
@@ -2874,7 +2930,31 @@ BEGIN
    tmpA := NewSym ("@@T1", KVar, TScalar, 2, 2, dc, 0, FALSE) ;
    dc := dc + 2 ;
    tmpB := NewSym ("@@T2", KVar, TScalar, 2, 2, dc, 0, FALSE) ;
-   dc := dc + 2
+   dc := dc + 2 ;
+   (* 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. *)
+   TU_InitMem  := RT_Entry (0) ;
+   TU_ProgEnd  := RT_Entry (1) ;
+   TU_StackChk := RT_Entry (2) ;
+   TU_WrInt    := RT_Entry (3) ;
+   TU_WrChar   := RT_Entry (4) ;
+   TU_WrBool   := RT_Entry (5) ;
+   TU_WrReal   := RT_Entry (6) ;
+   TU_WrLn     := RT_Entry (7) ;
+   TU_RdInt    := RT_Entry (8) ;
+   TU_RdChar   := RT_Entry (9) ;
+   TU_RdBool   := RT_Entry (10) ;
+   TU_RdLn     := RT_Entry (11) ;
+   TU_Halt     := RT_Entry (12) ;
+   TU_WrInl    := RT_Entry (13) ;      (* inline string literal *)
+   IF (TU_WrInl = 0) OR (TU_RdLn = 0) OR (TU_WrInt = 0) THEN
+      (* RT_Entry returns 0 for an unknown selector.  initmem sits at 0
+         legitimately, so it cannot appear in this test - but wrtinl, rdln
+         and wrint never can, so catching them is enough to catch a runtime
+         that failed to build or a selector that went stale. *)
+      Err (EMemOvf)
+   END
 END Inittur ;
 
 PROCEDURE HeadWord (VAR slot : CARDINAL) ;
@@ -2897,6 +2977,12 @@ BEGIN
       Eword (16) ;                  (* max open files *)
       Eword (0) ;                   (* input buffer word *)
       Eword (0) ;                   (* output buffer word *)
+      (* TU_InitMem takes the header offset in AX, not on the stack, so the
+         AX load has to precede the call.  Previously the prologue called
+         offset 8 - which in this layout is the hdrMax word - and that was
+         coherent only because the runtime was not there.  Now it is the
+         real header. *)
+      EmMovAxi (rtSz) ;
       DropC (EmCall (TU_InitMem)) ;
       EmMovBpSp () ;
       IF MatchKey (tok) AND (tok = TkProgram) THEN
@@ -2929,18 +3015,21 @@ BEGIN
                   EmXorAxAx () ;
                   DropC (EmCall (TU_ProgEnd)) ;
                   ResolvePatches () ;
-                  codeSz := pc ;
-                  dataSz := dc ;
-                  cbuf [hdrCS] := VAL (BYTE, (codeSz DIV 16) MOD 100H) ;
-                  cbuf [hdrCS + 1] := VAL (BYTE, ((codeSz DIV 16) DIV 100H) MOD 100H) ;
-                  cbuf [hdrDS] := VAL (BYTE, (dataSz DIV 16) MOD 100H) ;
-                  cbuf [hdrDS + 1] := VAL (BYTE, ((dataSz DIV 16) DIV 100H) MOD 100H) ;
-                  cbuf [hdrFlag] := 1 ;
-                  cbuf [hdrFlag + 1] := 0 ;
-                  cbuf [hdrHeap] := 0 ;
-                  cbuf [hdrHeap + 1] := 0 ;
-                  cbuf [hdrMax] := 0 ;
-                  cbuf [hdrMax + 1] := 0
+                  (* Program-only sizes.  The runtime is not part of the
+                     program's code, and the fixture table has always meant
+                     "the program's own code", so subtract it here rather
+                     than making every expectation in expected.tsv wrong. *)
+                  codeSz := pc - rtSz ;
+                  dataSz := dc - dataBase ;
+                  (* Header words.  The layout is ours (the original's is
+                     bigger and serves a real overlay loader), but
+                     Runtime.EmitInitMem reads +4 and +8, so hdrDS and
+                     hdrHeap must be the data base and the data end. *)
+                  PatchWord (hdrFlag, 1) ;
+                  PatchWord (hdrCS, pc) ;
+                  PatchWord (hdrDS, dataBase) ;
+                  PatchWord (hdrHeap, dc) ;
+                  PatchWord (hdrMax, 0)
                END
             ELSE
                Err (EUnknown)
@@ -2974,6 +3063,32 @@ BEGIN
    RETURN dataSz
 END DataBytes ;
 
+PROCEDURE ImageBytes () : CARDINAL ;
+(* Total linked image size: rtSz (the runtime) + CodeBytes (the program).
+   The program is NOT padded out to the data base here - the linker does
+   that, and only it knows the .COM's final size. *)
+BEGIN
+   RETURN rtSz + codeSz
+END ImageBytes ;
+
+PROCEDURE DataBase () : CARDINAL ;
+(* image-absolute offset at which the data area begins (rtSz + 1000H).  The
+   linker must place the program's data here and zero-fill from the end of
+   the code up to it. *)
+BEGIN
+   RETURN dataBase
+END DataBase ;
+
+PROCEDURE ImageByteAt (i : CARDINAL) : BYTE ;
+(* i-th byte of the WHOLE image, runtime included, so a test can check the
+   real thing a .COM would contain.  Returns 0 past the end. *)
+BEGIN
+   IF i >= rtSz + codeSz THEN
+      RETURN 0
+   END ;
+   RETURN cbuf [i]
+END ImageByteAt ;
+
 PROCEDURE CodeByteAt (i : CARDINAL) : BYTE ;
 (* i-th byte of the emitted image, for test harnesses that need to check
    the generated 8086 code rather than just its size.  Returns 0 past the

+ 7 - 4
shell/Makefile

@@ -4,9 +4,9 @@ FLAGS = -fiso
 
 all: tpshell
 
-tpshell: Editor.mod Shell.mod Shell.o Term.o Posix.o TextBuf.o Editor.o Compiler.o
-	$(GM2) $(FLAGS) -fgen-module-list=tpshell.lst -o /dev/null Shell.mod Term.o Posix.o TextBuf.o Editor.o Compiler.o || true
-	$(GM2) $(FLAGS) -fuse-list=tpshell.lst -o $@ Shell.mod Term.o Posix.o TextBuf.o Editor.o Compiler.o
+tpshell: Editor.mod Shell.mod Shell.o Term.o Posix.o TextBuf.o Editor.o Compiler.o Runtime.o
+	$(GM2) $(FLAGS) -fgen-module-list=tpshell.lst -o /dev/null Shell.mod Term.o Posix.o TextBuf.o Editor.o Compiler.o Runtime.o || true
+	$(GM2) $(FLAGS) -fuse-list=tpshell.lst -o $@ Shell.mod Term.o Posix.o TextBuf.o Editor.o Compiler.o Runtime.o
 
 Shell.o: Shell.mod Term.def Posix.def Editor.def TextBuf.def
 	$(GM2) $(FLAGS) -c Shell.mod
@@ -23,9 +23,12 @@ Editor.o: Editor.mod Editor.def Term.def TextBuf.def Posix.def
 Posix.o: Posix.c
 	$(CC) -c Posix.c
 
-Compiler.o: Compiler.mod Compiler.def TextBuf.def
+Compiler.o: Compiler.mod Compiler.def TextBuf.def Runtime.def
 	$(GM2) $(FLAGS) -c Compiler.mod
 
+Runtime.o: Runtime.mod Runtime.def
+	$(GM2) $(FLAGS) -c Runtime.mod
+
 clean:
 	rm -f *.o tpshell tpshell.lst
 .PHONY: all clean

+ 16 - 4
shell/Runtime.mod

@@ -238,6 +238,18 @@ PROCEDURE MovAxDi  ; BEGIN B (8BH) ; B (0C7H) END MovAxDi ;   (* 11 000 111 *)
 PROCEDURE MovAxDx  ; BEGIN B (8BH) ; B (0D2H) END MovAxDx ;
 PROCEDURE MovCxSi6 ; BEGIN B (8BH) ; B (4CH) ; B (6) END MovCxSi6 ;
 PROCEDURE MovDxSi2 ; BEGIN B (8BH) ; B (54H) ; B (2) END MovDxSi2 ;
+(* The program header is a block of words laid out by the compiler:
+      +0  hdrFlag   1 = image is valid
+      +2  hdrCS     end of the generated code, in bytes
+      +4  hdrDS     base of the data area     <- initmem wants these two
+      +6  hdrHeap   end of the data area      <-
+      +8  hdrMax    max open files
+   MovDxSi4/MovCxSi8 read the two that matter here.  The obvious +2/+6 would
+   be the code end and the data end, i.e. initmem would zero from the end of
+   the code to the end of the data - a 4 KiB gap of nothing, and the globals
+   themselves untouched.  Same instruction length, so no entry offset moves. *)
+PROCEDURE MovCxSi8 ; BEGIN B (8BH) ; B (4CH) ; B (8) END MovCxSi8 ;
+PROCEDURE MovDxSi4 ; BEGIN B (8BH) ; B (54H) ; B (4) END MovDxSi4 ;
 PROCEDURE MovAxBp4 ; BEGIN B (8BH) ; B (45H) ; B (4) END MovAxBp4 ;
 PROCEDURE MovBxBp4 ; BEGIN B (8BH) ; B (5EH) ; B (4) END MovBxBp4 ;
 PROCEDURE MovAlDh  ; BEGIN B (8AH) ; B (0C0H) END MovAlDh ;
@@ -274,15 +286,15 @@ PROCEDURE Loop8 (nm : ARRAY OF CHAR ) ; BEGIN Jcc (0E2H, nm) END Loop8 ;
 (* ---------------------------------------------------------------- *)
 
 PROCEDURE EmitInitMem ;
-(* AX = offset of the program header.  The header holds, at +2 the base of
-   the program's data area and at +6 its end, so the globals can be zeroed -
+(* AX = offset of the program header.  The header holds, at +4 the base of
+   the program's data area and at +8 its end, so the globals can be zeroed -
    Pascal leaves them undefined, TP3's runtime clears them.  Also makes
    ES = DS so that any string instruction in the library would work. *)
 BEGIN
    M ("initmem") ;
    MovSiAx ;               (* SI = AX = the header offset the caller passed *)
-   MovDxSi2 ;              (* DX = [SI+2] = data base *)
-   MovCxSi6 ;              (* CX = [SI+6] = data end *)
+   MovDxSi4 ;              (* DX = [SI+4] = hdrDS  = data base *)
+   MovCxSi8 ;              (* CX = [SI+8] = hdrHeap = data end   *)
    CmpCxDx ;
    Jbe8 ("im_done") ;
    MovDiDx ;               (* DI = data base *)

+ 4 - 4
shell/build_tpshell.sh

@@ -10,22 +10,22 @@ cd "$D" || exit 9
 FLAGS="-fiso"
 
 echo "== compiling each module (isolated -c) =="
-for m in Shell Term Posix TextBuf Editor Compiler ; do
+for m in Shell Term Posix TextBuf Editor Compiler Runtime ; do
    $GM2 $FLAGS -c $m.mod >/tmp/tp_c_$m 2>&1 \
       || { echo "COMPILE_FAIL $m"; grep -m3 "error:" /tmp/tp_c_$m; exit 1; }
 done
-echo "ok: Shell Term Posix TextBuf Editor Compiler"
+echo "ok: Shell Term Posix TextBuf Editor Compiler Runtime"
 
 echo "== Phase 1: generate module list =="
 rm -f modules.lst
 $GM2 $FLAGS -fgen-module-list=modules.lst -o /dev/null \
-    Shell.mod Term.o Posix.o TextBuf.o Editor.o Compiler.o >/tmp/tp_p1 2>&1
+    Shell.mod Term.o Posix.o TextBuf.o Editor.o Compiler.o Runtime.o >/tmp/tp_p1 2>&1
 echo "p1_rc=$?  list: $(tr '\n' ' ' < modules.lst)"
 
 echo "== Phase 2: link with the list =="
 rm -f tpshell
 $GM2 $FLAGS -fuse-list=modules.lst -o tpshell \
-    Shell.mod Term.o Posix.o TextBuf.o Editor.o Compiler.o >/tmp/tp_p2 2>&1
+    Shell.mod Term.o Posix.o TextBuf.o Editor.o Compiler.o Runtime.o >/tmp/tp_p2 2>&1
 echo "p2_rc=$?"
 grep -cE "error:|undefined" /tmp/tp_p2
 ls -l tpshell 2>/dev/null | awk '{print "tpshell bytes:",$5}'

+ 65 - 12
shell/tests/CompileTest.mod

@@ -15,7 +15,8 @@ MODULE CompileTest ;
 
 FROM Posix IMPORT read, write, open, close ;
 FROM TextBuf IMPORT TextLimit, Clear, Length, CharAt, InsertCh ;
-FROM Compiler IMPORT Compile, CodeBytes, DataBytes, CodeByteAt ;
+FROM Compiler IMPORT Compile, CodeBytes, DataBytes, CodeByteAt,
+                       ImageBytes, ImageByteAt, DataBase ;
 FROM SYSTEM IMPORT ADR, BYTE ;
 
 CONST
@@ -191,7 +192,7 @@ END ShowAt ;
 VAR
    errNo, errPos : CARDINAL ;
    ok : BOOLEAN ;
-   dump : BOOLEAN ;
+   dump, image : BOOLEAN ;
 
 PROCEDURE Hex (b : BYTE ; VAR out : ARRAY OF CHAR) ;
 (* ISO will not index a plain string constant as an array, so compute the
@@ -238,8 +239,9 @@ BEGIN
    PutStr (":  ")
 END PutCardHex4 ;
 
-PROCEDURE DumpCode () ;
-(* hex dump of the emitted image, 16 bytes per line *)
+PROCEDURE DumpCode ;
+(* hex dump of the emitted PROGRAM, 16 bytes per line.  The runtime is not
+   part of the program, so the program starts at 0 and is CodeBytes() long. *)
 VAR i, n : CARDINAL ;
     hx : ARRAY [0..1] OF CHAR ;
     b : BYTE ;
@@ -262,8 +264,55 @@ BEGIN
    END
 END DumpCode ;
 
-PROCEDURE IsDumpCmd () : BOOLEAN ;
-(* the line "@dump" switches the hex dump on for the rest of the run *)
+PROCEDURE DumpImage ;
+(* The whole linked image - [runtime][program header][program code] - which is
+   what a .COM would actually contain.  The runtime's own 385 bytes are shown
+   only at the head and the tail: enough to prove the blob is really in there,
+   without burying the program in 24 lines of library. *)
+VAR i, n, rtSz, total, from, lim : CARDINAL ;
+    hx : ARRAY [0..1] OF CHAR ;
+BEGIN
+   rtSz := ImageBytes () - CodeBytes () ;
+   total := ImageBytes () ;
+   PutStr ("        image=") ;
+   PutCard (total) ;
+   PutStr ("  rtSz=") ;
+   PutCard (rtSz) ;
+   PutStr ("  dataBase=") ;
+   PutCard (DataBase ()) ;
+   PutStr ("  dataEnd=") ;
+   PutCard (DataBase () + DataBytes ()) ;
+   NL ;
+   PutStr ("        rt head " ) ;
+   i := 0 ;
+   WHILE i < 16 DO
+      Hex (ImageByteAt (i), hx) ;
+      PutCh (hx [0]) ; PutCh (hx [1]) ; PutCh (" ") ;
+      INC (i)
+   END ;
+   NL ;
+   i := rtSz - 16 ;
+   WHILE i < total DO
+      from := i ;
+      lim := total ;
+      IF lim > from + 16 THEN
+         lim := from + 16
+      END ;
+      PutStr ("        " ) ;
+      PutCardHex4 (from) ;
+      n := 0 ;
+      WHILE (n < 16) AND (from + n < lim) DO
+         Hex (ImageByteAt (from + n), hx) ;
+         PutCh (hx [0]) ; PutCh (hx [1]) ; PutCh (" ") ;
+         INC (n)
+      END ;
+      NL ;
+      i := i + 16
+   END
+END DumpImage ;
+
+PROCEDURE IsCmd (c1, c2, c3, c4, c5 : CHAR ) : BOOLEAN ;
+(* the line "@word" switches a dump mode on for the rest of the run *)
 VAR i : CARDINAL ;
 BEGIN
    IF lineBuf [0] # "@" THEN
@@ -273,19 +322,21 @@ BEGIN
    WHILE (i <= HIGH (lineBuf)) AND (lineBuf [i] # 0C) DO
       INC (i)
    END ;
-   IF (i # 5) OR (lineBuf [1] # "d") OR (lineBuf [2] # "u") OR
-      (lineBuf [3] # "m") OR (lineBuf [4] # "p") THEN
+   IF i # 6 THEN
       RETURN FALSE
    END ;
-   RETURN TRUE
-END IsDumpCmd ;
+   RETURN (lineBuf [1] = c1) AND (lineBuf [2] = c2) AND (lineBuf [3] = c3)
+      AND (lineBuf [4] = c4) AND (lineBuf [5] = c5)
+END IsCmd ;
 
 BEGIN
    PutStr ("FIXTURE  RESULT") ;
    NL ;
    WHILE ReadLineStr (lineBuf) DO
-      IF IsDumpCmd () THEN
+      IF IsCmd ("d", "u", "m", "p", " ") THEN
          dump := TRUE                    (* "@dump": hex-dump from here on *)
+      ELSIF IsCmd ("i", "m", "a", "g", "e") THEN
+         image := TRUE                   (* "@image": whole image, layout too *)
       ELSE
          StrCopy (pathCopy, lineBuf) ;
          IF LoadFile (pathCopy) THEN
@@ -297,7 +348,9 @@ BEGIN
                PutStr (" data=") ;
                PutCard (DataBytes ()) ;
                NL ;
-               IF dump THEN
+               IF image THEN
+                  DumpImage ()
+               ELSIF dump THEN
                   DumpCode ()
                END
             ELSE

+ 41 - 24
shell/tests/fixtures/expected.tsv

@@ -25,31 +25,48 @@
 # inline-string path (CALL wrtinl, then <length><chars> in the code stream -
 # TPSRC8 pwrinlin / TPSRC4 xwrtinl), so their rows changed from ERR to OK and
 # their code sizes went up by the literal's own bytes.
+#
+# RE-BASELINED when the runtime was wired into the code buffer.  Every OK row
+# changed by exactly +3 code and -256 data, and both numbers are correct:
+#
+#   +3 code  the prologue now emits MOV AX,<header offset> before
+#            CALL TU_InitMem, because InitMem takes the header offset in a
+#            register (B8 lo hi) rather than on the stack.  Previously the
+#            prologue called offset 8, which is the hdrMax word - it only
+#            "worked" because the runtime was not in the image at all.
+#
+#   -256 data  DataBytes() used to return dc, the ABSOLUTE end of the data
+#            area, not a size.  dc started at 100H, so every program reported
+#            256 more than it allocated - 260 for a program whose only data is
+#            the two @@T1/@@T2 scratch words.  The field is documented as
+#            "emitted data size in bytes", so 4 is the true size and 260 was
+#            the bug.  It is a real correction, not a fit-to-the-test change:
+#            the 6-byte rows are the fixtures that declare one global.
 
-t01_minimal	OK	26	260
-t02_writeln	OK	35	260
-t03_inline_comment	OK	35	260
-t04_var	OK	45	262
-t05_own_line_comment	OK	35	260
-t06_two_args	OK	49	260
-t07_big	OK	99	262
-t08_const	OK	32	262
-t09_if	OK	64	262
-t10_while	OK	72	262
-t11_for	OK	66	262
-t12_repeat	OK	69	262
-t13_proc	OK	50	262
+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	35	6
+t09_if	OK	67	6
+t10_while	OK	75	6
+t11_for	OK	69	6
+t12_repeat	OK	72	6
+t13_proc	OK	53	6
 t14_types	ERR	102	83
-t15_label	OK	35	262
-t16_str1	OK	39	260
-t17_two_str	OK	42	260
-t18_writeln_bare	OK	29	260
-t19_int1	OK	39	260
-t20_str3	OK	59	260
-t21_mixed	OK	59	260
-t22_case	OK	93	262
-t23_str_empty	OK	33	260
-t24_str_quote	OK	38	260
+t15_label	OK	38	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	96	6
+t23_str_empty	OK	36	4
+t24_str_quote	OK	41	4
 t25_str_as_value	ERR	102	48
-t26_str_mixed_args	OK	55	260
+t26_str_mixed_args	OK	58	4
 uierror	ERR	41	331

+ 15 - 5
shell/tests/run_compile_tests.sh

@@ -12,12 +12,22 @@ cd "$D" || exit 9
 FLAGS="-fiso"
 
 if [ ! -f TextBuf.o ] || [ ! -f Compiler.o ] || [ ! -f Posix.o ] \
+   || [ ! -f Runtime.o ] \
    || [ TextBuf.mod -nt TextBuf.o ] || [ Compiler.mod -nt Compiler.o ] \
+   || [ Runtime.mod -nt Runtime.o ] || [ Runtime.def -nt Runtime.o ] \
+   || [ Compiler.def -nt Compiler.o ] \
    || [ Posix.c -nt Posix.o ] || [ Posix.def -nt Posix.o ]; then
    echo "building support modules (source newer than object)..."
-   # NB: Posix is a FOR "C" module (Posix.c + Posix.def, no Posix.mod), so it
-   # is not rebuilt here - only TextBuf and Compiler have a .mod to compile.
-   for m in TextBuf Compiler; do
+   # Runtime is listed because Compiler now links against it: the blob is
+   # copied into the code buffer and the TU_* offsets come from it.  A
+   # `make clean` (or a fresh checkout) removes every .o, so the C module has
+   # to be built here too - it used to be assumed present, which made the
+   # script fail with "cannot find Posix.o" after any clean.
+   if [ ! -f Posix.o ] || [ Posix.c -nt Posix.o ] || [ Posix.def -nt Posix.o ]; then
+      cc -c Posix.c >/tmp/ct_c_Posix 2>&1 \
+         || { echo "COMPILE_FAIL Posix"; grep -m5 "error:" /tmp/ct_c_Posix; exit 1; }
+   fi
+   for m in TextBuf Compiler Runtime; do
       $GM2 $FLAGS -c $m.mod >/tmp/ct_c_$m 2>&1
       if [ $? -ne 0 ]; then
          echo "COMPILE_FAIL $m"
@@ -30,10 +40,10 @@ fi
 
 rm -f tests/ct.lst compiletest
 $GM2 $FLAGS -fgen-module-list=tests/ct.lst -o /dev/null \
-    tests/CompileTest.mod TextBuf.o Posix.o Compiler.o >/tmp/ct_p1 2>&1
+    tests/CompileTest.mod TextBuf.o Posix.o Compiler.o Runtime.o >/tmp/ct_p1 2>&1
 p1=$?
 $GM2 $FLAGS -fuse-list=tests/ct.lst -o compiletest \
-    tests/CompileTest.mod TextBuf.o Posix.o Compiler.o >/tmp/ct_p2 2>&1
+    tests/CompileTest.mod TextBuf.o Posix.o Compiler.o Runtime.o >/tmp/ct_p2 2>&1
 p2=$?
 if [ $p2 -ne 0 ]; then
    echo "LINK_FAIL p1_rc=$p1 p2_rc=$p2"

+ 6 - 0
shell/toto.pas

@@ -0,0 +1,6 @@
+program toto;
+
+begin
+ writeln ( 'toto' );
+end.
+ 

+ 6 - 0
shell/toto.pas.BAK

@@ -0,0 +1,6 @@
+program toto;
+
+begin
+ writeln ( "toto" );
+end.
+