Browse Source

calc: match the original memory layout (code-only .COM)

The old layout put the globals a fixed 4 KiB above the runtime, so code
could not grow past that window even though the code buffer was 24,000
bytes.  The original TP3 keeps the runtime and program in CS and the
globals in a separate DS segment; its .COM contains no data.

Mirror that property in our flat boot harness: DataStart = 8000h (memory
8100h), CodeMax = 6F00h, and Linker.LinkSize = ImageBytes(), so the .COM
is code-only.  globals are zeroed by initmem's existing [hdrDS,hdrHeap)
loop.  comimage now locates the header from the entry jump and pins
hdrDS = 8100h and hdrCS - bias == file length; the independent checker
verifies the code-only file and the fixed data base.

Measured against Resources/TP3/CALC.COM (untracked reference): entry
E9 4A 2C, program at 2C4D, runtime 11,341 B, code 16,560 B, inline
initmem CS size 06E0h, DS size 0315h, heap/stack 0400h.  The file is
runtime + code, no data, exactly the shape now emitted.

The CALC ladder moves 20912 -> 22253; the blocker is now a function
declaration (Exist).  t43_codewindow is re-baselined: with the data out
of the way it reaches the 4,096-byte string pool before the old code
window, so it reports ERR 99 5093.

Verified: compile matrix 87/87, CALC ladder 1/1, 78 .COM linked,
76 executed under both qemu and Exec86, nonvacuity 121/121, run_all
ALL PASS.
Eric Streit 12 hours ago
parent
commit
f8a8db976f

+ 156 - 110
CALC-COMPILE.md

@@ -10,9 +10,9 @@ native GNU Modula-2 on top of `Term.mod`). That is a separate piece of work and
 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 (HEAD is
-`v-TP3-CALC-FILES`, and §1's rung table and the "Session state" section are the
-current figures).
+(`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).
 
 ---
 
@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 | position | blocker |
 |---|---|
-| **18267** | `Write(Value:FW:DEC)` in `GotoCell` — REAL output/arithmetic (`ERR 102`) |
+| **22253** | `function Exist(FileN : AnyString): boolean;` — function declarations/returns (`ERR 1`) |
 
 Rungs behind it (now passed):
 
@@ -124,12 +124,15 @@ Rungs behind it (now passed):
 | 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) |
 
 Rungs ahead:
 
 | position | blocker |
 |---|---|
-| 18267 | REAL output (`FW:DEC`) and real arithmetic; then the rest of the evaluator |
+| 22253 | function declarations/returns (`Exist`), then the rest of CALC's functions |
 
 Corrections the measurements force, worth stating plainly because the stage list
 below was written before them:
@@ -210,20 +213,20 @@ predefined constants `true`/`false` (both work today).
 
 ---
 
-## 3. Structural blocker: the memory layout is inverted
+## 3. Structural blocker: the memory layout (RESOLVED 2026-10-22)
 
-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.
+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 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 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`:
 
@@ -330,44 +333,40 @@ recipe above regenerates them, and the numbers are what this document keeps.
 
 **Consequences for this port:**
 
-1. **Q1 is answered.** The original keeps the data area *below* the program
-   code, so the binding limit is data running into the code base — the exact
-   opposite of this port, whose `dataBase := rtSz + 1000H` caps **code at
-   4 KiB**. CALC's code is **16,560 bytes**, so the port cannot emit it at all
-   today; this is a hard prerequisite, not a nice-to-have.
-2. **`MaxCode = 24,000` is not the binding constraint.** 16,560 + 773 (our
-   runtime) = 17,333, comfortably inside 24,000. It is `dataBase` alone that
-   must move, and the fix has to be an orientation/layout change rather than a
-   bigger buffer — which is what `t43_codewindow` was built to police.
+1. **Q1 is answered — and the first reading was wrong.** The original does
+   **not** put the data area below the code in one segment. `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.
+2. **`MaxCode = 24,000` is not the binding constraint for the compile goal.**
+   16,560 + 3,667 (our current runtime) = 20,227, 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).
 3. **The §3 data budget is confirmed, not inferred.** Reported data is
    12,032 B against a computed 11,760 B for `Sheet` alone (147 × 80 B
    `CellRec`), so the 80-byte `CellRec` sizing is right and the rest
    (`Line`, `FileName`, the file variables, scalars) accounts for ~272 B.
 4. **TP3's memory model has code and data in separate address spaces**
    (`cs 0x0813` vs `ds 0x11D7` in the header, and separate "free" figures).
-   A `.COM` collapses them to one segment, so we have to choose one contiguous
-   layout; the measurements bound what we must allow: **≥ 16,560 B of code and
-   ≥ 12,032 B of data**, with 16,384 B of stack and a heap large enough for
-   `Pages` (16,281 B, §3).
-5. **The runtime is 11,341 bytes** in the original. Ours is 773 B. Not needed
-   for this goal, but it explains where the original's fixed `pc := $2D7C` /
-   `dc := $0240` came from, and why `TPSRC7:81-82` looks the way it does.
-
-**Feasibility, now checkable.** A `.COM` is one 64 KiB segment holding
-`[runtime][code][data]`, with the heap growing up and the stack growing down
-out of what is left. Using the measured figures:
-
-| | runtime | + code | + data | left for heap+stack | after `Pages` (16,281 B) |
-|---|---|---|---|---|---|
-| original | 11,341 | 16,560 | 12,032 | 25,603 | 9,322 for stack |
-| this port | 773 | 16,560 | 12,032 | **36,171** | **19,890** for stack |
-
-So the goal is arithmetically reachable, and the original *proves* it is
-reachable with 10,568 bytes less room than we have — our runtime is that much
-smaller. There is no capacity reason to doubt the layout fix; the only
-question is which of the two orientations to implement. The same table shows
-`MaxCode = 24,000` is fine with 7,440 to spare, and that `TextLimit`'s 488
-bytes of slack (§5) is the one genuinely tight number in the whole budget.
+   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.
+5. **The runtime is 11,341 bytes** in the original. Ours is 3,667 B. It
+   explains where the original's fixed `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
@@ -381,6 +380,51 @@ 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.
 
+### 3.2 Layout landed (2026-10-22)
+
+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,667 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 `function Exist(FileN : AnyString): boolean;`.
+
 ---
 
 ## 4. Ordered stages
@@ -392,26 +436,25 @@ parallel:
   position in CALC and needs no memory-layout change to be developed or
   verified — every feature can be exercised on a fixture far smaller than
   4 KiB of code.
-- **The layout track** (Stage 16, §3) is the only thing that stops the
-  compiler from *emitting* the whole program once the front end accepts it.
-  It does not block the ladder, but it is a hard prerequisite for the goal as
-  stated, and it changes `dataBase`/`t43_codewindow` — so it belongs to one
-  commit of its own, with the re-baselining reasoned in the message.
+- **The layout track** (Stage 16, §3) was the hard prerequisite for
+  *emitting* the whole program once the front end accepts it. **It is DONE**
+  (`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).
 
 ### Session state — where to resume
 
-Last touched 2026-10-21. **Twenty-six rungs are landed across three sessions**
-(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, and string fields in WRITE).  The
-ladder row has moved from the first typed constant at 7791 to **18267, inside
-`CALC.INC`'s `GotoCell` — `Write(Value:FW:DEC)`, real output**.
+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`, and the original memory layout). The ladder has moved from the
+first typed constant at 7,791 to **22,253**, where the first **function**
+declaration appears.
 
 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
@@ -445,12 +488,13 @@ Exec86):
 | `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) |
+| *(this commit)* | `v-TP3-CALC-LAYOUT` | **`ERR 1 22253`** (function declarations) |
 
-- **Code HEAD** `c92d2a8` (`v-TP3-MBF-MUL`); this commit only updates this
-  document.  Nothing pushed; `origin/master` is behind by twenty-two code
-  commits.  Two non-ladder commits in the same period are the terminal-config
-  work (`v-TP3-TERMCFG`) and the editor key map (`v-TP3-EDITORKEYS`); they do
-  not move the ladder and are documented in `TERMINAL.md`.
+- **Code HEAD** is the layout rung (`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`.
 - **The measuring device:** `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.
