# 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*