Recreating Turbo Pascal 3.0 (shell / editor / compiler / interpreter) in
GNU Modula-2 (gm2 -fiso -Wall). Faithful to the original: screen
layout, keys and behaviour are reconstructed from the disassembled TP3.0
source in Resources/turbopascal3source/TP3/ (TPSRC1-10) and the reference
manual, not guessed.
| Milestone | Tag | State |
|---|---|---|
| TP3.0 main-menu shell | v0.1-shell |
done |
| WordStar-style editor | v0.2-editor |
done |
| Shell/editor polish, Ctrl-K-D / Ctrl-K-X quit | v-TP3-SHELL-EDITOR-QUIT |
done |
| Compiler skeleton + build recipe | v-TP3-SHELL-COMPILES |
done |
Parser Skip bug class (9 sites) |
v-TP3-PARSER-FIXES |
done |
Standard procedures + rel16 fix |
v-TP3-STDPROCS |
done |
| Runtime library + 8086 execution harness | v-TP3-RUNTIME-BLOB |
assembled, never run |
Inline string literals (writeln('hi')) |
v-TP3-STRLITERAL |
done, never run |
Linker + CmdRun |
— | not started |
cd shell && make # → shell/tpshell
Whole-program link must be two-phase; a single gm2 -o pass 3 silently
caps identifier/error counts on a compiler-sized program:
gm2 -fiso -c Compiler.mod # phase A
gm2 -fiso -fgen-module-list=modules.lst -o /dev/null \
Compiler.mod Term.o TextBuf.o Posix.o Editor.o # phase B1 (rc=1 expected)
gm2 -fiso -fuse-module-list=modules.lst -o tpshell \
Shell.mod Compiler.mod Term.o TextBuf.o Posix.o Editor.o # phase B2
Current clean build: make clean && make → rc=0, tpshell 125136 bytes.
The one diagnostic is ./Compiler.mod: ParseExpr: too many errors in pass 3,
which is the expected phase-1 rollup that the recipe tolerates — not a real
error. shell/build_tpshell.sh and shell/Makefile are authoritative.
Nothing here is "it compiles clean" — each claim below comes from a run.
shell/tests/run_compile_tests.shtests/CompileTest.mod links Compiler + TextBuf + Posix, reads fixture
paths from stdin, loads each exactly the way LoadWorkFile does
(LF→CR normalisation, ^Z ends the text), calls Compile, and prints a
verdict plus a source excerpt with a caret at errPos. No pty, instant.
cd shell && tests/run_compile_tests.sh # all fixtures
cd shell && tests/run_compile_tests.sh /some/dir # another fixture set
printf '@dump\ntests/fixtures/t19_int1.pas\n' | ./compiletest # hex-dump the image
The matrix asserts; it does not just count. tests/fixtures/expected.tsv
pins the verdict and the numbers per fixture — verdict plus code size plus
data size, or error number plus position — and the runner compares. A wrong
error position or a program that lost six bytes now fails the suite instead of
needing a squint. It was checked for vacuousness by reverting the string
scanner fix: 19/23 and exit 1, restored: 23/23 and exit 0.
25 of 27 fixtures compile, up from 1 (the empty program) when the direct harness was first built.
compile matrix: 27 passed, 0 failed (of 27)
Compiling: t01 minimal · t04 var+assign+writeln · t06 two args ·
t07 10 assignments · t08 const · t09 if/then/else · t10 while ·
t11 for/to · t12 repeat/until · t13 procedure + value param ·
t15 label + goto · t16 writeln('a') · t18 bare writeln ·
t19 writeln(1) · t20 3 string args · t21 mixed args ·
t22 case with two labels · t23 writeln('') · t24 writeln('don''t') ·
t26 mixed scalar/string args · t02/t03/t05/t17 multi-char literals.
Failing, all deliberately: t14 array [1..5] of integer at its point of use
and t25 a string literal used as a value (s := 'hi') both → ENoLib
(102), the original's "not implemented" path; uierror is a deliberate syntax
error used by the UI test.
shell/tests/uitest.pyDrives a real pty (tests/ptyharness.py supplies read-until-quiet, key
sending and a small VT100 emulator) and asserts the TP3 compile-error
jump: W load → C compile → ESC → editor opens with the cursor on the
error position → Ctrl-K D back to the menu → Q exit 0. 10/10 pass,
and the reported error number is 41 (not 0 — this is what caught the
errNo := errNo self-assignment where Compile's formal shadowed the
module variable).
The jump mirrors original TP3 kcwait + editor2 (TPSRC5:333-336, :919):
BX:=txerrpos; DEC BX; JMP editor2, and editor2 does ADD BX,txbeg;
INC BX — the DEC/INC cancel, so the net is txbeg+txerrpos = our 0-based
errPos. Armed as a sticky position (Editor.GotoOffset) rather than by
changing Run's signature, so Editor.def stays additive.
uitest.py uses a fixture with a deliberate syntax error
(x := 1 + ; → error 41, line 9 col 12) rather than a missing library
feature: the latter move as the compiler grows, and a test whose
expectations drift with it stops being a test.
The whole point of a Pascal→8086 compiler is that the output runs, and it still has not: no compiled image and no runtime entry has ever been executed on a correct CPU. So the plan stands: get a real CPU emulator, run the image, assert the exact stdout bytes.
Unicorn 2.1.4 cannot be used for this. UC_MODE_16 mis-decodes 16-bit
ModRM memory operands. The measurement, by loading a byte-pattern image so a
load reveals its own effective address, then executing a single
LEA AX,[r+disp8] and reading AX back with every register set to a distinct
value:
mod=01, disp8=4 got 8086 says
8D 40 04 LEA AX,[BX+4] AX=0d04 0x504 (= BX+SI+4) WRONG
8D 41 04 LEA AX,[BX+SI+4] AX=0e04 0xd04 WRONG
8D 42 04 LEA AX,[BX+DI+4] AX=0f04 0xe04 WRONG
8D 43 04 LEA AX,[BP+4] AX=1004 0x704 WRONG
8D 45 04 LEA AX,[DI+4] AX=0904 0x904 right
8D 46 04 LEA AX,[BP+4] AX=0704 0x704 right
8D 47 04 LEA AX,[DI+4] AX=0504 0x904 WRONG
mod=00 / mod=10 direct disp16
8B 1E 00 20 MOV BX,[2000] BX=1234 right
8D 06 34 12 LEA AX,[1234] AX=1234 right
So rm=5 and rm=6 decode correctly but rm=0,1,2,3,7 do not, and only
base-register-free addressing (direct disp16) is trustworthy. That rules
Unicorn out as an oracle for exactly the instruction forms generated code is
made of — LEA AX,[BP+d], MOV AX,[SI+d], LODSW-style loops, everything
with a frame pointer. It cannot distinguish "my codegen is wrong" from "the
emulator is wrong", which makes it worse than no emulator at all.
pip install --upgrade unicorn resolves to the same 2.1.4, so this is not
avoidable by upgrading.
Also ruled out, for the record:
/usr/bin/dosbox-x, plain root-owned
ELF, not a snap) starts cleanly headless under
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy, but its -c/autoexec
commands never observably execute: a md never appeared on the host, and a
.COM that creates OUT.TXT via INT 21h AH=3Dh/40h never produced the
file. Shell > is intercepted by dosbox-x's own wrapper
(SHELL:Redirect output to out.txt). A .BAT route timed out./usr/bin/qemu-system-i386) and is the
next candidate: a 512-byte boot sector can load the image and a INT 21h
shim can hand the output back. Not attempted yet.tests/rt_exec.py is the harness, written against Unicorn, and it is written
to survive the switch: it loads the runtime, calls each entry with a known
argument, and compares the bytes sent to INT 21h against expectations. It
currently fails, and the failures are the emulator's, not the library's —
wrint prints - for every value because MOV AX,[BP+4] reads the wrong
address. Do not read those results as a verdict on the runtime.
It holds 33 checks (7 pass, 33 fail — the 7 are the stackchk returns and
rdln cases that happen not to touch memory) covering initmem, wrint
(10 values incl. both INT16 extremes), wrchar, wrbool, wrln,
stackchk, a composed writeln(42) writeln TRUE sequence, and the read
entries against supplied input including EOF. It needs the venv that has the
package — unicorn is not in the system Python:
/home/eric/.venvs/tp3emu/bin/python tests/rt_exec.py # → 33 FAILURE(S), rc=1
It has no wrtinl case yet. That entry needs a harness change, not just a
machine change, because its argument is not a stack word: the caller must place
a length byte and the characters at the return address, which is precisely
the property the compiler relies on — so testing it also tests the encoding
contract between Compiler.IoCall and Runtime.EmitWrInl. Add it when the
executor exists, and expect all 33 current checks to pass first.
shell/Shell.mod, Term.mod, Posix.c, TextBuf.modExact TP3.0 screen (Logged drive, Active directory, Work file,
Main file, Edit Compile Run Save / Dir Quit compiler Options,
Text: n bytes, Free: n bytes, >), command letters drawn bold. Keys
L A W M E C R S D O Q; any other key redraws the menu, like TP3. W
auto-.PAS with Loading/New File; S writes ^Z EOF and rotates the
old file to .BAK (unlink+rename, TP3's order); D is a DOS *.* glob
listing with k bytes free; O is the options submenu; Q confirms and
prompts to save when the text changed.
E and C are wired. R is a stub — it prints "Interpreter pending".
shell/Editor.modWordStar-style full-screen editing over TextBuf. Status line
Line n Col n Insert/Overwrite Indent X:FILENAME; text on rows 2-24.
Movement (^S/^D/^E/^X/^A/^F/^R/^C/^W/^Z, ^Q S/D/E/X/R/C/B/K/P, arrows,
PgUp/PgDn/Home/End), editing (^V insert/overtype, ^G delete char,
backspace joins lines, ^T/^Y word/line, ^Q-Y to EOL, ^N/CR break,
TAB auto-indent to the word start above), block (^K B/T/H mark, word,
show, ^K C/V/Y copy/move/delete, ^K R/W file read/write), search
(^Q-F, ^Q-A, ^L repeat; options B/G/n/U/W, N = no-confirm; ^A any
char, CR LF matches a line break), ^P literal control char,
^U/ESC aborts a prompt. ^K-D returns to the shell with the text still in
memory and changed reported. Disk format matches TP3: CRLF plus trailing
^Z, load normalises, save re-expands.
Detail: TP3-EDITOR.md, TP3-EDITOR-PSEUDOCODE.md.
shell/Compiler.modSingle-pass Pascal → 8086, following TPSRC6 turbo / TPSRC7-10: one pass
over the shared TextBuf emitting machine code into cbuf, a patch list
for forward references, TP3-style error reporting (number + relative
position), and code/data size accounting. Emitted image is a byte array
(mode word, CS/DS, size words, CALL initmem, MOV BP,SP, generated code).
Working subset (v0.5): integer/char/boolean/byte scalars, constants with
folding, globals, locals, value parameters, procedures and scalar-result
functions, ARRAY[const..const] with constant indexing, control flow,
GOTO/EXIT, the standard procedures WRITE, WRITELN, READ,
READLN, HALT, and inline string literals as WRITE/WRITELN
arguments.
Standard procedures are KBuiltin, not KProc, because they are not
called generically. TP3 (TPSRC8 pwriteln/pwrloop/prdtyped) does not
pass a descriptor to the runtime: it inspects each argument's class and emits
a different call per type, so formatting is fixed at compile time and the
runtime only ever sees a value. IoCall mirrors that — one call per
argument, then a final call for the line break:
writeln(1) MOV AX,1 ; PUSH AX ; CALL 20H ; ADD SP,2 ; CALL 40H
writeln('a') MOV AX,'a' ; PUSH AX ; CALL 28H ; ADD SP,2 ; CALL 40H
writeln('hi') ; CALL 70H ; 02 'h' 'i' ; CALL 40H
readln(x) LEA AX,[0104]; PUSH AX ; CALL 48H ; ADD SP,2 ; CALL 60H
(Those are the TU_* placeholders — the real runtime is not wired in yet,
see the limitations. The third line is the inline-literal form, which differs
in kind: no value is pushed and the ADD SP,2 is absent, because the length
and the characters are the argument.)
READ/READLN push the address so the runtime can store
(EmPushVarAddr: 8D 46 disp / 8D 06 off); a non-variable argument is
ETypeErr (56), as in TP3. TU_WrInt/Char/Bool/Real, TU_WrLn,
TU_RdInt/Char/Bool, TU_RdLn, TU_WrInl, TU_Halt continue the existing
TU_* image-base space (TU_InitMem=8H, TU_ProgEnd=10H, TU_StackChk=18H,
TU_WrInl=70H).
Detail: TP3-COMPILER.md.
shell/Runtime.mod, shell/Runtime.defThe 8086 runtime, assembled byte by byte from Modula-2 — no external
assembler and no checked-in binary, so the tree stays self-contained and the
entry offsets are derived rather than guessed. Each emitter is one
instruction with its ModRM byte spelled out in a comment so the encoding can
be checked by hand against an 8086 table. This is the same approach
Compiler.mod already takes (Ebyte/Eword/EmCall).
It mirrors the original's own mechanism: TPSRC7 copyrt copies the runtime
into the front of the code buffer (SI=DI=0, REPZ MOVSB) and pc is then
initialised past it (MOV pc,#$2D7C). Same shape here — the compiler is meant
to copy the blob to the front of cbuf and start pc/dc past it. Runtime
data therefore lives at fixed low offsets and needs no relocation, and because
both sides of every CALL shift by the same amount, EmCall's displacement
arithmetic is unaffected by the runtime being prepended.
Current blob: 385 bytes, 14 entries, offsets read back out of the
assembled bytes by tests/RtProbe.mod:
| entry | offset | entry | offset | entry | offset |
|---|---|---|---|---|---|
initmem |
0 | wrint |
36 | rdint |
161 |
progend |
28 | wrchar |
97 | rdchar |
262 |
stackchk |
35 | wrbool |
106 | rdbool |
283 |
halt |
28 | wrreal |
126 | rdln |
329 |
wrln |
134 | wrtinl |
142 |
progend and halt deliberately share one address (XOR AX,AX / MOV AH,4C /
INT 21h / RET): the compiler already zeroes AX before progend and discards
the HALT argument at compile time, so both leave with exit code 0.
Conventions, matching Compiler.IoCall exactly:
| entry | argument | notes |
|---|---|---|
WrInt/WrChar/WrBool/WrReal |
one 16-bit value on the stack | caller pops |
RdInt/RdChar/RdBool |
one address on the stack | caller pops |
WrLn/RdLn/StackChk |
nothing | |
InitMem |
AX = offset of the program header |
a register, not a stack word |
ProgEnd/Halt |
nothing | exits, code 0 |
WrInl |
nothing — reads its own text via POP BX |
see below |
InitMem reads the data base and end out of the header words at +2/+6 and
zeroes that range, because Pascal leaves globals undefined. StackChk is a
bare RET — range and stack checking aren't compiled in yet, and the call site
sits mid-expression, so it must not touch a register.
Two label-name plus fixup list: rel8, rel16 and runtime-data addresses are
all patched after the blob is placed, so nothing depends on a hand-computed
displacement.
It has never executed. See the emulator finding below.
writeln('hi')writeln('toto') was the last thing standing between the front end and a
hello-world: string literals had no encoding at all, so anything past one
character was ENoLib. The encoding is not invented — it is the original's.
TPSRC8 pwrinlin peeks at the character after the literal: if it is , or
) the literal is a WRITE argument, not an expression, and it emits (TPSRC10
estring) the length byte and the characters into the code stream right
behind the call:
CALL wrtinl <length byte> <character>...
TPSRC4 xwrtinl is what makes that self-delimiting: POP BX takes the return
address — which is the address of the length byte — and the entry ends with
JMP BX, returning to just past the last character. So the literal needs no
terminator, no length table, and nothing at all in the data segment. The
arithmetic confirms it: t26 emits 02 68 69 inline and its data= stays
260, unchanged from a program with no strings at all.
wrtinl is 19 bytes at offset 142, hand-checked against the 8086 table
(5B POP BX · 31 C9 XOR CX,CX · 8A 0F MOV CL,[BX] · 43 INC BX ·
B4 02 MOV AH,2 · E3 07 JCXZ to the end label · 8A 07 MOV AL,[BX] ·
CD 21 · 43 · E2 F9 LOOP · FF E3 JMP BX).
A string literal is a value in exactly one place: a WRITE/WRITELN
argument. Everywhere else it is a hard error, and it is enforced in a single
place — LoadAtom — because assignment, IF, WHILE, FOR, REPEAT,
CASE, array subscripts and every operator all reach their operand through
LoadAtom, and none of them can use a counted string where a 16-bit word is
expected. ParseFactor therefore marks a literal (kind = 3) rather than
rejecting it, and IoCall handles it before LoadAtom is ever reached. The
alternative — letting it through and producing a machine word that happens to
be a pointer — would be a silently wrong program; t25 pins the error
instead.
Literal text has to survive from the scan to IoCall, since the parser does
not yet know it is writing rather than computing, so it is collected into a
pool as it is read: strPool[0..4095], strOff/strLen[0..255], strTop,
strCnt, plus StrNew/StrPut. The pool is reset in Inittur, so it is
per-compilation.
Details that are deliberate, not incidental:
'' is a zero-length literal, reaching the runtime's JCXZ path. It
used to be the scalar 39, so writeln('') printed a quote mark.'don''t' — the doubled quote becomes one character; t24 pins len 5.EConstRange (45), not truncated.
The length is one byte, so 300 characters would go out behind a length of
44 and the runtime would print 44 of them and silently drop the rest. TP3
strings are at most 255 characters, so refusing is the faithful answer.ENoLib, not wrong code. IoCall knows the
difference and refuses, because EmPushVarAddr's local form is still broken
(see bug 4 below) and a bad address prints garbage rather than failing.CmdRun is a stub, the
linker is unwritten, and no 8086 executor on this machine has yet proved
trustworthy (above). The emitted code is verified byte by byte against the
offsets the compiler intends, but nothing has ever run it.Runtime.mod assembles to 385 bytes
and its entry offsets are derived from the emitted bytes, but it has never
been executed on a correct CPU, so treat every encoding in it as unverified.
Two of its own bugs were found by decoding the hex dump, one by running it
under the broken emulator; wrtinl is the newest and the only entry no test
touches even in principle.wrreal is a deliberate stub. It writes the literal text ?REAL? — the
string lives in the runtime's own data block at D_REAL=24, which is what
makes it a real 9-byte routine rather than a trap. Reals are not formatted
yet, so writeln(1.5) "works" and prints nonsense. A trap would be louder;
this was chosen because the runtime is not yet reachable, and neither choice
is a real answer.Compiler.mod still
carries the hardcoded placeholder TU_* constants (TU_InitMem=8H,
TU_ProgEnd=10H, TU_WrInl=70H, …) and still starts pc at 0, so emitted
images do not contain the runtime and those offsets are still wrong — note
the real wrtinl is at 142 (8EH) and the placeholder is 70H. Runtime.mod
is not in make or run_compile_tests.sh yet for the same reason. Note the
prologue's CALL TU_InitMem targets offset 8, which is currently the
hdrMax header word — coherent only once the blob is really prepended.
*(Correction to the earlier note in this file: the TU_InitMem=8 "collision"
was a false alarm. Per TPSRC7 the runtime is copied to the front of the
code buffer and pc starts past it, so TU_* offsets are runtime-relative,
not image-absolute, and no rebasing of the displacement arithmetic is
needed. The placeholders stay on the ladder even though the real offsets are
now known, because the compiler cannot yet call the runtime — using the true
offsets would only make it look like it works.)*WRITE/WRITELN argument list is emitted inline and needs no runtime
support beyond wrtinl. Declaring s : string, assigning to it and
printing it are all ENoLib — there is no string type, no length word,
no assignment, and EmPushVarAddr is wrong for locals. The chr flag on
ERes is what keeps writeln('a') calling the character writer instead of
the integer writer; without it the compiler emitted the integer path and
printed 97 while the test still said OK.ENoLib): real, set, record, file, string
variables, and any type wider than 2 bytes. with is ENoLib. case is
implemented (cascade CMP/JNZ per label, per RESUME-TP3.md §3.6) but only
over scalar labels — subrange labels and label lists are untested.ARRAY OF CHAR assignment copies the literal plus a
NUL and leaves the tail untouched, so NUL-terminated tables are safe —
this was checked, not assumed.None of these produced a compile error, and two produced passing tests. All three were found by hex-dumping the emitted image and decoding it by hand — code sizes looked perfectly plausible throughout.
rel16 off by 2 in every direct CALL/JMP. EmCall/EmJmpNear/
EmJcc computed the displacement from pc at a point where pc already
pointed past the opcode and at the displacement field; x86 measures
from the end of the instruction (pc + 2). ResolvePatches, on the
forward-patched path, was already right — which is why forward gotos
looked fine and backward ones did not.
EmMovAxSp emitted 8B 04 with no SIB byte. ModRM 04 means "a SIB
byte follows", so the CMP AX,imm16 of the next instruction was eaten
as that SIB byte, and the intended MOV AX,[SP] became
MOV AX,[BP+DI+disp]. Every case label comparison therefore vanished
and case compiled to a chain of loads from garbage addresses — while
the fixture reported OK. Correct encoding is 8B 44 24 00 ([SP] cannot
use mod=00, that computes BP+SP).
The dump tool's own offset column was wrong. It printed 4 nibbles
through a helper that formats a byte, so every row was labelled 16×
too large (0010 shown as 00000100). A misleading tool is worse than
none — it corrupts any offset arithmetic done from its output.
Executing the hand-assembled runtime — even under a broken emulator — and running the emitted images back through the harness paid for itself immediately, because a wrong encoding executes rather than failing to assemble. Seven so far, none of which a compiler diagnostic would ever have reported.
B() silently truncated multi-byte opcodes. PROCEDURE B emits exactly
one byte and masks with MOD 100H, so B (8BE4H) — a two-byte opcode passed
as one literal — emitted just E4. MOV BP,SP was missing from every frame
in the runtime, so BP stayed 0 and every BP-relative access read
address 4. Found by decoding the hex dump; the byte is gone, not wrong.MOV BP,SP encoded as 8B E4, which is MOV SP,SP — a no-op. The
ModRM byte is mod·64 + reg·8 + rm, and I mis-derived it. Compiler.mod's
EmMovBpSp had it right all along (8B 0CH); only the new module was wrong.
This is why the encoding is now written as three explicit B calls with the
arithmetic in a comment rather than as one hex literal.InitMem read its argument from [SP] — the return address. The
convention is a register (AX), unlike the per-argument I/O entries which
do take a stack word. Caught because the data area was never cleared.EmPushVarAddr computes the wrong base register for locals (pre-existing,
not yet fixed). It emits 8D 46 disp, which is LEA AX,[SI+disp8], but
intends LEA AX,[BP+disp8] = 8D 45 disp. So read into a local variable
has always addressed the wrong cell, silently. It also truncates the offset
with off MOD 100H, losing displacements above 255. The global form
8D 06 off (LEA AX,[disp16]) is correct. Found while writing the runtime's
read entries, which needed the same encoding to be right.Length() — one past the last
character of the buffer — so the editor landed past the final . of the
program. The scanner loop tested CurCh # quote, but its "closing quote
detected" branch consumed two characters (the content character and the
quote), so the cursor moved past the quote and the next condition test saw
the character after the literal, was satisfied, and scanned on to
end-of-buffer. The IF after the loop that was meant to consume the closing
quote was unreachable for any string of two or more characters — which is
exactly why writeln('a') always worked and writeln('hi') never did.
Worse than a bad caret: it destroyed the parse, so anything after a literal
was consumed as string contents and a genuine later error was misattributed
to end-of-file.FOR over
IoCall's argument list needed a "did this argument push a value" flag, and
it was never set, so the first argument's CALL and its ADD SP,2 were
skipped. Nothing looked wrong — a slightly smaller image looks more
plausible, not less. Only expected.tsv pinning code sizes caught it.writeln('hi') emitted 02 69 00 — i then NUL. StrNew recorded the
first character of a literal but did not advance strTop, so the first
StrPut landed on top of the seeded character and overwrote it. t17_two_str
is what pinned it down: its third emitted character was e, the second
literal's character, which had been written into that slot.The last three were each found by a different means — (5) by noticing that every
error position was exactly the buffer length, (6) by expected.tsv, (7) by
hex-dumping the image — and it is worth being precise about why all three were
invisible to a check that only asks "does it compile": (5) still produced a
plausible error number, (6) a plausible code size, and (7) a plausible
character. Each is precisely the shape of bug a compile-only fixture ships.
EXIT inside the program-header parameter WHILE ICEs gm2 in pass 3
(ExitStatement → PopExit → M2StackWord_PopWord → invalidloc).
Extracting the loop into its own procedure did not help. Use a BOOLEAN
"advanced" flag, never EXIT. (LOOP+EXIT is fine, and is used widely.)HALT aborts under -fiso (SIGABRT, exit 134); HALT (0) is
correct.CHAR is not the ZType: ch = 09H must be ORD (ch) = 09H.SYSTEM exports ORD but not Ord — the import is
case-sensitive here despite gm2's usual case-insensitivity, so
FROM SYSTEM IMPORT Ord fails with "unknown symbol" while plain ORD (c)
compiles. Write ORD, never Ord.DropCh/
DropB/DropC discard helpers wrap ~39 call sites.AND/OR/NOT on 16-bit CARDINAL → BitAnd/BitOr/BitNot.PROCEDURE f : T is invalid; the file's convention is
PROCEDURE f () : T.CHR instead.FOR "C") must use ADDRESS, not Modula-2 pointer
types; cast at the call site. Posix has no Posix.mod (do not try to
rebuild it) and no argv binding.CR LF.Posix cannot pass argv; the test harness reads
fixture paths from stdin instead.TP3-COMPILER.md — the compiler: what was changed, the Skip bug class,
the rel16 off-by-2, standard procedures, current matrix.TP3-EDITOR.md / TP3-EDITOR-PSEUDOCODE.md — the original TP3 editor,
from TPSRC5/TPSRC6.RESUME-TP3.md — book-derived reference for the 8086 code TP3 generates
per Pascal construct (types, skeletons, arithmetic, IF/CASE/REPEAT/WHILE/
FOR, procedures, parameters, functions, I/O, typed constants, absolutes),
plus the TU_* runtime entry list.Resources/turbopascal3source/TP3/ — TPSRC1-10, the disassembled original.
This is the ground truth; when our behaviour and the book disagree, the
disassembly wins.Resources/coeur-tp-ocr/ — OCR of "Au coeur de Turbo Pascal".qemu-system-i386 with a
boot-sector loader and an INT 21h shim is the plan; keep rt_exec.py's
expectations and change only the machine behind them. (Do not reach for
Unicorn's 16-bit mode again, and do not re-derive the rm table by hand
again — check it against a real decoder.) A last resort is writing the 8086
interpreter CmdRun needs anyway, cross-checked against Unicorn only on
disp16-only code, which is the one addressing form Unicorn 2.1.4 gets
right.EmPushVarAddr — 8D 45/8D 85 for locals, full disp16, per
bug 4 above. Two lines, and read into a local is wrong until it is done.pc := RT_Size,
dc := RT_Size + 1000H (a fixed 4 KiB code/data gap, so a program's data
can't collide with its code in a single 64 K .COM segment), take the
TU_* offsets from RT_Entry instead of the hardcoded constants, patch the
header words (hdrDS = data base, hdrHeap = data end, so InitMem can
zero globals), pad the image to cover the data area, and emit the .COM.
Add Runtime to the make and run_compile_tests.sh rebuild lists.writeln('hi') → hi). That single assertion is what turns
this from "assembles" into "works".CmdRun — run the emitted image from the R menu key.s : string, s := 'hi', writeln(s). Blocked on
item 2: IoCall deliberately raises ENoLib for a string variable rather
than emitting the wrong address, so the moment EmPushVarAddr is fixed this
needs a length word, an assignment path, and a WrStr entry (TPSRC4
xwrtstr).var parameters (the SEG:OFF push from
RESUME-TP3.md §3.11), range/index checks (TU_RANGE_CHECK,
TU_INDEX_CHECK), typed constants (RESUME-TP3.md §3.14), array at its
point of use (t14).program p(1;)) with a BOOLEAN flag — not EXIT, which ICEs gm2.