@@ -550,10 +594,10 @@ Exec86):
   been green and only `t40_readln` (and later CALC) would have noticed.
 - **Verified baseline, run on the code HEAD above:** `./tests/run_all.sh` →
   **OVERALL: ALL PASS**, compile matrix **87/87**, CALC ladder **1/1** at
-  `ERR 98 20912`, terminal config **4/4**, 78 `.COM` linked and **76 executed
+  `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
-  3664 bytes (wrintw, real_from_int, real_add, real_sub, real_mul, real_div,
+  3,667 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
@@ -650,12 +694,10 @@ commit.
 
 Then the two that are *outside* the ladder and gate the goal as stated:
 
-12. **The layout change (§3)** — re-orient the image so the compiler can emit
-    CALC's **16,560 bytes** of code and hold **12,032 bytes** of data; the
-    port's `dataBase := rtSz + 1000H` caps code at 4 KiB.  Decide "match the
-    original" (data low, code high) vs a dynamic data base with fixups, and
-    re-baseline `t43_codewindow` in the same commit.  This is the only stage
-    that blocks *emitting* the whole program.
+12. **The layout change (§3) — DONE** (`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`).
 13. **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
@@ -764,20 +806,19 @@ Then the two that are *outside* the ladder and gate the goal as stated:
         `>`, `>=` and set-expression assignment all work.  `Succ`/`Pred` were
         the next blocker and are now builtins too.  Fixtures
         `t85_setops`/`t86_succ`.
-      * **NEXT BLOCKER: THE §3 LAYOUT.**  CALC now reaches 20912 and fails
-        with TP3 error **98 (memory overflow)**: the compiler reserves a
-        FIXED 4 KiB code window above the runtime (`dataBase`), and CALC's own
-        code has reached it.  This is not a missing construct; code and data
-        cannot share the 64 KiB .COM segment with a static 4 KiB split.
-        Re-orienting the image (data after the actual code, or a far model)
-        is the layout change item 12, and is the next rung.
-      * **STILL TO COME:** `Str`/`Val` wiring, then real expressions (a 6-byte
-        temporary/value model), real comparisons and real function results.
-      * **HEADROOM:** the runtime is 3664 bytes and the probe's scaffold starts
-        at HDR=0x1100 with RT_BASE=0x0190, so the ceiling is 3952 bytes - about
-        288 bytes left.  The boot read allows a .COM up to 0x6F00, so the next
-        additions can move HDR again (e.g. 0x2000) with plenty of room below
-        the input at 0x7000.
+      * **DONE: THE §3 LAYOUT** (`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.
+      * **STILL TO COME:** function declarations/returns, then `Str`/`Val`
+        wiring, real expressions (a 6-byte temporary/value model), real
+        comparisons and real function results.
+      * **HEADROOM:** the runtime is 3,667 bytes and the probe's scaffold
+        starts at HDR=0x1100 with RT_BASE=0x0190, so the ceiling is 3,952
+        bytes - about 285 bytes left.  The boot read allows a .COM up to
+        0x6F00, so the next additions can move HDR again (e.g. 0x2000) with
+        plenty of room below the input at 0x7000.
       * **Exec86** needed the FE group (INC/DEC r/m8) and CMC/CLC/STC/LAHF/
         SAHF for the formatter; t84 is the first fixture that executes the
         real conversion under the interpreter, and it cross-checks qemu.
@@ -803,8 +844,9 @@ Then the two that are *outside* the ladder and gate the goal as stated:
   that emit such data are listed in `CODE_RESIDENT_CONST` and the clause is
   skipped for them.
 
