# CALC-COMPILE — make the TP3 compiler accept and compile `CALC.PAS` The goal of this line of work is narrow and stated as a verdict: > `shell/compiletest` returns **OK** for `Resources/TP3/CALC.PAS`, and `CmdCompile` > writes a `.COM`. It is deliberately **not** the same goal as `CALC-PORT.md` (porting MicroCalc to native GNU Modula-2 on top of `Term.mod`). That is a separate piece of work and a separate session; nothing here changes it, and nothing here depends on it. The measurements were taken against the tree at tag `v-TP3-INTERRUPT-RESTORE` (`2ae00d1`); the ladder and the stages have moved on since (the layout rung is tagged `v-TP3-CALC-LAYOUT`, and §1's rung table and the "Session state" section are the current figures). --- ## 0. Scope decisions, fixed before any code 1. **Target the `TURBO.COM` dialect, i.e. the 6-byte binary `REAL`.** `CALC.PAS` says so itself: it will not build with `TURBO-87.COM` without an 8087, and its help text notes that "Sqrt, Sin and friends are not implemented in the BCD compiler". So this is a `TURBO.COM` program and `REAL` is the 6-byte format of `RESUME-TP3.md` §2 ("REAL (6 octets)"), not the 10-byte BCD variant. Implement exactly one real format. 2. **Compile first, run second.** The gate is the compiler's verdict, not a working spreadsheet. A construct may be *incomplete* only if it is *loudly* incomplete (`Err (ENoLib)`); it may never be accepted and mis-compiled. This project has already shipped that bug once. One of the two cases below is now closed (Stage 2); the other is not: - `var` parameters are **silently accepted and passed by value** — `ProcFunc` drops the `var` (`DropB (MatchKey (tok))`, comment "VAR parameter recorded as value in this milestone"); `writeln (g)` after `q (g)` prints `7`, not `42`. Ten CALC routines take `var` parameters. - `{$I CALC.INC}` was **silently swallowed as a comment**, so `CALC.PAS` without its include compiled green (`program p; {$I part.inc} begin halt end.` → `OK code=32 data=4`, `part.inc` never opened). **Closed at Stage 2**: the include is expanded before parsing and an unopenable file is error 90, not a comment. The `var`-parameter case is still open and must be closed by implementation, or by an explicit `Err` until it is (Stage 10). 3. **One fixture and one ratchet per step.** Every stage lands with a fixture that is provably red before and green after, plus a move of the ratchet (§1). Nothing is "done" on the strength of a compile that a human eyeballed. --- ## 1. The measuring device: the CALC ladder A 1,763-line program (182 + 1,581 lines) that stops on the 111th line gives no sense of progress unless "how far did we get" is a number in a test. Make it one. **Implemented (Stage 1, 2026-10-19) — `tests/fixtures/calc_ladder.tsv` plus `tests/calc_ladder.sh`,** run by `run_all.sh` as `CALC ladder`. Each row names a program and gives the exact verdict in the same shape as `tests/fixtures/expected.tsv`: ``` CALC.PAS ERR ``` The script stages a *copy* of the named program — and of the companions that sit beside it, so `CALC.INC` is already there for Stage 2 — into `tmp/calc_ladder/`, runs the freshly built compiler on it, and compares. It does not build the compiler itself: it calls `tests/run_compile_tests.sh --build-only`, which is the one recipe that freshness-checks `compiletest`, so the ladder cannot become the second staleness test that drifts from the first. The copy is deliberate: the include (Stage 2) resolves relative to the including file, so `CALC.INC` has to be beside `CALC.PAS` in the staged tree, and the row then measures the compiler rather than an edit-in-place of `Resources/TP3`. It is not a fixture in `tests/fixtures/*.pas` (which would enter the compile/COM/exec matrices and need a second copy of `CALC.PAS` kept in sync). Each stage updates the row; the row may only move forward. Two traps are written next to the file itself: - **`pos` is not a file offset.** `LoadFile` collapses CRLF *and* lone LF to a single CR, so `pos` indexes the *normalised* buffer. `CALC.PAS` is 11,241 bytes on disk and 11,059 bytes normalised; the reported `pos 7791` is file byte **7901**. Every expected value in the ladder must be computed in normalised coordinates. Once `{$I}` lands, the buffer is `CALC.PAS` with the directive replaced by `CALC.INC`, so all downstream positions shift again — which is exactly why this must be a computed number, not a remembered one. - **Nobody edits the ladder row without editing the fixture in the same commit**, per the repo's re-baselining rule. Current rung (measured today, normalised position): | position | blocker | |---|---| | **22706** | `var Line : AnyString` in `GetFileName` — VAR string parameters (`ERR 102`) | Rungs behind it (now passed): | was | blocker | done by | |---|---|---| | 7791 | `FXMax : Char = 'G'` — scalar typed constant | Stage 4 (scalar) | | 7972 | `Numbers : set of Char = ['0'..'9']` — set typed constant | sets, increment 1 | | 8098 | `Attributes = (Constant,…)` — enum | Stage 5 (enums) | | 8362 | `CellRec = record … end` — record type | Stage 7 (record types) | | 8705 | `Cells = array['A'..'G',1..FYMax]`, `Sheet : Cells`, `XPOS` | Stage 8 (declarations) | | 9202 | `MCFile : file of CellRec` — file type; the whole `var` block; `{$I}` | Stage 13 (file types) | | 10350 | `procedure Center(S : AnyString)` — a **string value parameter** | calling convention, steps 1-3 | | 10405 | `Length(S)` — the first string builtin | `LENGTH`, inline | | 10753 | `ClrScr` — CRT identifiers begin | `ClrScr` + `GotoXY` (real INT 10h) | | 10927 | `Read(Kbd, Ch)` — a READ with a leading file | `Kbd` + `ClrEol` | | 12083 | `NormVideo`/`LowVideo`/`Delay` | attribute byte + busy loop; and the repeat-body parser bug fixed | | 12672 | `KeyPressed` | `INT 21h AH=0B`, in Exec86 and bootcom too | | 12901 | `var` parameters | by reference + DupTest shadowing (`t73_varparam`) | | 13052 | comma-separated CASE labels | plus real CASE ranges (`t74_caselist`) | | 14195 | variable array subscripts | scaled addressing, 1 byte or 2 (`t75_arrvar`) | | 14330 | write field widths | `wrintw`, runtime 770 -> 878 (`t76_width`) | | 15026 | WITH + n-D subscripts | row strides, fields via DI (`t77_with`) | | 15131 | set field assignment | typed by the target; run-time `in` (`t78_setfield`) | | 15201 | string field through DI | ``, truncated (`t79_strfield`) | | 15272 | REAL constants and copies | measured MBF, 6-byte stores (`t80_realconst`) | | 15911 | `UpCase` + run-time `in` | set fields through DI in WRITE (`t81`, `t82`) | | 18267 | `Write(Value:FW:DEC)` — REAL output | `real_write` (`t84`) | | 19847 | run-time set algebra / `Succ` / `Pred` | `set_op`/`set_cmp`/`copy` (`t85`, `t86`) | | 20912 | the fixed 4 KiB code window | original layout: code-only `.COM`, high zeroed data base (§3.2) | | 22253 | `var F : file;` — the untyped file type in `Exist` | optional `of T`; the parser reported ENoSemi at the semicolon | Rungs ahead: | position | blocker | |---|---| | 22280 | file operations (`Assign`, `Reset`, `Rewrite`, `Close`, `IoResult`) - done by the file control-block rung | | 22706 | `var Line : AnyString` - a by-reference string parameter | Corrections the measurements force, worth stating plainly because the stage list below was written before them: - **The include (Stage 2) does not move the rung.** It is at normalised `9404`, past the stop, so Stages 3-4 had to happen before the include is ever reached. - **`7791 → 8036` in one stage was optimistic.** `CALC.PAS:114`, `Numbers : set of Char = ['0'..'9']`, is a typed constant whose *type* is a set, and it comes before the enumeration at 8036. Sets had to go first, and now they have — the compiler stops at **8098**, on the enumeration. **The measured ladder is the authority; the stage numbering below is the original plan, not the dependency order.** --- ## 2. The census — what CALC actually needs Counts are from `CALC.PAS` + `CALC.INC` with comments and string literals stripped. ### 2.1 Type and language constructs | construct | where | status today | |---|---|---| | `{$I CALC.INC}` | `CALC.PAS:150` | **done (Stage 2)** — expanded before parsing; `t56`/`t57`/`t58` | | typed constants (`X : T = v`) — **scalar** | `CALC.PAS:111` | **done (Stage 4)** — declared class kept; `t59_typedconst` | | typed constants — **set** | `CALC.PAS:114` | **done (sets, increment 1)** — code-resident bytes; `t61_setconst` | | typed constants — **array** | `CALC.PAS:137`; `CALC.INC:777` | `ERR 102` — needs initialised storage | | lexical `^M`, `#nn` | `CALC.PAS:113` | **done (Stage 3)** — `t60_charcaret` | | enum type `(Constant,Formula,…)` | `CALC.PAS:120` | **done (Stage 5)** — ordinals; bounds carried; `t62_enum` | | subrange types `'A'..'G'`, `0..20`, `string[70]` | `CALC.PAS:117,119,130` | OK | | `set of Char` / `set of ` | `CALC.PAS:114` | **type + sizing done** (hybrid bytes) | | `set of ` | `CALC.PAS:121,127` | **sizing done** (enum bounds → 1 byte for Attributes) | | set constructor `[a..b, c]`, `in` (compile-time) | `t61_setconst` | **done** — folding only | | set `in` (run-time), `+`, `*`, `=`/`<>`, assignment, set *variables* | 47 × `in`, 6 × `+ [`, 2 × `*` | `ENoLib` — next sets increment | | `record` + field select `Sheet[FX,FY].CellStatus` | `CALC.PAS:126` | **type done (Stage 7)** — CellRec = 80 bytes; field SELECT is next | | `with` | 13 sites | after field selection | | 2-D array `array['A'..'G',1..FYMax] of CellRec` | `CALC.PAS:133` | **type + variable done (Stage 8)** — `Sheet` = 11760 | | array typed constant `XPOS : array[…] of Integer = (…)` | `CALC.PAS:137` | **done (Stage 8)** — code-resident | | array **element** access `b[i]` (constant index), `r.f` | `t14`, `t65` | **done** — width on the expression | | array **element** access with a VARIABLE index `Sheet[fx,fy]`, `with` | 13 `with` sites | `ENoLib` — scaled addressing next | | array indexed by an **enum type** | `CALC.INC:773,927` | `ERR 41` | | pointers `^Pages`, `New`, `Dispose` | `CALC.INC:579,587,639,690` | `ERR 41` | | string **value** parameters (`S : AnyString`) | `CALC.INC:17`, … | **done** — whole-slot copy; `t67_strparam` | | `var` parameters | 10 routines, 14 occurrences (e.g. `CALC.INC:765,979,995,1036`) | **silently by value** | | self-recursion | `Fact` — `Fact := I * Fact(I - 1)`, `CALC.INC:747,754` | `ERR 1` at the `(` | | mutual recursion via `forward` | `Expression` fwd `CALC.INC:742` ↔ `Factor` calls it at `820`,`932` | `forward` parses; definition untested | | forward definition repeating only the name | `CALC.INC:1036` (params commented out) | to check | | nested procedures / functions | `DisplayMessage` in `Help`; `Fact`/`Factor` in `Evaluate` | parses OK | | local `type`/`const`/`var` sections in a nested function | `CALC.INC:767-778` | `ERR 41`/`ERR 102` | | `case` labels `^E`, `^X, ^J`, `#27`, comma lists | `CALC.PAS:169`, `CALC.INC:91,1200,1567` | comma lists/`^X`/`#` unknown | | `file of CellRec`, `text` | `CALC.PAS:144`, `CALC.INC:582` | **types done (Stage 13)** — operations next | | lexical `#27` and `^M` char constants | many | `ERR 41` at the `#`/`^` | | `write`/`writeln`/`read` with a leading file argument | `CALC.INC:562,625` | not implemented | Not used by CALC, so explicitly **not** needed: `downto`, `goto`, `absolute`, `shr`/`shl`, `packed`, `Pos`, `Concat`, `Round`, `Trunc`, `Frac`, `Ord`. ### 2.2 Identifiers used but not declared in the source This is the exact builtin surface. `strPool`/`strOff`/`strLen` are compiler tables, so this list is also the `KProc`/`KFunc` work list. | category | builtins (count in CALC) | |---|---| | **present today** | `write` 41, `writeln` 5, `read` 15, `readln` 1, `halt` 1 | | strings | `length` 19 (**done**, `t68_length`), `copy` 5, `delete` 3, `insert` 1, `str` 3, `val` 5 | | ordinal | `chr` 5, `upcase` 12, `succ` 5, `pred` 1 | | reals | `ln` 5, `exp` 3, `abs` 3, `sqrt` 2, `sqr` 1, `sin` 1, `cos` 1, `arctan` 1, `int` 1, `str`/`val` above | | heap | `new` 1, `dispose` 1 | | files | `assign` 5, `reset` 3, `rewrite` 2, `close` 4, `endoffile` 5, `ioresult` 1 | | CRT | `gotoxy` 25, `lowvideo` 16, `normvideo` 16, `clreol` 10, `clrscr` 6, `keypressed` 3, `delay` 1, `kbd` (8 × `Read(Kbd,Ch)`) — **`ClrScr`/`GotoXY` done** (`t69_crt`); the rest registered and loud | | directives | `{$I CALC.INC}` (include), `{$I-}`/`{$I+}` (2), `{$A-}` (1, harmless/ignored) | Also needed by name for the *declarations* to resolve: predefined types `integer`, `char`, `boolean`, `real`, `string`, `text`, `file`, and the predefined constants `true`/`false` (both work today). --- ## 3. Structural blocker: the memory layout (RESOLVED 2026-10-22) This was a **hard prerequisite**: CALC's code is 16,560 bytes (§3.1, measured) and the old port refused to emit more than 4,096. **It is now resolved; the implementation is in §3.2.** The paragraphs below are the historical diagnosis and the reference measurements, kept because they explain why the old fixed 4 KiB split was not a capacity problem but a layout one. **What this port did before the change.** `pc` started at `rtSz` (773) and `dataBase := rtSz + 1000H` (4869). Code occupied `[rtSz, dataBase)` and data occupied `[dataBase, …)`. The *data* sat immediately above the *code*, so the enforced limit was **code ≤ 4 KiB**, reported as TP3 error 98 by `Ebyte` and pinned by `t43_codewindow` (`v-TP3-CODEWINDOW`). `MaxCode` is 24,000, so the buffer was not the constraint — `dataBase` was. **What the original does.** `TPSRC7:81-82`: ``` MOV pc,#$2D7C ;PC: start of code MOV dc,#$0240 ;DC: start of file list ``` `pc` (the code position) starts at `$2D7C` = 11,644 and `dc` (the data counter, `TPSRC7:1286-1287`, `vardef2` uses `AX := dc` as the global's offset) starts at `$0240` = 576. That is the **opposite orientation**: data low and growing up, code above it and growing up. **Inferred** consequence: in the original the binding limit is *data* running into the code base, not code running into the data. That is consistent with the original having a runtime measured in hundreds of bytes while reserving ~11 KiB between `$0240` and `$2D7C`. **Why CALC makes this binding.** Measured data budget: | item | bytes | |---|---| | `Sheet : array['A'..'G',1..21] of CellRec`, 147 cells × 80 | 11,760 | | `CellRec` = `set of Attributes` 1 + `String[70]` 71 + `Real` 6 + `DEC,FW` 1+1 | 80 | | `XPOS : array['A'..'G'] of Integer = (…)` (typed const) | 14 | | `Line : string[100]`, `FileName : string[70]` | 101 + 71 | | `Help.Buffer : ^Pages` with `Pages = array[0..200] of string[80]`, `New (Buffer)` (`CALC.INC:579,587,639`) | 201 × 81 = **16,281 on the heap** | So ≈ 12 KiB of globals **plus** ≈ 16.3 KiB of heap, on top of code and stack, in one 64 KiB segment. `MaxCode` = 24,000 gives code room; the layout is what has to change. **This question is now closed — see §3.1, which measures the original's output directly.** What remains open is *which* of the two candidate fixes below to implement, and that is a decision for the session that does the work, not an unknown. Method: read the `.COM` write path in `TPSRC7` from `copyrt` and the `cdptr`/`cdbegpt`/`cdfoff` handling through the `MOV AX,dc` at `TPSRC7:1286`, and cross-check against `RESUME-TP3.md` §3.1 (program skeleton) and §3.4. `Resources/turbopascal3source/TP3/` is the ground truth by project rule; when the book and the disassembly disagree, the disassembly wins. Do **not** decide this from our `dataBase` constant and a plausible story. The two candidate fixes, for reference, once the question is closed: - *Match the original*: data at a fixed low base (`dataBase := rtSz`), code at a fixed higher base, code unbounded above. Cheap, faithful, one orientation change — but wastes the gap and needs a real data-window number. - *Dynamic data base*: emit global addresses as fixups and resolve them once the final code size is known (`FixUp` kinds already exist on the `Runtime` side, `.def` comment "FixUp, kind 2"). More correct, more invasive: every emitter that bakes a DS offset has to register it. Either way `t43_codewindow` and the `Ebyte` check must be re-baselined deliberately in the same commit as the change, with the reason in the message. **Cross-check worth making while you are in there:** the original's global overflow raises `$62` = 98 ("Memory overflow", `TPSRC7:1286-1288`), which is the *same* number our `Ebyte` uses, but `CALC.PAS`'s own banner warns about "Compiler Overflow - Error #99". If 98 and 99 are two different limits — data into code vs. something else — the banner is telling us which one CALC hits and which one the 'C' (.COM) workaround moves. That is cheap evidence and it bears directly on the fix. ### 3.1 Reference measurements (resolved 2026-10-17) Compiled on a real DOS (dosemu) with the original `TURBO.COM`, from a directory holding `CALC.PAS`, `CALC.INC` and `CALC.HLP`, using the `.COM` destination (`O`, `C`, `Q`, `C`, `Q`) exactly as `CALC.PAS`'s banner instructs. A six-line `HELLO` program was compiled the same way as the baseline. The two artifacts are sitting in `Resources/TP3/` for analysis and are deliberately **not committed** — they are derived from the proprietary `TURBO.COM`; the recipe above regenerates them, and the numbers are what this document keeps. **The compiler's own statistics line:** | | HELLO | CALC | |---|---|---| | lines | 6 | 1,763 | | **code** | 0005 paragraphs = **80 B** | 040B paragraphs = **16,560 B** | | code free | 0D26 par = 53,856 B | 0920 par = 23,360 B | | **data** | 0002 paragraphs = **32 B** | 02F0 paragraphs = **12,032 B** | | data free | 0FD9 par = 64,912 B | 0CEB par = 52,400 B | | stack min / max | 16,384 B / 655,360 B | 16,384 B / 655,360 B | | `.COM` file | 11,410 B | 27,901 B | **Image layout (both files share it):** - File offset `0` is a 3-byte entry jump `E9 4A 2C` → address `0x2D4D` (= file `0x2C4D`; the `.COM` offset-to-address bias is **0x100**, the same constant as this port's `LoadBias`). So the fixed TP3 runtime is file `[0, 0x2C4D)` = **11,341 bytes**, identical in both programs. - At `0x2D4D`: `call TU_PARAGRAPHS`, then the 20-byte program header (`options 0006`, `cs 0813`, `ds 11D7`, …), then `mov bp,sp`, `call TU_TAILLE_CODE`, the code-size word (`0x4092` for CALC = 16,530, i.e. the reported 16,560 minus the 30 bytes of preamble), the `SAUT_PRINCIPAL` address (`0x2D6D` in both), and `jmp PRINCIPAL`. - **Typed constants live in the code stream**, exactly as `RESUME-TP3.md` §3.14 says. Located byte-for-byte in `CALC.COM`: `FXMax = 'G'` (`47`) at `0x2D70`; `Numbers : set of Char = ['0'..'9']` as 32 bytes with `FF 03` in bytes 6-7; `XPOS` as the seven words `03 00 0E 00 19 00 24 00 2F 00 3A 00 44 00` at `0x2D91`; and `StandardfunctionNames` (the nested function's `string[6]` array) as length-prefixed `'ABS'`, `'SQRT'`, … around `0x4700`. **Consequences for this port:** 1. **Q1 is answered — and the first reading was wrong.** The original does **not** put the data area below the code in one segment. `pc` is a **CS** offset (`0x2D7C`) and `dc` is a **DS** offset (`0x0240`): `TPSRC1`'s `initmem` computes `DS = CS + [inline+6]` and puts the heap/stack above the DS data area. Code and data live in **separate segments**, so neither can grow into the other. CALC's code is **16,560 bytes**, which the old fixed 4 KiB window could not emit; that was a port artefact, not a TP3 limit. 2. **`MaxCode = 24,000` is not the binding constraint for the compile goal.** 16,560 + 3,907 (our current runtime) = 20,467, comfortably inside 24,000. The old `dataBase` alone had to move; `t43_codewindow` was the fixture that policed it and has been re-baselined (§3.2). 3. **The §3 data budget is confirmed, not inferred.** Reported data is 12,032 B against a computed 11,760 B for `Sheet` alone (147 × 80 B `CellRec`), so the 80-byte `CellRec` sizing is right and the rest (`Line`, `FileName`, the file variables, scalars) accounts for ~272 B. 4. **TP3's memory model has code and data in separate address spaces** (`cs 0x0813` vs `ds 0x11D7` in the header, and separate "free" figures). The inline `initmem` parameters measure **CS size `06E0h` (28,160 B)**, **DS size `0315h` (12,624 B)**, and **heap/stack `0400h` (16,384 B)**, with `A000h` as the maximum (640 KiB, in paragraphs). A `.COM` still uses separate CS/DS segments; the original proves it. 5. **The runtime is 11,341 bytes** in the original. Ours is 3,907 B. It explains where the original's fixed `pc := $2D7C` / `dc := $0240` came from, and why `TPSRC7:81-82` looks the way it does. **Feasibility.** The original's total CS+DS+heap/stack is 28,160 + 12,624 + 16,384 = 57,168 B, inside the 640 KiB DOS arena because the segments are separate. Our boot harness is flat, so the equivalent properties are: **code-only `.COM`**, globals outside the file at a fixed high image offset, and `initmem` zeroing them. That is what §3.2 implements. It is enough to **compile** CALC; running it with the original 16,384-byte minimum stack plus the 16,281-byte `Pages` heap is a separate capacity question (the fixed data base leaves ~20,478 B between the data end and the stack, not 32,665), and is explicitly deferred — the stated goal is the compiler verdict. **Still open from this experiment:** whether *memory-mode* compilation of CALC reports "Compiler Overflow #99" as the banner warns. That is one more run of `TURBO` (step 1 of the banner, `R` instead of the `.COM` path) and it is the only remaining piece of the 98-vs-99 evidence. **Not resolved by these measurements:** the meaning of the two header words after `ds` (the book calls them `code` and `globales`; they read 730/39 for HELLO and 1760/789 for CALC, which match neither the reported code/data nor each other). They are *not* the code and data sizes, so the book's field list should not be trusted here — the statistics line and the file layout above are the authoritative numbers. ### 3.2 Layout landed (2026-10-22) The implementation is a **flat-segment equivalent of the original's CS/DS separation**, not a literal second segment: the boot harness loads one segment and the INT 21h stdin descriptor lives in it, so a separate DS would require re-basing every absolute data reference in the compiler and runtime. The properties that matter for the compile goal are preserved: | property | original | this port after the change | |---|---|---| | `.COM` contents | runtime + program code (CS), no data | runtime + program code, no data | | entry | `E9 rel16` over the runtime | `E9 rel16` over the runtime + 16-byte header | | globals | separate DS, offsets from `dc` | fixed image `DataStart = 8000h` (memory `8100h`) | | globals zeroed | DS data area | `initmem` zeroes `[hdrDS,hdrHeap)` | | code limit | CS capacity | `MaxCode = 24,000` (and `CodeMax = 6F00h` before the stdin descriptor) | | data limit | DS capacity | address space below the stack | | heap/stack | above DS | above `hdrHeap`, down from `SP=FFFEh` | `Linker.LinkSize` is now `ImageBytes()`, so the file ends at `hdrCS`; it is not padded to the data base. `comimage.find_header` locates the header from the entry jump (`target - 16`) and requires `hdrDS = 8100h`, `hdrHeap > hdrDS`, and `hdrCS - bias == file length`. `t43_codewindow` no longer measures the old window: with the data out of the way it reaches the **4,096-byte string pool** first and is re-baselined to **`ERR 99 5093`** (reported overflow, not silent truncation). The code window itself is pinned by the independent `.COM` checker and by every executed fixture. **Verification against `Resources/TP3/CALC.COM`:** ``` size 0x6CFD (27,901 B) entry E9 4A 2C -> program at 0x2C4D runtime in file 0x2C4D (11,341 B) program code 0x40B0 (16,560 B) inline initmem mode 0006, CS 0813, DS 11D7, CS size 06E0, DS size 0315, heap/stack 0400, max A000 ``` The file is runtime + code, exactly the shape we now emit; the data is in the separate DS described by the inline parameters. Our CALC image will be smaller in runtime (3,907 B instead of 11,341 B) but has the same code-only, high-data shape. The ladder moved **20,912 → 22,253** on this change; the next blocker is not layout but the untyped `file` declaration; that is fixed in the next rung. --- ## 4. Ordered stages There are two parallel tracks, and it is worth being explicit that they are parallel: - **The ladder track** (Stages 1-15) is front-end work. It advances the error position in CALC and needs no memory-layout change to be developed or verified — every feature can be exercised on a fixture far smaller than 4 KiB of code. - **The layout track** (Stage 16, §3) was the hard prerequisite for *emitting* the whole program once the front end accepts it. **It is DONE** (`v-TP3-CALC-LAYOUT`, §3.2); the ladder is now front-end-bound again. Ordering rule within the ladder: cheapest-and-most-independent first, then the ladder order (each stage removes the blocker the previous one exposed). ### Session state — where to resume Last touched 2026-10-22. The rungs listed below are landed (Stages 1-4, sets, enums, record types, array declarations, element/field access, file types, the calling convention and string value parameters, `Length`, the CRT identifiers, `Kbd`, `KeyPressed`, `var` parameters, the CASE labels, variable array subscripts, write field widths, WITH with n-dimensional subscripts, set-field assignment, string fields through DI, REAL constants, UpCase, run-time set membership, string fields in WRITE, real output, run-time set algebra, `Succ`/`Pred`, the original memory layout, the untyped file type, and the file control-block operations). The ladder has moved from the first typed constant at 7,791 to **22,706**, where `var Line : AnyString` is the first by-reference string parameter. The rung commits and their tags, in order (each is one rung with its own fixture, red proof, and — where the harness allows — execution under qemu and Exec86): | commit | tag | ladder after | |---|---|---| | `a2b09a9` | `v-TP3-CALC-LADDER` | `ERR 1 7791` (the measuring device) | | `7eec556` | `v-TP3-CALC-INCLUDE` | `ERR 1 7791` (include does not move it: it is at 9404) | | `c40d29b` | `v-TP3-CALC-TYPEDCONST` | `ERR 102 7972` | | `eed46b2` | `v-TP3-CALC-SETS` | `ERR 41 8098` | | `a7d06cd` | `v-TP3-CALC-ENUMS` | `ERR 102 8362` | | `7be2276` | `v-TP3-CALC-RECORDS` | `ERR 1 8705` | | `123193d` | `v-TP3-CALC-ARRAYS` | `ERR 102 9202` | | `2a31b35` | `v-TP3-CALC-ELEMACCESS` | `ERR 102 9202` (element access) | | `14dbcdb` | `v-TP3-CALC-FILES` | **`ERR 102 10350`** (inside `CALC.INC`) | | `018bf0c` | `v-TP3-CALC-PARAMSLOTS` | `ERR 102 10350` (slot sizes; the rung does not move yet) | | `6b86175` | `v-TP3-CALC-PARAMLIST` | `ERR 102 10350` (call sites size each argument) | | `2b90a8d` | `v-TP3-CALC-STRPARAM` | `ERR 41 10405` (`Length(S)`) | | `45baf26` | `v-TP3-CALC-LENGTH` | `ERR 41 10753` (`ClrScr`) | | `14ce590` | `v-TP3-CALC-CRT` | `ERR 41 10927` (`Read(Kbd, Ch)`) | | `4c165e1` | `v-TP3-CALC-KBD` | `ERR 102 12083` (`NormVideo`) | | `89c6ca6` | `v-TP3-CALC-CRT2` | `ERR 41 12672` (`KeyPressed`) | | `033eb8e` | `v-TP3-CALC-KEYPRESSED` | `ERR 41 12901` (`IBMCh`'s `var` parameter) | | `5eb82c4` | `v-TP3-CALC-VARPARAM` | `ERR 1 13052` (comma-separated CASE labels) | | `8a265e7` | `v-TP3-CALC-CASELIST` | `ERR 102 14195` (variable array subscript) | | `b41c669` | `v-TP3-CALC-ARRVAR` | `ERR 1 14330` (write field width) | | `099f8fd` | `v-TP3-CALC-WIDTH` | `ERR 102 15026` (2-D subscript + `with`) | | `a559843` | `v-TP3-CALC-WITH` | `ERR 102 15131` (set assignment to a field) | | `21ef4e1` | `v-TP3-CALC-SETFIELD` | `ERR 102 15201` (string field through DI) | | `a2e5e69` | `v-TP3-CALC-STRFIELD` | `ERR 102 15272` (REAL field) | | `0946e80` | `v-TP3-CALC-REALCONST` | `ERR 41 15911` (`Upcase`) | | `17f7f7c` | `v-TP3-CALC-UPCASE` | **`ERR 102 18267`** (real output) | | `203f77e` | `v-TP3-CALC-SETOPS` | `ERR 98 20912` (layout limit reached) | | `ff05141` | `v-TP3-CALC-LAYOUT` | `ERR 1 22253` (untyped file, initially misread as a function) | | `ed9e790` | `v-TP3-CALC-UNTYPEDFILE` | `ERR 41 22280` (`assign`) | | *(this commit)* | `v-TP3-CALC-FILEOPS` | **`ERR 102 22706`** (VAR string parameter) | - **Code HEAD** is the layout rung (`v-TP3-CALC-LAYOUT`). Nothing is pushed. The terminal-config work (`v-TP3-TERMCFG`) and the editor key map (`v-TP3-EDITORKEYS`) are non-ladder commits in the same period and are documented in `TERMINAL.md`. - **The measuring device:** `tests/fixtures/calc_ladder.tsv` + `tests/calc_ladder.sh`, wired into `run_all.sh` as `CALC ladder`. The row may only move forward; every move is recorded in the tsv next to the blocker. - **Stage 2 (`{$I file}`) is landed.** `ExpandIncludes` resolves includes as a pre-pass (not in `Skip`, because `DeclaresProc`'s lookahead rewinds `srcPos`), `Compiler.SetSource` gives the compiler the source path so an include resolves beside its including file, and the errors are TP3's own: 90 file-not-found, 96 illegal nesting. Fixtures `t56_include` (OK, and its `.out` proves the included procedure runs), `t57_include_missing` (ERR 90), `t58_include_nest` (ERR 96). `TextBuf.InsertStr` is the new bulk, capacity-checked paste. - **Stage 3 (`^X`/`#nn`) and Stage 4 (scalar typed constants) are landed.** `RdConst` reads `^M` and `#nn`, `ParseAtom` accepts them as literals, and `DefConst` accepts `name : Type = value` for a scalar type and keeps the declared class. Fixtures `t60_charcaret` and `t59_typedconst`. The scalar typed-constant red proof is the interesting one: dropping the class keeps the compile matrix green at the same `OK 65 4` and only execution sees `71` for `'G'`. - **Sets, increment 1, are landed** — the hybrid type, the constructor, set typed constants, and compile-time `in`. Fixture `t61_setconst` (`.out` TRUE FALSE TRUE TRUE), and the storage decision is recorded in Stage 6: a typed constant's bytes are emitted in the code stream behind a `jmp` (as string literals already are), addressable through `DS` because a `.COM` has one segment — no `seg cs`, which is the single divergence from the original. Still `ENoLib`: run-time `in`, set *variables*, `+`/`*`/`=`/`<>`, assignment. - **Enumerations are landed (Stage 5)** — ordinals 0..n-1, one-byte type, bounds carried so `set of Attributes` sizes to one byte. Fixture `t62_enum`. `RdConst` now resolves a named constant, which set literals and `case` labels need. - **Array TYPES, array VARIABLES and array TYPED CONSTANTS are landed (Stage 8, declarations half)** — multi-range array types with row-major sizing stored in an `ArrRec` (`Sheet` = 11760), globals of array type, and `XPOS : array[…] of Integer = (…)` emitted code-resident like a set. Fixture `t64_arrays` (compile-only because it has no `.out`; its ~16 KB image now fits the raised 0x7000-byte read). `t14` re-baselined: the array DECLARES now; the refusal moved to element access. - **File TYPES and the control-block OPERATIONS are landed (Stage 13).** `text`, `file of T`, and untyped `file`; a file variable is our control block (`FileVarSize` = 132: handle, mode, 128-byte NUL-terminated name). The operations `Assign`/`Reset`/`Rewrite`/`Close`/`IoResult` are runtime entries using real DOS `INT 21h`; `t88_fileops` runs the missing-file path under both qemu and Exec86, and `rt_exec` pins the name copy. Data `Read`/`Write` with a leading file argument is the remaining file increment and is still loud. File types were what carried the compiler **inside `CALC.INC`**: the whole `var` block passed and the `{$I}` expanded, so the rung jumped from 9202 to **10350**. - **String VALUE parameters are landed** (TP3's whole-slot copy). Three commits: slot sizes (`v-TP3-CALC-PARAMSLOTS`), the parameter list on the proc symbol (`v-TP3-CALC-PARAMLIST`), and the copy + `size > 2` for strings (`v-TP3-CALC-STRPARAM`). Fixture `t67_strparam` proves value semantics: the callee mutates `S[1]`, the caller's string is unchanged. This moved the rung from 10350 to **10405**. - **`LENGTH` is landed** — the first string builtin FUNCTION, loaded inline from the string's length byte (no runtime entry). Fixture `t68_length` (5/3/0). The rung moved 10405 → **10753**. - **The CRT group has started: `ClrScr` and `GotoXY` are real** `INT 10h` builtins (clear the window + home; set the cursor from a 1-based x,y), fixture `t69_crt`. `LowVideo`, `NormVideo`, `ClrEol` and `Delay` are registered but **refused with `ENoLib`** — loud, not a silent no-op. **Exec86 now provides `INT 10h`** (cursor tracked, screen calls no-ops, everything else faults), so `t69_crt` runs under BOTH oracles; removing that facility would have left the CRT group with only one execution oracle. The rung moved 10753 → **10927**. - **`Read(Kbd, Ch)` and `ClrEol` are landed.** `KBD` is a predefined console TEXT file; a READ's leading file argument is dropped when it is `Kbd` (the runtime's read entries already read the console) and is a loud `ENoLib` for a real file variable. `ClrEol` is a real `INT 10h` call (AH=03 reads the cursor, AH=06 blanks to column 79). Fixtures `t70_kbd` (executed with a `.in`), `t69_crt` extended. The rung moved 10927 → **12083**. - **`NormVideo`/`LowVideo`/`Delay` are landed, and a parser bug behind them was fixed.** LowVideo/NormVideo set a one-byte attribute that `ClrScr`/`ClrEol` load at run time (the console writer is INT 21h teletype and has no attribute channel; the clears do, so the setting is observable, not a no-op). `Delay` is TP3's calibrated busy `LOOP`. Along the way: a **`repeat` body was one statement, not a sequence** — `repeat a; b until c` was ENoSemi at the first `;`; `t12_repeat` only ever had single-statement bodies. Fixed and pinned by `t71_repeat_seq`. Exec86 also gained opcode `C6` (`MOV r/m8,imm8`), which the prologue now uses. The rung moved 12083 → **12672**. - **`KeyPressed` is landed; the rung moved 12672 → 12901.** It is `INT 21h AH=0B` (AL=FFh/0, masked with `AND AX,1` - not `AND AL,1`: AH is still 0Bh after the interrupt, and the boolean test would read 0B00h). bootcom answers it from its input buffer and Exec86 from a one-byte peek buffer, so a status call never consumes the character the next read wants. Fixture `t72_keypress`, executed under both oracles. - **The ladder now stops at `ERR 41` at `12901`** — `procedure IBMCh(var Ch: Char)`. This is the `var`-parameter rung, and it needs the real thing: today a `var` parameter is **silently recorded as a value parameter** ("recorded as value in this milestone"), which is exactly the §7 failure mode. It is masked here only because `DupTest` rejects the parameter name `Ch` for shadowing the global `Ch`; ISO Pascal allows that shadowing, so fixing DupTest alone would turn CALC's next rung into a silent miscompile. Do both together: pass addresses, and let inner declarations shadow outer ones (duplicates at the SAME level stay errors). - **One surprise on resuming:** the working tree was found holding the nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the `locFree := 0FFFEH` / pre-decrement-base revert), i.e. a mutation from a `tests/nonvacuity.sh` run that the trap did not restore — presumably killed before the restore. It was reverted with `git checkout -- shell/Compiler.mod` before anything was measured. Worth knowing: the mutation's whole shape is byte-for-byte the same for INTEGER locals, so the compile matrix would have been green and only `t40_readln` (and later CALC) would have noticed. - **Verified baseline, run on the code HEAD above:** `./tests/run_all.sh` → **OVERALL: ALL PASS**, compile matrix **87/87**, CALC ladder **1/1** at `ERR 1 22253`, terminal config **4/4**, 78 `.COM` linked and **76 executed under both qemu and Exec86** (`t64_arrays` remains compile-only because it has no `.out`). `tests/nonvacuity.sh`: **121 ok, 0 failed**. The runtime is 3,907 bytes (wrintw, real_from_int, real_add, real_sub, real_mul, real_div, real_cmp, real_int, real_fmt, real_val, real_write, set_op, set_cmp, set_copy; the golden was re-blessed) and `tests/rt_exec.py` passes **275/275**, including the MBF arithmetic/output/parse cases and eleven set-operation cases. `check_realconst.py` pins the measured 6-byte Real operand bytes. - **`tests/nonvacuity.sh`:** re-run after this commit's cases (the set membership bit among them); see the commit message for the count. It is not in the default `run_all.sh` and must be run **manually** after touching any check. - **Still present and deliberately untracked:** `Resources/TP3/CALC.COM` and `Resources/TP3/HELLO.COM`, the two reference binaries used for §3.1. They are derived from the proprietary `TURBO.COM`; do not stage them. The recipe and the numbers are in §3.1, which is what this document keeps. - **Corpus for later, already in the tree (untracked):** `Sources/Dezip/` holds 5,887 files, of which **4,919 are `.PAS`** — plus 122 `.EXE`, 53 `.OBJ`, 26 `.TPU` and a SWAG collection (`swag9702`, `swag9708`, `swag-pascal-master`, …). `Sources/` is a user directory and is **never staged**; it is here as test material only. Two things it enables, neither of which this plan currently has: 1. A **population metric** — how far does the compiler get across thousands of *real* programs, not one. That is a far stronger regression signal than the CALC ladder alone, and it would have caught bug classes that a single hand-written fixture cannot. 2. **Dialect triage** — not all of it is TP3. Expect TP1/TP2/TP4, DOS interrupt units, and units (`.TPU`) this compiler has no linker for. Any batch driver must report "rejected" as a *result*, not as a failure, or the corpus will look like a catastrophe rather than a baseline. Do this **after** the ladder, not before: measure the baseline once, so later stages can be shown to move it. A first pass needs a batch driver that feeds `compiletest` a file list (it already reads filenames from stdin, one per line) and tallies `(verdict, errno)` — cheap, and it turns the corpus into a number the moment it exists. - **Cheap evidence still missing (Q6):** one memory-mode run of `TURBO` in dosemu to see whether CALC reports "Compiler Overflow #99" as its banner warns. The `.COM` path succeeds; the memory path was never tried. - **The one decision not to defer past the first sitting:** which candidate fix to implement for §3 — match the original's orientation, or a dynamic data base with fixups. Everything else in the stages below is mechanical. ### Next session — the concrete plan The ladder is at `ERR 102 18267`, **`Write(Value:FW:DEC)` — real output and arithmetic**. Everything below is ordered by what the ladder will hit next, not by the original stage numbers (the measurements have reordered them repeatedly). Each item is the same shape as the rungs already landed: one focused change, one fixture that is red before and green after, an execution proof where the image fits, and a commit. 1. **String VALUE parameters — DONE** (the rung moved 10350 → 10405; the blocker is now `Length(S)`, a builtin). TP3 puts a value parameter's whole size on the stack (`TPSRC7` `tstscal`), so a `string[n]` parameter is `n+1` bytes. The three steps landed as separate commits/tags: `v-TP3-CALC-PARAMSLOTS` (slot sizes + reverse layout), `v-TP3-CALC-PARAMLIST` (the procedure symbol carries `parmFirst`/`parmCount`; call sites size each argument), and `v-TP3-CALC-STRPARAM` (the caller copies `` into the slot; `EmStrBlob` + `EmPushStrSlot`, a DS-relative byte loop, no ES dependency). Fixture `t67_strparam`: the callee mutates `S[1]` and the caller's string is unchanged, so it is a copy and not an address. `var` parameters proper are item 6. 2. **The rest of the declaration part**, if the rung is still there: local `type`/`const`/`var` inside a nested function (`CALC.INC:767-778`, the enum-indexed array of strings), `Succ`/`Pred`/`Ord`/`Chr`. 3. **File OPERATIONS — control block DONE** (`v-TP3-CALC-FILEOPS`): `Assign`, `Reset`, `Rewrite`, `Close`, `IoResult` are runtime entries over real DOS `INT 21h`; `t88_fileops` checks the missing-file path under both oracles. **Still open:** `EndOfFile` and `read`/`write` with a leading file argument, which need text-mode newline/`^Z` and typed-file block I/O. 4. **Array subscripts and `with` — DONE** (`v-TP3-CALC-ARRVAR`, `v-TP3-CALC-WITH`): n-dimensional subscripts use per-dimension strides (the last varies fastest), the element's address rides in DI, and `with` resolves fields against a scope stack, parking a DI base in a data slot. Constant-index elements stay symbol+offset. Field reads/writes one or two bytes wide work; wider fields are the next item. 5. **Pointers and the heap** (`^T`, `New`, `Dispose`, `Buffer^[i]`), 4-byte (offset then segment); the heap must hold `Pages` (16,281 bytes). 6. **`var` parameters by reference — DONE** (`v-TP3-CALC-VARPARAM`). The slot holds an address; loads and stores go through it; READ into one pushes the pointer. `DupTest` now rejects only same-scope duplicates, so a parameter may shadow a global. `t73_varparam` executes all the address paths. 7. **6-byte `real`** — literals, `+ - * /` and the six comparisons, `and`/`or` on results, integer conversions, `Str`/`Val` with widths and decimals, and `ln`/`exp`/`abs`/`sqrt`/`sqr`/`sin`/`cos`/`arctan`/`int`. 8. **The string builtins** (`length`, `copy`, `delete`, `insert`, `str`, `val`) and named string constants. 9. **The CRT identifiers — DONE** (`ClrScr`/`GotoXY`/`ClrEol`, `LowVideo`/`NormVideo`, `Delay`, `Read (Kbd, Ch)`, `KeyPressed`), with `INT 10h` in Exec86 and `AH=0Bh` in Exec86 and bootcom. A running CALC still needs the attribute to reach ordinary text, which is a later question. 10. **Recursion, `forward`, nesting** — `Fact` (direct self-recursion) and `Expression`/`Factor` (mutual via `forward`, the definition repeating only the name). 11. **The `case` labels — DONE** (`v-TP3-CALC-CASELIST`): comma-separated lists, and ranges now really test `lo <= AX <= hi` (they used to require `AX = lo = hi`, so `1..5` matched nothing). `t74_caselist` pins both. Then the two that are *outside* the ladder and gate the goal as stated: 12. **The layout change (§3) — DONE** (`v-TP3-CALC-LAYOUT`). The `.COM` is code-only; globals are at image `8000h` and `initmem` zeroes them. CALC's 16,560 bytes fit the 24,000-byte code buffer, and `t43_codewindow` is re-baselined to the string-pool overflow it now reaches (`ERR 99 5093`). 13. **Write field widths — DONE for integers** (`v-TP3-CALC-WIDTH`, entry 20 `wrintw`). **Still open:** `Real:width:decimals` (CALC's `Value:FW:DEC`, which waits for real support) and widths on CHAR/BOOLEAN/STRING, which are loud today. 15. **Record-field writes — SET, STRING and REAL CONSTANTS are DONE** (`v-TP3-CALC-SETFIELD`, `-STRFIELD`, `-REALCONST`). The 6-byte MBF representation, integer constants and six-byte copies are in; real ARITHMETIC, real function results and real formatting remain item 7. 16. **`UpCase` + run-time `in` — DONE** (`v-TP3-CALC-UPCASE`). UpCase folds a CHAR constant and maps a value; `e in set` works with a run-time element against literals, typed constants and data sets; a string field is readable/writable through DI; Exec86 gained D0-D3. 17. **REAL arithmetic, IN PROGRESS — THE NEXT RUNG is still 18267.** The core is being ported from TPSRC2/3's own MBF library (the measured 6-byte format in CALC-PORT.md), not re-invented: * **DONE: `real_from_int`** (`v-TP3-MBF-FROMINT`, entry 21). ABI: source address at [BP+6], destination at [BP+4]; TPSRC3 `intreal` in AL/AH/BX/DX. 13 rt_exec cases against the measured table. * **DONE: `real_add`/`real_sub`** (`v-TP3-MBF-ADD`, entries 22/23). ABI: [BP+4] is a 12-byte block (a then b); the result overwrites a. TPSRC2 `readd`/`resub`, one shared core, the reference's scratch bytes and the AX/BX/DX + CX/SI/DI convention. 16 rt_exec cases (alignment, cancellation, signs, carry out, the exponent-overflow STC path). Two encoder traps: `XOR [mem],reg` is 30h not 32h, and FCML rejects `OR AL,AL`. * **DONE: `real_mul`** (`v-TP3-MBF-MUL`, entry 24). TPSRC2 `remult` with `testexp` inlined; ten rt_exec cases. Two jump bugs found: rel8 overflow to the far zero/return targets (JccNear/JmpNear plus a FixUp range guard now), and JccNear's condition polarity. * **DONE: `real_div`** (`v-TP3-MBF-DIV`, entry 25). TPSRC2/3 `rediv`, sharing `testexp`'s tail and `remult`'s `rm_noadj`/`rm_zero`/`rm_ret` exits. Fourteen rt_exec cases, including 5/2 = 2.5 and 42/6, the one quotient below 1 (where the five-byte loop normalises by a left shift). Three traps found: - the div core's first label was `rd_core`, which add/sub already used; `LblOff` returned the FIRST spelling, so `real_div` called `readd` and every "quotient" was a sum. `M` now refuses a duplicate label. - TPSRC's `SUB.B AH,remul21a` must be SUB, not SBB: in the carry-out path CF is 1, and SBB made 42/6 two ulps low. The normal compare path has CF=0, which hid it in 13 of 14 cases. - the runtime outgrew the probe scaffold: rtdrv.s/rt_exec.py moved their records and blocks up 0x200 (HDR=0x0A00, blocks 0x0B30+). * **DONE: `real_cmp`** (`v-TP3-MBF-CMPINT`, entry 26). TPSRC3 `recmp`. ABI: [BP+4] is the same 12-byte block as the arithmetic entries; nothing is written; the result is AX = -1/0/+1, cmpstr's convention, so the compiler has ONE comparison arm for reals and strings. The reference's core keeps the flags; the last step is the untested flag-to-AX conversion. Different signs are decided by RCL on a's high word (the sign lands in CF); two negatives invert the magnitude result with CMC. Thirty rt_exec cases. The harness gained an observable AX: rtdrv.s stores every call's AX at D_AX (0x0AA6), so a case can dump it. Until now cmpstr's return sign was only checked by whole programs. * **DONE: `real_int`** (`v-TP3-MBF-CMPINT`, entry 27). TPSRC3 `reint`, in place at [BP+4]. It is NOT integer-valued: `Int(real)` returns a REAL, so the result stays six bytes. The reference works directly on the packed bytes (no hidden-bit setup): it builds a mask by shifting 1s right by `exponent - 129` and ANDs the mantissa, which clears the fraction and leaves the sign bit and the implicit leading 1 alone. Exponents >= 0A8h (2^39) are already integral and return unchanged; <= 80h give zero. Eighteen rt_exec cases, including 12345.75 -> 12345 and 2^40 unchanged. * **DONE: `real_fmt`** (`v-TP3-MBF-CONV`, entry 28) and **`real_val`** (entry 29). TPSRC3's `fmtreal` and `ascreal`, with their helpers (`realdec`, `decround`, `fdig*`, `m10`, `rrange`, the fltdec table) transcribed. `real_fmt`: [BP+4] real, [BP+6] destination, CX = (decimals << 8) | width; writes a length-prefixed, left-padded field and returns its length. DEC >= 25 selects the reference's scientific mode and derives the decimals from the width. `real_val`: [BP+4] ASCIIZ source, [BP+6] destination; returns AX = the first unparsed offset, or FFFFh on a hard error (destination untouched). Ten format and thirty parse exact-byte cases. Traps: - `LdBxDiOff`/`LdDxDiOff` were `8B 5F/57`, i.e. **[BX+off]**, not [DI+off]; only the exact output showed it. - `StDataImm` is a WORD store. Zeroing the one-byte exponent counter also zeroed the first fltdec byte (1e0), so every negative-exponent parse divided by zero. Added `StDataByte`. - `rd_core` is the post-testexp add core: its caller must set `resign`. The parser's digit add left the previous result's sign there, corrupting every digit after the first. - `rc_fmt`'s DEC>=25 path used `SUB DL,AL` (28 C2) instead of `SUB AL,DL` (2A C2). * **DONE: real output wired** (`v-TP3-CALC-REALWRITE`, entry 30 `real_write`). `Write(Value:FW:DEC)`, `Write(Value:FW)` and the default `Write(real)` push the real's ADDRESS (or DI for a field), the width and the decimals, then call real_write; TP3's defaults are width 18 and decimals FFFFh, which selects the scientific mode. The entry formats into D_FMTBUF, pads and prints. `t84_realwrite` is the end-to-end fixture. The same rung makes a string FIELD addressable in a comparison: `if Sheet[UFX,UFY].Contents <> '' then' pushes the left address before parsing the right, so DI survives; pinned by `t83_strfield_cmp`. The ladder moved **18267 -> 19847**. * **DONE: run-time SET ALGEBRA** (`v-TP3-CALC-SETOPS`). `set_op` (entry 31: intersection/union/difference over `size` bytes), `set_cmp` (entry 32: equality/strict-subset/strict-superset mask) and `set_copy` (entry 33) are in Runtime.mod. The compiler pushes set ADDRESSES, chooses the byte count from the typed set (an inline literal is the default 32-byte `set of Char`; a typed field wins and the literal's first bytes are used), and allocates one scratch data buffer per set expression. `+`, `-`, `*`, `=`, `<>`, `<`, `<=`, `>`, `>=` and set-expression assignment all work. `Succ`/`Pred` were the next blocker and are now builtins too. Fixtures `t85_setops`/`t86_succ`. * **DONE: THE §3 LAYOUT** (`v-TP3-CALC-LAYOUT`, see §3.2). The `.COM` is code-only, globals live at image `8000h` and are zeroed by `initmem`, and the 4 KiB window is gone. CALC moved **20912 -> 22253**; the new blocker is a **function declaration**, `function Exist(FileN : AnyString): boolean;`, not a layout limit. * **STILL TO COME:** function declarations/returns, then `Str`/`Val` wiring, real expressions (a 6-byte temporary/value model), real comparisons and real function results. * **HEADROOM:** the runtime is 3,907 bytes and the probe's scaffold starts at HDR=0x1100 with RT_BASE=0x0190, so the ceiling is 3,952 bytes - only about 45 bytes left. The next runtime addition must move HDR (and the probe's data block with it); the boot read allows a .COM up to 0x6F00, so there is room below the input at 0x7000. * **Exec86** needed the FE group (INC/DEC r/m8) and CMC/CLC/STC/LAHF/ SAHF for the formatter; t84 is the first fixture that executes the real conversion under the interpreter, and it cross-checks qemu. 14. **Raise the execution harness's read — DONE (`v-TP3-MBF-CONV`).** `tests/exec/bootcom.s` now reads **72 sectors (36,864 bytes)** into `1000:0000`, one sector per INT 13h call with the loop state pushed around the call (the BIOS may clobber every register; POP does not touch CF), and copies `COPY_BYTES = 0x7000` down to `0100`. The input descriptor moved to `0x7000`, so the image may reach `0x6F00` (**28,416 bytes**) while staying below the boot sector at `0x7C00`. CALC's measured `.COM` is **27,901 bytes**, which now fits. `t64_arrays` remains compile-only only because it has no `.out`, not because of the read. **Two things settled this session, not to re-litigate:** - **The typed-constant storage decision** (Stage 6): a typed constant's bytes are emitted in the code stream behind a `jmp`, addressed through `DS`; no `seg cs`, because a `.COM` is one segment. Sets, `XPOS` and (later) the enum-indexed string array all use it. - **The byte-checker's clause F** (`check_8086.py`) cannot assert "sweeps clean ⇒ pure code" over a region that holds code-resident constant DATA; fixtures that emit such data are listed in `CODE_RESIDENT_CONST` and the clause is skipped for them. **Still open from the plan (§6):** Q6 (one memory-mode `TURBO` run in dosemu to see whether CALC reports overflow #99), and the run-time capacity note in §3.2. The layout decision is closed. The per-stage entries below are the historical record of how each landed. **Stage 1 — the ratchet (§1). DONE.** `tests/calc_ladder.tsv` + `tests/calc_ladder.sh`, wired into `run_all.sh`; proved red against 7792 before it was set to 7791. It also resolved Q4 in favour of staging copies in `tmp/calc_ladder/` rather than teaching the compile matrix about companion files. **Stage 2 — `{$I file}` include. DONE.** Implemented as a **pre-pass, not in `Skip`**, and that is the one design point worth recording. `Skip` is called by lookahead routines (`DeclaresProc`, `PeekKw`, `KwAhead`) that save and restore `srcPos`; splicing inside `Skip` runs once during the lookahead and again for real, and the second pass sees the first splice's text and reports error 96 (and a splice grows the buffer, which a rewind cannot undo). So `ExpandIncludes` resolves every include in one forward scan *before parsing*, and the parser — including `DeclaresProc`'s scan for a `PROCEDURE` keyword, which must see a procedure that lives only in the include — sees one finished buffer. The scan recognises what the lexer does (a quoted literal, then star-paren comments, then brace comments, then a brace directive), so `{$I}` written inside a comment stays inert (t56 depends on it). - `{$I }` is spliced at the directive (buffer grows, `srcLen` updated) in the **expanded-buffer** coordinates the ladder already measures in; `{$I+}` / `{$I-}` remain the I/O-check toggle (`dirs.chk`, `CALC.INC:396,399`). - Path resolved against the **including file's** directory — the compiler now gets the source path through `Compiler.SetSource`, called by the three loaders. Extension defaults to `.PAS` when absent (TPSRC10 `kpasext`). - An unopenable file is **error 90** ("File not found", TPSRC10 `cdincl` `errb`) at the directive; a nested include is **error 96** ("Illegal nesting of include files", TPSRC10 `cdincl`). Both measured, not assumed. - `{$A-}` is in fact a real TP3 directive (TPSRC10:783, Absolute code) that TP3 accepts and this port ignores; the "not a TP3 directive" note was wrong. The rule that mattered is kept: an include can never fall through to the comment path. - Fixtures: `t56_include` (+`t56_include.inc`, red 41 → green OK, and its `.out` proves the included procedure *runs*), `t57_include_missing` (red OK → `ERR 90`), `t58_include_nest` (+`t58_include_a.inc`, red OK → `ERR 96`). Q4 resolved: the compile matrix needs no companion-file notion, because the `.inc` simply sits beside the `.pas` in `tests/fixtures/` and the include resolves there. - **The ladder does not move at this stage.** `CALC.PAS`'s include is at normalised `9404`, and the compiler stops at `7791` — the typed constant of Stage 4 — so the include is never reached for `CALC.PAS` yet. Stage 2 is pinned by t56/t57/t58 until Stage 4 lets the compiler get to it; the rung moving is a Stage 4 event. **Stage 3 — lexical `#nn` and `^X` character constants. DONE.** `RdConst` reads `^M` (the upper-cased character minus 64, so `^@` = 0 and `^[` = 27) and `#nn` (decimal, via `RdIntConst`, so `#$FF` works too). `ParseAtom`'s literal test had to admit `^` and `#` as well, or `write (^M)` in an *expression* would still be a syntax error — only a declaration goes through `RdConst`. Both spellings are marked as characters, so they are written by the char writer. `EofLine = ^M` (`CALC.PAS:113`) now parses. Fixture `t60_charcaret` (red: `ERR 41` at the `^M` in `write` → green; its `.out` starts with the raw bytes `0D 1B`, which is what proves they are characters and not the text "13"/"27"). **Stage 4 — typed constants: the SCALAR form is DONE; structured is deferred.** `DefConst` takes the `name : Type = value` spelling (TPSRC7's constant reader) and, for a scalar type, records the *declared class* on the symbol. Because the value still folds to an immediate, that is the whole payoff: `FXMax : Char = 'G'` becomes a `TChar` constant holding 71, so `write (FXMax)` prints `G` rather than `71`. Integer and Boolean take the same path, which also pulled in `RdConst` reading `TRUE`/`FALSE` (it could not before). Fixture `t59_typedconst` — and its red proof is worth noting: dropping the class (`cls := typCls` → `cls := cls`) leaves the compile matrix GREEN at the same `OK 65 4` and only execution sees `7142` where `G42` belongs. What is **not** done, deliberately, is a typed constant of a structured type: - A set (`Numbers : set of Char = ['0'..'9']`, `CALC.PAS:114`) or an array (`XPOS : array['A'..'G'] of Integer = (…)`, `137`) is an *initialised value with a layout*. This compiler has no initialised data — `InitMem` zeroes the data area — so accepting one and discarding the initialiser would be exactly the silent mis-compile §0.2 forbids. `DefConst` refuses a type bigger than a word with `ENoLib`, and **that is where the ladder now stops: `ERR 102` at 7972.** - So the plan's `7791 → 8036` was optimistic: sets (Stage 6) must be laid out before the enumeration at 8036 can be reached. The enum-indexed array of strings in a nested function (`CALC.INC:767-778`) needs the same storage decision, on top of sets, arrays and `string[n]` — it is the hardest single item in this plan and belongs to its own write-up, not this one. - The original keeps typed constants **in the code stream**, reached with `seg cs`, with the program jumping over them (`RESUME-TP3.md` §3.14); the decision between that and an init block copied at startup is still open, and is now the first thing the set/array typed-constant work must settle. ### Then, in ladder order **Stage 5 — enumerations. DONE.** `Attributes = (Constant,Formula,Txt,OverWritten,Locked,Calculated)`. Each enumerator becomes a constant holding its ordinal position (0..n-1); the TYPE is one byte (TP3 uses one below 256 values, `RESUME-TP3.md` §2) and carries its bounds `0..n-1` in the symbol, which is what `set of Attributes` sizes its base from — one byte, against 32 for `set of Char`. `ParseType` grew the `(` arm, `SymEntry` grew `lo`/`hi`, and `RdConst` now resolves a NAMED constant, so `[Constant, Formula]` and (soon) `case` labels over enumerators work. `Succ`/`Pred` over enums are a later milestone (they are the standard-function milestone's job, not the type's). Fixture `t62_enum`. Ladder: 8098 → 8362 (the enum AND `SetOfAttri = Set of Attributes`). **Stage 6 — sets. Increment 1 DONE; the rest is the next milestone.** Done: - **Type sizing** — TP3's hybrid layout (coeur-tp-ocr 4.7.2): only the whole bytes covering the base range are stored, element `e` is bit `(e MOD 8)` of byte `((e DIV 8) - (base DIV 8))`, high byte last. `set of Char` is 32 bytes, `set of 12..17` is 2. `elem` carries the origin (a multiple of 8). - **The constructor** `[a..b, c]`, and `[]`. - **Set typed constants** — `Numbers : set of Char = ['0'..'9']`. - **Compile-time `in`** — a constant element against a set whose bytes are in the image, answered as TPSRC2 `xsetin` does (AH > 255 → FALSE, else test the bit). Fixture `t61_setconst`, `.out` TRUE FALSE TRUE TRUE. - **The storage decision the plan demanded**, and it is the original's own: a typed constant's bytes are emitted **in the code stream behind a `jmp`**, exactly as string literals already are, and the name becomes addressable. The **divergence** from `RESUME-TP3.md` §3.14 is one line: the original had separate code and data segments and therefore prefixed `seg cs`; a `.COM` has one segment, so `DS` already covers the code area and no prefix is needed. The bytes are ordinary RAM, so a typed constant stays assignable, as TP3's static typed constants are. Still to do (all currently `ENoLib`, none of them silently accepted): - **Run-time `in`** — a variable element or a set variable. The bit arithmetic is the same, but it must be emitted, not folded. - **Set variables** — a 32-byte local/global (`DefVar` still refuses `size > 2` for a non-string) and their layout in the frame. - **`+` union, `*` intersection, `=`/`<>`, assignment, `[]` in context.** - **`set of `** — needs the enum base first (the current rung). Reference note: `Resources/COMPIL4/` (Wirth's Pascal-P4, `P.PAS` + `P41..P48`) was added as a cross-check. It is a **different** design — a fixed `setsize = 8`-byte bitset over `0..sethigh (63)`, with `in` as a built-in runtime opcode — so it informs the *algorithms* but not the representation. TP3's hybrid bytes remain the ground truth here, per the repo rule. Ladder: 8060 → 8237 (now `8098` → the enum). **Stage 7 — records. Record TYPES done; field selection is the next increment.** Done: the `record … end` arm of `ParseType`. Fields are non-compacted and juxtaposed (`RESUME-TP3.md` §2); each name in an `a, b : T` group is its own field at the running offset. Fields live in a table of their own (`FldRec`, keyed per record) because a field name is scoped to its record, not global. A type carries its field range in `SymEntry.fldFirst/fldLast`. `CellRec` comes out at exactly **80 bytes**, which is what §3 measured `Sheet` against, so the layout is pinned by `t63_record`'s `data=84`. This also required **subrange sizing**: `0..20` is one byte (both bounds in 0..255, `RESUME-TP3.md` §2), not the two this compiler used to give it. Not yet: **field selection** (`c.DEC`, `Sheet[FX,FY].CellStatus`) and `with`. A field access needs the expression record to carry a width (a field's size is not its variable's), which is a small `ERes` change plus the addressing; today it is a **loud error, not a wrong address**. Records as **array elements** come with the 2-D array in Stage 8 — `Sheet[FX,FY].CellStatus` is both. **Stage 8 — arrays at their point of use. Declarations DONE; element access next.** Done: multi-range array types (`array['A'..'G',1..FYMax] of T`), row-major with **the last index varying fastest** (`RESUME-TP3.md` §2); the shape is kept in an `ArrRec` (`n`, `lo[]`, `hi[]`, `elem`) and the symbol points at it (`SymEntry.arrIdx`), so indexing has everything it needs. Array **variables** (global, and of any size) are laid out, and array **typed constants** (`XPOS : array['A'..'G'] of Integer = (…)`) are accepted: their bytes are emitted code-resident, exactly like a set, each element little-endian. `Sheet` is 11760 bytes and `XPOS`'s 14 bytes are in the code stream, which is why `t64_arrays`'s `data=11764` is `Sheet` plus the two `@@T` temporaries. `t14` is re-baselined: the array declares now, and the refusal moved from the declaration (pos 83) to the subscript (pos 109). **Element and field access are DONE for constant indices.** An expression now carries its own WIDTH (`ERes.sz`, via `VarSize`), because an element's size is not its array's: a constant subscript folds `(index - lower bound) * element size` into the address and hands the element's size and class to the store, and `r.f` adds the field's offset and takes the field's size and class. The width is threaded through `LoadAtom`, `Assignment` and `IoCall`, and `Statmnt` (which builds an assignment target directly, not through `ParseVar`) had to set it too - that omission is what the first cut got wrong. Fixture `t65_array_access` (`v[1] := 11`, `v[3] := v[1] + v[2]`, `r.A`, `r.B`; `.out` 33 then 12), and `t14` is re-baselined from `ERR 102 109` to `OK 41 15`. **Not yet:** a VARIABLE index (`Sheet[FX,FY]`, `XPOS[Ch]`, `b[i]`) - scaled run-time addressing, including the second dimension - which is a loud `ENoLib` today, and `with`. Both wait on the same scaled-addressing work. **Two consequences for the harness, both now recorded:** - **`check_8086` clause F had to narrow.** It asserted "this region sweeps clean, therefore it is pure code, therefore no `0F`". A code-resident set or array constant breaks that premise - the data can decode cleanly and then present an `0F` that is a data byte. Fixtures that emit code-resident constants are now listed in `CODE_RESIDENT_CONST` and clause F is skipped for them; the runtime sweep (the backbone) and the execution oracles still cover the emitted instructions. - **The execution harness's read is raised.** `tests/exec/bootcom.s` reads 72 sectors and copies 0x7000 bytes; the input is at 0x7000, the image may reach 0x6F00, and the boot sector at 0x7C00 is never overwritten. CALC's 27,901 bytes fit; `t64_arrays` is compile-only because it has no `.out`, not for size. The remaining CALC execution gate is the layout change (§3), not the loader. **Stage 9 — pointers and the heap.** `^T` (4 bytes, offset then segment), `New`/`Dispose`, `Buffer^[i]` read/write, `Buffer^[0] := st` (assignment to a string through a pointer). The heap must hold ≥ 16,281 bytes for `Pages`, which is a §3 budget item, not a detail. **Stage 10 — `var` parameters, by reference.** `RESUME-TP3.md` §3.11: the caller pushes **SEG then OFFSET** (4 bytes), the callee reads `LES DI,[BP+off]`, and `ret imm` frees 2 bytes per value parameter and 4 per `var` parameter. Ten CALC routines need it (14 occurrences). **Also remove the silent by-value acceptance in `ProcFunc` in this same commit** — until the SEG:OFF push exists, `var` must `Err`, not quietly pass by value. **Stage 11 — `real`, 6-byte.** The largest single item, and the one to plan in its own write-up. - storage (declaring a `real` variable is `ERR 102` today), literals (`0.0`, `1.0`, `34.0`, `1.5`, exponentials). - `+ - * /` and the six comparisons over the 6-byte format. - `and`/`or` on the results (e.g. `(I > 0.0) and (I < 34.0)`). - integer↔real conversions. - `Str`/`Val` with widths and decimals (CALC's display and input path). - `ln`, `exp`, `abs`, `sqrt`, `sqr`, `sin`, `cos`, `arctan`, `int`. - a **recursive real function with a by-value real parameter** is the natural end-to-end test: `Fact` (`CALC.INC:747`), `Fact := I * Fact (I - 1)` (754). - Runtime: **nothing real-related exists** in the 20 current `RT_Entry` selectors. `TU_*` equivalents are in `TPSRC1`/`TPSRC2`. **Stage 12 — the string builtins.** `length`, `copy`, `delete`, `insert`, `str`, `val`, plus **named string constants** (`const s = 'abc'` is `ERR 102` today) and string-typed-constant arrays. Also revisit the data cost: each `+` currently reserves a fresh **256-byte** global scratch (`NewScratchStr`), which for CALC's concat sites buys ~2 KiB of the budget in §3 — it needs to be sized to the operand, or reused. **Stage 13 — files. Types DONE, including untyped `file`; operations next.** Done: `text`, `file of T`, and untyped `file` (`t87_untypedfile`). A file variable is our own control block (`FileVarSize = 132`: the DOS handle word, a mode word, and a 128-byte NUL-terminated filename), because this runtime does unbuffered `INT 21h` I/O where TP3 keeps a 128-byte buffer inside the variable - a recorded divergence. `file of T` parses its element type so the RECORD size a Read/Write must move is known. Fixture `t66_files`. Control-block OPERATIONS are done (`t88_fileops`): `Assign` copies the counted filename, `Reset`/`Rewrite`/`Close` issue DOS `INT 21h` open/create/close, and `IoResult` reads and clears the status. The boot harnesses have no filesystem, so both answer `AH=3Dh` with `CF=1, AX=2`; the fixture checks that error path under qemu and Exec86, and `rt_exec` pins the name copy. Not yet: `EndOfFile`, and `read`/`write` with a leading file argument (`writeln (MCFile, Line)`, `Readln (H, Buffer^[count])`). These need text-mode newline/`^Z` semantics and typed-file block I/O (`RESUME-TP3.md` §2). The next ladder blocker is not file data I/O, though: it is `var Line : AnyString`, a by-reference string parameter. **Stage 14 — the CRT identifiers.** `GotoXY` (25), `LowVideo`/`NormVideo` (16 + 16), `ClrEol` (10), `ClrScr` (6), `KeyPressed` (3), `Delay` (1), `Read (Kbd, Ch)`. There is no unit system, so these become builtins with runtime entries: `GotoXY` and `ClrEol` from the cursor position, video attributes, a `Delay` busy loop, `KeyPressed`/`Kbd` from `INT 16h`. A compiling CALC needs them *to resolve*; a *running* CALC needs them to be right, which is a later question and explicitly not this goal. **Stage 15 — recursion, `forward`, nesting.** Two distinct shapes, both in `Evaluate`: - **Direct self-recursion**, `Fact`: `Fact := I * Fact (I - 1)` (`CALC.INC:747,754`). The name must be visible inside its own body. - **Mutual recursion through `forward`**: `Expression` is declared `function Expression(var ExpressionCh : Char): Real; forward;` at `CALC.INC:742` and defined at `1036` — with the **parameter list commented out**, `function Expression{(var ExpressionCh : Char):Real;forward};` — while `Factor` calls it two ways (`CALC.INC:820,932`). So the definition may repeat only the name and must inherit the forward's parameters, result type and level; keep the `var`-ness intact (Stage 10). Today a self-call is `ERR 1` at the `(` (proved with a probe). Grouping a callee's parameters must agree with Stage 10's 2-vs-4-byte accounting, so land this after parameters — or land the symbol-visibility half first and gate the rest. **Stage 16 — the layout (§3, the layout track). DONE (`v-TP3-CALC-LAYOUT`).** The data area is no longer immediately above the code: globals live at image `8000h` (memory `8100h`), `initmem` zeroes them, and the `.COM` is code-only (`LinkSize = ImageBytes`). This emits CALC's measured 16,560 B of code inside the 24,000-byte code buffer and removes the 4 KiB window. `t43_codewindow` is deliberately re-baselined (`ERR 99 5093`, the 4 KiB string pool, not the old window); the layout itself is pinned by the independent `.COM` checker. See §3.2 for the CALC.COM comparison and the remaining run-time capacity note. --- ## 5. Capacity — the numbers that will bite | limit | value | CALC needs | verdict | |---|---|---|---| | `TextLimit` (source buffer) | 62,903 | `CALC.PAS` + `CALC.INC`, normalised = **62,415** | **488 bytes of slack** | | `MaxCode` | 24,000 | 16,560 B program + 3,907 B runtime = **20,467 B** | **OK, 3,533 B spare** (§3.2) | | code window | `CodeMax = 6F00h`, capped by `MaxCode` | 20,227 B | **OK**; no data gap (§3.2) | | data area | fixed image `8000h`, zeroed by `initmem` | **12,032 B** (measured, §3.1) | **OK for compile**; see the run-time cargo note (§3.2) | | stack | 16,384 min / 655,360 max | 16,384 min | matches the original's `minstk`/`maxstk` | | `MaxSym` | 3,000 | 47 routines + locals + params | **inferred** OK, verify | | `strOff`/`strLen` | `[0..255]` | 57 distinct string literals (67 occurrences) | OK | | `strPool` | 4,096 chars | ≤ 69 bytes each | OK | | heap | `hdrHeap = dc` to the stack | 16,281 for `Pages` | must be checked in §3's budget | | data scratch per `+` | 256 bytes/site | ~9 sites | trim (Stage 12) | The `TextLimit` number is the sharpest: a 62 KB program in a 62,903-byte buffer is not a design, it is luck. Raising `TextLimit` (and the `buf` array in `TextBuf.mod`) costs only compiler memory and should happen in the same commit as the include, because the include is what consumes the slack. --- ## 6. Open questions, each with a method - **Q1 — RESOLVED (§3.1, §3.2).** The original keeps code and data in **separate CS/DS segments**; `pc` and `dc` are offsets in different segments, not two ends of one segment. Our flat boot harness emulates the property with a code-only `.COM` and a fixed high global area zeroed by `initmem`; the 4 KiB window is gone and `t43_codewindow` is re-baselined. - **Q2.** Is `{$A-}` really a no-op for `TURBO.COM`? Check `TPSRC` for an `A` directive before treating it as ignorable. - **Q3.** `Pages = array[0..200] of string[80]` — confirm the element size is 81 and that `New (Buffer)` gets 16,281 contiguous bytes from the heap the way TP3's `New` does. - **Q4 — RESOLVED (Stage 1).** `run_compile_tests.sh`/`CompileTest.mod` still have no fixture-with-companion-file notion, and they do not need one: the ladder is a small dedicated script (`tests/calc_ladder.sh`) that stages `CALC.PAS` and `CALC.INC` as copies in `tmp/calc_ladder/`. That is also what makes the Stage 2 include resolve, since it opens the `.inc` relative to the including file. - **Q5.** Are range/index checks on by default for `TURBO.COM`? `CALC` sets neither `{$R}` nor `{$V}`; if the default is *on*, `TU_RANGE_CHECK` and `TU_INDEX_CHECK` are on the critical path for every index in CALC, not an optional hardening. `RESUME-TP3.md` §4 (compiler options) is the place to check. - **Q6 (cheap, one run in dosemu).** Does *memory-mode* compilation of CALC report "Compiler Overflow #99" as `CALC.PAS`'s banner warns? The `.COM` path succeeds (§3.1); the memory path is the only untested claim about which limit bites. Banner step 1 (`R`) is all it takes. - **Q7.** What the two header words after `ds` actually mean. They read 730/39 for HELLO and 1760/789 for CALC, matching neither the reported code/data sizes nor each other, so `RESUME-TP3.md` §3.1's field list is unreliable there. Only matters if we adopt the original's header shape; until then the statistics line and the file layout are authoritative (§3.1). --- ## 7. What not to do - Do not "support" a construct by parsing it and emitting nothing. That is the `var`-parameter bug and the `{$I}` bug, and both were green. - Do not put the data back immediately above the code, and do not pad the `.COM` to the data base: §3 solved the compile goal by keeping the file code-only and the globals at a zeroed high base. `t43_codewindow` is now a string-pool fixture; the layout is pinned by the independent `.COM` checker, not by that fixture. - Do not implement BCD reals, `Pos`, `Concat`, `Round`, `Trunc` or `goto` for this goal. CALC uses none of them. - Do not let this plan absorb `CALC-PORT.md`. Same program, different target, different session. --- ## 8. Pointers - `SUMMARY.md` — current state, the fixture tables, "Next steps". - `RESUME-TP3.md` — §2 types and layouts, §3.1 skeleton, §3.4 assignment, §3.11 parameters (the SEG:OFF push), §3.14 typed constants, §4 options. - `TP3-COMPILER.md` — what the compiler does today and why. - `Resources/turbopascal3source/TP3/TPSRC1-10` — the ground truth. - `Resources/coeur-tp-ocr/coeur-tp-03-d.txt` §4.7 — sets explained: the hybrid byte layout this port implements (4.7.2) and the memory order (4.7.3). - `Resources/COMPIL4/` — Wirth's Pascal-P4 (`P.PAS`, `P41..P48`), added as a cross-check. Its sets are a *fixed* 8-byte bitset over 0..63 with `in` as a runtime opcode, so it informs the algorithm, not the representation. - `Resources/TP3/CALC.PAS`, `CALC.INC`, `CALC.HLP` — the target. - `shell/tests/probe/README.md` — read before touching any emitter.