CALC.PASThe goal of this line of work is narrow and stated as a verdict:
shell/compiletestreturns OK forResources/TP3/CALC.PAS, andCmdCompilewrites 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.
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.
Compile first, run second. The gate is the compiler's verdict, not a
working spreadsheet. A construct may be incomplete only if it is loudly
incomplete (Err (ENoLib)); it may never be accepted and mis-compiled.
This project has already shipped that bug once. One of the two cases below
is now closed (Stage 2); the other is not:
var parameters are silently accepted and passed by value —
ProcFunc drops the var (DropB (MatchKey (tok)), comment "VAR
parameter recorded as value in this milestone"); writeln (g) after
q (g) prints 7, not 42. Ten CALC routines take var parameters.{$I CALC.INC} was silently swallowed as a comment, so CALC.PAS
without its include compiled green (program p; {$I part.inc} begin halt
end. → OK code=32 data=4, part.inc never opened). Closed at Stage
2: the include is expanded before parsing and an unopenable file is
error 90, not a comment.The var-parameter case is still open and must be closed by
implementation, or by an explicit Err until it is (Stage 10).
A 1,763-line program (182 + 1,581 lines) that stops on the 111th line gives no sense of progress unless "how far did we get" is a number in a test. Make it one.
Implemented (Stage 1, 2026-10-19) — tests/fixtures/calc_ladder.tsv plus
tests/calc_ladder.sh, run by run_all.sh as CALC ladder. Each row names a
program and gives the exact verdict in the same shape as
tests/fixtures/expected.tsv:
CALC.PAS ERR <errno> <pos>
The script stages a copy of the named program — and of the companions that
sit beside it, so CALC.INC is already there for Stage 2 — into
tmp/calc_ladder/, runs the freshly built compiler on it, and compares. It
does not build the compiler itself: it calls
tests/run_compile_tests.sh --build-only, which is the one recipe that
freshness-checks compiletest, so the ladder cannot become the second
staleness test that drifts from the first.
The copy is deliberate: the include (Stage 2) resolves relative to the
including file, so CALC.INC has to be beside CALC.PAS in the staged tree,
and the row then measures the compiler rather than an edit-in-place of
Resources/TP3. It is not a fixture in tests/fixtures/*.pas (which would
enter the compile/COM/exec matrices and need a second copy of CALC.PAS kept
in sync).
Each stage updates the row; the row may only move forward. Two traps are written next to the file itself:
pos is not a file offset. LoadFile collapses CRLF and lone LF to a
single CR, so pos indexes the normalised buffer. CALC.PAS is 11,241
bytes on disk and 11,059 bytes normalised; the reported pos 7791 is file
byte 7901. Every expected value in the ladder must be computed in
normalised coordinates. Once {$I} lands, the buffer is CALC.PAS with the
directive replaced by CALC.INC, so all downstream positions shift again —
which is exactly why this must be a computed number, not a remembered one.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) |
The include is at normalised 9404, after the current stop, so Stage 2
does not move the row (it is pinned by t56/t57/t58 instead); the first rung
that moves it is Stage 4, which removes the 7791 blocker and lets the compiler
reach the include and everything behind it.
Counts are from CALC.PAS + CALC.INC with comments and string literals
stripped.
| construct | where | status today |
|---|---|---|
{$I CALC.INC} |
CALC.PAS:150 |
done (Stage 2) — expanded before parsing; t56/t57/t58 |
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.
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).
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:
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.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.
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):
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.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.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:
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.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.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.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).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.
There are two parallel tracks, and it is worth being explicit that they are parallel:
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).
Last touched 2026-10-19. The front end now has its first CALC feature — the
{$I} include (Stage 2) — and the measuring device (Stage 1) under it.
ce7e8cd (the plan) plus the Stage 1 and Stage 2 commits. Nothing
pushed; origin/master is behind.tests/fixtures/calc_ladder.tsv +
tests/calc_ladder.sh, wired into run_all.sh as CALC ladder. The row is
CALC.PAS ERR 1 7791, and it was proved red first (row set to 7792 →
FAIL expected ERR 1 7792 / actual ERR 1 7791, rc 1) before the real value
was written back.{$I file}) is landed. ExpandIncludes resolves includes as a
pre-pass (not in Skip, because DeclaresProc's lookahead rewinds srcPos),
Compiler.SetSource gives the compiler the source path so an include resolves
beside its including file, and the errors are TP3's own: 90 file-not-found, 96
illegal nesting. Fixtures t56_include (OK, and its .out proves the
included procedure runs), t57_include_missing (ERR 90),
t58_include_nest (ERR 96). TextBuf.InsertStr is the new bulk,
capacity-checked paste. The ladder row does not move — the include is at
normalised 9404 and the compiler still stops at 7791 — which is expected and
pinned by the three fixtures instead.shell/Compiler.mod (the
locFree := 0FFFEH / pre-decrement-base revert), i.e. a mutation from a
tests/nonvacuity.sh run that the trap did not restore — presumably killed
before the restore. It was reverted with git checkout -- shell/Compiler.mod
before anything was measured. Worth knowing: the mutation's whole shape is
byte-for-byte the same for INTEGER locals, so the compile matrix would have
been green and only t40_readln (and later CALC) would have noticed../tests/run_all.sh →
OVERALL: ALL PASS, compile matrix 59/59, CALC ladder 1/1, 49 .COM
linked and 49 executed under both qemu and Exec86.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 — including, now, run_compile_tests.sh's
--build-only branch.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.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:
.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.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.Stage 1 — the ratchet (§1). DONE. tests/calc_ladder.tsv +
tests/calc_ladder.sh, wired into run_all.sh; proved red against 7792 before
it was set to 7791. It also resolved Q4 in favour of staging copies in
tmp/calc_ladder/ rather than teaching the compile matrix about companion
files.
Stage 2 — {$I file} include. DONE.
Implemented as a pre-pass, not in Skip, and that is the one design point
worth recording. Skip is called by lookahead routines (DeclaresProc,
PeekKw, KwAhead) that save and restore srcPos; splicing inside Skip
runs once during the lookahead and again for real, and the second pass sees the
first splice's text and reports error 96 (and a splice grows the buffer, which
a rewind cannot undo). So ExpandIncludes resolves every include in one
forward scan before parsing, and the parser — including DeclaresProc's scan
for a PROCEDURE keyword, which must see a procedure that lives only in the
include — sees one finished buffer. The scan recognises what the lexer does (a
quoted literal, then star-paren comments, then brace comments, then a brace
directive), so {$I} written inside a comment stays inert (t56 depends on it).
{$I <name>} is spliced at the directive (buffer grows, srcLen updated) in
the expanded-buffer coordinates the ladder already measures in; {$I+} /
{$I-} remain the I/O-check toggle (dirs.chk, CALC.INC:396,399).Compiler.SetSource, called by the three
loaders. Extension defaults to .PAS when absent (TPSRC10 kpasext).cdincl errb)
at the directive; a nested include is error 96 ("Illegal nesting of
include files", TPSRC10 cdincl). Both measured, not assumed.{$A-} is in fact a real TP3 directive (TPSRC10:783, Absolute code) that TP3
accepts and this port ignores; the "not a TP3 directive" note was wrong. The
rule that mattered is kept: an include can never fall through to the comment
path.t56_include (+t56_include.inc, red 41 → green OK, and its
.out proves the included procedure runs), t57_include_missing
(red OK → ERR 90), t58_include_nest (+t58_include_a.inc, red OK →
ERR 96). Q4 resolved: the compile matrix needs no companion-file notion,
because the .inc simply sits beside the .pas in tests/fixtures/ and the
include resolves there.CALC.PAS's include is at
normalised 9404, and the compiler stops at 7791 — the typed constant of
Stage 4 — so the include is never reached for CALC.PAS yet. Stage 2 is
pinned by t56/t57/t58 until Stage 4 lets the compiler get to it; the rung
moving is a Stage 4 event.Stage 3 — lexical #nn and ^X character constants. 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):
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.
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.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.
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)).Str/Val with widths and decimals (CALC's display and input path).ln, exp, abs, sqrt, sqr, sin, cos, arctan, int.Fact (CALC.INC:747), Fact := I * Fact (I - 1) (754).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:
Fact: Fact := I * Fact (I - 1)
(CALC.INC:747,754). The name must be visible inside its own body.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.
| 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.
t43_codewindow in the same commit.{$A-} really a no-op for TURBO.COM? Check TPSRC for an A
directive before treating it as ignorable.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.run_compile_tests.sh/CompileTest.mod still
have no fixture-with-companion-file notion, and they do not need one: the
ladder is a small dedicated script (tests/calc_ladder.sh) that stages
CALC.PAS and CALC.INC as copies in tmp/calc_ladder/. That is also what
makes the Stage 2 include resolve, since it opens the .inc relative to the
including file.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.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.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).var-parameter bug and the {$I} bug, and both were green.dataBase/MaxCode and call §3 solved; the orientation is the
question, and t43_codewindow exists precisely to catch a silently corrupt
image.Pos, Concat, Round, Trunc or goto for
this goal. CALC uses none of them.CALC-PORT.md. Same program, different target,
different session.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.