-**Still open from the plan (§6):** the layout decision above, and Q6 (one
-memory-mode `TURBO` run in dosemu to see whether CALC reports overflow #99).
+**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.
 
@@ -1085,13 +1127,15 @@ 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.
+**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.
 
 ---
 
@@ -1100,9 +1144,9 @@ 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 |
+| `MaxCode` | 24,000 | 16,560 B program + 3,667 B runtime = **20,227 B** | **OK, 3,773 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 |
@@ -1119,11 +1163,11 @@ as the include, because the include is what consumes the slack.
 
 ## 6. Open questions, each with a method
 
-- **Q1 — RESOLVED (§3.1).** The data area sits below the program code; the
-  binding limit is data running into the code base, and this port has it the
-  other way round. Remaining as a *decision*, not an unknown: implement
-  "match the original" or "dynamic data base", and re-baseline
-  `t43_codewindow` in the same commit.
+- **Q1 — RESOLVED (§3.1, §3.2).** The original keeps code and data in
+  **separate CS/DS segments**; `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.
 - **Q2.** Is `{$A-}` really a no-op for `TURBO.COM`? Check `TPSRC` for an `A`
   directive before treating it as ignorable.
 - **Q3.** `Pages = array[0..200] of string[80]` — confirm the element size is 81
@@ -1156,9 +1200,11 @@ as the include, because the include is what consumes the slack.
 
 - Do not "support" a construct by parsing it and emitting nothing. That is the
   `var`-parameter bug and the `{$I}` bug, and both were green.
-- Do not raise `dataBase`/`MaxCode` and call §3 solved; the orientation is the
-  question, and `t43_codewindow` exists precisely to catch a silently corrupt
-  image.
+- Do not put the data back immediately above the code, and do not pad the
+  `.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.
 - Do not implement BCD reals, `Pos`, `Concat`, `Round`, `Trunc` or `goto` for
   this goal. CALC uses none of them.
 - Do not let this plan absorb `CALC-PORT.md`. Same program, different target,

+ 32 - 27
SUMMARY.md

@@ -32,7 +32,7 @@ manual, not guessed.
 | String *variables* — `s : string[10]`, `s := 'hi'`, `writeln(s)` | `v-TP3-STRVAR` | done, **the first declared type bigger than a word that is not an error. `string [n]` is n+1 bytes — a length byte and characters, the layout TPSRC8's `string` type gives a variable (`INC BX / MOV varsize,BX`) — so `DefVar`'s `size > 2` catch-all (still the answer for an `array` at its point of use) exempts `TString`. `s := 'hi'` copies the literal byte by byte and truncates a longer one exactly as TPSRC2 `strstore` does; `writeln(s)` pushes the variable's address and calls a new `wrstr` entry (TPSRC4 `xwrtstr` minus its field-width formatting), which prints the length byte's worth of characters and nothing for `''`. Both a global and a local string are exercised — the absolute and the `[BP+off]` address paths — and the `.out` is compared byte for byte under qemu and Exec86 (36/36)** |
 | String *reads* and string-to-string assignment — `readln(s)`, `s := t` | `v-TP3-STRREAD-ASSIGN` | done, **`readln(s)`/`read(s)` and `s := t` on string variables. Two runtime entries carry selectors 16/17 (`rdstr`, `strstore`); the caller sets the limit in `CX` with a full `MOV CX,imm16`. `rdstr` (TPSRC4 `xrdstr`) reads a line, stops at CR/LF/^Z, leaves the terminator pending for the existing `rdln` wrapper, truncates at the limit, and stores the length last; `strstore` (TPSRC2 `strstore` + TPSRC8 `asnvar`) copies `min(len(t), size(s)-1)` characters, never writes the source, and leaves the destination tail past the new length untouched. The milestone also exposed a pre-existing frame-layout bug: a local `string` was packed with the *integer* rule, so its characters grew UP over the saved BP at `[BP]` and the return address at `[BP+2]` — `readln` into such a local then ran the string's own bytes as its return address. Locals are now packed top-anchored, which keeps every integer offset byte-for-byte and puts a `string[5]` at `BP-6..BP-1`, inside the frame (38/38 executed, 51/51 entry checks)** |
 | Multi-name declarations, both spellings — `var a, b, c : integer`, `procedure p (x, y : integer ; z : integer)` | `v-TP3-MULTIDECL` | done, **definitions and formal parameters read a comma-separated list of NAMES and then one `: type`, exactly TPSRC7 `rdvarlst`/`rdvloop` (VAR) and `prfcnt`/`ccomma` (parameters), with `;` between parameter groups. The older `name : type, name : type` spelling still parses, because both reach the same symbols; the names are created as placeholders and given the group's one type afterwards. One `GetWord` that did not `Skip` first attached the space after a comma to the next name (`" B"`), which is why the first name worked and the second did not; `t42_multidecl` prints each name's value to prove every one landed on its own slot (43/43 compile, 39/39 executed)** |
-| The 4 KiB code window enforced — TP3 error 98 instead of writing over the data | `v-TP3-CODEWINDOW` | done, **`dataBase` is `rtSz + 1000H`, so code and data share one 64 KiB `.COM` segment and code may not reach it. `Ebyte` now refuses past `dataBase` (guarded by `imgReady`, since `dataBase` is 0 until `Compile` measures it) and reports TP3's error 98, "Memory overflow" — the same number `DefVar` already uses when `dc` overflows. `t43_codewindow` emits more than 4 KiB of inline literal bytes and must report `ERR 98`; disabling the check turns the fixture green-as-`OK`, which is the bug (a silently corrupt image), so the matrix row is the proof (44/44 compile)** |
+| The 4 KiB code window enforced — TP3 error 98 instead of writing over the data | `v-TP3-CODEWINDOW` | historical, **superseded by `v-TP3-CALC-LAYOUT`**: the data no longer sits immediately above the code. Globals live at image `8000h` (memory `8100h`), `initmem` zeroes them, and the `.COM` is code-only (`LinkSize = ImageBytes`). `t43_codewindow` now reaches the 4,096-byte string pool first and is re-baselined to `ERR 99 5093`; the code-only layout is pinned by the independent `.COM` checker |
 | String subscripts — `s[i]` read, write, and `readln (s[i])` | `v-TP3-STRINDEX` | done, **a STRING is a length byte at `s[0]` followed by the characters, so `s[i]` is the single byte at `off+i`: reading it yields a `CHAR`, assigning to it stores one byte, and `readln (s[i])` reads one byte at `off+i`. A constant index folds into `boff` as an array's does and `r.cls` drops to `TChar`, so `LoadAtom`, `Assignment` and `IoCall` all see one byte. Only a constant index is accepted — a variable index is `ENoLib`, exactly as it already was for an array. The fix uncovered a latent bug: `ParseAtom` did `r.boff := 0` *after* `ParseVar`/`ParseSub`, discarding every subscript, so `s[i]` addressed `off+0` (the length byte). `ParseVar` already zeroes `boff`, so the reset was redundant and was removed. `t44_strindex` prints every indexed byte and `t45_strindex_read` feeds `readln (s[i])`; the indexed read is red before the parse fix and green after (46/46 compile, 41/41 executed)** |
 | Variable string subscripts — `s[i]` with a *computed* index | `v-TP3-STRIDX-VAR` | done, **the address of `s[i]` is now computed at run time, so a variable index, a computed one (`i+1`) and a nested one all work — `v-TP3-STRINDEX` accepted a constant only. `ParseSub` evaluates the index into `AX`, then `LEA DI,[base]` / `ADD DI,AX` puts the element's address in `DI` and `r.kind` drops to `5`; `LoadAtom` reads `[DI]`, `Assignment` `PUSH DI`es the destination across its right-hand side (so `g[i] := h[j]` stores where it read — TPSRC8 `asnscal2`/`asnscal3`'s `flgpshdi`, and `SaveLeft` materialises a `DI`-addressed left operand *before* parsing the right), and `IoCall` pushes `DI` for `readln (s[i])`. No runtime entry: the arithmetic is compile-time emitted, so the runtime image is unchanged. Five new one-line emitters are name-audited (`AddDiAx`, `LdAlDi`, `StDiAl`, `PushDi`, `PopDi`) and `LeaDi` is the one documented computed emitter. `t46_strindex_var` reads, writes, compares, passes as an argument and assigns both-indexed (`g[i] := h[j]`), globally and locally; `t47_strindex_var_read` is `readln (g[i])` (48/48 compile, 43/43 executed)** |
 | String comparison — `= <> < <= > >=` with both operands strings | `v-TP3-STRCMP` | done, **`ParseCmp` gets a string arm ahead of the constant-fold arm: two addresses are pushed (`EmPushStrAddr`), `cmpstr` — runtime selector 18 — returns `AX = -1/0/+1`, and the shared `SETcc` table turns the sign into a Boolean. `cmpstr` (83 bytes, `0249h`..`029Bh`) compares `min(lenL,lenR)` bytes **unsigned** — first difference wins — and breaks a tie on the lengths, so a proper prefix is *less* (`'ab' < 'abc'`, `'' < everything`); the compiler's test on `-1` is **signed** (`SETL`, not `SETB`: `-1` is `0FFFFh`). It returns a value rather than flags because the caller drops the two addresses with `ADD SP,4`, which destroys the flags (TP3's own `cmpstr` removes its arguments itself and so can leave them). A string literal travels inline in the code stream (`JMP` over `<len><chars>`, address = the length byte) because a `.COM` carries no initialised data; a one-character literal is a `CHAR`, so `s = 'a'` is `ETypeErr` 56 while `''` is the empty string. The first epilogue saved `AX`, which restored the *caller's* `AX` (the right operand's address) and inverted every ordering comparison — only executing `t48` could see it. `t48_strcmp` weights the six operators 1/2/4/8/16/32 so the printed sum identifies which held; `t49_strcmp_lit` is the literal operand; `t50_strcmp_type` is the `CHAR` refusal (51/51 compile, 45/45 executed). Running `tests/nonvacuity.sh` also exposed three mutation anchors `v-TP3-STRIDX-VAR` had silently invalidated, and a `LOOP` that the runtime's branch scan had never checked; both fixed** |
@@ -180,8 +180,9 @@ direction is still unimplemented) both → `ENoLib`
 (102), the original's "not implemented" path; `t38` `readln (b)` of a
 `boolean` → error 66, TP3's "I/O not allowed here" (TPSRC8 `prderrtp`), which
 refuses the whole class because the runtime has no boolean reader at all;
-`t43_codewindow` a program whose code exceeds the 4 KiB window → error 98,
-"Memory overflow", rather than a corrupt image;
+`t43_codewindow` a program whose inline literals overflow the 4,096-byte
+string pool → error 99, reported rather than silently truncated (the old 4 KiB
+code window is gone; see `v-TP3-CALC-LAYOUT`);
 `t50_strcmp_type` `s = 'a'` → error 56, because a one-character literal is a
 `CHAR` and not a string;
 `t53_strcat_type` `'a' + 'b'` → error 56, two CHARs and no string;
@@ -1124,14 +1125,17 @@ assertion, rather than asking the compiler where it thinks the header is.
 
 `CmdCompile` honours the Destination option: 0 = memory, 1 = `.COM`, 2 = `.CHN`
 (refused). A `.COM` is named after its source with the extension swapped at the
-last dot, padded with a zero gap to `max(pc, dc)`; its stack sits at the segment
-top (`SS = SP = CS:FFFE`), which is where DOS puts it.
-
-**Known limit:** the data area starts at a fixed `rtSz + 1000H` (773 + 4096 =
-4869), so a program whose code exceeds 4 KiB runs into its own data. Every
-fixture's image is between 4873 and 6711 bytes. Documented rather than fixed,
-because the
-original has the same fixed-offset behaviour.
+last dot and contains **exactly the runtime and the program's code**; the
+globals are at image `8000h` (memory `8100h`), outside the file, and `initmem`
+zeroes them. Its stack sits at the segment top (`SS = SP = CS:FFFE`), which is
+where DOS puts it.
+
+**Layout:** the old fixed `rtSz + 1000H` data base is gone. `DataStart = 8000h`,
+`CodeMax = 6F00h`, and `MaxCode = 24,000` bound the code image; the `.COM`
+writer emits `ImageBytes()` and no padding. This is the flat-segment equivalent
+of the original's separate CS/DS layout (see `CALC-COMPILE.md` §3.2). It is
+enough to compile CALC; running CALC with its full 16,384-byte stack and
+16,281-byte `Pages` heap is still a separate capacity item.
 
 ### String literals — `writeln('hi')`
 
