Resources/TP3/CALC.PAS to GNU Modula-2Status: 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.
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.
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.
This is the part that decides whether the port is mechanical. Run it:
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:
FUNCTION → expecting one of: «FORWARD» «END» «BEGIN» «CONST» «TYPE» «VAR» «PROCEDURE» «MODULE»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.
| 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.
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.
.MCS file format — measured, and CALCDEMO.MCS is an oracleResources/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.
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.
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.
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.
Not the parser. Two things:
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.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.
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 |
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.
Str(v:w:d) produce, and
when does it fall back to scientific notation? Unknown; needs experiments.Gm2 PIM string modules — do StrLib/StrIO/NumberIO remove the
need for a hand-written string type?GotoX (101 lines), UpdateCells (136) and
GetPages (78) were sized but not read. A surprise would live there..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.Print — writes printer control codes to a file. Not examined.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.'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.wc -l the file before compiling.
Silent mangling reads as "the toolchain lacks the feature".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.