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 blob, linker, CmdRun interpreter |
— | 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 120792 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
17 of 23 fixtures compile, up from 1 (the empty program) when the direct harness was first built.
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.
Failing, all deliberately: t02/t03/t05/t17 multi-char string literals and
t14 array [1..5] of integer → 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.
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.4): 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, and the standard procedures WRITE, WRITELN, READ,
READLN, HALT.
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
readln(x) LEA AX,[0104]; PUSH AX ; CALL 48H ; ADD SP,2 ; CALL 60H
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_Halt continue the existing TU_*
image-base space (TU_InitMem=8H, TU_ProgEnd=10H, TU_StackChk=18H).
Detail: TP3-COMPILER.md.
CmdRun is a stub and the
runtime blob does not exist. The emitted code is verified byte by byte
against the offsets the compiler intends, but nothing has ever run it.TU_* entry offsets are placeholders under an assumed model — a
runtime blob prepended to the image, with every pc rebased by its size.
That model is assumed, not implemented; the linker is unwritten. Note the
prologue's CALL TU_InitMem targets offset 8, which is currently the
hdrMax header word, so the scheme is only coherent once the runtime is
actually prepended.RdConst gives a 1-character literal as TScalar
(its char code) and only longer literals as TString, so multi-char
literals raise ENoLib. writeln('a') works via a chr flag on ERes;
without it the compiler emitted the integer writer and would have printed
97 while the test still said OK.ENoLib): real, set, record, file, string, 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.
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.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".TU_* entries have to land somewhere real and every pc needs rebasing.CmdRun — the interpreter. Without it no compiled program has ever
run, so the codegen is unproven in the only way that counts.var parameters (the SEG:OFF push from
RESUME-TP3.md §3.11), range/index checks (TU_RANGE_CHECK,
TU_INDEX_CHECK), and typed constants (RESUME-TP3.md §3.14).