@@ -1202,12 +1206,12 @@ Details that are deliberate, not incidental:
   prove the *path* exists (compile → poke → run → report → no file written),
   which it does with eight assertions. The other 37 are covered by
   `run_exec86.py`, which calls the same interpreter directly.
-- **Every fixture that compiles is now also run.** 43 of 48 execute; the 5 that
-  do not are `t14` and `t25` (`ENoLib`, by design), `t38` (a boolean read,
-  refused with TP3's error 66, by design), `t43` (a program whose code exceeds
-  the 4 KiB window, refused with error 98, by design) and `uierror` (a
-  deliberate syntax
-  error). The gap this replaces was nine fixtures that compiled and were
+- **Every fixture that compiles is now also run.** 78 `.COM` files are linked
+  and 76 execute under both qemu and Exec86; the compile-only ones are
+  `t64_arrays` (no `.out`) and the deliberately refused fixtures.  `t14` and
+  `t25` are `ENoLib`, `t38` is a boolean read refused with error 66, `t43` is
+  now the string-pool overflow (`ERR 99`), and `uierror` is a deliberate
+  syntax error. The gap this replaces was nine fixtures that compiled and were
   *never executed* — `t08` const, `t09` if/then/else, `t10` while, `t11` for/to,
   `t12` repeat/until, `t13` procedure + value param, `t15` label + goto,
   `t27` five locals, `t28` the 70-parameter declaration — and all nine
@@ -1291,12 +1295,12 @@ Details that are deliberate, not incidental:
   yet, so `writeln(1.5)` "works" and prints nonsense. A trap would be louder;
   neither choice is a real answer, and this is now reachable code rather than
   an unreachable one, which raises the stakes on the choice.
-- **Code above 4 KiB overruns the data area.** The data base is fixed at
-  `rtSz + 1000H` = 4432 and a `.COM` is padded to `max(pc, dc)`, so the fixed
-  4 KiB code window is real and not advisory. The largest fixture (`t32_forexit`,
-  135 bytes of code) is nowhere near it, so nothing has hit this and nothing
-  tests it. `rtSz` also *moves* every time the runtime grows, so the window
-  shrinks silently — another derived value that must never be restated.
+- **Code above 4 KiB no longer overruns the data area.** The layout rung
+  (`v-TP3-CALC-LAYOUT`) moved the globals to image `8000h`, made the `.COM`
+  code-only, and left `initmem` to zero the data. The effective code bound is
+  `MaxCode = 24,000` (with `CodeMax = 6F00h` before the boot harness's stdin
+  descriptor), not a 4 KiB window. The remaining capacity question is running
+  CALC with its original stack and heap, deferred in `CALC-COMPILE.md` §3.2.
 - **`[SP]` cannot be encoded on an 8086, and the fix is a shape change.**
   `EmMovAxSp`/`EmMovCxSp` now emit `POP reg` / `PUSH reg` — two instructions
   where there used to be one, so anything that assumed a one-instruction
@@ -2089,10 +2093,11 @@ independent of both.
    Ctrl-K sub-commands and Esc/ANSI sequences are the remaining slice. See
    `TERMINAL.md`; `tests/term_config.sh` asserts both loaders byte for byte.
 
-The 4 KiB code window is no longer a documented limitation but an enforced
-one (`v-TP3-CODEWINDOW`): `Ebyte` reports TP3's error 98 when the code
-reaches `dataBase`, and `t43_codewindow` is the fixture that makes it red
-(the run below it, `t43` not in `run_com_exec`).
+The 4 KiB code window is gone (`v-TP3-CALC-LAYOUT`): the globals are at a
+fixed high base outside the code-only `.COM`, so code no longer runs into its
+own data. `t43_codewindow` is now a string-pool overflow fixture (`ERR 99`);
+the layout is pinned by the independent `.COM` checker and the CALC ladder has
+moved past the old error 98 to a function-declaration blocker at 22,253.
 
 Multi-name declarations both ways are `v-TP3-MULTIDECL`: `var a, b, c :
 integer` and `procedure p (x, y : integer ; z : integer)` parse as TPSRC7

+ 32 - 21
shell/Compiler.mod

@@ -76,6 +76,17 @@ CONST
       image is EntSize further along than it was before the jump existed. *)
    EntSize   = 3 ;
    MaxCode   = 24000 ;
+   (* Program layout, image offsets.  The original puts the runtime and the
+      program's code in CS and the globals in a SEPARATE DS segment, so the
+      .COM contains no data and the code is not limited by a data gap.  We
+      have one flat segment (our boot loader), so the same property is
+      obtained by putting the globals at a fixed high image offset: the .COM
+      is code-only, hdrDS points at DataStart, and initmem zeroes it.  The
+      code must end below CodeMax, where the boot harness puts the stdin
+      descriptor (memory 7000h). *)
+   DataStart = 8000H ;       (* image offset of the first global (memory 8100h) *)
+   CodeMax   = 6F00H ;       (* code must end below memory 7000h *)
+
    MaxSym    = 3000 ;
    MaxPatch  = 2000 ;
    MaxPend   = 400 ;
