|
|
@@ -27,6 +27,7 @@ manual, not guessed.
|
|
|
| `CmdRun` (the `R` key) + `Exec86`, the in-process 8086 interpreter | `v-TP3-CMDRUN` | done, **a second execution oracle: 33/33 fixtures agree with qemu byte for byte, and `R` runs one inside the shell** |
|
|
|
| Image layout in one file; a check's sweep region measured, not restated | `v-TP3-IMAGELAYOUT` | done, **every reader of a linked `.COM` imports `tests/comimage.py`, and the region `check_framedisp` sweeps is two readings of the file required to agree** |
|
|
|
| Arguments pushed as they are parsed; the parameter frame read from BP+4 backwards | `v-TP3-ARGORDER` | done, **`t36_argclobber` runs: a computed argument survives the parse of the next one, and the callee's slots match the order they were pushed in** |
|
|
|
+| A `byte` reader of its own, and bug 36's four sites proved re-derivable | `v-TP3-NONVACUITY-RDBYTE` | done, **`readln (b)` of a `byte` prints the byte and leaves the variable beside it alone; reverting any one of the argument-order fix's four sites turns `t36_argclobber` red** |
|
|
|
|
|
|
Every row that names a tag has one, and every tag points at a commit on
|
|
|
`master`; verified by diffing the rows against `git tag -l`, which is how
|
|
|
@@ -120,11 +121,11 @@ needing a squint. It was checked for vacuousness by reverting the string
|
|
|
scanner fix: the suite went red with exit 1, and came back green only when the
|
|
|
fix was restored.
|
|
|
|
|
|
-**34 of 37 fixtures compile clean**, up from 1 (the empty program) when the
|
|
|
+**35 of 38 fixtures compile clean**, up from 1 (the empty program) when the
|
|
|
direct harness was first built.
|
|
|
|
|
|
```
|
|
|
-compile matrix: 37 passed, 0 failed (of 37)
|
|
|
+compile matrix: 38 passed, 0 failed (of 38)
|
|
|
```
|
|
|
|
|
|
Compiling: `t01` minimal · `t04` var+assign+`writeln` · `t06` two args ·
|
|
|
@@ -140,16 +141,17 @@ expression · `t31` a value parameter *and* a call across statements ·
|
|
|
`t32` `EXIT` out of a `for` body · `t33` all six comparisons · `t34` the
|
|
|
operators nothing else uses (`div mod and or`, unary `-`, a variable `*`) ·
|
|
|
`t35` `not`, both halves of TPSRC9's `neglevel` split · `t36` a call whose
|
|
|
-arguments are computed in various positions, each one keeping its own value.
|
|
|
+arguments are computed in various positions, each one keeping its own value. ·
|
|
|
+`t37` `readln (b)` of a `byte` sitting next to an `integer`
|
|
|
|
|
|
Failing, all deliberately: `t14` `array [1..5] of integer` at its point of use
|
|
|
and `t25` a string literal used as a *value* (`s := 'hi'`) both → `ENoLib`
|
|
|
(102), the original's "not implemented" path; `uierror` is a deliberate syntax
|
|
|
error (41) used by the UI test.
|
|
|
|
|
|
-`run_com_tests.sh` additionally links **all 34 that compile** to a real `.COM`
|
|
|
+`run_com_tests.sh` additionally links **all 35 that compile** to a real `.COM`
|
|
|
and re-verifies the bytes with an independent Python checker that *measures*
|
|
|
-the layout instead of restating it: `independent .COM check: 34 checked, 0
|
|
|
+the layout instead of restating it: `independent .COM check: 35 checked, 0
|
|
|
failed`. That last part was itself a bug fix — see "the two restated
|
|
|
constants" below.
|
|
|
|
|
|
@@ -190,11 +192,11 @@ proves each one can go red.
|
|
|
| `probe/modrm11.py` | the mod=11 register identities, by **encoding** with GNU `as` and decoding with FCML, against hard-coded bytes | the table agreeing with itself |
|
|
|
| `audit_helpers.py` | every one-line emitter in **both** `Runtime.mod` and `Compiler.mod` decodes to what its *name* says — **101/101**, from an inventory scanned independently of the parser | anything longer than one instruction |
|
|
|
| `check_comimage.py` | a linked image's layout is described in **exactly one file**: `comimage.py` defines each part once, every reader imports it, no second `find_header` exists anywhere in `tests/` | a check that *hand-rolls* the layout from literals (`hdr = 3 + rt_size`), which defines none of those names — the readers catch that a different way: each states the layout twice, from two independent sources, and requires the two to agree |
|
|
|
-| `check_runtime.py` + `runtime.golden` | the built runtime's code region (436 bytes, code ends at 405) sweeps cleanly through FCML, every entry and all branch targets land on an instruction boundary, and the whole disassembly is byte-for-byte the committed golden | whether the golden is *right* |
|
|
|
+| `check_runtime.py` + `runtime.golden` | the built runtime's code region (465 bytes, code ends at 434) sweeps cleanly through FCML, every entry and all branch targets land on an instruction boundary, and the whole disassembly is byte-for-byte the committed golden | whether the golden is *right* |
|
|
|
| `check_framedisp.py` | `[BP+off]` uses disp8 iff `off <= 127`, for locals (negative) and far parameters (>127) | which of the two encodings was chosen, if the other also works |
|
|
|
| `run_com_exec.py` | the **emitted image executes** and prints exactly the expected bytes | semantics the fixture never exercises |
|
|
|
| `run_exec86.py` | the same image runs in a **second, independent 8086** and agrees with qemu byte for byte, 33/33 | the `.out` file it shares with `run_com_exec.py` — a wrong expectation fails both at once |
|
|
|
-| `rt_exec.py` | the **runtime entries themselves** are called directly, one qemu boot per call, 35 cases plus a pre-flight, 36/36 — plus a `check_bp_contract` pre-flight over the built blob: an entry that borrows BP must open with `55 8B EC` (exact) and contain a `5D` (a screen, because `5D` is also a displacement byte) | whether the *caller's* half of a contract is right, where the only witness is a case that happens to make two calls in a row |
|
|
|
+| `rt_exec.py` | the **runtime entries themselves** are called directly, one qemu boot per call, 40 cases plus a pre-flight, 41/41 — plus a `check_bp_contract` pre-flight over the built blob: an entry that borrows BP must open with `55 8B EC` (exact) and contain a `5D` (a screen, because `5D` is also a displacement byte) | whether the *caller's* half of a contract is right, where the only witness is a case that happens to make two calls in a row |
|
|
|
|
|
|
Two of these deserve the detail, because the reason they exist is the reason
|
|
|
they are hard.
|
|
|
@@ -266,16 +268,16 @@ and `tests/check_comimage.py` asserting there is only one copy of each.
|
|
|
### Execution under qemu — `tests/run_com_exec.py`
|
|
|
|
|
|
The check that cannot be written as a byte comparison, so also the one that
|
|
|
-finds the most: 34 fixtures are compiled to `.COM`, put on a floppy, booted,
|
|
|
+finds the most: 35 fixtures are compiled to `.COM`, put on a floppy, booted,
|
|
|
and their serial output compared to a committed `.out` file **exactly** — CRLF
|
|
|
included — plus the exit code passed to `INT 21h AH=4Ch`.
|
|
|
|
|
|
```
|
|
|
-execution: 34 passed, 0 failed (of 34)
|
|
|
+execution: 35 passed, 0 failed (of 35)
|
|
|
```
|
|
|
|
|
|
This is no longer the only execution oracle: `run_exec86.py` below runs the
|
|
|
-same 34 images a second time, in a different machine, and requires the two to
|
|
|
+same 35 images a second time, in a different machine, and requires the two to
|
|
|
agree byte for byte.
|
|
|
|
|
|
It also found bug 33 — the branch polarity inverted in *every* conditional in
|
|
|
@@ -298,7 +300,7 @@ signature (`hdrFlag = 1`, `hdrDS == hdrOff + 1000h + bias`, `hdrHeap > hdrDS`,
|
|
|
`prologAt` and `dataBase` per file:
|
|
|
|
|
|
```
|
|
|
-measured runtime size: 436 bytes (header at image offset 439)
|
|
|
+measured runtime size: 465 bytes (header at image offset 468)
|
|
|
```
|
|
|
|
|
|
A restated constant that has drifted is worse than a derived one, and the two
|
|
|
@@ -325,8 +327,8 @@ compiler: **does the 8086 have this instruction at all?**
|
|
|
8086 check: 38 comparison sites, 34 lowered to a Boolean value, 13 lowered to a branch
|
|
|
value conditions : = x6 <> x2 < x5 >= x3 <= x2 > x16
|
|
|
branch conditions : IF / REPEAT x9 CASE x2 FOR downto x1 FOR to x3
|
|
|
- runtime: 436 bytes, 219 swept, 0 0F-prefixed
|
|
|
- program code: 31 of 34 fixtures swept end to end, 3373 bytes
|
|
|
+ runtime: 465 bytes, 235 swept, 0 0F-prefixed
|
|
|
+ program code: 32 of 35 fixtures swept end to end, 3496 bytes
|
|
|
t33_cmpops: 13 comparisons matched against their source operators, in order
|
|
|
clause H: 8 of 8 fixtures matched the branch conditions read off their source
|
|
|
```
|
|
|
@@ -380,7 +382,7 @@ check runs every fixture through it as well as through qemu and requires the
|
|
|
two to agree **byte for byte**:
|
|
|
|
|
|
```
|
|
|
-exec86: 34 passed, 0 failed (of 34), cross-checked against qemu
|
|
|
+exec86: 35 passed, 0 failed (of 35), cross-checked against qemu
|
|
|
```
|
|
|
|
|
|
Three assertions per fixture, in order of how much they are worth: the output
|
|
|
@@ -438,27 +440,40 @@ goes red before a single instruction has run.
|
|
|
|
|
|
### Non-vacuity — `tests/nonvacuity.sh`
|
|
|
|
|
|
-Every assertion in this file is proved able to fail: **62 deliberate
|
|
|
+Every assertion in this file is proved able to fail: **66 deliberate
|
|
|
breakages, each asserted to turn exactly one named check red for the stated
|
|
|
-reason, then restored and re-asserted green** — `non-vacuity: 62 ok, 0 failed`.
|
|
|
+reason, then restored and re-asserted green** — `non-vacuity: 66 ok, 0 failed`.
|
|
|
They cover the runtime's emitter audit and its restored source, the mod=11
|
|
|
table (including restoring the exact wrong table this project once shipped),
|
|
|
the `[BP+off]` rule, the behavioural bugs, the emitter-name audit of
|
|
|
`Compiler.mod`, the BP contract, the `.COM` layout checker, the image-layout
|
|
|
helper and the three checks that read it, the 8086 lowering, the interpreter
|
|
|
-itself, and the `R` key.
|
|
|
-
|
|
|
-One assertion is proved *outside* that suite, and the difference is stated
|
|
|
-rather than papered over: `t36_argclobber`'s `.out` was compiled and run
|
|
|
-against the compiler as it stood **before** the argument-order fix, on both
|
|
|
-oracles, and came back red with every line whose arguments included a computed
|
|
|
-value wrong while the two lines built from plain names stayed correct — so the
|
|
|
-two that would catch a one-sided flip were seen green before the fix, not after
|
|
|
-it. The evidence is quoted in the commit that introduces the fixture, and green
|
|
|
-followed only with the fix in. What is *not* there yet is the in-suite form of
|
|
|
-that proof: reverting each of the three call parsers and the parameter-offset
|
|
|
-remap in turn and requiring `run_com_exec.py t36_argclobber` red. That is the
|
|
|
-first of the next steps, and until it exists the count above does not cover it.
|
|
|
+itself, the `R` key, and — since `v-TP3-NONVACUITY-RDBYTE` — the four
|
|
|
+independent sites of the argument-order fix.
|
|
|
+
|
|
|
+Those four replace a proof that could only be quoted. `t36_argclobber`'s
|
|
|
+`.out` had been compiled and run against the compiler as it stood **before**
|
|
|
+the argument-order fix, on both oracles, and came back red on every line whose
|
|
|
+arguments included a computed value while the two lines built from plain names
|
|
|
+stayed correct — the two that would catch a one-sided flip. That run is a fact
|
|
|
+about the past: it cannot be re-derived, and nothing stopped the four sites of
|
|
|
+the fix from drifting apart. So each one is now reverted in turn and
|
|
|
+`run_com_exec.py t36_argclobber` must come back red — the parameter-offset
|
|
|
+remap in `ProcFunc` deleted, and each of the three call parsers
|
|
|
+(`ParseCallArgs`, `ParseCall`, `IoCall`) put back to its deferred-push form by
|
|
|
+splicing the *whole procedure* out of the tree the fix landed on top of
|
|
|
+(`git show v-TP3-IMAGELAYOUT:shell/Compiler.mod`), so the old code's source of
|
|
|
+truth is the old code rather than a copy of it living in this file and free to
|
|
|
+drift from what it claims to reproduce.
|
|
|
+
|
|
|
+The four are not one case wearing four hats. Only `ParseCall` moves the
|
|
|
+function call's result (`19` → `10`) and only `IoCall` moves the last line
|
|
|
+(`FALSE 1` → `TRUE 544`); and the callee and `ParseCallArgs` both turn `5 3 7`
|
|
|
+into `7 3 5` and then part company — the callee swaps the plain-name rows
|
|
|
+(`8 0` → `0 8`, `TRUE FALSE` → `FALSE TRUE`) where the deferred parser zeroes
|
|
|
+them (`0 0`, `FALSE FALSE`). A revert that leaves the fixture green is reported
|
|
|
+as NOT NON-VACUOUS, and one that does not apply at all is a BROKEN CASE; both
|
|
|
+count as failures, so neither can pass by being skipped.
|
|
|
|
|
|
Eight arrived with the previous milestone (`v-TP3-CMDRUN`), and each one is the
|
|
|
same shape: code that compiled clean and passed every byte-level check, until it
|
|
|
@@ -646,12 +661,13 @@ told apart from a broken harness without re-deriving anything.
|
|
|
never calls `rdint` without a `readln` behind it, never calls `rdbool` twice in
|
|
|
a row, and never exercises the `INT16` extremes individually.
|
|
|
|
|
|
-35 cases, one fresh qemu boot each, plus a pre-flight that runs before any of
|
|
|
+40 cases, one fresh qemu boot each, plus a pre-flight that runs before any of
|
|
|
them: `initmem` (data poisoned to `AA` first, so
|
|
|
a zero is a *result* and not a leftover), `wrint` ×10 including both `INT16`
|
|
|
extremes, `wrchar` ×2, `wrbool` ×3, `wrln`, `stackchk`, a composed
|
|
|
`writeln(42) writeln TRUE` sequence, `rdint` ×6 against supplied input,
|
|
|
-`rdchar` ×2, `rdbool` ×6, `rdint` at EOF, and `wrtinl`.
|
|
|
+`rdchar` ×2, `rdbool` ×6, `rdbyte` ×4 against supplied input with a poison
|
|
|
+byte sitting past the one-byte variable, `rdint`/`rdbyte` at EOF, and `wrtinl`.
|
|
|
|
|
|
Three properties are load-bearing, and each of them was chosen against the
|
|
|
easier alternative:
|
|
|
@@ -798,7 +814,7 @@ catches a runaway. Execution outside `[0100h, loadHi)` faults, which is what
|
|
|
makes the `R` non-vacuity case fail on its very first step.
|
|
|
|
|
|
The instruction set is not "the 8086" but the measured union of (a) every byte
|
|
|
-`Runtime.mod` emits — 219 instructions, swept by `check_8086.py` — and (b)
|
|
|
+`Runtime.mod` emits — 235 instructions, swept by `check_8086.py` — and (b)
|
|
|
every byte `Compiler.mod`'s `Em*` procedures can write (the generated region
|
|
|
cannot be swept: inline string literals desynchronise a sweep, so the authority
|
|
|
there is the source). Everything outside that union faults rather than
|
|
|
@@ -898,21 +914,22 @@ initialised past it (`MOV pc,#$2D7C`). Same shape here, and now actually done:
|
|
|
image-absolute. **No relocation pass is needed** — worth having paid for, since
|
|
|
a linker that has to walk fixups is a linker that can get them wrong.
|
|
|
|
|
|
-Current blob: **436 bytes**, 14 entries, 38 branch targets, offsets read back
|
|
|
+Current blob: **465 bytes**, 15 entries, 41 branch targets, offsets read back
|
|
|
out of the assembled bytes by `tests/check_runtime.py` rather than asserted by
|
|
|
hand:
|
|
|
|
|
|
| entry | offset | entry | offset | entry | offset |
|
|
|
|---|---|---|---|---|---|
|
|
|
| `initmem` | 0 | `wrint` | 36 | `rdint` | 171 |
|
|
|
-| `progend` | 28 | `wrchar` | 97 | `rdchar` | 279 |
|
|
|
-| `stackchk` | 35 | `wrbool` | 111 | `rdbool` | 302 |
|
|
|
-| `halt` | 28 | `wrreal` | 136 | `rdln` | 350 |
|
|
|
-| | | `wrln` | 144 | `wrtinl` | 152 |
|
|
|
+| `progend` | 28 | `wrchar` | 97 | `rdbyte` | 278 |
|
|
|
+| `stackchk` | 35 | `wrbool` | 111 | `rdchar` | 308 |
|
|
|
+| `halt` | 28 | `wrreal` | 136 | `rdbool` | 331 |
|
|
|
+| | | `wrln` | 144 | `rdln` | 379 |
|
|
|
+| | | `wrtinl` | 152 | | |
|
|
|
|
|
|
Every number in that table is measured, and that is the point: it was last
|
|
|
correct when the blob was 391 bytes, and the entries quietly moved as it grew
|
|
|
-to 436 — six of the fourteen offsets in the previous version of this table were
|
|
|
+past 436 to 465 — six of the offsets in an earlier version of this table were
|
|
|
stale, and the size was stale too. A restated constant that has drifted is
|
|
|
worse than a derived one, which is why the compiler asks
|
|
|
`Runtime.RT_Entry` instead (below) and why the checkers locate the runtime by
|
|
|
@@ -931,7 +948,7 @@ Conventions, matching `Compiler.IoCall` exactly:
|
|
|
| entry | argument | notes |
|
|
|
|---|---|---|
|
|
|
| `WrInt/WrChar/WrBool/WrReal` | one 16-bit **value** on the stack | caller pops |
|
|
|
-| `RdInt/RdChar/RdBool` | one **address** on the stack | caller pops |
|
|
|
+| `RdInt/RdByte/RdChar/RdBool` | one **address** on the stack | caller pops |
|
|
|
| `WrLn/RdLn/StackChk` | nothing | |
|
|
|
| `InitMem` | `AX` = offset of the program header | a *register*, not a stack word |
|
|
|
| `ProgEnd/Halt` | nothing | exits, code 0 |
|
|
|
@@ -943,6 +960,12 @@ leaves globals undefined. `StackChk` is a bare `RET` — range and stack checkin
|
|
|
aren't compiled in yet, and the call site sits mid-expression, so it must not
|
|
|
touch a register.
|
|
|
|
|
|
+`rdscan` is the one routine the readers share and is **not** an entry: it takes
|
|
|
+no argument and no frame, clobbers `AX BX CX DX DI`, returns the value in `AX`
|
|
|
+and a state in `CX`, and `CX = 2` is the `^Z` path — nothing was read, so the
|
|
|
+entry that called it stores nothing. `rdint` and `rdbyte` each open their own
|
|
|
+`PUSH BP` frame, call it, and store `AX` or `AL` themselves.
|
|
|
+
|
|
|
**Five emitter bugs were fixed here in one session, all found by the
|
|
|
`audit_helpers.py` name-vs-decode pass described above**, and all of them were
|
|
|
invisible to everything else that was already in place:
|
|
|
@@ -968,7 +991,7 @@ all patched after the blob is placed, so nothing depends on a hand-computed
|
|
|
displacement.
|
|
|
|
|
|
**It executes.** Every entry is called directly under qemu by
|
|
|
-`tests/rt_exec.py` — 36 cases, all passing — with the register contract each
|
|
|
+`tests/rt_exec.py` — 41 cases, all passing — with the register contract each
|
|
|
entry must honour stated rather than assumed, which is how `wrchar` and
|
|
|
`wrbool` were caught destroying the caller's BP. That milestone
|
|
|
(`v-TP3-BP-CONTRACT`) is the reason this paragraph no longer says the runtime
|
|
|
@@ -1001,9 +1024,9 @@ assertion, rather than asking the compiler where it thinks the header is.
|
|
|
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` (436 + 4096 =
|
|
|
-4532), so a program whose code exceeds 4 KiB runs into its own data. Every
|
|
|
-fixture's image is between 4539 and 4547 bytes. Documented rather than fixed,
|
|
|
+**Known limit:** the data area starts at a fixed `rtSz + 1000H` (465 + 4096 =
|
|
|
+4561), so a program whose code exceeds 4 KiB runs into its own data. Every
|
|
|
+fixture's image is between 4568 and 4576 bytes. Documented rather than fixed,
|
|
|
because the
|
|
|
original has the same fixed-offset behaviour.
|
|
|
|
|
|
@@ -1072,9 +1095,9 @@ Details that are deliberate, not incidental:
|
|
|
`t34_arith`
|
|
|
through the pty because a pty test is expensive and this one's job is to
|
|
|
prove the *path* exists (compile → poke → run → report → no file written),
|
|
|
- which it does with eight assertions. The other 33 are covered by
|
|
|
+ which it does with eight assertions. The other 34 are covered by
|
|
|
`run_exec86.py`, which calls the same interpreter directly.
|
|
|
-- **Every fixture that compiles is now also run.** 34 of 37 execute; the 3 that
|
|
|
+- **Every fixture that compiles is now also run.** 35 of 38 execute; the 3 that
|
|
|
do not are `t14` and `t25` (`ENoLib`, by design) and `uierror` (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,
|
|
|
@@ -1131,11 +1154,11 @@ Details that are deliberate, not incidental:
|
|
|
(`7x 03 E9`, searching for the `E9`) and by `t22_case`'s execution, but the
|
|
|
label immediate itself is unchecked.
|
|
|
- **The runtime's entries are now each called directly, and two of its
|
|
|
- invariants are stated rather than implied.** 436 bytes, 14 entries, 101
|
|
|
+ invariants are stated rather than implied.** 465 bytes, 15 entries, 102
|
|
|
emitter helpers decoded against their own names across both modules, the
|
|
|
whole code region golden-pinned, all branch targets on instruction
|
|
|
- boundaries — and 35 cases that call the entries one at a time under qemu
|
|
|
- (`rt_exec.py`, 36/36). What is *not* covered is what a direct call cannot
|
|
|
+ boundaries — and 40 cases that call the entries one at a time under qemu
|
|
|
+ (`rt_exec.py`, 41/41 with its BP pre-flight). What is *not* covered is what a direct call cannot
|
|
|
see: `wrtinl` is checked, but only from the harness's side of the calling
|
|
|
contract. (The nine fixtures that compiled but were never executed used to sit
|
|
|
here as an extra gap; they are now run, so every fixture that reaches the
|
|
|
@@ -1182,10 +1205,11 @@ Details that are deliberate, not incidental:
|
|
|
still said OK.
|
|
|
- **Comma-separated names are not supported.** `var i, c : integer;` is a
|
|
|
parse error. Pre-existing, unrelated to any of the above, and still open.
|
|
|
-- **`readln` of a `BYTE` is a latent 1-byte overflow.** It calls `rdint`, which
|
|
|
- stores a 2-byte word, so the high byte lands on the next variable. TP3 has a
|
|
|
- separate `xrdbyte` for exactly this. No fixture declares a `BYTE`, which is
|
|
|
- the only reason this has never been observed. Write the fixture first.
|
|
|
+- **`readln` of a `BOOLEAN` still stores a word into a one-byte variable.**
|
|
|
+ This is bug 37's shape, unfixed: the BYTE half now has its own `rdbyte`
|
|
|
+ entry, but a boolean read still dispatches to `rdbool`, which stores `CX`.
|
|
|
+ TP3 refuses a boolean read with error 66 rather than implementing it, so the
|
|
|
+ honest options are to refuse it too or to add the one-byte entry.
|
|
|
- **The program-header parameter loop is unguarded against non-advancing
|
|
|
input.** `program p(1;)` would loop forever. The fix needs a `BOOLEAN` flag
|
|
|
and **must not** use `EXIT`, which ICEs gm2 in pass 3 (see the pitfalls
|
|
|
@@ -1393,9 +1417,11 @@ Eleven more, and the two worst in the project are here.
|
|
|
so `rdint` consumed the delimiter and the following statement lost its
|
|
|
first character. The fix is a one-character pushback slot `D_PUSH` plus
|
|
|
`ungetch`, and the slot's *address moves whenever the runtime grows*
|
|
|
- (`rtSz + LoadBias + dataAt + D_PUSH` — at 436 bytes, image `0x1B4`, memory
|
|
|
- `0x2B4`, one pad byte before the program header at `0x1B7`). It is computed,
|
|
|
- never hard-coded: the same class of restated-constant bug as `RT_SZ` above.
|
|
|
+ (`LdBxVx (D_PUSH)` is fixed up to `dataAt + D_PUSH + LoadBias`; in the
|
|
|
+ current blob that is `8B 1E CE 02`, so memory `0x2CE` / image `0x1CE`, one
|
|
|
+ unused byte before `rtSz` at image `0x1D1` and the program header three
|
|
|
+ bytes past that). It is computed, never hard-coded: the same class of
|
|
|
+ restated-constant bug as `RT_SZ` above.
|
|
|
23. **`rdint` did not mirror TP3's `xrdint`/`readnum`.** TP3 checks for `^Z`
|
|
|
*first*, then skips characters `<= 20h`, then an optional sign, then digits,
|
|
|
then **pushes the terminator back**. Ours skipped leading whitespace only
|
|
|
@@ -1694,7 +1720,46 @@ reason for existing.
|
|
|
compiler got the two plain-name lines right and the other six wrong, on
|
|
|
both oracles at once. Those two greens matter — they are what would catch a
|
|
|
fix that flipped only one of the two halves — and they were green *before*
|
|
|
- the change as well as after it.
|
|
|
+ the change as well as after it. `nonvacuity.sh` now regenerates that red
|
|
|
+ in suite as well: see "Non-vacuity" above.
|
|
|
+
|
|
|
+### Then a `byte` variable printed the bytes beside it, and one more appeared
|
|
|
+
|
|
|
+37. **`readln (b)` of a `byte` went through `rdint`, which stores a *word* — so
|
|
|
+ the second byte written was the byte after the variable.** `b` came out
|
|
|
+ right and the variable beside it was silently rewritten; `t37_readbyte`
|
|
|
+ exists because no fixture declared a `byte`, which is why nothing had ever
|
|
|
+ seen it.
|
|
|
+
|
|
|
+ The fixture reads a `byte` `b` sitting next to an `integer` `n` that is
|
|
|
+ assigned `1000` once and never touched again, twice, so its `.out` has two
|
|
|
+ lines — and both were derived by hand from that layout before anything was
|
|
|
+ changed: line 1 is `200 1000`, line 2 is `255 1000`. The pre-fix compiler
|
|
|
+ printed `200 768`
|
|
|
+ and `255 1023`, because `1000` is `$03E8` and a word store of `$00C8`
|
|
|
+ overwrites its low byte with `00` (`$0300` = 768) while a store of `$FFFF`
|
|
|
+ overwrites it with `FF` (`$03FF` = 1023). The `n` that came out wrong had
|
|
|
+ never been read from the input at all: it was written while `b` was being
|
|
|
+ read, one byte to the right of where the store was supposed to stop.
|
|
|
+
|
|
|
+ TP3 has a separate `xrdbyte` for exactly this (TPSRC4) and TPSRC8's
|
|
|
+ `prdemit` dispatches on the variable's size, so the fix is the same shape:
|
|
|
+ `IoCall` picks `TU_RdByte` when `symtab [arg.idx].size = 1` and `TU_RdInt`
|
|
|
+ otherwise, and `Runtime.mod` grew a `rdbyte` entry. The reader was *not*
|
|
|
+ duplicated — skipping, the optional sign, the digits and pushing the
|
|
|
+ terminator back are one routine with no frame, and `rdint` and `rdbyte` are
|
|
|
+ two thin entries over it, each with its own `PUSH BP` frame, storing `AX`
|
|
|
+ or `AL`. The state comes back in `CX` and is checked inside each entry,
|
|
|
+ where it is live: the check the old shared tail had could never fire,
|
|
|
+ because `CX` there was only ever 0 or 1. `CX = 2` is the `^Z` path, where
|
|
|
+ nothing was read and so nothing is stored.
|
|
|
+
|
|
|
+ The *width* of the store is asserted by execution and not by a size —
|
|
|
+ `t37`'s code and data sizes did not move when the fix landed, because both
|
|
|
+ entries take their argument the same way, and no byte count can tell a
|
|
|
+ 1-byte store from a 2-byte one. The `rt_exec` probes put a poison byte
|
|
|
+ just past the variable and require it to survive, and the fixture's `.out`
|
|
|
+ is the same assertion at the level of a whole program.
|
|
|
|
|
|
## The bug family, stated once
|
|
|
|
|
|
@@ -1775,14 +1840,11 @@ independently-scanned inventory at all.
|
|
|
|
|
|
## Next steps
|
|
|
|
|
|
-1. **The in-suite non-vacuity cases for bug 36.** `t36_argclobber`'s `.out`
|
|
|
- was proved able to fail by running the pre-fix compiler — red on both
|
|
|
- oracles, quoted in the commit that introduced the fixture — but
|
|
|
- `nonvacuity.sh` does not yet mutate the fix *back*. Revert `ParseCallArgs`,
|
|
|
- `ParseCall`, `IoCall` and the parameter-offset remap in turn, and require
|
|
|
- `run_com_exec.py t36_argclobber` red for the stated reason, green on
|
|
|
- restore. Until that exists, the `62 ok` under "Non-vacuity" does not cover
|
|
|
- this fixture, and the text there says so.
|
|
|
+1. **`readln` of a `BOOLEAN`** is bug 37's shape with the fix not yet written:
|
|
|
+ `rdbool` stores a word (`CX`) into a one-byte variable. TP3 itself refuses a
|
|
|
+ boolean read with error 66 rather than implementing it, so the choice is to
|
|
|
+ refuse it the same way or to add the one-byte entry. Fixture first, so the
|
|
|
+ bug is red before the fix.
|
|
|
2. **String *variables*** — `s : string`, `s := 'hi'`, `writeln(s)`. The
|
|
|
encoding blocker is gone (`EmBpDisp`); what is left is a length word, an
|
|
|
assignment path, and a `WrStr` entry (TPSRC4 `xwrtstr`). `IoCall` currently
|
|
|
@@ -1791,21 +1853,17 @@ independently-scanned inventory at all.
|
|
|
RESUME-TP3.md §3.11), range/index checks (`TU_RANGE_CHECK`,
|
|
|
`TU_INDEX_CHECK`), typed constants (RESUME-TP3.md §3.14), `array` at its
|
|
|
point of use (`t14`), `case` with subrange labels.
|
|
|
-4. **`readln` of a `BYTE`** calls `rdint`, which stores 2 bytes and overflows
|
|
|
- into the next variable. TP3 has a separate `xrdbyte`; a `TU_RdByte` entry is
|
|
|
- the fix. No fixture exists yet, which is why it has not been done — write
|
|
|
- the fixture first, so the bug is red before the fix.
|
|
|
-5. Make the 4 KiB code window an enforced limit rather than a documented one:
|
|
|
+4. Make the 4 KiB code window an enforced limit rather than a documented one:
|
|
|
report an error when `pc` reaches `dc`, instead of writing over the data.
|
|
|
-6. Both spellings of a multi-name declaration. `var i, c : integer;` is
|
|
|
+5. Both spellings of a multi-name declaration. `var i, c : integer;` is
|
|
|
error 1 at the comma and needs two `var` lines; `procedure f (a : integer;
|
|
|
b : integer)` is error 1 at the semicolon and needs a comma. Neither is
|
|
|
wrong Pascal, so a program that compiles under one compiler may not under
|
|
|
another. A parameter may also not shadow a global (`DupTest` rejects any
|
|
|
name `Search` finds at any level), which Pascal allows.
|
|
|
-7. Harden the program-header parameter loop against non-advancing input
|
|
|
+6. Harden the program-header parameter loop against non-advancing input
|
|
|
(`program p(1;)`) with a `BOOLEAN` flag — **not** `EXIT`, which ICEs gm2.
|
|
|
-8. FreeDOS (`freedos.qcow2`, FD14-LiveCD) is still untried. Now that `R` runs
|
|
|
+7. FreeDOS (`freedos.qcow2`, FD14-LiveCD) is still untried. Now that `R` runs
|
|
|
the image in-process, a real DOS is no longer needed for any claim above —
|
|
|
but it is still the only way to get a third opinion on the `INT 21h` shim,
|
|
|
and `Exec86` deliberately implements only the three functions the runtime
|