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.
The measurements were taken against the tree at tag v-TP3-INTERRUPT-RESTORE
(2ae00d1); the ladder and the stages have moved on since (the layout rung is
tagged v-TP3-CALC-LAYOUT, and §1's rung table and the "Session state" section
are the current figures).
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 rung (measured today, normalised position):
| position | blocker |
|---|---|
| 22706 | var Line : AnyString in GetFileName — VAR string parameters (ERR 102) |
Rungs behind it (now passed):
| was | blocker | done by |
|---|---|---|
| 7791 | FXMax : Char = 'G' — scalar typed constant |
Stage 4 (scalar) |
| 7972 | Numbers : set of Char = ['0'..'9'] — set typed constant |
sets, increment 1 |
| 8098 | Attributes = (Constant,…) — enum |
Stage 5 (enums) |
| 8362 | CellRec = record … end — record type |
Stage 7 (record types) |
| 8705 | Cells = array['A'..'G',1..FYMax], Sheet : Cells, XPOS |
Stage 8 (declarations) |
| 9202 | MCFile : file of CellRec — file type; the whole var block; {$I} |
Stage 13 (file types) |
| 10350 | procedure Center(S : AnyString) — a string value parameter |
calling convention, steps 1-3 |
| 10405 | Length(S) — the first string builtin |
LENGTH, inline |
| 10753 | ClrScr — CRT identifiers begin |
ClrScr + GotoXY (real INT 10h) |
| 10927 | Read(Kbd, Ch) — a READ with a leading file |
Kbd + ClrEol |
| 12083 | NormVideo/LowVideo/Delay |
attribute byte + busy loop; and the repeat-body parser bug fixed |
| 12672 | KeyPressed |
INT 21h AH=0B, in Exec86 and bootcom too |
| 12901 | var parameters |
by reference + DupTest shadowing (t73_varparam) |
| 13052 | comma-separated CASE labels | plus real CASE ranges (t74_caselist) |
| 14195 | variable array subscripts | scaled addressing, 1 byte or 2 (t75_arrvar) |
| 14330 | write field widths | wrintw, runtime 770 -> 878 (t76_width) |
| 15026 | WITH + n-D subscripts | row strides, fields via DI (t77_with) |
| 15131 | set field assignment | typed by the target; run-time in (t78_setfield) |
| 15201 | string field through DI | <len><chars>, truncated (t79_strfield) |
| 15272 | REAL constants and copies | measured MBF, 6-byte stores (t80_realconst) |
| 15911 | UpCase + run-time in |
set fields through DI in WRITE (t81, t82) |
| 18267 | Write(Value:FW:DEC) — REAL output |
real_write (t84) |
| 19847 | run-time set algebra / Succ / Pred |
set_op/set_cmp/copy (t85, t86) |
| 20912 | the fixed 4 KiB code window | original layout: code-only .COM, high zeroed data base (§3.2) |
| 22253 | var F : file; — the untyped file type in Exist |
optional of T; the parser reported ENoSemi at the semicolon |
Rungs ahead:
| position | blocker |
|---|---|
| 22280 | file operations (Assign, Reset, Rewrite, Close, IoResult) - done by the file control-block rung |
| 22706 | var Line : AnyString - a by-reference string parameter |
Corrections the measurements force, worth stating plainly because the stage list below was written before them:
9404,
past the stop, so Stages 3-4 had to happen before the include is ever reached.7791 → 8036 in one stage was optimistic. CALC.PAS:114,
Numbers : set of Char = ['0'..'9'], is a typed constant whose type is a
set, and it comes before the enumeration at 8036. Sets had to go first, and
now they have — the compiler stops at 8098, on the enumeration. The
measured ladder is the authority; the stage numbering below is the original
plan, not the dependency order.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) — scalar |
CALC.PAS:111 |
done (Stage 4) — declared class kept; t59_typedconst |
| typed constants — set | CALC.PAS:114 |
done (sets, increment 1) — code-resident bytes; t61_setconst |
| typed constants — array | CALC.PAS:137; CALC.INC:777 |
ERR 102 — needs initialised storage |
lexical ^M, #nn |
CALC.PAS:113 |
done (Stage 3) — t60_charcaret |
enum type (Constant,Formula,…) |
CALC.PAS:120 |
done (Stage 5) — ordinals; bounds carried; t62_enum |
subrange types 'A'..'G', 0..20, string[70] |
CALC.PAS:117,119,130 |
OK |
set of Char / set of <subrange> |
CALC.PAS:114 |
type + sizing done (hybrid bytes) |
set of <enum> |
CALC.PAS:121,127 |
sizing done (enum bounds → 1 byte for Attributes) |
set constructor [a..b, c], in (compile-time) |
t61_setconst |
done — folding only |
set in (run-time), +, *, =/<>, assignment, set variables |
47 × in, 6 × + [, 2 × * |
ENoLib — next sets increment |
record + field select Sheet[FX,FY].CellStatus |
CALC.PAS:126 |
type done (Stage 7) — CellRec = 80 bytes; field SELECT is next |
with |
13 sites | after field selection |
2-D array array['A'..'G',1..FYMax] of CellRec |
CALC.PAS:133 |
type + variable done (Stage 8) — Sheet = 11760 |
array typed constant XPOS : array[…] of Integer = (…) |
CALC.PAS:137 |
done (Stage 8) — code-resident |
array element access b[i] (constant index), r.f |
t14, t65 |
done — width on the expression |
array element access with a VARIABLE index Sheet[fx,fy], with |
13 with sites |
ENoLib — scaled addressing next |
| array indexed by an enum type | CALC.INC:773,927 |
ERR 41 |
pointers ^Pages, New, Dispose |
CALC.INC:579,587,639,690 |
ERR 41 |
string value parameters (S : AnyString) |
CALC.INC:17, … |
done — whole-slot copy; t67_strparam |
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 |
types done (Stage 13) — operations next |
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 (done, t68_length), 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)) — ClrScr/GotoXY done (t69_crt); the rest registered and loud |
| 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 was a hard prerequisite: CALC's code is 16,560 bytes (§3.1, measured) and the old port refused to emit more than 4,096. It is now resolved; the implementation is in §3.2. The paragraphs below are the historical diagnosis and the reference measurements, kept because they explain why the old fixed 4 KiB split was not a capacity problem but a layout one.
What this port did before the change. pc started at rtSz (773) and
dataBase := rtSz + 1000H (4869). Code occupied [rtSz, dataBase) and data
occupied [dataBase, …). The data sat immediately above the code, so the
enforced limit was 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 was not the constraint — dataBase was.
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:
pc is a CS
offset (0x2D7C) and dc is a DS offset (0x0240): TPSRC1's
initmem computes DS = CS + [inline+6] and puts the heap/stack above the
DS data area. Code and data live in separate segments, so neither can
grow into the other. CALC's code is 16,560 bytes, which the old fixed
4 KiB window could not emit; that was a port artefact, not a TP3 limit.MaxCode = 24,000 is not the binding constraint for the compile goal.
16,560 + 3,907 (our current runtime) = 20,467, comfortably inside 24,000.
The old dataBase alone had to move; t43_codewindow was the fixture that
policed it and has been re-baselined (§3.2).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).
The inline initmem parameters measure CS size 06E0h (28,160 B),
DS size 0315h (12,624 B), and heap/stack 0400h (16,384 B), with
A000h as the maximum (640 KiB, in paragraphs). A .COM still uses
separate CS/DS segments; the original proves it.pc := $2D7C / dc := $0240 came
from, and why TPSRC7:81-82 looks the way it does.Feasibility. The original's total CS+DS+heap/stack is
28,160 + 12,624 + 16,384 = 57,168 B, inside the 640 KiB DOS arena because the
segments are separate. Our boot harness is flat, so the equivalent properties
are: code-only .COM, globals outside the file at a fixed high image
offset, and initmem zeroing them. That is what §3.2 implements. It is enough
to compile CALC; running it with the original 16,384-byte minimum stack
plus the 16,281-byte Pages heap is a separate capacity question (the fixed
data base leaves ~20,478 B between the data end and the stack, not 32,665),
and is explicitly deferred — the stated goal is the compiler verdict.
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.
The implementation is a flat-segment equivalent of the original's CS/DS separation, not a literal second segment: the boot harness loads one segment and the INT 21h stdin descriptor lives in it, so a separate DS would require re-basing every absolute data reference in the compiler and runtime. The properties that matter for the compile goal are preserved:
| property | original | this port after the change |
|---|---|---|
.COM contents |
runtime + program code (CS), no data | runtime + program code, no data |
| entry | E9 rel16 over the runtime |
E9 rel16 over the runtime + 16-byte header |
| globals | separate DS, offsets from dc |
fixed image DataStart = 8000h (memory 8100h) |
| globals zeroed | DS data area | initmem zeroes [hdrDS,hdrHeap) |
| code limit | CS capacity | MaxCode = 24,000 (and CodeMax = 6F00h before the stdin descriptor) |
| data limit | DS capacity | address space below the stack |
| heap/stack | above DS | above hdrHeap, down from SP=FFFEh |
Linker.LinkSize is now ImageBytes(), so the file ends at hdrCS; it is not
padded to the data base. comimage.find_header locates the header from the
entry jump (target - 16) and requires hdrDS = 8100h, hdrHeap > hdrDS, and
hdrCS - bias == file length. t43_codewindow no longer measures the old
window: with the data out of the way it reaches the 4,096-byte string pool
first and is re-baselined to ERR 99 5093 (reported overflow, not silent
truncation). The code window itself is pinned by the independent .COM
checker and by every executed fixture.
Verification against Resources/TP3/CALC.COM:
size 0x6CFD (27,901 B)
entry E9 4A 2C -> program at 0x2C4D
runtime in file 0x2C4D (11,341 B)
program code 0x40B0 (16,560 B)
inline initmem mode 0006, CS 0813, DS 11D7,
CS size 06E0, DS size 0315,
heap/stack 0400, max A000
The file is runtime + code, exactly the shape we now emit; the data is in the
separate DS described by the inline parameters. Our CALC image will be smaller
in runtime (3,907 B instead of 11,341 B) but has the same code-only, high-data
shape. The ladder moved 20,912 → 22,253 on this change; the next blocker is
not layout but the untyped file declaration; that is fixed in the next rung.
There are two parallel tracks, and it is worth being explicit that they are parallel:
v-TP3-CALC-LAYOUT, §3.2); the ladder is now front-end-bound again.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-22. The rungs listed below are landed (Stages 1-4, sets,
enums, record types, array declarations, element/field access, file types, the
calling convention and string value parameters, Length, the CRT identifiers,
Kbd, KeyPressed, var parameters, the CASE labels, variable array
subscripts, write field widths, WITH with n-dimensional subscripts, set-field
assignment, string fields through DI, REAL constants, UpCase, run-time set
membership, string fields in WRITE, real output, run-time set algebra,
Succ/Pred, the original memory layout, the untyped file type, and the
file control-block operations). The
ladder has moved from the first typed constant at 7,791 to 22,706, where
var Line : AnyString is the first by-reference string parameter.
The rung commits and their tags, in order (each is one rung with its own fixture, red proof, and — where the harness allows — execution under qemu and Exec86):
| commit | tag | ladder after |
|---|---|---|
a2b09a9 |
v-TP3-CALC-LADDER |
ERR 1 7791 (the measuring device) |
7eec556 |
v-TP3-CALC-INCLUDE |
ERR 1 7791 (include does not move it: it is at 9404) |
c40d29b |
v-TP3-CALC-TYPEDCONST |
ERR 102 7972 |
eed46b2 |
v-TP3-CALC-SETS |
ERR 41 8098 |
a7d06cd |
v-TP3-CALC-ENUMS |
ERR 102 8362 |
7be2276 |
v-TP3-CALC-RECORDS |
ERR 1 8705 |
123193d |
v-TP3-CALC-ARRAYS |
ERR 102 9202 |
2a31b35 |
v-TP3-CALC-ELEMACCESS |
ERR 102 9202 (element access) |
14dbcdb |
v-TP3-CALC-FILES |
ERR 102 10350 (inside CALC.INC) |
018bf0c |
v-TP3-CALC-PARAMSLOTS |
ERR 102 10350 (slot sizes; the rung does not move yet) |
6b86175 |
v-TP3-CALC-PARAMLIST |
ERR 102 10350 (call sites size each argument) |
2b90a8d |
v-TP3-CALC-STRPARAM |
ERR 41 10405 (Length(S)) |
45baf26 |
v-TP3-CALC-LENGTH |
ERR 41 10753 (ClrScr) |
14ce590 |
v-TP3-CALC-CRT |
ERR 41 10927 (Read(Kbd, Ch)) |
4c165e1 |
v-TP3-CALC-KBD |
ERR 102 12083 (NormVideo) |
89c6ca6 |
v-TP3-CALC-CRT2 |
ERR 41 12672 (KeyPressed) |
033eb8e |
v-TP3-CALC-KEYPRESSED |
ERR 41 12901 (IBMCh's var parameter) |
5eb82c4 |
v-TP3-CALC-VARPARAM |
ERR 1 13052 (comma-separated CASE labels) |
8a265e7 |
v-TP3-CALC-CASELIST |
ERR 102 14195 (variable array subscript) |
b41c669 |
v-TP3-CALC-ARRVAR |
ERR 1 14330 (write field width) |
099f8fd |
v-TP3-CALC-WIDTH |
ERR 102 15026 (2-D subscript + with) |
a559843 |
v-TP3-CALC-WITH |
ERR 102 15131 (set assignment to a field) |
21ef4e1 |
v-TP3-CALC-SETFIELD |
ERR 102 15201 (string field through DI) |
a2e5e69 |
v-TP3-CALC-STRFIELD |
ERR 102 15272 (REAL field) |
0946e80 |
v-TP3-CALC-REALCONST |
ERR 41 15911 (Upcase) |
17f7f7c |
v-TP3-CALC-UPCASE |
ERR 102 18267 (real output) |
203f77e |
v-TP3-CALC-SETOPS |
ERR 98 20912 (layout limit reached) |
ff05141 |
v-TP3-CALC-LAYOUT |
ERR 1 22253 (untyped file, initially misread as a function) |
ed9e790 |
v-TP3-CALC-UNTYPEDFILE |
ERR 41 22280 (assign) |
| (this commit) | v-TP3-CALC-FILEOPS |
ERR 102 22706 (VAR string parameter) |
v-TP3-CALC-LAYOUT). Nothing is pushed.
The terminal-config work (v-TP3-TERMCFG) and the editor key map
(v-TP3-EDITORKEYS) are non-ladder commits in the same period and are
documented in TERMINAL.md.tests/fixtures/calc_ladder.tsv +
tests/calc_ladder.sh, wired into run_all.sh as CALC ladder. The row may
only move forward; every move is recorded in the tsv next to the blocker.{$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.^X/#nn) and Stage 4 (scalar typed constants) are landed.
RdConst reads ^M and #nn, ParseAtom accepts them as literals, and
DefConst accepts name : Type = value for a scalar type and keeps the
declared class. Fixtures t60_charcaret and t59_typedconst. The scalar
typed-constant red proof is the interesting one: dropping the class keeps the
compile matrix green at the same OK 65 4 and only execution sees 71 for
'G'.in. Fixture t61_setconst (.out TRUE
FALSE TRUE TRUE), and the storage decision is recorded in Stage 6: a typed
constant's bytes are emitted in the code stream behind a jmp (as string
literals already are), addressable through DS because a .COM has one
segment — no seg cs, which is the single divergence from the original.
Still ENoLib: run-time in, set variables, +/*/=/<>, assignment.set of Attributes sizes to one byte. Fixture t62_enum.
RdConst now resolves a named constant, which set literals and case
labels need.ArrRec (Sheet = 11760), globals of array type, and
XPOS : array[…] of Integer = (…) emitted code-resident like a set.
Fixture t64_arrays (compile-only because it has no .out; its ~16 KB
image now fits the raised 0x7000-byte read). t14 re-baselined: the array
DECLARES now; the refusal moved to element access.text, file of T, and untyped file; a file variable is our control block
(FileVarSize = 132: handle, mode, 128-byte NUL-terminated name). The
operations Assign/Reset/Rewrite/Close/IoResult are runtime entries
using real DOS INT 21h; t88_fileops runs the missing-file path under both
qemu and Exec86, and rt_exec pins the name copy. Data Read/Write with
a leading file argument is the remaining file increment and is still loud.
File types were what carried the compiler inside CALC.INC: the whole
var block passed and the {$I} expanded, so the rung jumped from 9202 to
10350.v-TP3-CALC-PARAMSLOTS), the parameter list on the proc
symbol (v-TP3-CALC-PARAMLIST), and the copy + size > 2 for strings
(v-TP3-CALC-STRPARAM). Fixture t67_strparam proves value semantics: the
callee mutates S[1], the caller's string is unchanged. This moved the rung
from 10350 to 10405.LENGTH is landed — the first string builtin FUNCTION, loaded inline
from the string's length byte (no runtime entry). Fixture t68_length
(5/3/0). The rung moved 10405 → 10753.ClrScr and GotoXY are real INT 10h
builtins (clear the window + home; set the cursor from a 1-based x,y), fixture
t69_crt. LowVideo, NormVideo, ClrEol and Delay are registered but
refused with ENoLib — loud, not a silent no-op. Exec86 now provides
INT 10h (cursor tracked, screen calls no-ops, everything else faults), so
t69_crt runs under BOTH oracles; removing that facility would have left the
CRT group with only one execution oracle. The rung moved 10753 → 10927.Read(Kbd, Ch) and ClrEol are landed. KBD is a predefined console
TEXT file; a READ's leading file argument is dropped when it is Kbd (the
runtime's read entries already read the console) and is a loud ENoLib for a
real file variable. ClrEol is a real INT 10h call (AH=03 reads the
cursor, AH=06 blanks to column 79). Fixtures t70_kbd (executed with a
.in), t69_crt extended. The rung moved 10927 → 12083.NormVideo/LowVideo/Delay are landed, and a parser bug behind them was
fixed. LowVideo/NormVideo set a one-byte attribute that ClrScr/ClrEol
load at run time (the console writer is INT 21h teletype and has no attribute
channel; the clears do, so the setting is observable, not a no-op). Delay
is TP3's calibrated busy LOOP. Along the way: a repeat body was one
statement, not a sequence — repeat a; b until c was ENoSemi at the first
;; t12_repeat only ever had single-statement bodies. Fixed and pinned by
t71_repeat_seq. Exec86 also gained opcode C6 (MOV r/m8,imm8), which the
prologue now uses. The rung moved 12083 → 12672.KeyPressed is landed; the rung moved 12672 → 12901. It is
INT 21h AH=0B (AL=FFh/0, masked with AND AX,1 - not AND AL,1: AH is
still 0Bh after the interrupt, and the boolean test would read 0B00h).
bootcom answers it from its input buffer and Exec86 from a one-byte peek
buffer, so a status call never consumes the character the next read wants.
Fixture t72_keypress, executed under both oracles.ERR 41 at 12901 — procedure IBMCh(var Ch:
Char). This is the var-parameter rung, and it needs the real thing:
today a var parameter is silently recorded as a value parameter
("recorded as value in this milestone"), which is exactly the §7 failure
mode. It is masked here only because DupTest rejects the parameter name
Ch for shadowing the global Ch; ISO Pascal allows that shadowing, so
fixing DupTest alone would turn CALC's next rung into a silent miscompile.
Do both together: pass addresses, and let inner declarations shadow outer
ones (duplicates at the SAME level stay errors).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 87/87, CALC ladder 1/1 at
ERR 1 22253, terminal config 4/4, 78 .COM linked and 76 executed
under both qemu and Exec86 (t64_arrays remains compile-only because it
has no .out). tests/nonvacuity.sh: 121 ok, 0 failed. The runtime is
3,907 bytes (wrintw, real_from_int, real_add, real_sub, real_mul, real_div,
real_cmp, real_int, real_fmt, real_val, real_write, set_op, set_cmp,
set_copy; the golden was re-blessed) and tests/rt_exec.py passes
275/275, including the MBF arithmetic/output/parse cases and eleven
set-operation cases. check_realconst.py pins the measured 6-byte Real
operand bytes.tests/nonvacuity.sh: re-run after this commit's cases (the set
membership bit among them); see the commit message for the count. It is not in
the default run_all.sh and must be run manually after touching any check.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.The ladder is at ERR 102 18267, Write(Value:FW:DEC) — real output
and arithmetic. Everything
below is ordered by what the ladder will hit next, not by the original stage
numbers (the measurements have reordered them repeatedly). Each item is the
same shape as the rungs already landed: one focused change, one fixture that is
red before and green after, an execution proof where the image fits, and a
commit.
Length(S), a builtin). TP3 puts a value parameter's whole
size on the stack (TPSRC7 tstscal), so a string[n] parameter is n+1
bytes. The three steps landed as separate commits/tags:
v-TP3-CALC-PARAMSLOTS (slot sizes + reverse layout),
v-TP3-CALC-PARAMLIST (the procedure symbol carries parmFirst/parmCount;
call sites size each argument), and v-TP3-CALC-STRPARAM (the caller copies
<len><chars> into the slot; EmStrBlob + EmPushStrSlot, a DS-relative
byte loop, no ES dependency). Fixture t67_strparam: the callee mutates
S[1] and the caller's string is unchanged, so it is a copy and not an
address. var parameters proper are item 6.type/const/var inside a nested function (CALC.INC:767-778, the
enum-indexed array of strings), Succ/Pred/Ord/Chr.v-TP3-CALC-FILEOPS):
Assign, Reset, Rewrite, Close, IoResult are runtime entries over
real DOS INT 21h; t88_fileops checks the missing-file path under both
oracles. Still open: EndOfFile and read/write with a leading file
argument, which need text-mode newline/^Z and typed-file block I/O.with — DONE (v-TP3-CALC-ARRVAR,
v-TP3-CALC-WITH): n-dimensional subscripts use per-dimension strides
(the last varies fastest), the element's address rides in DI, and with
resolves fields against a scope stack, parking a DI base in a data slot.
Constant-index elements stay symbol+offset. Field reads/writes one or two
bytes wide work; wider fields are the next item.^T, New, Dispose, Buffer^[i]), 4-byte
(offset then segment); the heap must hold Pages (16,281 bytes).var parameters by reference — DONE (v-TP3-CALC-VARPARAM). The slot
holds an address; loads and stores go through it; READ into one pushes the
pointer. DupTest now rejects only same-scope duplicates, so a parameter
may shadow a global. t73_varparam executes all the address paths.real — literals, + - * / and the six comparisons,
and/or on results, integer conversions, Str/Val with widths and
decimals, and ln/exp/abs/sqrt/sqr/sin/cos/arctan/int.length, copy, delete, insert, str, val)
and named string constants.ClrScr/GotoXY/ClrEol,
LowVideo/NormVideo, Delay, Read (Kbd, Ch), KeyPressed), with
INT 10h in Exec86 and AH=0Bh in Exec86 and bootcom. A running CALC
still needs the attribute to reach ordinary text, which is a later question.forward, nesting — Fact (direct self-recursion) and
Expression/Factor (mutual via forward, the definition repeating only
the name).case labels — DONE (v-TP3-CALC-CASELIST): comma-separated
lists, and ranges now really test lo <= AX <= hi (they used to require
AX = lo = hi, so 1..5 matched nothing). t74_caselist pins both.Then the two that are outside the ladder and gate the goal as stated:
v-TP3-CALC-LAYOUT). The .COM is
code-only; globals are at image 8000h and initmem zeroes them. CALC's
16,560 bytes fit the 24,000-byte code buffer, and t43_codewindow is
re-baselined to the string-pool overflow it now reaches (ERR 99 5093).Write field widths — DONE for integers (v-TP3-CALC-WIDTH, entry 20
wrintw). Still open: Real:width:decimals (CALC's
Value:FW:DEC, which waits for real support) and widths on
CHAR/BOOLEAN/STRING, which are loud today.
Record-field writes — SET, STRING and REAL CONSTANTS are DONE
(v-TP3-CALC-SETFIELD, -STRFIELD, -REALCONST). The 6-byte MBF
representation, integer constants and six-byte copies are in; real
ARITHMETIC, real function results and real formatting remain item 7.
UpCase + run-time in — DONE (v-TP3-CALC-UPCASE). UpCase folds a
CHAR constant and maps a value; e in set works with a run-time element
against literals, typed constants and data sets; a string field is
readable/writable through DI; Exec86 gained D0-D3.
REAL arithmetic, IN PROGRESS — THE NEXT RUNG is still 18267. The core is being ported from TPSRC2/3's own MBF library (the measured 6-byte format in CALC-PORT.md), not re-invented:
real_from_int (v-TP3-MBF-FROMINT, entry 21). ABI:
source address at [BP+6], destination at [BP+4]; TPSRC3 intreal in
AL/AH/BX/DX. 13 rt_exec cases against the measured table.real_add/real_sub (v-TP3-MBF-ADD, entries 22/23).
ABI: [BP+4] is a 12-byte block (a then b); the result overwrites a.
TPSRC2 readd/resub, one shared core, the reference's scratch
bytes and the AX/BX/DX + CX/SI/DI convention. 16 rt_exec cases
(alignment, cancellation, signs, carry out, the exponent-overflow
STC path). Two encoder traps: XOR [mem],reg is 30h not 32h, and
FCML rejects OR AL,AL.real_mul (v-TP3-MBF-MUL, entry 24). TPSRC2 remult
with testexp inlined; ten rt_exec cases. Two jump bugs found:
rel8 overflow to the far zero/return targets (JccNear/JmpNear plus a
FixUp range guard now), and JccNear's condition polarity.real_div (v-TP3-MBF-DIV, entry 25). TPSRC2/3 rediv,
sharing testexp's tail and remult's rm_noadj/rm_zero/rm_ret
exits. Fourteen rt_exec cases, including 5/2 = 2.5 and 42/6, the one
quotient below 1 (where the five-byte loop normalises by a left
shift). Three traps found:
rd_core, which add/sub already
used; LblOff returned the FIRST spelling, so real_div called
readd and every "quotient" was a sum. M now refuses a
duplicate label.SUB.B AH,remul21a must be SUB, not SBB: in the
carry-out path CF is 1, and SBB made 42/6 two ulps low. The
normal compare path has CF=0, which hid it in 13 of 14 cases.real_cmp (v-TP3-MBF-CMPINT, entry 26). TPSRC3 recmp.
ABI: [BP+4] is the same 12-byte block as the arithmetic entries;
nothing is written; the result is AX = -1/0/+1, cmpstr's convention,
so the compiler has ONE comparison arm for reals and strings. The
reference's core keeps the flags; the last step is the untested
flag-to-AX conversion. Different signs are decided by RCL on a's
high word (the sign lands in CF); two negatives invert the magnitude
result with CMC. Thirty rt_exec cases.
The harness gained an observable AX: rtdrv.s stores every call's AX
at D_AX (0x0AA6), so a case can dump it. Until now cmpstr's return
sign was only checked by whole programs.real_int (v-TP3-MBF-CMPINT, entry 27). TPSRC3 reint,
in place at [BP+4]. It is NOT integer-valued: Int(real) returns a
REAL, so the result stays six bytes. The reference works directly on
the packed bytes (no hidden-bit setup): it builds a mask by shifting
1s right by exponent - 129 and ANDs the mantissa, which clears the
fraction and leaves the sign bit and the implicit leading 1 alone.
Exponents >= 0A8h (2^39) are already integral and return unchanged;
<= 80h give zero. Eighteen rt_exec cases, including 12345.75 -> 12345
and 2^40 unchanged.real_fmt (v-TP3-MBF-CONV, entry 28) and real_val
(entry 29). TPSRC3's fmtreal and ascreal, with their helpers
(realdec, decround, fdig*, m10, rrange, the fltdec table)
transcribed. real_fmt: [BP+4] real, [BP+6] destination, CX =
(decimals << 8) | width; writes a length-prefixed, left-padded field
and returns its length. DEC >= 25 selects the reference's scientific
mode and derives the decimals from the width. real_val: [BP+4]
ASCIIZ source, [BP+6] destination; returns AX = the first unparsed
offset, or FFFFh on a hard error (destination untouched). Ten format
and thirty parse exact-byte cases.
Traps:
LdBxDiOff/LdDxDiOff were 8B 5F/57, i.e. [BX+off], not
[DI+off]; only the exact output showed it.StDataImm is a WORD store. Zeroing the one-byte exponent
counter also zeroed the first fltdec byte (1e0), so every
negative-exponent parse divided by zero. Added StDataByte.rd_core is the post-testexp add core: its caller must set
resign. The parser's digit add left the previous result's sign
there, corrupting every digit after the first.rc_fmt's DEC>=25 path used SUB DL,AL (28 C2) instead of
SUB AL,DL (2A C2).v-TP3-CALC-REALWRITE, entry 30
real_write). Write(Value:FW:DEC), Write(Value:FW) and the
default Write(real) push the real's ADDRESS (or DI for a field),
the width and the decimals, then call real_write; TP3's defaults are
width 18 and decimals FFFFh, which selects the scientific mode. The
entry formats into D_FMTBUF, pads and prints. t84_realwrite is the
end-to-end fixture. The same rung makes a string FIELD addressable
in a comparison: if Sheet[UFX,UFY].Contents <> '' then' pushes the
left address before parsing the right, so DI survives; pinned by
t83_strfield_cmp`. The ladder moved 18267 -> 19847.v-TP3-CALC-SETOPS). set_op
(entry 31: intersection/union/difference over size bytes),
set_cmp (entry 32: equality/strict-subset/strict-superset mask) and
set_copy (entry 33) are in Runtime.mod. The compiler pushes set
ADDRESSES, chooses the byte count from the typed set (an inline
literal is the default 32-byte set of Char; a typed field wins and
the literal's first bytes are used), and allocates one scratch data
buffer per set expression. +, -, *, =, <>, <, <=,
>, >= and set-expression assignment all work. Succ/Pred were
the next blocker and are now builtins too. Fixtures
t85_setops/t86_succ.v-TP3-CALC-LAYOUT, see §3.2). The .COM
is code-only, globals live at image 8000h and are zeroed by
initmem, and the 4 KiB window is gone. CALC moved
20912 -> 22253; the new blocker is a function declaration,
function Exist(FileN : AnyString): boolean;, not a layout limit.Str/Val
wiring, real expressions (a 6-byte temporary/value model), real
comparisons and real function results.Raise the execution harness's read — DONE (v-TP3-MBF-CONV).
tests/exec/bootcom.s now reads 72 sectors (36,864 bytes) into
1000:0000, one sector per INT 13h call with the loop state pushed around
the call (the BIOS may clobber every register; POP does not touch CF), and
copies COPY_BYTES = 0x7000 down to 0100. The input descriptor moved to
0x7000, so the image may reach 0x6F00 (28,416 bytes) while staying
below the boot sector at 0x7C00. CALC's measured .COM is 27,901
bytes, which now fits. t64_arrays remains compile-only only because it
has no .out, not because of the read.
Two things settled this session, not to re-litigate:
jmp, addressed through DS; no
seg cs, because a .COM is one segment. Sets, XPOS and (later) the
enum-indexed string array all use it.check_8086.py) cannot assert "sweeps clean
⇒ pure code" over a region that holds code-resident constant DATA; fixtures
that emit such data are listed in CODE_RESIDENT_CONST and the clause is
skipped for them.Still open from the plan (§6): Q6 (one memory-mode TURBO run in dosemu to
see whether CALC reports overflow #99), and the run-time capacity note in
§3.2. The layout decision is closed.
The per-stage entries below are the historical record of how each landed.
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. DONE.
RdConst reads ^M (the upper-cased character minus 64, so ^@ = 0 and
^[ = 27) and #nn (decimal, via RdIntConst, so #$FF works too).
ParseAtom's literal test had to admit ^ and # as well, or write (^M)
in an expression would still be a syntax error — only a declaration goes
through RdConst. Both spellings are marked as characters, so they are written
by the char writer. EofLine = ^M (CALC.PAS:113) now parses.
Fixture t60_charcaret (red: ERR 41 at the ^M in write → green; its
.out starts with the raw bytes 0D 1B, which is what proves they are
characters and not the text "13"/"27").
Stage 4 — typed constants: the SCALAR form is DONE; structured is deferred.
DefConst takes the name : Type = value spelling (TPSRC7's constant reader)
and, for a scalar type, records the declared class on the symbol. Because the
value still folds to an immediate, that is the whole payoff: FXMax : Char =
'G' becomes a TChar constant holding 71, so write (FXMax) prints G
rather than 71. Integer and Boolean take the same path, which also pulled in
RdConst reading TRUE/FALSE (it could not before). Fixture t59_typedconst
— and its red proof is worth noting: dropping the class (cls := typCls →
cls := cls) leaves the compile matrix GREEN at the same OK 65 4 and only
execution sees 7142 where G42 belongs.
What is not done, deliberately, is a typed constant of a structured type:
Numbers : set of Char = ['0'..'9'], CALC.PAS:114) or an array
(XPOS : array['A'..'G'] of Integer = (…), 137) is an initialised value
with a layout. This compiler has no initialised data — InitMem zeroes the
data area — so accepting one and discarding the initialiser would be exactly
the silent mis-compile §0.2 forbids. DefConst refuses a type bigger than a
word with ENoLib, and that is where the ladder now stops: ERR 102 at
7972.7791 → 8036 was optimistic: sets (Stage 6) must be laid out
before the enumeration at 8036 can be reached. The enum-indexed array of
strings in a nested function (CALC.INC:767-778) needs the same storage
decision, on top of sets, arrays and string[n] — it is the hardest single
item in this plan and belongs to its own write-up, not this one.seg cs, with the program jumping over them (RESUME-TP3.md §3.14); the
decision between that and an init block copied at startup is still open, and
is now the first thing the set/array typed-constant work must settle.Stage 5 — enumerations. DONE.
Attributes = (Constant,Formula,Txt,OverWritten,Locked,Calculated). Each
enumerator becomes a constant holding its ordinal position (0..n-1); the TYPE is
one byte (TP3 uses one below 256 values, RESUME-TP3.md §2) and carries its
bounds 0..n-1 in the symbol, which is what set of Attributes sizes its base
from — one byte, against 32 for set of Char. ParseType grew the (
arm, SymEntry grew lo/hi, and RdConst now resolves a NAMED constant, so
[Constant, Formula] and (soon) case labels over enumerators work.
Succ/Pred over enums are a later milestone (they are the standard-function
milestone's job, not the type's). Fixture t62_enum.
Ladder: 8098 → 8362 (the enum AND SetOfAttri = Set of Attributes).
Stage 6 — sets. Increment 1 DONE; the rest is the next milestone.
Done:
e is bit (e MOD 8) of
byte ((e DIV 8) - (base DIV 8)), high byte last. set of Char is 32 bytes,
set of 12..17 is 2. elem carries the origin (a multiple of 8).[a..b, c], and [].Numbers : set of Char = ['0'..'9'].in — a constant element against a set whose bytes are in
the image, answered as TPSRC2 xsetin does (AH > 255 → FALSE, else test the
bit). Fixture t61_setconst, .out TRUE FALSE TRUE TRUE.jmp,
exactly as string literals already are, and the name becomes addressable.
The divergence from RESUME-TP3.md §3.14 is one line: the original had
separate code and data segments and therefore prefixed seg cs; a .COM has
one segment, so DS already covers the code area and no prefix is needed.
The bytes are ordinary RAM, so a typed constant stays assignable, as TP3's
static typed constants are.Still to do (all currently ENoLib, none of them silently accepted):
in — a variable element or a set variable. The bit arithmetic
is the same, but it must be emitted, not folded.DefVar still refuses size > 2
for a non-string) and their layout in the frame.+ union, * intersection, =/<>, assignment, [] in context.set of <enum> — needs the enum base first (the current rung).Reference note: Resources/COMPIL4/ (Wirth's Pascal-P4, P.PAS + P41..P48)
was added as a cross-check. It is a different design — a fixed
setsize = 8-byte bitset over 0..sethigh (63), with in as a built-in
runtime opcode — so it informs the algorithms but not the representation.
TP3's hybrid bytes remain the ground truth here, per the repo rule.
Ladder: 8060 → 8237 (now 8098 → the enum).
Stage 7 — records. Record TYPES done; field selection is the next increment.
Done: the record … end arm of ParseType. Fields are non-compacted and
juxtaposed (RESUME-TP3.md §2); each name in an a, b : T group is its own
field at the running offset. Fields live in a table of their own (FldRec,
keyed per record) because a field name is scoped to its record, not global. A
type carries its field range in SymEntry.fldFirst/fldLast. CellRec comes
out at exactly 80 bytes, which is what §3 measured Sheet against, so the
layout is pinned by t63_record's data=84. This also required subrange
sizing: 0..20 is one byte (both bounds in 0..255, RESUME-TP3.md §2), not
the two this compiler used to give it.
Not yet: field selection (c.DEC, Sheet[FX,FY].CellStatus) and with.
A field access needs the expression record to carry a width (a field's size is
not its variable's), which is a small ERes change plus the addressing; today
it is a loud error, not a wrong address. Records as array elements come
with the 2-D array in Stage 8 — Sheet[FX,FY].CellStatus is both.
Stage 8 — arrays at their point of use. Declarations DONE; element access next.
Done: multi-range array types (array['A'..'G',1..FYMax] of T), row-major with
the last index varying fastest (RESUME-TP3.md §2); the shape is kept in an
ArrRec (n, lo[], hi[], elem) and the symbol points at it
(SymEntry.arrIdx), so indexing has everything it needs. Array variables
(global, and of any size) are laid out, and array typed constants
(XPOS : array['A'..'G'] of Integer = (…)) are accepted: their bytes are
emitted code-resident, exactly like a set, each element little-endian.
Sheet is 11760 bytes and XPOS's 14 bytes are in the code stream, which is
why t64_arrays's data=11764 is Sheet plus the two @@T temporaries.
t14 is re-baselined: the array declares now, and the refusal moved from the
declaration (pos 83) to the subscript (pos 109).
Element and field access are DONE for constant indices. An expression now
carries its own WIDTH (ERes.sz, via VarSize), because an element's size is
not its array's: a constant subscript folds (index - lower bound) * element
size into the address and hands the element's size and class to the store, and
r.f adds the field's offset and takes the field's size and class. The width
is threaded through LoadAtom, Assignment and IoCall, and Statmnt (which
builds an assignment target directly, not through ParseVar) had to set it too
t65_array_access
(v[1] := 11, v[3] := v[1] + v[2], r.A, r.B; .out 33 then 12), and
t14 is re-baselined from ERR 102 109 to OK 41 15.Not yet: a VARIABLE index (Sheet[FX,FY], XPOS[Ch], b[i]) - scaled
run-time addressing, including the second dimension - which is a loud ENoLib
today, and with. Both wait on the same scaled-addressing work.
Two consequences for the harness, both now recorded:
check_8086 clause F had to narrow. It asserted "this region sweeps
clean, therefore it is pure code, therefore no 0F". A code-resident set or
array constant breaks that premise - the data can decode cleanly and then
present an 0F that is a data byte. Fixtures that emit code-resident
constants are now listed in CODE_RESIDENT_CONST and clause F is skipped for
them; the runtime sweep (the backbone) and the execution oracles still cover
the emitted instructions.tests/exec/bootcom.s reads 72
sectors and copies 0x7000 bytes; the input is at 0x7000, the image may reach
0x6F00, and the boot sector at 0x7C00 is never overwritten. CALC's 27,901
bytes fit; t64_arrays is compile-only because it has no .out, not for
size. The remaining CALC execution gate is the layout change (§3), not the
loader.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. Types DONE, including untyped file; operations next.
Done: text, file of T, and untyped file (t87_untypedfile). A file
variable is our own control block (FileVarSize = 132: the DOS handle word, a
mode word, and a 128-byte NUL-terminated filename), because this runtime does
unbuffered INT 21h I/O where TP3 keeps a 128-byte buffer inside the variable
file of T parses its element type so the RECORD
size a Read/Write must move is known. Fixture t66_files.Control-block OPERATIONS are done (t88_fileops): Assign copies the counted
filename, Reset/Rewrite/Close issue DOS INT 21h open/create/close, and
IoResult reads and clears the status. The boot harnesses have no filesystem,
so both answer AH=3Dh with CF=1, AX=2; the fixture checks that error path
under qemu and Exec86, and rt_exec pins the name copy.
Not yet: EndOfFile, and read/write with a leading file argument
(writeln (MCFile, Line), Readln (H, Buffer^[count])). These need
text-mode newline/^Z semantics and typed-file block I/O (RESUME-TP3.md
§2). The next ladder blocker is not file data I/O, though: it is var Line :
AnyString, a by-reference string parameter.
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). DONE
(v-TP3-CALC-LAYOUT). The data area is no longer immediately above the
code: globals live at image 8000h (memory 8100h), initmem zeroes them,
and the .COM is code-only (LinkSize = ImageBytes). This emits CALC's
measured 16,560 B of code inside the 24,000-byte code buffer and removes the
4 KiB window. t43_codewindow is deliberately re-baselined (ERR 99 5093,
the 4 KiB string pool, not the old window); the layout itself is pinned by
the independent .COM checker. See §3.2 for the CALC.COM comparison and the
remaining run-time capacity note.
| 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 program + 3,907 B runtime = 20,467 B | OK, 3,533 B spare (§3.2) |
| code window | CodeMax = 6F00h, capped by MaxCode |
20,227 B | OK; no data gap (§3.2) |
| data area | fixed image 8000h, zeroed by initmem |
12,032 B (measured, §3.1) | OK for compile; see the run-time cargo note (§3.2) |
| 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.
pc and dc are offsets in different
segments, not two ends of one segment. Our flat boot harness emulates the
property with a code-only .COM and a fixed high global area zeroed by
initmem; the 4 KiB window is gone and t43_codewindow is re-baselined.{$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..COM to the data base: §3 solved the compile goal by keeping the file
code-only and the globals at a zeroed high base. t43_codewindow is now a
string-pool fixture; the layout is pinned by the independent .COM checker,
not by that fixture.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/coeur-tp-ocr/coeur-tp-03-d.txt §4.7 — sets explained: the hybrid
byte layout this port implements (4.7.2) and the memory order (4.7.3).Resources/COMPIL4/ — Wirth's Pascal-P4 (P.PAS, P41..P48), added as a
cross-check. Its sets are a fixed 8-byte bitset over 0..63 with in as a
runtime opcode, so it informs the algorithm, not the representation.Resources/TP3/CALC.PAS, CALC.INC, CALC.HLP — the target.shell/tests/probe/README.md — read before touching any emitter.