Explorar o código

docs: CALC-COMPILE.md — the plan to compile MicroCalc, with measured ground truth

Records the plan of record for making the TP3 compiler accept and compile
Resources/TP3/CALC.PAS. This is a documentation commit: no compiler change,
so nothing is re-baselined.

The load-bearing content is section 3.1, the reference measurements taken
from the original TURBO.COM compiled under dosemu (recipe in the section;
the derived .COM artifacts are deliberately not committed, being proprietary):

  CALC  1,763 lines   code 16,560 B   data 12,032 B   .COM 27,901 B
  HELLO     6 lines      code     80 B   data     32 B   .COM 11,410 B

These resolve the question that had been blocking: the original keeps data
*below* the program code, so its binding limit is data running into the code
base. This port has it inverted — dataBase := rtSz + 1000H caps code at
4 KiB — and CALC needs 16,560, so the port cannot emit it at all today.
That makes the layout a hard prerequisite rather than a late stage, and it
is written up as a track parallel to the front-end ladder.

Also measured from the .COM images: the fixed TP3 runtime is 11,341 B, the
COM offset-to-address bias is 0x100 (the same constant as our LoadBias), and
typed constants are code-resident, confirming RESUME-TP3.md 3.14 byte for
byte (FXMax 'G', the Numbers set, XPOS, StandardfunctionNames).

The two header words after ds are recorded as *not* meaning what the book
says: they read 730/39 for HELLO and 1760/789 for CALC, matching neither the
reported code/data nor each other. The statistics line and the file layout
are treated as authoritative instead, and the discrepancy is logged as Q7.

SUMMARY.md gains a pointer, since the next session should find the plan
without hunting for it.
Eric Streit hai 2 días
pai
achega
ce7e8cd6da
Modificáronse 2 ficheiros con 586 adicións e 0 borrados
  1. 569 0
      CALC-COMPILE.md
  2. 17 0
      SUMMARY.md

+ 569 - 0
CALC-COMPILE.md

@@ -0,0 +1,569 @@
+# 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   <errno>   <pos>
+```
+
+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).
+
+### 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 <name>}` 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.

+ 17 - 0
SUMMARY.md

@@ -2015,6 +2015,13 @@ independently-scanned inventory at all.
   the shifted table this project shipped, in full.
 - `shell/tests/run_all.sh` / `nonvacuity.sh` headers — what runs, in what
   order, and which breakage is supposed to turn which check red.
+- `CALC-COMPILE.md` — **the plan of record for compiling MicroCalc with the
+  TP3 compiler**: measured census of what `CALC.PAS` needs, the reference
+  measurements taken from the original `TURBO.COM` (code 16,560 B, data
+  12,032 B, the layout of the `.COM`), the staged roadmap, and the capacity
+  table. Read this before starting anything CALC-shaped.
+- `CALC-PORT.md` — a different target entirely: porting MicroCalc to native
+  GNU Modula-2 on top of `Term.mod`. Separate from the above.
 - `Resources/turbopascal3source/TP3/` — TPSRC1-10, the disassembled original.
   **This is the ground truth**; when our behaviour and the book disagree, the
   disassembly wins.
@@ -2022,6 +2029,16 @@ independently-scanned inventory at all.
 
 ## Next steps
 
+**CALC is now a separate, planned line of work — see `CALC-COMPILE.md`.** Its
+plan of record is a staged roadmap (ratchet → `{$I}` include → `#nn`/`^X` →
+typed constants → enums → sets → records → arrays → heap → `var` params →
+reals → string builtins → files → CRT → recursion → layout), plus the measured
+census of what `CALC.PAS` actually needs and the reference measurements from
+the original `TURBO.COM`. Two tracks run in parallel: the ladder (front-end,
+verifiable on small fixtures) and the layout (§3, the only thing that stops
+the compiler *emitting* a 16,560-byte program). Items 1-4 below are the
+string/robustness work and are independent of both.
+
 1. The rest of the string language. `v-TP3-STRVAR`/`v-TP3-STRREAD-ASSIGN` cover
    string variables, `s := 'literal'`, `writeln (s)`, `readln (s)`/`read (s)`
    and `s := t`; `v-TP3-STRINDEX` covers `s[i]` with a constant index,