TP3-COMPILER.md 8.3 KB

TP3-comp — Turbo Pascal 3 clone in GNU Modula-2 (ISO)

Status: compiler-core (Compiler.mod) + shell compiled clean with gm2 -fiso -Wall, and the ISO shell tpshell builds and runs (ESC-key driven, TP3-style DOS screen redraws). This is the working implementation companion — the human overview is TP3-COMPILER.md history summary and RESUME-TP3.md.

Build (proven, two-phase — the ONLY recipe that beats GNU gm2's

pass-3 "too many errors / too many identifiers" cap on compiler-sized whole-program programs)

gm2 -fiso -c Compiler.mod          # phase A: small per-module pass
gm2 -fiso -fgen-module-list=modules.lst -o /dev/null \
    Compiler.mod Term.o TextBuf.o Posix.o Editor.o  # phase B1: build list
gm2 -fiso -fuse-module-list=modules.lst -o tpshell \
    Shell.mod Compiler.mod Term.o TextBuf.o Posix.o Editor.o  # phase B2: link

(Equivalent make target tpshell in shell/Makefile.) The single gm2 -o whole-program pass 3 silently caps identifier/error counts — see build_tpshell.sh.)

What was changed inside Compiler.mod (all ISO-legal, gm2-verifiable)

  1. CHAR vs hex-int comparisons (lines ~829, ~1086): Modula-2 CHAR is not the ZType; ch = 09H → ORD (ch) = 09H (and likewise 0DH/0AH/0CH).
  2. Return-value discard (ISO forbids drop in statements; gm2 treats as error and has no -Wno-): added DropCh/DropB/DropC discard helpers and wrapped ~39 statement call sites of GetCh, MatchKey, NewSym, EmCall, EmJmpNear (no output reground clutter; TP3 TP5 cascadepos waitkey semantics preserved via the editor's ESC wait).
  3. AND/OR/NOT on W16-folded CARDINAL (lines ~1233/1246/1454) are illegal on ZType in gm2 → added BitAnd/BitOr/BitNot (BITSET */+/- fold, VAL-projected back to LONGINT) and rewrote the operands.
  4. Compile error-return params aligned to Compiler.def (errNo/errPos, was eNo/ePos) — gm2 ISO rejects a disparate name in the proper procedure.
  5. HALT -> HALT (0) in Shell.mod. A bare HALT (no operand) aborts under gm2 -fiso: a minimal repro exits 134 (SIGABRT) with a bare HALT but 0 with HALT (0). Quitting the shell died on SIGABRT.
  6. Module-level errNo renamed errNum. Compile's formal errNo shadowed the module variable, so errNo := errNo was a self-assignment and every error code reached the caller as 0.

The compiler's whole-program 8086-emit, TEXT-equivalent, pastres and errexit flow are otherwise unchanged (mirrors TP3 TPSRC file, ConvertTP3).

Lexer/parser bug class: "Alpha test without Skip"

The single largest source of wrong behaviour. Skip () is what folds blanks, { } and (* *) comments, and CR; neither PeekKw nor MatchKey leaves the cursor past it (PeekKw save/restores srcPos, MatchKey stops right after the word it matched). So a routine entered immediately after a keyword sees the CR/blank that follows that keyword, and any

IF NOT Alpha (CurCh ()) THEN Err (EUnknown) ; RETURN

fires with EUnknown (41) pointing at a blank. That is why every program used to fail on program's program name, and why writeln(...) failed on its (.

Fixed by adding the missing Skip () at each entry point (it is idempotent, so it is safe even where a Skip already ran):

routine entered after
program header PROGRAM
DefVar VAR
DefConst CONST
DefType TYPE
ParseType : / =
ProcFunc (name) PROCEDURE / FUNCTION
ProcFunc (parameter name) (, or a var-parameter's VAR
Statmnt, TkFor branch FOR
Statmnt, TkGoto branch GOTO

DefLabelPart, MatchKey, MatchDelim and MatchAssign already had theirs.

Three related defects fixed alongside:

  • ProcFunc's parameter loop used MatchKey (tok) AND (tok = TkVar) to detect a var-parameter. MatchKey consumes the word it reads, so using it as a lookahead ate the parameter's own name. Replaced with PeekKw (non-consuming), consuming with DropB (MatchKey (tok)) only on a VAR hit.
  • ProcFunc called ParseType straight after the parameter name without consuming the : of name : type; added ExpectDelim (':', ENoSemi).
  • ParseLabelStmt consumed n : but never parsed the statement the label is attached to, so Compound then demanded a ; that does not exist in 1: x := 1. Statmnt now parses the labelled statement.

Tests

Two harnesses live in shell/tests/ -- in the repo on purpose, because /tmp is wiped between sessions and an earlier /tmp-only harness was lost with it.

run_compile_tests.sh -- compiler front end, no pty, instant

tests/CompileTest.mod links Compiler + TextBuf + Posix and reads fixture paths from stdin, one per line. For each it does exactly what the shell's LoadWorkFile does (LF -> CR normalisation, ^Z ends the text), calls Compile, and prints a one-line verdict plus a source excerpt with a caret under the error position. It rebuilds Compiler.o when the source is newer, so an edit is picked up automatically.

cd shell && tests/run_compile_tests.sh            # all fixtures
cd shell && tests/run_compile_tests.sh /some/dir  # another fixture set

Current matrix (15 fixtures) -- 8 compile, up from 1:

fixture result
t01_minimal program t01; begin end. OK code=26
t08_const const decls, 'A' char const OK code=32
t09_if if/then/else OK code=64
t10_while while OK code=72
t11_for for/to OK code=66
t12_repeat repeat/until OK code=69
t13_proc procedure + value parameter OK code=50
t15_label label + goto OK code=35
t02, t03, t04, t05, t06, t07 (any use of writeln) ERROR 41
t14_types array [1..5] of integer ERROR 102 (ENoLib)

uitest.py -- shell/editor behaviour that only exists interactively

Drives a real pty (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 -> the editor opens with the cursor exactly on the error position -> Ctrl-K D back to the menu -> Q exit 0. 10/10 pass on t02_writeln.pas, and the reported error is 41 -- i.e. the errNo self-assignment fix is now proven through the real UI.

The jump mirrors original TP3 kcwait + editor2 (Resources/turbopascal3source/TP3/TPSRC5:333-336 and :919): waitesc; BX:=txerrpos; DEC BX; JMP editor2, where editor2 then does ADD BX,txbeg; INC BX -- the DEC/INC cancel, so the net position is txbeg+txerrpos = our 0-based errPos. It is armed as a sticky position (Editor.GotoOffset) instead of by changing Run's signature, so Editor.def stays additive.

Note LoadWorkFile ends with a Pause, so a driver must send one filler key after the path; skipping it desynchronises every later keypress.

Known gap: no standard procedure library

Inittur defines six predefined types (INTEGER/BYTE/CHAR/BOOLEAN/REAL/ STRING), TRUE/FALSE and two temporaries -- and zero procedures. So WRITELN is never in the symbol table, the ELSE branch of Statmnt fails its Search, and every program that prints anything dies with EUnknown (41) on the ( after the call name. That is why 6 of the 15 fixtures still fail, and it is the next milestone. EmCallMost already has the right shape: a KProc symbol with defnd := TRUE and goPos = runtime entry emits a direct near call. The runtime entries TU_InitMem=8H, TU_ProgEnd=10H, TU_StackChk=18H are image-base offsets, i.e. the runtime blob is meant to be prepended at link time by the not-yet-written linker. CmdRun, the interpreter, is likewise still a stub.

ENoLib (102) is the deliberate "not implemented yet" path for real/set/record/file/string and for any type wider than 2 bytes.

gm2 pitfall: EXIT inside the program-header WHILE crashes pass 3

Adding an EXIT to the program-header parameter loop (to guard against non-advancing input on malformed input such as program p(1;)) ICEs gm2:

internal compiler error: Abandon
  ... ExitStatement -> PopExit -> M2StackWord_PopWord -> invalidloc

Extracting the loop into its own procedure did not help. So the header fix is deliberately exactly one added Skip () and no EXIT. If that loop must be hardened, use a BOOLEAN "advanced" flag, never EXIT.

Generated: 2026-09-26