|
|
@@ -0,0 +1,321 @@
|
|
|
+# CALC-PORT — porting `Resources/TP3/CALC.PAS` to GNU Modula-2
|
|
|
+
|
|
|
+**Status: analysis only. Nothing has been ported. No code written for CALC yet.**
|
|
|
+
|
|
|
+Feasibility study, done 2026-10-05, kept here so the next session starts from
|
|
|
+measurements instead of re-deriving them. Everything below that is labelled
|
|
|
+*measured* was compiled or decoded on this machine; everything labelled
|
|
|
+*unknown* is a gap the next session has to close.
|
|
|
+
|
|
|
+The target is `/home/eric/bin/Modula2/Gm2/bin/gm2 -fiso`, i.e. the same
|
|
|
+toolchain `shell/` builds with, but running natively on Linux — not the 8086
|
|
|
+`.COM` path that `shell/Compiler.mod` implements. So nothing here constrains
|
|
|
+the TP3 compiler work; this is a separate program.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. What the program is
|
|
|
+
|
|
|
+`Resources/TP3/CALC.{PAS,INC,HLP}` — MicroCalc 2.0, a full-screen spreadsheet
|
|
|
+with its own formula parser, on-line help, save/load and print. `CALC.PAS:1-108` is the banner comment and the module index; the code starts
|
|
|
+at `CALC.PAS:108`.
|
|
|
+
|
|
|
+Stripping both comment styles (`{ }` and `(* *)`):
|
|
|
+
|
|
|
+| file | raw lines | non-blank code lines |
|
|
|
+|---|---|---|
|
|
|
+| `CALC.PAS` | 183 | 58 |
|
|
|
+| `CALC.INC` | 1582 | 1207 |
|
|
|
+| **total** | 1765 | **~1265** |
|
|
|
+
|
|
|
+47 routines. Largest:
|
|
|
+
|
|
|
+| lines | routine | what it is |
|
|
|
+|---|---|---|
|
|
|
+| 174 | `Factor` (`CALC.INC:765`) | the parser core: numbers, cell refs, ranges, the 10 built-in functions |
|
|
|
+| 136 | `UpdateCells` | redraw |
|
|
|
+| 101 | `GotoX` | horizontal cursor movement |
|
|
|
+| 78 | `GetPages` | `.HLP` paging for the help system |
|
|
|
+| 53 | `Print` | printer output |
|
|
|
+| 51 | `GetInt` | numeric input |
|
|
|
+| 36 | `Recalculate` | dependency walk |
|
|
|
+| 33 | `Expression` | parser, one level up from `Factor` |
|
|
|
+
|
|
|
+Only 6 external dependencies beyond plain Pascal, all from Turbo Pascal's
|
|
|
+`CRT` unit (`GotoXY` ×25, `KeyPressed`, `ClrScr`, `LowVideo`, `NormVideo`,
|
|
|
+`Delay`), plus `Read(Kbd,Ch)` for raw keys. **No inline assembly.**
|
|
|
+
|
|
|
+Two historical constraints in the source, both of which disappear in the port:
|
|
|
+`CALC.INC:749-751` warns that `Fact` will not compile with `TURBO-87.COM`
|
|
|
+without an 8087 coprocessor, and `CALC.INC:937-942` warns that `Sqrt`, `Sin`
|
|
|
+and friends are not implemented in the BCD compiler, so use `TURBO.COM`.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. The screen layer already exists
|
|
|
+
|
|
|
+`shell/Term.mod` + `shell/Posix.c` (raw termios keyboard, ANSI screen) cover
|
|
|
+essentially all of CALC's platform needs. Mapping is 1:1:
|
|
|
+
|
|
|
+| Turbo Pascal `CRT` | `Term.mod` (`Term.def`) |
|
|
|
+|---|---|
|
|
|
+| `ClrScr` | `ClrScr` |
|
|
|
+| `GotoXY(row, col)` | `GotoXY(row, col)` — same 1-based convention |
|
|
|
+| `Write` of char / string / integer | `PutCh` / `PutStr` / `PutCard` |
|
|
|
+| `LowVideo`, `NormVideo` | `Marked`, `Normal` |
|
|
|
+| `KeyPressed` | `Avail` |
|
|
|
+| `Read(Kbd, Ch)` | `GetCh` (blocking), `GetKey` (non-blocking) |
|
|
|
+| `Delay` | `Beep` |
|
|
|
+
|
|
|
+`LowVideo`/`NormVideo` are the *only* two intensities CALC uses — it never
|
|
|
+calls `HighVideo` — so `Marked`/`Normal` is an exact match, not an
|
|
|
+approximation.
|
|
|
+
|
|
|
+Arrow keys: `shell/Editor.mod:1388-1410` already decodes `ESC` sequences.
|
|
|
+The only new work is mapping them onto TP3's codes (`^E` up, `^X`/`^J` down,
|
|
|
+`^D`/`^M`/`^F` right, `^S`/`^A` left — see `CALC.PAS:170-175`).
|
|
|
+
|
|
|
+**Estimated cost of the whole platform layer: about an hour.**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. Measured Gm2 constraints
|
|
|
+
|
|
|
+This is the part that decides whether the port is mechanical. Run it:
|
|
|
+
|
|
|
+```sh
|
|
|
+sh calc-port/gm2-probes.sh # 13 probes, asserts the matrix below
|
|
|
+sh calc-port/gm2-probes.sh --bless # re-baseline, deliberately only
|
|
|
+```
|
|
|
+
|
|
|
+Measured on **gm2 (GCC) 16.0.1 20260325 (experimental)**, `-fiso -Wall`:
|
|
|
+
|
|
|
+| construct | verdict | what CALC needs it for |
|
|
|
+|---|---|---|
|
|
|
+| `c := 'A'` | **accept** | — |
|
|
|
+| `ARRAY [0..70] OF CHAR` | **accept** | `string[70]`, `string[100]`, `string[6]` |
|
|
|
+| `s := "ABS"` into `ARRAY [0..5] OF CHAR` | **accept** | the function-name table |
|
|
|
+| `ARRAY [0..20] OF INTEGER` | **accept** | subrange *bounds* are fine |
|
|
|
+| nested `PROCEDURE` + result type + `; FORWARD ;` | **accept** | **the parser** |
|
|
|
+| nested `FUNCTION` | **reject** | `Expression`, `Factor`, `Fact` |
|
|
|
+| `TYPE Dec = 0..20` (named shortsubrange) | **reject** | `DEC, FW : 0..20` |
|
|
|
+| `VAR d : 0..20` (inline shortsubrange) | **reject** | same |
|
|
|
+| `TYPE Col = 'A'..'G'` (CHAR subrange) | **reject** | `ColumnName`, and `Cells`' first index |
|
|
|
+| `ARRAY [Sf] OF …` (enumeration index) | **reject** | `StandardfunctionNames` |
|
|
|
+| `c IN {'0'..'9'}` (`SET OF CHAR`) | **reject** | `Numbers` |
|
|
|
+| `BITSET` + `INCL` + `n IN b` | **accept** | the replacement for `SET OF` |
|
|
|
+| `(c >= '0') AND (c <= '9')` | **accept** | the replacement for `set of Char` |
|
|
|
+
|
|
|
+Exact diagnostics are in the script's output; the notable ones:
|
|
|
+
|
|
|
+- nested `FUNCTION` → `expecting one of: «FORWARD» «END» «BEGIN» «CONST» «TYPE» «VAR» «PROCEDURE» «MODULE»`
|
|
|
+- shortsubrange → `syntax error, found «integer number»`
|
|
|
+- `TYPE Col = 'A'..'G'` → `syntax error, found «string»` (it lexes `'A'` as a *string*)
|
|
|
+
|
|
|
+**Cross-check that these are real and not a broken harness:** the entire
|
|
|
+working project in `shell/` uses **none** of the rejected constructs — 0 named
|
|
|
+subranges, 0 enumeration-indexed arrays, 0 `SET OF` declarations, 0 CHAR
|
|
|
+subranges, only `ARRAY [0..n] OF` (19× `ARRAY [0..255]`). `BITSET` appears
|
|
|
+only in `shell/Compiler.mod` (6×), where sets would be natural. The existing
|
|
|
+code has been written to live inside exactly these limits, which is consistent
|
|
|
+with them being real.
|
|
|
+
|
|
|
+### Workarounds, all mechanical
|
|
|
+
|
|
|
+| CALC | replacement | cost |
|
|
|
+|---|---|---|
|
|
|
+| `function Factor(VAR C: Char): Real` | `PROCEDURE Factor(VAR C: CHAR; VAR F: LONGREAL)` | ~30 call sites inside 174 lines; CALC already passes all its state through `var` params, so this is close to 1:1 |
|
|
|
+| `DEC, FW : 0..20` | `CARDINAL` | trivial; keep the range checks by hand, which the port arguably wants anyway |
|
|
|
+| `array['A'..'G', 1..21] of CellRec` | `ARRAY [0..6, 0..20] OF CellRec` + a column-letter↔index accessor | trivial, and it isolates every place the letter arithmetic appeared |
|
|
|
+| `StandardfunctionNames : array[Standardfunction] of string[6]` | `ARRAY [0..9] OF ARRAY [0..5] OF CHAR`, one constant per function | trivial |
|
|
|
+| `Numbers : set of Char` | `IsDigit(c)` as two comparisons | trivial, arguably clearer |
|
|
|
+| `CellStatus : set of Attributes` | `BITSET` + `INCL`/`EXCL` | trivial |
|
|
|
+| `with Sheet[i,j] do` (8 blocks) | qualify: `Sheet[i,j].Field` | mechanical, ~350 lines touched |
|
|
|
+| `^M`, `^[`, `^E`, … | `CHR(13)`, `CHR(27)`, … | trivial |
|
|
|
+| `file of CellRec` | 80-byte manual records | see §4 — verifiable |
|
|
|
+| `Real`, `Str(v:w:d)` | `LONGREAL` + a formatter to write | see §5 — the real cost |
|
|
|
+
|
|
|
+None of these is a redesign.
|
|
|
+
|
|
|
+### Open question about Gm2
|
|
|
+
|
|
|
+Whether these constructs are legal in a **definition module** is *unknown*:
|
|
|
+compiling a fresh standalone definition module failed with `the file
|
|
|
+containing the definition module … cannot be found`, even inside `shell/`
|
|
|
+where the toolchain demonstrably works, so the test could not be set up. If
|
|
|
+shortsubranges and enumeration-indexed arrays *are* legal there, phases 0-1
|
|
|
+below get easier. Worth ten minutes to resolve.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. The `.MCS` file format — measured, and `CALCDEMO.MCS` is an oracle
|
|
|
+
|
|
|
+`Resources/TP3/CALCDEMO.MCS` is a real worksheet saved by the original. That
|
|
|
+makes the file format *measurable* rather than guessable, which is unusual
|
|
|
+and valuable.
|
|
|
+
|
|
|
+**Record size.** 11760 bytes ÷ 147 cells (`'A'..'G'` × 1..21) = **exactly 80
|
|
|
+bytes**, no remainder.
|
|
|
+
|
|
|
+**Layout**, read off the bytes:
|
|
|
+
|
|
|
+```
|
|
|
++0 1 byte SET OF Attributes bit n = nth of
|
|
|
+ Constant, Formula, Txt,
|
|
|
+ OverWritten, Locked, Calculated
|
|
|
++1 71 bytes string[70] length byte, then 70 data bytes
|
|
|
++72 6 bytes Real Turbo Pascal short real
|
|
|
++78 1 byte DEC 0..20
|
|
|
++79 1 byte FW 0..20
|
|
|
+```
|
|
|
+
|
|
|
+Confirmed by the status histogram over all 147 records: 69 × `Txt`, 68 ×
|
|
|
+`Txt+OverWritten`, 2 × `Txt+Locked`, 5 × `Constant+Calculated`, 3 ×
|
|
|
+`Constant+Formula+Calculated`; `DEC` is 2 in 145 records and `FW` is 10 in 124.
|
|
|
+
|
|
|
+**The 6-byte Real format, decoded and cross-validated** against five cells
|
|
|
+whose contents are known to be 1, 2, 3, 4, 5:
|
|
|
+
|
|
|
+| cell | contents | bytes +72..+77 |
|
|
|
+|---|---|---|
|
|
|
+| D4 | `1` | `81 00 00 00 00 00` |
|
|
|
+| E4 | `2` | `82 00 00 00 00 00` |
|
|
|
+| F4 | `3` | `82 00 00 00 00 40` |
|
|
|
+| G4 | `4` | `83 00 00 00 00 00` |
|
|
|
+| A5 | `5` | `83 00 00 00 00 20` |
|
|
|
+
|
|
|
+Fitting those: **value = (1 + mantissa) × 2^(byte0 − 129)**, where the mantissa
|
|
|
+is the 5 bytes after the exponent read **little-endian** as a fraction
|
|
|
+(3.0 → 0.5, 5.0 → 0.25). An all-zero record means 0.0.
|
|
|
+
|
|
|
+### Where the measurement disagrees with the book
|
|
|
+
|
|
|
+`RESUME-TP3.md` documents the same three things, from Colibri. Two agree, two
|
|
|
+conflict, and per this project's rule the measurement wins:
|
|
|
+
|
|
|
+| topic | `RESUME-TP3.md` | measured | verdict |
|
|
|
+|---|---|---|---|
|
|
|
+| `string[n]` | length in byte 0, size `n+1` | `+1` length, `+2..+72` data | **agree** |
|
|
|
+| records | fields juxtaposed, not compacted | contiguous, offsets sum to 80 | **agree** |
|
|
|
+| data files | records back to back | 147 × 80, no padding, no trailer | **agree** |
|
|
|
+| 6-byte Real | 1 exponent byte *"complément-à-128"*, then sign + 39-bit mantissa, LSB first | mantissa interpretation matches exactly; bias measures as **129**, not 128 | **conflict, off by one** — likely OCR (`RESUME-TP3.md:6` warns `OCR fréquemment faux`), but unconfirmed |
|
|
|
+| 2-D arrays | *"le dernier indice déclaré varie le plus vite"* | **the first** index (`'A'..'G'`) varies fastest | **conflict** — see below |
|
|
|
+
|
|
|
+The array-order conflict is the one that matters for a byte-exact writer.
|
|
|
+`CALC.INC` declares `Cells = array['A'..'G', 1..FYMax] of CellRec`, so the
|
|
|
+book predicts the order `A1, A2, …, A21, B1, …`. Reading `CALCDEMO.MCS`
|
|
|
+that way produces gibberish; reading it as `A1, B1, …, G1, A2, …` yields
|
|
|
+coherent English rows:
|
|
|
+
|
|
|
+```
|
|
|
+ 1 | This is Micr | | | Item one | Item Two …
|
|
|
+ 4 | | | | 1 | 2 …
|
|
|
+ 5 | 5 | | (B4>B8) | | …
|
|
|
+```
|
|
|
+
|
|
|
+So the original stores these row-major. Trust the file.
|
|
|
+
|
|
|
+### Two records are corrupt — and that is not a layout error
|
|
|
+
|
|
|
+Records 94 (D14) and 114 (C17) hold 6 bytes of high-entropy data
|
|
|
+(`7D 18 5B 48 FF 15`, `87 1D 44 10 F6 C3`) where the Real and `DEC`/`FW`
|
|
|
+should be, giving `DEC = 255`, outside `0..20`. Their formulas are
|
|
|
+`(LN(B6)/B10)` and `(B7/B10*100)`, and the cells they divide by are **empty or
|
|
|
+hold text** — so these are genuine floating-point garbage from dividing by
|
|
|
+zero, left in a demo file, not evidence of a wrong layout. The exact
|
|
|
+provenance is not nailed down. A reader must therefore tolerate `DEC > 20`.
|
|
|
+
|
|
|
+### What this buys the port
|
|
|
+
|
|
|
+Phase 3's oracle: **assert that the Modula-2 reader reproduces all 147 records
|
|
|
+of `CALCDEMO.MCS` byte-for-byte**, including the two garbage records. That is
|
|
|
+the only part of this port that can be proved against the original rather than
|
|
|
+against my own judgement. It also means the port reads and writes the
|
|
|
+original's own files.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. Where the effort actually is
|
|
|
+
|
|
|
+Not the parser. Two things:
|
|
|
+
|
|
|
+1. **The string layer.** `Anystring = string[70]`, plus `string[2]`,
|
|
|
+ `string[3]`, `string[6]`, `string[100]`, and every `Copy`, `Pos`, `Length`,
|
|
|
+ comparison and concatenation across 1200 lines. Modula-2 has
|
|
|
+ `ARRAY [0..n] OF CHAR` and no length. Widest blast radius by far.
|
|
|
+ **First move for the next session: check whether Gm2's own PIM modules
|
|
|
+ help** — `StrLib.mod`, `StrIO.mod`, `NumberIO.mod`, `StringConvert.mod`
|
|
|
+ all ship in `lib/gcc/x86_64-pc-linux-gnu/16.0.1/m2/m2pim/`. If they cover
|
|
|
+ it, this phase mostly disappears.
|
|
|
+2. **Real formatting.** `Str(v:FW:DEC)`, `Str(v:FW)` and the matching `Write`
|
|
|
+ forms — 8 sites (`CALC.INC:244, 246, 268, 270, 551, 553, 1103, 1105`).
|
|
|
+ **No real formatter exists anywhere in this project** — `Runtime.mod:57`
|
|
|
+ still emits the placeholder `"?REAL?"`. Turbo Pascal's exact behaviour
|
|
|
+ (rounding mode, and the scientific-notation fallback that the comment at
|
|
|
+ `CALC.INC:244` alludes to) is *unknown* and will need experiments against
|
|
|
+ the original.
|
|
|
+
|
|
|
+Precision note: `LONGREAL` gives more precision than TP3's 6-byte Real ever
|
|
|
+had, so formula results will differ in the last digits. That is a deliberate,
|
|
|
+documented difference, and it is testable by running the same formulas through
|
|
|
+both.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. Estimate and phasing
|
|
|
+
|
|
|
+Roughly 1100-1300 lines of Modula-2. Honest guess: **phases 0-1 are an hour or
|
|
|
+two**; **the whole thing is most of a day**, with the risk in §5 and not in
|
|
|
+the parser.
|
|
|
+
|
|
|
+| phase | ~lines | verified by |
|
|
|
+|---|---|---|
|
|
|
+| 0 — string type + real formatter + column↔index mapping | ~250 | hand-derived cases, **not** blessed from output |
|
|
|
+| 1 — `Init` `Clear` `Grid` `DisplayType` `GotoCell` `LeaveCell` `Update` `Move*` `UpdateCells` `GotoX` | ~250 | screen dumps through the existing `shell/tests/ptyharness.py` |
|
|
|
+| 2 — `Factor` `Expression` `NextCh` `Recalculate` | ~250 | hand-derived expected output per operator |
|
|
|
+| 3 — `Commands` `GetCell` `Format` `Save` `Load` | ~150 | **byte-exact against `CALCDEMO.MCS`** |
|
|
|
+| 4 — `Help` `GetPages` `Print` | ~180 | help-page dumps |
|
|
|
+
|
|
|
+### Suggested next step
|
|
|
+
|
|
|
+Spike phases 0 and 3 first. They are the two ends of the difficulty range —
|
|
|
+phase 3 because `CALCDEMO.MCS` makes it *provable*, phase 0 because the real
|
|
|
+formatter is the one genuinely unknown. A spike turns this estimate into a
|
|
|
+measurement, and it produces the first thing that can be asserted.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. Open questions
|
|
|
+
|
|
|
+1. **Real formatting** — what exactly does TP3's `Str(v:w:d)` produce, and
|
|
|
+ when does it fall back to scientific notation? Unknown; needs experiments.
|
|
|
+2. **Gm2 definition modules** — are shortsubrange types and enumeration-indexed
|
|
|
+ arrays legal there? If yes, phases 0-1 get easier. See §3.
|
|
|
+3. **`Gm2` PIM string modules** — do `StrLib`/`StrIO`/`NumberIO` remove the
|
|
|
+ need for a hand-written string type?
|
|
|
+4. **Unread routine bodies** — `GotoX` (101 lines), `UpdateCells` (136) and
|
|
|
+ `GetPages` (78) were sized but not read. A surprise would live there.
|
|
|
+5. **`.HLP` handling** — `CALC.INC:577-699` reads the help file through a
|
|
|
+ `^` pointer and `Readln`; needs replacing with an index. Not yet examined
|
|
|
+ in detail.
|
|
|
+6. **`Print`** — writes printer control codes to a file. Not examined.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. Method notes (learned the hard way, do not repeat)
|
|
|
+
|
|
|
+- **Writing probe modules.** Use a heredoc with a quoted delimiter, or
|
|
|
+ `printf "$fmt"` where `\n` is in the **format**. `printf '%s' "$src"` does
|
|
|
+ *not* expand `\n`: the module lands on one line and every diagnostic becomes
|
|
|
+ nonsense. This produced a whole round of false "Gm2 rejects…" conclusions.
|
|
|
+- **CHAR vs STRING literals in Modula-2.** `'A'` is a character, `"A"` is a
|
|
|
+ string. Writing `TYPE Col = "A".."G"` is a *string* range and fails — which
|
|
|
+ looks exactly like a missing feature.
|
|
|
+- **Probes must be verified on disk.** `wc -l` the file before compiling.
|
|
|
+ Silent mangling reads as "the toolchain lacks the feature".
|
|
|
+- **Linear byte scans are not an oracle.** Finding a bare `0F` in the runtime
|
|
|
+ blob looks like a 386-only opcode; the decode sweep in
|
|
|
+ `shell/tests/check_8086.py` shows it is inside a displacement. Same trap as
|
|
|
+ the bare `3D` anchor documented in `v-TP3-8086-LOWERING.md`.
|
|
|
+- **When a measurement contradicts the book, say so and trust the artefact.**
|
|
|
+ Two such conflicts turned up here (§4).
|