@@ -332,17 +343,17 @@ VAR
    codeSz, dataSz : CARDINAL ;
 
    (* Image layout, all image-absolute.  rtSz is where the runtime ends and
-      the program header begins; dataBase is where the data area begins
-      (rtSz + 1000H, a fixed 4 KiB above the code).  codeSz and dataSz are
-      PROGRAM sizes - the runtime is excluded - so the numbers the fixture
-      table pins keep meaning what they meant before the runtime was
-      prepended. *)
+      the program header begins; dataBase is the fixed high global area
+      (DataStart = 8000H image, 8100H memory), which is NOT part of the .COM
+      image: initmem zeroes it at startup, as the original's separate DS
+      segment is zeroed.  codeSz and dataSz are PROGRAM sizes - the runtime is
+      excluded. *)
    rtSz, dataBase : CARDINAL ;
 
    (* TRUE once Compile has computed rtSz/dataBase, i.e. once the code window
-      has been measured.  Ebyte refuses to write past dataBase (the 4 KiB code
-      window) and needs to know the window is real before it can: dataBase is
-      0 until it is set, and `pc >= 0' would abort on the very first byte. *)
+      has been measured.  Ebyte refuses to write past CodeMax/MaxCode and
+      needs to know the window is real before it can: imgReady is FALSE until
+      then, and a zero CodeMax would abort on the very first byte. *)
    imgReady : BOOLEAN ;
 
    (* Image offset of the entry jump's rel16 operand, patched at the end of
@@ -745,13 +756,13 @@ END ExpandIncludes ;
 
 PROCEDURE Ebyte (b : BYTE) ;
 BEGIN
-   IF (pc >= MaxCode) OR (imgReady AND (pc >= dataBase)) THEN
-      (* The image buffer bound, and the code window.  dataBase is a fixed
-         4 KiB above the end of the runtime, so a program whose code reaches
-         it has grown into its own data area - the two cannot share a 64 KiB
-         .COM segment.  Reported as TP3's error 98 (memory overflow) rather
-         than a silent truncation; see the layout note in Compile and
-         SUMMARY.md "Known limit". *)
+   IF (pc >= MaxCode) OR (imgReady AND (pc >= CodeMax)) THEN
+      (* The image buffer bound (MaxCode) and the boot harness's code ceiling
+         (CodeMax).  Globals no longer sit immediately above the code - they
+         live at DataStart and initmem zeroes them - so this is purely a
+         capacity check: cbuf cannot hold another byte, or the image would
+         reach the stdin descriptor at memory 7000h.  Reported as TP3's error
+         98 (memory overflow) rather than a silent truncation. *)
       Err (EMemOvf)
    ELSE
       cbuf [pc] := b ;
@@ -6680,7 +6691,7 @@ BEGIN
    END ;
    pc := rt + EntSize ;
    rtSz := rt + EntSize ;
-   dataBase := rtSz + 1000H ;
+   dataBase := DataStart ;
    dc := dataBase ;
    imgReady := TRUE ;
    strTop := 0 ;
@@ -6974,16 +6985,16 @@ END DataBytes ;
 
 PROCEDURE ImageBytes () : CARDINAL ;
 (* Total linked image size: rtSz (the runtime) + CodeBytes (the program).
-   The program is NOT padded out to the data base here - the linker does
-   that, and only it knows the .COM's final size. *)
+   The data is NOT part of this image: globals live at DataBase (8000h image)
+   and initmem zeroes them.  The .COM is exactly these bytes. *)
 BEGIN
    RETURN rtSz + codeSz
 END ImageBytes ;
 
 PROCEDURE DataBase () : CARDINAL ;
-(* image-absolute offset at which the data area begins (rtSz + 1000H).  The
-   linker must place the program's data here and zero-fill from the end of
-   the code up to it. *)
+(* image-absolute offset at which the data area begins (DataStart = 8000H).
+   The data is outside the .COM; initmem zeroes [dataBase, dataBase+dataSz)
+   through the header's hdrDS/hdrHeap words. *)
 BEGIN
    RETURN dataBase
 END DataBase ;

+ 20 - 45
shell/Linker.mod

@@ -5,40 +5,31 @@ IMPLEMENTATION MODULE Linker ;
    The compiler has already produced the whole image - Runtime is copied to
    offset 0 of the code buffer and the program follows it, so every address in
    the image is already absolute (see Compiler.Inittur).  There is therefore
-   no relocation to do: "linking" here means
-
-     1. pad the image so the data area exists, and
-     2. write it out as a DOS .COM file.
-
-   Why padding is needed at all: a .COM file is loaded at CS:0100 with the
-   whole 64 KiB segment available, but the program's globals live at
-   Compiler.DataBase = rtSz + 1000H, which for a small program is far past
-   the last code byte.  Those globals are read and written by absolute
-   address, so the bytes have to be present in the file.  The gap between
-   code and data is zero-filled, which is also what makes it safe: a .COM's
-   stack lives at the TOP of the segment (DOS gives a .COM SS=SP=CS:FFFE), so
-   code and data both have to stay well below it.  A 4 KiB gap keeps them
-   apart for programs up to 4 KiB of code, which is the documented limit.
-
-   Faithfulness note: the original did this in the compiler's overlay loader
-   (TPSRC7 opendest) and wrote a .COM from a different buffer layout with a
-   larger header served by the overlay segment.  Our header is our own - see
-   Runtime.EmitInitMem - and the file layout is the same idea, smaller. *)
+   no relocation to do: "linking" simply writes the code image as a DOS .COM.
+
+   The globals are NOT in that file.  They live at Compiler.DataBase = 8000h
+   image (8100h after the load bias), and the runtime's initmem zeroes
+   [hdrDS, hdrHeap) before the program runs.  This is the flat-segment
+   equivalent of the original's layout: TP3 puts the runtime and program in CS
+   and the globals in a separate DS segment, so its .COM is code-only too.
+   Keeping the data out of the file is what lets the code grow past 4 KiB
+   without overlapping it, and it is why the file length is ImageBytes ().
+
+   The original wrote its .COM through the overlay loader (TPSRC7 opendest)
+   from a different buffer layout with a larger header.  Our header is our
+   own - see Runtime.EmitInitMem - but the file/data separation is the same. *)
 
 FROM SYSTEM IMPORT BYTE, ADR ;
 
 FROM Posix IMPORT open, write, close, unlink ;
 
-FROM Compiler IMPORT ImageBytes, ImageByteAt, DataBase, DataBytes ;
+FROM Compiler IMPORT ImageBytes, ImageByteAt ;
 
 CONST
    O_WRONLY = 1 ;
    O_CREAT  = 64 ;
    O_TRUNC  = 512 ;
 
-VAR
-   padByte : BYTE ;             (* the filler for the code/data gap *)
-
 PROCEDURE DropCh (v : CHAR) ;
 BEGIN
 END DropCh ;
@@ -73,26 +64,15 @@ BEGIN
 END ZCopy ;
 
 PROCEDURE LinkSize () : CARDINAL ;
-(* Size of the linked file in bytes: the image, padded out to cover the whole
-   data area.  This is what a .COM must be, and it is larger than
-   Compiler.ImageBytes whenever the program declares no globals, because the
-   data base is a fixed 4 KiB above the code. *)
+(* The .COM contains the runtime and the program's code ONLY.  The globals
+   live at Compiler.DataBase = 8000H in memory (8100H after the load bias),
+   outside the file, and the runtime's initmem zeroes them at startup.  This
+   matches the original's separate DS segment: the file is code, the data
+   area is zero-filled memory. *)
 BEGIN
-   IF DataBase () + DataBytes () > ImageBytes () THEN
-      RETURN DataBase () + DataBytes ()
-   END ;
    RETURN ImageBytes ()
 END LinkSize ;
 
-PROCEDURE MakePad () ;
-BEGIN
-   (* A local array is not required to be zeroed, and relying on it would be
-      a silent-corruption bug: an uninitialised pad byte would appear in the
-      middle of a program's data area.  ISO will not let an array be assigned
-      the ZType, so the filler is a single explicit BYTE. *)
-   padByte := 0
-END MakePad ;
-
 PROCEDURE WriteCom (path : ARRAY OF CHAR) : BOOLEAN ;
 (* Write the linked image to `path` as a DOS .COM.  Returns TRUE on success;
    on failure the file is removed, so a half-written .COM is never left
@@ -103,7 +83,6 @@ VAR fd : INTEGER ;
     buf : ARRAY [0..255] OF BYTE ;
     z   : ARRAY [0..511] OF CHAR ;
 BEGIN
-   MakePad ;
    ZCopy (z, path) ;
    total := LinkSize () ;
    fd := open (ADR (z), O_WRONLY + O_CREAT + O_TRUNC, 420) ;
@@ -114,11 +93,7 @@ BEGIN
    WHILE i < total DO
       chunk := 0 ;
       WHILE (chunk < 256) AND (i + chunk < total) DO
-         IF i + chunk < ImageBytes () THEN
-            buf [chunk] := ImageByteAt (i + chunk)
-         ELSE
-            buf [chunk] := padByte            (* the gap before the data *)
-         END ;
+         buf [chunk] := ImageByteAt (i + chunk) ;
          INC (chunk)
       END ;
       n := write (fd, ADR (buf), chunk) ;

+ 16 - 14
shell/tests/ComTest.mod

@@ -12,25 +12,25 @@ MODULE ComTest ;
 
    What is actually being asserted, and why these bytes:
 
-     * the file must be LinkSize bytes, i.e. padded past the code to cover
-       the data area.  A .COM shorter than the data area leaves the globals
-       outside the file, so the first read of a global reads whatever the
-       loader happened to leave there.
+     * the file must be LinkSize = ImageBytes bytes, i.e. exactly the
+       runtime and the program's code.  The globals live outside the file at
+       Compiler.DataBase (8000h image, 8100h memory) and initmem zeroes them
+       at startup, as the original's separate DS segment is zeroed.  A file
+       padded to the data base would be a different layout, not this one.
 
-     * the file's byte 0 must be the runtime's first byte (8B C0 = MOV AX,AX,
-       the head of initmem).  If it is not, the runtime was not prepended and
-       every CALL target is wrong.
+     * the file's byte 0 must be the E9 entry jump (Compiler.Inittur); the
+       runtime starts three bytes later.  If it is not, execution starts in
+       the wrong place and every absolute address is still 0100h low.
 
-     * the bytes from ImageBytes to the end must be ZERO.  That gap is what
-       the program's globals live in, so an uninitialised filler byte would
-       be a global that starts as garbage.
+     * there must be no byte past ImageBytes: the file is code-only, so a
+       longer file means the linker is padding to the old data base again.
 
    The @hex dump is the last line of defence: it is what caught the header
    words being written with a nibble shift instead of a byte shift. *)
 
 FROM Posix IMPORT read, write, open, close ;
 FROM TextBuf IMPORT TextLimit, Clear, Length, CharAt, InsertCh ;
-FROM Compiler IMPORT Compile, CodeBytes, DataBytes, ImageBytes, ImageByteAt, DataBase, SetSource ;
+FROM Compiler IMPORT Compile, CodeBytes, DataBytes, ImageBytes, ImageByteAt, SetSource ;
 FROM Linker IMPORT LinkSize, WriteCom ;
 FROM SYSTEM IMPORT ADR, BYTE ;
 
@@ -252,8 +252,10 @@ BEGIN
    NL
 END DumpAt ;
 
-(* Count non-zero bytes in [a,b) - the gap between the code and the data must
-   be entirely zero, or a program's globals start as garbage. *)
+(* Count non-zero bytes in [a,b).  The .COM is code-only now, so this is
+   called with a = ImageBytes and b = fsize: it must be zero because there is
+   no byte past the image.  The independent checker pins the file length; this
+   line keeps the ComTest report honest about what was read back. *)
 PROCEDURE NonZero (a, b : CARDINAL) : CARDINAL ;
 VAR i, k : CARDINAL ;
 BEGIN
@@ -347,7 +349,7 @@ BEGIN
                      IF hexOn THEN
                         DumpAt (0, 16) ;              (* runtime head *)
                         DumpAt (ImageBytes () - 16, 16) ;  (* code tail *)
-                        DumpAt (ImageBytes (), 16)     (* the gap *)
+                        DumpAt (ImageBytes (), 16)     (* past the file *)
                      END
                   ELSE
                      PutStr (lineBuf) ; PutStr ("  CANNOT READ BACK") ; NL

+ 27 - 20
shell/tests/comimage.py

@@ -65,6 +65,7 @@ HDR_SZ = 16                   # 5 header words + 3 buffer words (Compiler)
 # both of their operands shift together.
 # Restated, not asked of the code under test.  See Runtime.LoadBias.
 LOAD_BIAS = 0x100
+DATA_IMAGE = 0x8000         # image offset of the globals; memory 8100h
 
 # initmem's prologue, which is the runtime's only reader of the program header.
 # The displacements +4 and +6 below are the whole point of this constant: the
@@ -95,15 +96,13 @@ def find_header(d):
 
         +0  1                 hdrFlag, always 1
         +2  code end + bias   hdrCS
-        +4  data base + bias  hdrDS, where data base = hdrOff + 1000h
+        +4  data base + bias  hdrDS = 8000h + bias, a FIXED high area
         +6  data end  + bias  hdrHeap, which is hdrDS + dataBytes
 
-    hdrDS ties the header to its OWN offset: the data base is header offset +
-    1000h, and hdrDS is that with the load bias added, so the offset is
-    recoverable from the file with no remembered runtime size - which is the
-    whole point.  A candidate is accepted only if hdrFlag is 1, hdrDS satisfies
-    that equation, hdrHeap is above hdrDS (a heap below its own base is not a
-    layout, it is a coincidence), and hdrCS leaves room for the header itself.
+    The .COM is code-only: hdrDS is above the file and initmem zeroes it at
+    startup, exactly as the original's separate DS segment is zeroed.  The
+    header offset comes from the entry jump (the jump targets hdrOff+HDR_SZ);
+    hdrCS - bias must equal the file length.
 
     initmem is the only code in the image that reads the header, so its bytes
     are pinned at ENT_SZ: a candidate that also has them there is not a
@@ -114,20 +113,28 @@ def find_header(d):
     wrong bytes - which is what the restated runtime size used to do.
     """
     head = bytes(int(x, 16) for x in HEAD.split())
-    if d[ENT_SZ:ENT_SZ + len(head)] != head:
+    if len(d) < ENT_SZ or d[ENT_SZ:ENT_SZ + len(head)] != head:
         return None                      # no runtime: nothing to measure
-    for off in range(ENT_SZ, len(d) - HDR_SZ + 1):
-        w = (lambda b: int.from_bytes(d[off + b:off + b + 2], "little"))
-        if w(0) != 1:                                    # hdrFlag
-            continue
-        if w(HDR_DS_WORD) != off + 0x1000 + LOAD_BIAS:    # hdrDS
-            continue
-        if w(HDR_HEAP_WORD) <= w(HDR_DS_WORD):            # hdrHeap
-            continue
-        if w(2) - LOAD_BIAS < off + HDR_SZ:               # hdrCS
-            continue
-        return off
-    return None
+    if d[0] != 0xE9:
+        return None
+    # The program header sits immediately before the first instruction, and the
+    # entry jump names that instruction.  The data base is now a FIXED high
+    # area (DataStart = 8000h image, 8100h memory), not header+1000h: the .COM
+    # is code-only, as the original's separate DS segment made it.
+    target = ENT_SZ + int.from_bytes(d[1:ENT_SZ], "little", signed=True)
+    off = target - HDR_SZ
+    if off < ENT_SZ or off + HDR_SZ > len(d):
+        return None
+    w = (lambda b: int.from_bytes(d[off + b:off + b + 2], "little"))
+    if w(0) != 1:                                        # hdrFlag
+        return None
+    if w(HDR_DS_WORD) != DATA_IMAGE + LOAD_BIAS:         # hdrDS
+        return None
+    if w(HDR_HEAP_WORD) <= w(HDR_DS_WORD):               # hdrHeap
+        return None
+    if w(2) - LOAD_BIAS != len(d):                       # hdrCS = code end
+        return None
+    return off
 
 
 def entry_target(d):

+ 14 - 18
shell/tests/comtest.py

@@ -8,9 +8,9 @@ This covers the success path, which is the one that produces an artifact:
   2. O opens the Options submenu and C selects Destination = .COM
   3. ESC returns to the main menu
   4. C compiles, and the shell reports the .COM it wrote
-  5. The .COM is then INDEPENDENTLY checked on disk: the runtime's first
-     bytes, a fully zero code/data gap, a size that covers the data area, and
-     the program header words
+  5. The .COM is then INDEPENDENTLY checked on disk: the entry jump and the
+     runtime's first bytes, a code-only file ending at hdrCS, the fixed data
+     base outside the file, and the program header words
 
 Step 5 is the point.  "The shell said it wrote a file" is not evidence that
 the file is right, and the shell reports success from the same state that
@@ -53,8 +53,8 @@ COM = os.path.splitext(FIXTURE)[0] + ".COM"
 # .COM began by executing initmem with whatever the loader left in AX.  A
 # checker that pins a wrong invariant is worse than none, because it makes the
 # wrong thing look tested.
-from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,
-                      HDR_HEAP_WORD, find_header)
+from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,
+                      HDR_DS_WORD, HDR_HEAP_WORD, find_header)
 
 
 def check_com(path, expected_src_len):
@@ -73,7 +73,9 @@ def check_com(path, expected_src_len):
                 "be worse than saying so once"]
     RTSZ = hdr_off
     PROLOG = hdr_off + HDR_SZ
-    DATAB = hdr_off + 0x1000
+    DATAB = DATA_IMAGE
+    cs = (int.from_bytes(d[RTSZ + 2:RTSZ + 4], "little") - LOAD_BIAS) \
+        if len(d) >= RTSZ + 4 else 0
     # The entry jump.  This is the only assertion in the project that can see
     # where execution STARTS, because it is the only one that cares.  It has
     # caught three real bugs in these same three bytes: no jump at all; a jump
@@ -100,9 +102,12 @@ def check_com(path, expected_src_len):
                     % (ENT_SZ,
                        d[ENT_SZ:ENT_SZ + len(HEAD.split())].hex(" ").upper(),
                        HEAD))
-    if len(d) < DATAB:
-        errs.append("file is %d bytes, shorter than the data base %d - the "
-                    "globals would be outside the file" % (len(d), DATAB))
+    # The .COM is code-only.  hdrCS - bias is the code end, which must be the
+    # file length; the globals at DATA_IMAGE are outside the file and initmem
+    # zeroes them.
+    if cs != len(d):
+        errs.append("file is %d bytes, but hdrCS says the code ends at %d"
+                    % (len(d), cs))
     # the runtime must be followed immediately by the program header,
     # whose first word is the "image is valid" flag
     if len(d) >= RTSZ + 2:
@@ -122,15 +127,6 @@ def check_com(path, expected_src_len):
                         "and data end are at +%d/+%d"
                         % (d[ENT_SZ + 4], d[ENT_SZ + 7],
                            HDR_DS_WORD, HDR_HEAP_WORD))
-    # The gap between the end of the code and the data area must be all zero.
-    # hdrCS is a SEGMENT offset, like every other offset in the header, so
-    # LOAD_BIAS comes back off before it is used to index the file.
-    cs = (int.from_bytes(d[RTSZ + 2:RTSZ + 4], "little") - LOAD_BIAS) \
-        if len(d) >= RTSZ + 4 else 0
-    if cs and cs < DATAB:
-        nz = sum(1 for b in d[cs:DATAB] if b)
-        if nz:
-            errs.append("%d non-zero bytes in the code/data gap" % nz)
     return errs
 
 

+ 7 - 7
shell/tests/fixtures/calc_ladder.tsv

@@ -28,10 +28,10 @@
 # Current rung (2026-10-21): `Write(Value:FW:DEC)' in CALC.INC's GotoCell - a
 # REAL write with a field width and decimals.  UpCase (t81_upcase), run-time
 # set membership (t82_setin) and a string field written through DI (t79
-# extended) moved the ladder 15911 -> 18267 -> 19847 -> 20912.  Real output,
-# string comparisons, run-time set algebra (`*`, `+`, `-`, set comparisons and
-# set-expression assignment) and Succ/Pred now work.  The blocker is now the
-# LAYOUT limit: the compiler reserves a fixed 4 KiB code window above the
-# runtime, and CALC's own code has reached it (TP3's error 98, pos 20912).
-# That is the §3 layout change, not a missing construct.
-CALC.PAS	ERR	98	20912
+# extended) moved the ladder 15911 -> 18267 -> 19847 -> 20912 -> 22253.  Real
+# output, string comparisons, run-time set algebra, Succ/Pred and the ORIGINAL
+# MEMORY LAYOUT (code-only .COM, globals at a fixed high data base zeroed by
+# initmem, as the original's separate DS segment) now work.  The blocker is now
+# FUNCTION declarations/returns: `function Exist(FileN : AnyString): boolean;'
+# is not parsed (TP3 error 1, pos 22253).
+CALC.PAS	ERR	1	22253

+ 9 - 8
shell/tests/fixtures/expected.tsv

@@ -305,14 +305,15 @@ t41_strassign	OK	268	52
 #                   .out proves each name landed on its own slot.
 t42_multidecl	OK	309	13
 uierror	ERR	41	331
-#   t43_codewindow  a program whose code exceeds the 4 KiB window between the
-#                   end of the runtime and the data base (runtime + 1000h).  It
-#                   must be REPORTED as TP3 error 98 (memory overflow); before
-#                   Ebyte was bounded by dataBase it overran its own data and
-#                   produced a corrupt image with no diagnostic.  Each of the 20
-#                   writeln cases below embeds 240 bytes inline (wrtinl), so the
-#                   overflow lands inside one of them at pos 4978.
-t43_codewindow	ERR	98	4978
+#   t43_codewindow  the old 4 KiB code-window fixture, re-baselined after the
+#                   original layout landed.  Globals are now at 8000h and the
+#                   code buffer is 24,000 bytes, so the 20 inline 240-byte
+#                   string literals overflow the 4,096-byte string pool first
+#                   (TP3 error 99 at pos 5093) instead of reaching the old
+#                   window.  The overflow must still be REPORTED, not silently
+#                   truncated; the code-only .COM layout is now pinned by the
+#                   independent checker and by every executed fixture.
+t43_codewindow	ERR	99	5093
 #   t44_strindex    s[i] read and write on a string, as a CHAR.  A STRING is a
 #                   length byte at s[0] then the characters, so s[i] is the one
 #                   byte at off+i; a constant index folds into boff exactly as

+ 9 - 8
shell/tests/fixtures/t43_codewindow.pas

@@ -1,12 +1,13 @@
 program t43;
-(* Code-window overflow.  TP3-comp places the data area a fixed 4 KiB above the
-   end of the runtime, so the program code and the program's data share one
-   64 KiB .COM segment and the code may NOT reach the data base.  This program
-   emits more than 4 KiB of code - each string case below is written inline
-   into the code stream by wrtinl (TPSRC4 xwrtstr's literal path) - and the
-   compiler must REPORT that (TP3 error 98, "Memory overflow") rather than
-   write over the data area.  Before the limit was enforced this fixture
-   overran dataBase silently and the image was corrupt. *)
+(* Capacity overflow.  This was the 4 KiB code-window fixture before the
+   original layout landed.  The data no longer sits immediately above the code
+   (globals are at 8000h and initmem zeroes them), and the code buffer is
+   24,000 bytes, so these 240-byte inline literals no longer reach the old
+   window: each writeln embeds its string in the code stream (wrtinl), and the
+   4,096-byte string pool overflows first.  The compiler must still REPORT an
+   overflow (TP3 error 99) rather than truncate or corrupt the image.  The
+   code-window rule itself is now pinned by the .COM layout checker: the file
+   is code-only, ends at hdrCS, and stays below the data base. *)
 begin
   writeln ('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') ;
   writeln ('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') ;

+ 25 - 29
shell/tests/nonvacuity.sh

@@ -1,4 +1,11 @@
 #!/bin/sh
+# The layout cases below rewrite tests/comimage.py and restore it within the
+# same second.  A .pyc names the source by (mtime, size), and two versions of
+# this file can have both in common, so Python would keep executing the
+# MUTATED module after the restore.  That made later cases report the mutation
+# as if it were the baseline.  Do not write bytecode here.
+PYTHONDONTWRITEBYTECODE=1
+export PYTHONDONTWRITEBYTECODE
 # nonvacuity.sh -- prove the runtime checks can actually fail.
 #
 # A test that has never been seen red is not a test.  This script breaks the
@@ -533,8 +540,8 @@ cp "$SAVED_CI" tests/comimage.py
 python3 - <<'PYEOF'
 p = 'tests/comimage.py'
 s = open(p).read()
-old = '        if w(HDR_DS_WORD) != off + 0x1000 + LOAD_BIAS:    # hdrDS'
-new = '        if w(HDR_DS_WORD) != off + 0x1000 + LOAD_BIAS + 1:  # hdrDS'
+old = '    if w(HDR_DS_WORD) != DATA_IMAGE + LOAD_BIAS:         # hdrDS'
+new = '    if w(HDR_DS_WORD) != DATA_IMAGE + LOAD_BIAS + 1:     # hdrDS'
 assert s.count(old) == 1, "the hdrDS equation not found -- update this mutation"
 open(p, 'w').write(s.replace(old, new))
 PYEOF
@@ -569,9 +576,9 @@ cp "$SAVED_T" tests/comtest.py
 python3 - <<'PYEOF'
 p = 'tests/comtest.py'
 s = open(p).read()
-old = 'from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,'
+old = 'from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,'
 new = ('HDR_SZ = 16                   # a second copy of the layout\n'
-       'from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,')
+       'from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,')
 assert s.count(old) == 1, "the comimage import not found -- update this mutation"
 open(p, 'w').write(s.replace(old, new))
 PYEOF
@@ -599,8 +606,8 @@ cp "$SAVED_CI" tests/comimage.py
 python3 - <<'PYEOF'
 p = 'tests/comtest.py'
 s = open(p).read()
-old = ('from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,\n'
-       '                      HDR_HEAP_WORD, find_header)')
+old = ('from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,\n'
+       '                      HDR_DS_WORD, HDR_HEAP_WORD, find_header)')
 new = '# MUTATION: the shared import is gone, but the calls remain\n'
 assert s.count(old) == 1, "the comimage import not found -- update this mutation"
 open(p, 'w').write(s.replace(old, new))
@@ -2997,33 +3004,22 @@ PYEOF
     expect_red "a header that cannot be located is reported, not assumed" \
         "no program header found" $COMCHK "$KEEPDIR"
 
-    # 2. A complete, self-consistent header four bytes later, so the measured
-    #    runtime size becomes $((BASE_RT + 4)) instead of $BASE_RT.  This is the
-    #    positive half of the same check: the derivation must FOLLOW the file,
-    #    and the entry jump assertion - expressed in terms of the measurement -
-    #    must follow it too, demanding $SHIFTED_WANT rather than $((BASE_OFF + 16 - 3)).
-    #
-    #    hdrCS is copied from the header already in the file rather than written
-    #    as a literal.  It used to be the literal 464+100h, which was the image
-    #    length when the image was 720 bytes; four bytes of runtime later it was
-    #    464+100h against a 724-byte image, so this case was ALSO failing on
-    #    hdrCS, for a reason three lines below the one it was written to test.
+    # 2. The entry jump is now the locator: it names the first instruction,
+    #    and the header sits HDR_SZ bytes before it.  Moving the jump four
+    #    bytes off means no header is named, and the check must say so rather
+    #    than fall back on a remembered runtime size.
     rm -rf "$KEEPDIR"; mkdir -p "$KEEPDIR"
     cp "$KEEP"/*.COM "$KEEP"/raw.txt "$KEEPDIR"/
-    python3 - "$KEEPDIR/t01_minimal.COM" "$BASE_OFF" "$SHIFTED_WANT" <<'PYEOF'
+    python3 - "$KEEPDIR/t01_minimal.COM" <<'PYEOF'
 import sys
-p, off, want = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
+p = sys.argv[1]
 d = bytearray(open(p, 'rb').read())
-hdrCS = int.from_bytes(d[off + 2:off + 4], 'little')   # unchanged: still the image end
-off += 4
-ds = off + 0x1000 + 0x100
-w = [1, hdrCS, ds, ds + 4, 0, 0, 0, 0]
-for i, x in enumerate(w):
-    d[off + 2 * i:off + 2 * i + 2] = x.to_bytes(2, 'little')
+rel = int.from_bytes(d[1:3], 'little')
+d[1:3] = ((rel + 4) & 0xFFFF).to_bytes(2, 'little')
 open(p, 'wb').write(bytes(d))
 PYEOF
-    expect_red "the measured runtime size follows the image ($BASE_RT -> $((BASE_RT + 4)))" \
-        "want $SHIFTED_WANT" $COMCHK "$KEEPDIR"
+    expect_red "the entry jump must name the header it follows" \
+        "no program header found" $COMCHK "$KEEPDIR"
 
     # 3. The entry jump's opcode.  One byte, and the only assertion in the
     #    project that can see where execution STARTS.
@@ -3037,7 +3033,7 @@ d[0] = 0xEA
 open(p, 'wb').write(bytes(d))
 PYEOF
     expect_red "the entry jump must be E9, not a near JMP" \
-        "not the E9 of the entry jump" $COMCHK "$KEEPDIR"
+        "no program header found" $COMCHK "$KEEPDIR"
 
     # 4. The entry jump's target, moved one instruction earlier.  A .COM that
     #    lands in the middle of the prologue runs, prints something and exits
@@ -3052,7 +3048,7 @@ d[1:3] = (100).to_bytes(2, 'little')
 open(p, 'wb').write(bytes(d))
 PYEOF
     expect_red "the entry jump must land on the first instruction" \
-        "entry jump rel16=100" $COMCHK "$KEEPDIR"
+        "no program header found" $COMCHK "$KEEPDIR"
 
     rm -rf "$KEEPDIR"
 fi

+ 17 - 17
shell/tests/run_com_tests.sh

@@ -4,9 +4,9 @@
 #
 # The independent pass matters: ComTest computes the expectations from the same
 # Compiler state it is testing, so a bug in the compiler would be invisible to
-# it.  The Python pass re-derives what the file must contain - the runtime's
-# first bytes, a zero gap, a size that covers the data area - from the
-# constants only, and cross-checks.
+# it.  The Python pass re-derives what the file must contain - the entry jump,
+# the runtime's first bytes, a code-only file ending at hdrCS, and the fixed
+# data base outside it - from the constants only, and cross-checks.
 set -u
 D=/home/eric/Projets/Projets-Modula2/MyWork/TP3-comp/shell
 GM2=/home/eric/bin/Modula2/Gm2/bin/gm2
@@ -129,8 +129,8 @@ out = sys.argv[1]
 # last one; the byte checks below are all satisfied by an address that is
 # consistently 0100h wrong.  See comimage.LOAD_BIAS and Runtime.LoadBias.
 sys.path.insert(0, sys.argv[2])
-from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, HEAD, HDR_DS_WORD,
-                      HDR_HEAP_WORD, find_header)
+from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,
+                      HDR_DS_WORD, HDR_HEAP_WORD, find_header)
 
 raw = open(os.path.join(out, 'raw.txt')).read()
 rows = re.findall(r'(\S+\.pas)\s+OK\s+com=(\d+)\s+image=(\d+)\s+data=(\d+)\s+nonzeroInGap=(\d+)', raw)
@@ -160,11 +160,11 @@ for name, com, image, data, nzg in rows:
     if hdrOff is None:
         errs.append('no program header found: the layout this checker knows '
                     'how to look for is not the one in the file')
-        RTSZ, PROLOG, DATAB = ENT_SZ, ENT_SZ + HDR_SZ, ENT_SZ + 0x1000
+        RTSZ, PROLOG, DATAB = ENT_SZ, ENT_SZ + HDR_SZ, DATA_IMAGE
     else:
         RTSZ = hdrOff
         PROLOG = hdrOff + HDR_SZ
-        DATAB = hdrOff + 0x1000
+        DATAB = DATA_IMAGE
         if RTSZ != last_rt:
             last_rt = RTSZ
             print('  measured runtime size: %d bytes (header at image offset '
@@ -177,9 +177,8 @@ for name, com, image, data, nzg in rows:
     #
     #   1. no jump at all, so a .COM began by executing the runtime's initmem
     #      with whatever the loader left in AX;
-    #   2. a jump to `pc`, one byte past the last instruction, into the
-    #      zero-filled code/data gap, where the CPU slides through
-    #      `ADD [BX+SI],AL` until it faults;
+    #   2. a jump to `pc`, one byte past the last instruction, where the CPU
+    #      slides through `ADD [BX+SI],AL` until it faults;
     #   3. a jump to RTSZ, which is the program HEADER - sixteen bytes of DATA
     #      that the CPU then decodes as instructions.  This one is the reason
     #      the target is pinned to PROLOG and not to RTSZ: whether it works
@@ -210,14 +209,15 @@ for name, com, image, data, nzg in rows:
                        HEAD))
     if len(d) != com:
         errs.append('file is %d bytes, harness said %d' % (len(d), com))
-    if DATAB + data != len(d):
-        errs.append('size %d != dataBase+data %d' % (len(d), DATAB + data))
-    # the gap between the image and the data area must be entirely zero
-    gap = d[image:DATAB]
-    if any(gap):
-        errs.append('%d non-zero bytes in the code/data gap' % sum(1 for b in gap if b))
+    # The .COM is code-only: the globals live at DATAB outside the file and
+    # initmem zeroes them.  So the file length must be the code end.
+    if len(d) != image:
+        errs.append('file is %d bytes, but the code ends at %d'
+                    % (len(d), image))
+    if image > DATAB:
+        errs.append('code ends at %d, past the data base %d' % (image, DATAB))
     if nzg != 0:
-        errs.append('harness itself reported %d non-zero gap bytes' % nzg)
+        errs.append('harness reported %d bytes past the image' % nzg)
     # the program must start exactly where the runtime ends
     if image < RTSZ:
         errs.append('image %d shorter than the runtime %d' % (image, RTSZ))