# 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. Everything below is measured against the tree at tag `v-TP3-INTERRUPT-RESTORE` (`2ae00d1`) unless it is marked **inferred** or **unknown**. --- ## 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 and it is currently shipping it twice: - `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}` is **silently swallowed as a comment**, so `CALC.PAS` without its include compiles green. Measured: `program p; {$I part.inc} begin halt end.` → `OK code=32 data=4`, with `part.inc` never opened. Both must be closed by implementation, and until then by an explicit `Err`. 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. **Proposal — `tests/fixtures/calc_ladder.tsv` (or a `run_compile_tests.sh` section):** run the compiler on a *copy* of `CALC.PAS` (plus, once it exists, `CALC.INC`) and assert the exact verdict: ``` calc_ladder ERR ``` Each stage updates the row; the row may only move forward. This is the same shape as `tests/fixtures/expected.tsv`, so it reuses the existing harness instead of inventing a second one. Two traps to write down next to it: - **`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 rungs (measured today, normalised positions): | position | blocker | |---|---| | 7791 | `FXMax : Char = 'G'` — typed constant (`DefConst`, `ENoSemi` 1) | | 8036 | `Attributes = (Constant,…)` — enumeration (`ERR 41`) | | 8060 | `SetOfAttri = Set of Attributes` (`ERR 102`, `ENoLib`) | | 8237 | `CellRec = record` (`ERR 102`, `ENoLib`) | --- ## 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` | **swallowed silently** | | typed constants (`X : T = v`) | `CALC.PAS:111,113,114,137`; `CALC.INC:777` | `ERR 1` | | enum type `(Constant,Formula,…)` | `CALC.PAS:120` | `ERR 41` | | subrange types `'A'..'G'`, `0..20`, `string[70]` | `CALC.PAS:117,119,130` | OK | | `set of Char`, `set of Attributes` | `CALC.PAS:114,121,127` | `ERR 102` | | set constructor `[a..b, c]`, `in`, `+`, `*`, `=`/`<>`, assignment | 47 × `in`, 6 × `+ [`, 2 × `*` | not implemented | | `record` + field select `Sheet[FX,FY].CellStatus` | `CALC.PAS:126` | `ERR 102` | | `with` | 13 sites | `ERR 102` (via `record`) | | 2-D array `array['A'..'G',1..FYMax] of CellRec` | `CALC.PAS:133` | one range only | | array indexed by an **enum type** | `CALC.INC:773,927` | `ERR 41` | | pointers `^Pages`, `New`, `Dispose` | `CALC.INC:579,587,639,690` | `ERR 41` | | `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` | `ERR 102` | | 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, `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)`) | | 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 is inverted This is not a feature; it is a **hard prerequisite**. CALC's code is 16,560 bytes (§3.1, measured) and this port refuses to emit more than 4,096. It has to be settled as one of the first things, not "before the fifth stage" — until it moves, every later stage can only be developed on fixtures a fraction of CALC's size, and the ladder cannot advance past the first few rungs. **What this port does today.** `pc` starts at `rtSz` (773) and `dataBase := rtSz + 1000H` (4869). Code occupies `[rtSz, dataBase)` and data occupies `[dataBase, …)`. The *data* sits above the *code*, so the enforced limit is **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 is not the constraint — `dataBase` is. **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.** The original keeps the data area *below* the program code, so the binding limit is data running into the code base — the exact opposite of this port, whose `dataBase := rtSz + 1000H` caps **code at 4 KiB**. CALC's code is **16,560 bytes**, so the port cannot emit it at all today; this is a hard prerequisite, not a nice-to-have. 2. **`MaxCode = 24,000` is not the binding constraint.** 16,560 + 773 (our runtime) = 17,333, comfortably inside 24,000. It is `dataBase` alone that must move, and the fix has to be an orientation/layout change rather than a bigger buffer — which is what `t43_codewindow` was built to police. 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). A `.COM` collapses them to one segment, so we have to choose one contiguous layout; the measurements bound what we must allow: **≥ 16,560 B of code and ≥ 12,032 B of data**, with 16,384 B of stack and a heap large enough for `Pages` (16,281 B, §3). 5. **The runtime is 11,341 bytes** in the original. Ours is 773 B. Not needed for this goal, but it explains where the original's fixed `pc := $2D7C` / `dc := $0240` came from, and why `TPSRC7:81-82` looks the way it does. **Feasibility, now checkable.** A `.COM` is one 64 KiB segment holding `[runtime][code][data]`, with the heap growing up and the stack growing down out of what is left. Using the measured figures: | | runtime | + code | + data | left for heap+stack | after `Pages` (16,281 B) | |---|---|---|---|---|---| | original | 11,341 | 16,560 | 12,032 | 25,603 | 9,322 for stack | | this port | 773 | 16,560 | 12,032 | **36,171** | **19,890** for stack | So the goal is arithmetically reachable, and the original *proves* it is reachable with 10,568 bytes less room than we have — our runtime is that much smaller. There is no capacity reason to doubt the layout fix; the only question is which of the two orientations to implement. The same table shows `MaxCode = 24,000` is fine with 7,440 to spare, and that `TextLimit`'s 488 bytes of slack (§5) is the one genuinely tight number in the whole budget. **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. --- ## 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) is the only thing that stops the compiler from *emitting* the whole program once the front end accepts it. It does not block the ladder, but it is a hard prerequisite for the goal as stated, and it changes `dataBase`/`t43_codewindow` — so it belongs to one commit of its own, with the re-baselining reasoned in the message. 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-17. **No compiler change has been made yet**; this line of work is still entirely at the planning stage, and everything below is the *pre*-CALC baseline that Stage 1 must not break. - **HEAD** `ce7e8cd` (this document). Nothing pushed; `origin/master` is behind. - **Verified baseline, run on this commit:** `./tests/run_all.sh` → **OVERALL: ALL PASS** (~74 s), and the compile matrix is **56/56**. Those are the numbers Stage 1's ratchet must leave intact. - **Not re-run this session:** `tests/nonvacuity.sh` (78 ok / 0 failed at the last run). 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 **Stage 1 — the ratchet (§1).** Small. Lands the measuring device before the work it measures. Green for the current blockers, and it must be *proved red* first (assert a deliberately wrong position, watch it fail, then set the real one). **Stage 2 — `{$I file}` include.** Small, self-contained, and a prerequisite: `CALC.PAS:150` *is* the program. - In `Skip` (`Compiler.mod:1315`), the `{$` path currently reads a letter and a sign and discards to `}`. Split it: `{$I+}` / `{$I-}` stay the I/O-check toggle (`dirs.chk`, used at `CALC.INC:396,399`); `{$I }` reads a file name and splices that file's text into the buffer at `srcPos`. - `{$A-}` (`CALC.INC:712`) is not a TP3 directive; being ignored is correct. But *unknown-but-evaluated* directives must not be the same code path as *include*: a directive we cannot honour is an `Err`, not a comment. - Design points: path resolved relative to the including file; a nesting cap; an unopenable file is an error; the splice grows `srcLen` and leaves `srcPos` moving forward so `pos` stays meaningful (see the ratchet trap in §1). - Fixture: a pair (`*.pas` + a companion `.inc`) — **check first that `run_compile_tests.sh` can stage a companion file**, which is an open question (§6 Q4). Prove red by the current behaviour: `program p; {$I part.inc} begin halt end.` compiles OK today. **Stage 3 — lexical `#nn` and `^X` character constants.** Small; both are `ERR` today. `#27` (decimal) and `#255`, `$FF` (hex works already), `^M`/`^E`/`^[`. Needed before `case` labels and `EofLine = ^M` can be read. Touch `RdConst`/`GetWord`. **Stage 4 — typed constants (`§3.14`).** The first ladder rung, and it recurs: `FXMax : Char = 'G'` (`CALC.PAS:111`), `EofLine = ^M` (`113`), `Numbers : set of Char = ['0'..'9']` (`114`), `XPOS : array['A'..'G'] of Integer = (3,14,…)` (`137`), and the hardest one, an enum-indexed **array of strings** in a *nested function* (`CALC.INC:767-778`): ```pascal type Standardfunction = (fabs,fsqrt,fsqr,fsin,fcos,farctan,fln,flog,fexp,ffact); StandardfunctionList = array[Standardfunction] of string[6]; const StandardfunctionNames : StandardfunctionList = ('ABS','SQRT','SQR','SIN','COS','ARCTAN','LN','LOG','EXP','FACT'); ``` which is then *read* with a variable enum index — `Length (StandardfunctionNames[sf])`, `copy (Formula, Position-1, l) = StandardfunctionNames[sf]` (`CALC.INC:927-928`) — so it exercises the initialiser form, enum indexing, controlled-max `string[6]`, and `length`/`copy` at once. - The original puts typed constants **in the middle of the code stream**, reached with `seg cs`, with the program jumping over them (`RESUME-TP3.md` §3.14). Our model has no initialised data: `InitMem` zeroes the data area and `hdrHeap = dc`. Either emit an init block and copy it at startup, or adopt the original's code-resident scheme. Decide and record the divergence. - `DefConst` (~3933) currently demands `=` via `ExpectDelim`; scalars, chars, sets and arrays each need their initialiser form. - Ladder: 7791 → 8036. ### Then, in ladder order **Stage 5 — enumerations.** `Attributes = (Constant,Formula,Txt,OverWritten, Locked,Calculated)`. 1 byte if fewer than 256 values, else 2 (`RESUME-TP3.md` §2). Ordinal values 0..n; this feeds `Succ`/`Pred` and `set of T`. Ladder: 8036 → 8060. **Stage 6 — sets.** Type sizing (1 bit per element, ≤ 32 bytes, hybrid bounds per `RESUME-TP3.md` §2), constructor `[a..b, c]`, `in` (bit test — 47 sites, the single most-used operator in CALC), `+` union, `*` intersection, `=`/`<>` against `[]`, and assignment. `set of Char` is a 32-byte value; decide inline vs a runtime helper. Ladder: 8060 → 8237. **Stage 7 — records.** Non-compacted field layout (`RESUME-TP3.md` §2), field selection as address arithmetic, `with` (13 sites), records as array elements. Ladder: past 8237, into the `var` block, then to the include. **Stage 8 — arrays at their point of use.** Two ranges `array['A'..'G', 1..FYMax] of T`, row-major with **the last index varying fastest** (`RESUME-TP3.md` §2); this is the repo's existing `t14` gap. Index types are subranges (`'A'..'G'`), so index scaling and the `Ebyte`/`dc` arithmetic have to agree with §3's layout. **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.** `text` and `file of CellRec`; `Assign`, `Reset`, `Rewrite`, `Close`, `EndOfFile`, `IoResult`; `read`/`write` with a leading file argument (`writeln (MCFile, Line)`, `Readln (H, Buffer^[count])`). DOS `INT 21h` open/create/read/write/close in the runtime, with text-mode newline and `^Z` semantics (`RESUME-TP3.md` §2). `{$I-}`/`{$I+}` (Stage 2) already gate the checking that `IoResult` reports. **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).** Re-orient the image so the compiler can emit **16,560 B of code** and hold **12,032 B of data** (both measured, §3.1), choosing between the two candidate fixes in §3 and re-baselining `t43_codewindow` in the same commit. This is the only stage that blocks the *goal* rather than the ladder. Then capacity (§5), which follows from it: `TextLimit` is 488 bytes from overflowing and should rise with the include stage. --- ## 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** (measured, §3.1) | enough in itself; `dataBase`'s 4,096 B window is the blocker (§3) | | code window (`dataBase − rtSz`) | 4,096 | ≥ 16,560 | **must change orientation** (§3) | | data area | unbounded past `dc` | **12,032 B** (measured, §3.1) | must be able to hold it under/over the code | | 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).** The data area sits below the program code; the binding limit is data running into the code base, and this port has it the other way round. Remaining as a *decision*, not an unknown: implement "match the original" or "dynamic data base", and re-baseline `t43_codewindow` in the same commit. - **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.** Does `run_compile_tests.sh`/`CompileTest.mod` support a fixture with a companion `.inc` (Stage 2's fixture, and `calc_ladder` needs a copy of `CALC.INC`)? If not, decide whether the ladder lives in the harness or in a small dedicated script. - **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 raise `dataBase`/`MaxCode` and call §3 solved; the orientation is the question, and `t43_codewindow` exists precisely to catch a silently corrupt image. - 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/TP3/CALC.PAS`, `CALC.INC`, `CALC.HLP` — the target. - `shell/tests/probe/README.md` — read before touching any emitter.