|
|
@@ -30,6 +30,7 @@ manual, not guessed.
|
|
|
| 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** |
|
|
|
| A `BOOLEAN` read refused with TP3's own error 66 | `v-TP3-BOOLREAD` | done, **`readln (b)` of a boolean is an error, not a word store into a one-byte variable — exactly TPSRC8 `prderrtp`; the compile matrix demands `ERR 66` at the character after the variable, and the accepting dispatch is one of the in-suite reverts** |
|
|
|
| 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)** |
|
|
|
|
|
|
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
|
|
|
@@ -71,7 +72,7 @@ gm2 -fiso -fuse-module-list=modules.lst -o tpshell \
|
|
|
Shell.mod Compiler.mod Term.o TextBuf.o Posix.o Editor.o # phase B2
|
|
|
```
|
|
|
|
|
|
-Current clean build: `make clean && make` → rc=0, `tpshell` **163616 bytes**.
|
|
|
+Current clean build: `make clean && make` → rc=0, `tpshell` **191072 bytes**.
|
|
|
The one diagnostic is `./Compiler.mod: ParseExpr: too many errors in pass 3`,
|
|
|
which is the expected phase-1 rollup that the recipe tolerates — not a real
|
|
|
error. `shell/Makefile` is the single authoritative build recipe.
|
|
|
@@ -123,11 +124,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.
|
|
|
|
|
|
-**36 of 40 fixtures compile clean**, up from 1 (the empty program) when the
|
|
|
+**38 of 42 fixtures compile clean**, up from 1 (the empty program) when the
|
|
|
direct harness was first built.
|
|
|
|
|
|
```
|
|
|
-compile matrix: 40 passed, 0 failed (of 40)
|
|
|
+compile matrix: 42 passed, 0 failed (of 42)
|
|
|
```
|
|
|
|
|
|
Compiling: `t01` minimal · `t04` var+assign+`writeln` · `t06` two args ·
|
|
|
@@ -146,7 +147,9 @@ operators nothing else uses (`div mod and or`, unary `-`, a variable `*`) ·
|
|
|
arguments are computed in various positions, each one keeping its own value. ·
|
|
|
`t37` `readln (b)` of a `byte` sitting next to an `integer` ·
|
|
|
`t39` `s : string [10]`, assigned a literal and written — globally and from
|
|
|
-inside a procedure, so both address paths run
|
|
|
+inside a procedure, so both address paths run ·
|
|
|
+`t40` `readln` into a global *and* a local `string` ·
|
|
|
+`t41` `s := t` between two strings, with the truncation both ways.
|
|
|
|
|
|
Failing, all deliberately: `t14` `array [1..5] of integer` at its point of use
|
|
|
and `t25` a string literal assigned to an *integer* (`i := 'ab'`, the reverse
|
|
|
@@ -156,9 +159,9 @@ direction is still unimplemented) both → `ENoLib`
|
|
|
refuses the whole class because the runtime has no boolean reader at all; and
|
|
|
`uierror` is a deliberate syntax error (41) used by the UI test.
|
|
|
|
|
|
-`run_com_tests.sh` additionally links **all 36 that compile** to a real `.COM`
|
|
|
+`run_com_tests.sh` additionally links **all 38 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: 36 checked, 0
|
|
|
+the layout instead of restating it: `independent .COM check: 38 checked, 0
|
|
|
failed`. That last part was itself a bug fix — see "the two restated
|
|
|
constants" below.
|
|
|
|
|
|
@@ -197,13 +200,13 @@ proves each one can go red.
|
|
|
|---|---|---|
|
|
|
| `probe/run_modrm19.py` | the mod=00/01/10 effective addresses, by **executing** 23 cases on a real 8086 under qemu and scanning for where the marker landed | mod=11 — see below |
|
|
|
| `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 — **102/102**, from an inventory scanned independently of the parser | anything longer than one instruction |
|
|
|
+| `audit_helpers.py` | every one-line emitter in **both** `Runtime.mod` and `Compiler.mod` decodes to what its *name* says — **105/105**, 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 (497 bytes, code ends at 466) 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 (585 bytes, code 0..584) 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, 36/36 | 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, 42 cases plus a pre-flight, 43/43 — 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 |
|
|
|
+| `run_exec86.py` | the same image runs in a **second, independent 8086** and agrees with qemu byte for byte, 38/38 | 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, 50 cases plus a pre-flight, 51/51 — 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.
|
|
|
@@ -275,16 +278,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: 35 fixtures are compiled to `.COM`, put on a floppy, booted,
|
|
|
+finds the most: 38 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: 35 passed, 0 failed (of 35)
|
|
|
+execution: 38 passed, 0 failed (of 38)
|
|
|
```
|
|
|
|
|
|
This is no longer the only execution oracle: `run_exec86.py` below runs the
|
|
|
-same 35 images a second time, in a different machine, and requires the two to
|
|
|
+same 38 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
|
|
|
@@ -307,7 +310,7 @@ signature (`hdrFlag = 1`, `hdrDS == hdrOff + 1000h + bias`, `hdrHeap > hdrDS`,
|
|
|
`prologAt` and `dataBase` per file:
|
|
|
|
|
|
```
|
|
|
-measured runtime size: 497 bytes (header at image offset 500)
|
|
|
+measured runtime size: 616 bytes (header at image offset 619)
|
|
|
```
|
|
|
|
|
|
A restated constant that has drifted is worse than a derived one, and the two
|
|
|
@@ -334,8 +337,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: 497 bytes, 256 swept, 0 0F-prefixed
|
|
|
- program code: 34 of 36 fixtures swept end to end, 3720 bytes
|
|
|
+ runtime: 616 bytes, 330 swept, 0 0F-prefixed
|
|
|
+ program code: 38 of 38 fixtures swept end to end, 4315 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
|
|
|
```
|
|
|
@@ -389,7 +392,7 @@ check runs every fixture through it as well as through qemu and requires the
|
|
|
two to agree **byte for byte**:
|
|
|
|
|
|
```
|
|
|
-exec86: 35 passed, 0 failed (of 35), cross-checked against qemu
|
|
|
+exec86: 38 passed, 0 failed (of 38), cross-checked against qemu
|
|
|
```
|
|
|
|
|
|
Three assertions per fixture, in order of how much they are worth: the output
|
|
|
@@ -447,9 +450,9 @@ goes red before a single instruction has run.
|
|
|
|
|
|
### Non-vacuity — `tests/nonvacuity.sh`
|
|
|
|
|
|
-Every assertion in this file is proved able to fail: **67 deliberate
|
|
|
+Every assertion in this file is proved able to fail: **78 deliberate
|
|
|
breakages, each asserted to turn exactly one named check red for the stated
|
|
|
-reason, then restored and re-asserted green** — `non-vacuity: 67 ok, 0 failed`.
|
|
|
+reason, then restored and re-asserted green** — `non-vacuity: 78 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
|
|
|
@@ -464,6 +467,23 @@ must come back red on `t38`'s `ERR 66` row. Nothing that runs programs could
|
|
|
see that one — an `ERR` fixture is never executed, so the refusal has to be
|
|
|
asserted where the refusal happens.
|
|
|
|
|
|
+Eight more arrived with `v-TP3-STRREAD-ASSIGN`, the string milestone. Two break
|
|
|
+the new **runtime entries**: `rdstr`'s `IncSi` (removed → `hello` comes back as
|
|
|
+`05 65 6c 6c 6f`, caught only by `rt_exec.py`'s byte dump) and `strstore`'s
|
|
|
+empty-source `Jcxz8` guard (removed → `LOOP` with `CX = 0` runs 65536 times,
|
|
|
+wraps the segment and corrupts the driver's own record, which `rt_exec.py`
|
|
|
+reports as a malformed record). One reverts the **frame-layout fix** — the
|
|
|
+pre-fix rule packs a local `string` over `[BP]` and `[BP+2]` — and requires
|
|
|
+*two* independent readers to go red: `run_compile_tests.sh` on `t39`'s size
|
|
|
+(`217` → `215`) and `run_com_exec.py` on `t40` (readln into the local never
|
|
|
+sees its terminator and the machine hangs). Four more break `Compiler.mod`
|
|
|
+arms: the destination limit loaded into `AX` instead of `CX` (the audit's
|
|
|
+`MovCxi` row is `COMP`, so no byte check can see it — only `t41`'s truncation
|
|
|
+can, and only there: `t40` passes either way because its `CX` luckily survives
|
|
|
+large enough), the assignment's two pushes swapped so `strstore` reads the
|
|
|
+destination as its own source, the string-read dispatch pointed at `TU_RdInt`
|
|
|
+so `readln(s)` prints a blank line, and the same round trip re-run green.
|
|
|
+
|
|
|
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
|
|
|
@@ -561,7 +581,7 @@ checker.
|
|
|
compile to `.COM` images, are booted on a floppy by a 512-byte hand-assembled
|
|
|
boot sector, and are compared **byte for byte** against the exact output the
|
|
|
fixture demands — `tests/run_com_exec.py`, wired into `run_all.sh`, 21/21 at
|
|
|
-that tag and 36/36 now.
|
|
|
+that tag and 38/38 now.
|
|
|
|
|
|
Everything below is about establishing what *can* be believed, because the
|
|
|
first attempt at this used an emulator that was wrong, and a wrong oracle is
|
|
|
@@ -674,19 +694,24 @@ 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.
|
|
|
|
|
|
-42 cases, one fresh qemu boot each, plus a pre-flight that runs before any of
|
|
|
+50 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, `rdbyte` ×4 against supplied input with a poison
|
|
|
byte sitting past the one-byte variable, `rdint`/`rdbyte` at EOF, `wrtinl`,
|
|
|
-and `wrstr` ×2 — a five-character string and an empty one, seeded in the
|
|
|
+`wrstr` ×2 — a five-character string and an empty one, seeded in the
|
|
|
driver's own memory so the entry dereferences a real pointer rather than the
|
|
|
-call site's return address. The two `wrstr` cases exist because the entry has
|
|
|
-the *opposite* argument contract from `wrtinl`: its stack word is an
|
|
|
-address, and the only way to test "prints the length byte's worth and stops"
|
|
|
-twice is two lengths.
|
|
|
+call site's return address — plus `rdstr` ×4 (a line, a line longer than the
|
|
|
+limit, an empty line, and end of input) and `strstore` ×4 (a whole copy, a
|
|
|
+truncation, an empty source, and a dump of the *source* to prove it is not
|
|
|
+written). The two `wrstr` cases exist because the entry has the *opposite*
|
|
|
+argument contract from `wrtinl`: its stack word is an address, and the only
|
|
|
+way to test "prints the length byte's worth and stops" twice is two lengths.
|
|
|
+The string cases seed each destination with `0xEE` before the call, so a byte
|
|
|
+the entry does not write is visibly still `EE`: "the tail past the new length
|
|
|
+is left untouched" is a checked claim, not a comment.
|
|
|
|
|
|
Three properties are load-bearing, and each of them was chosen against the
|
|
|
easier alternative:
|
|
|
@@ -694,11 +719,12 @@ easier alternative:
|
|
|
- **One boot per case.** A machine that has already run a case has already run
|
|
|
a runtime entry, and if that entry corrupts something the next case inherits
|
|
|
it. About a tenth of a second a boot buys a machine that has provably never
|
|
|
- executed anything, and 35 of them cost under four.
|
|
|
+ executed anything, and all 51 checks cost about twenty seconds.
|
|
|
- **One case record, written twice.** `rt_exec.py` and `exec/rtdrv.s` each
|
|
|
- state the 40-byte layout, and the cases only pass if the two agree on every
|
|
|
- offset. A driver that silently read the wrong field would still boot, still
|
|
|
- run, and still print — so the duplication is the check.
|
|
|
+ state the 12-byte slot and 64-byte case layout, and the cases only pass if
|
|
|
+ the two agree on every offset. A driver that silently read the wrong field
|
|
|
+ would still boot, still run, and still print — so the duplication is the
|
|
|
+ check.
|
|
|
- **A BP-contract pre-flight, before any machine starts.** See below.
|
|
|
|
|
|
The harness reuses `tests/exec/bootcom.s`, the same 512-byte boot sector
|
|
|
@@ -884,7 +910,9 @@ position), and code/data size accounting. Emitted image is a byte array
|
|
|
|
|
|
Working subset (v0.5): integer/char/boolean/byte scalars, constants with
|
|
|
folding, globals, locals, value parameters, procedures and scalar-result
|
|
|
-functions, `ARRAY[const..const]` with constant indexing, control flow,
|
|
|
+functions, `ARRAY[const..const]` with constant indexing, `string [n]`
|
|
|
+variables with literal assignment, string-to-string assignment and
|
|
|
+`readln`/`read` into a string, control flow,
|
|
|
`GOTO`/`EXIT`, the standard procedures `WRITE`, `WRITELN`, `READ`,
|
|
|
`READLN`, `HALT`, and **inline string literals** as `WRITE`/`WRITELN`
|
|
|
arguments.
|
|
|
@@ -910,6 +938,17 @@ inline-literal form, which differs in kind: no value is pushed and the
|
|
|
`ADD SP,2` is absent, because the length and the characters *are* the
|
|
|
argument.)
|
|
|
|
|
|
+`readln(s)`/`read(s)` and `s := t` keep the same one-call-per-argument shape
|
|
|
+and add one thing: the *limit*. A string entry reads it from `CX`, set with a
|
|
|
+full `MOV CX,imm16` (`EmMovCxi`, opcode `B9`) rather than a byte move into
|
|
|
+`CL`, because this runtime has no byte-register move and `rdstr`/`strstore`
|
|
|
+use `CX` for the count outright. So `readln(s)` is `LEA AX,[addr] ; PUSH AX ;
|
|
|
+MOV CX,size-1 ; CALL rdstr ; ADD SP,2` followed by the ordinary `rdln` wrapper
|
|
|
+that consumes the terminator `rdstr` leaves pending, and `s := t` is
|
|
|
+`PUSH src ; PUSH dest ; MOV CX,size-1 ; CALL strstore ; ADD SP,4` — source
|
|
|
+first so it lands at `[BP+6]` and the destination at `[BP+4]`, both popped by
|
|
|
+the caller exactly as TP3's `asnvar`/`estcpl` pair does.
|
|
|
+
|
|
|
`READ`/`READLN` push the *address* so the runtime can store
|
|
|
(`EmPushVarAddr`: `LEA AX,[BP+off]` via `EmBpDisp` for locals, `8D 06 off` for
|
|
|
globals); a non-variable argument is `ETypeErr` (56), as in TP3.
|
|
|
@@ -933,7 +972,7 @@ 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: **497 bytes**, 16 entries, 42 branch targets, offsets read back
|
|
|
+Current blob: **616 bytes**, 18 entries, 51 branch targets, offsets read back
|
|
|
out of the assembled bytes by `tests/check_runtime.py` rather than asserted by
|
|
|
hand:
|
|
|
|
|
|
@@ -944,17 +983,23 @@ hand:
|
|
|
| `stackchk` | 35 | `wrbool` | 111 | `rdchar` | 340 |
|
|
|
| `halt` | 28 | `wrreal` | 136 | `rdbool` | 363 |
|
|
|
| | | `wrln` | 144 | `rdln` | 411 |
|
|
|
-| | | `wrtinl` | 152 | | |
|
|
|
-| | | `wrstr` | 171 | | |
|
|
|
+| | | `wrtinl` | 152 | `rdstr` | 466 |
|
|
|
+| | | `wrstr` | 171 | `strstore` | 530 |
|
|
|
|
|
|
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
|
|
|
-past 436 to 465 and again to 497 (`wrstr` joined, `v-TP3-STRVAR`) — 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
|
|
|
-its own signature rather than by a literal.
|
|
|
+past 436 to 465, to 497 (`wrstr` joined, `v-TP3-STRVAR`) and to 616 (`rdstr`
|
|
|
+and `strstore` joined, `v-TP3-STRREAD-ASSIGN`) — 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 its own signature rather than by a literal.
|
|
|
+
|
|
|
+The two newest entries are the string readers and stores. `rdstr` is TPSRC4's
|
|
|
+`xrdstr`; `strstore` is TPSRC2's `strstore` as TPSRC8's `asnvar` calls it. Both
|
|
|
+read the **limit** from `CX` rather than a stack word, and both were emitted
|
|
|
+last (selectors 16 and 17) so the entries below keep their offsets.
|
|
|
+
|
|
|
|
|
|
The `TU_*` constants in `Compiler.mod` are **assigned from
|
|
|
`Runtime.RT_Entry` in `Inittur`**, not written down, so a runtime edit that
|
|
|
@@ -971,6 +1016,8 @@ Conventions, matching `Compiler.IoCall` exactly:
|
|
|
| `WrInt/WrChar/WrBool/WrReal` | one 16-bit **value** on the stack | caller pops |
|
|
|
| `RdInt/RdByte/RdChar/RdBool` | one **address** on the stack | caller pops |
|
|
|
| `WrStr` | one **address** on the stack — the string variable's length byte first | caller pops |
|
|
|
+| `RdStr` | one **address** on the stack, and the limit in **`CX`** | caller pops; `CX` = `size-1` |
|
|
|
+| `StrStore` | **two** addresses on the stack — destination at `[BP+4]`, source at `[BP+6]` | caller pops both; `CX` = the destination's `size-1` |
|
|
|
| `WrLn/RdLn/StackChk` | nothing | |
|
|
|
| `InitMem` | `AX` = offset of the program header | a *register*, not a stack word |
|
|
|
| `ProgEnd/Halt` | nothing | exits, code 0 |
|
|
|
@@ -1013,9 +1060,9 @@ 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` — 43 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
|
|
|
+`tests/rt_exec.py` — 50 cases plus a pre-flight, 51/51 — 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
|
|
|
has never run; it did, for two releases. What is *not* covered is what a direct
|
|
|
call cannot reach: the runtime is still measured entry-by-entry rather than
|
|
|
@@ -1046,9 +1093,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` (497 + 4096 =
|
|
|
-4593), so a program whose code exceeds 4 KiB runs into its own data. Every
|
|
|
-fixture's image is between 4600 and 4611 bytes. Documented rather than fixed,
|
|
|
+**Known limit:** the data area starts at a fixed `rtSz + 1000H` (616 + 4096 =
|
|
|
+4712), so a program whose code exceeds 4 KiB runs into its own data. Every
|
|
|
+fixture's image is between 4719 and 4766 bytes. Documented rather than fixed,
|
|
|
because the
|
|
|
original has the same fixed-offset behaviour.
|
|
|
|
|
|
@@ -1117,9 +1164,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 34 are covered by
|
|
|
+ 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.** 36 of 40 execute; the 4 that
|
|
|
+- **Every fixture that compiles is now also run.** 38 of 42 execute; the 4 that
|
|
|
do not are `t14` and `t25` (`ENoLib`, by design), `t38` (a boolean read,
|
|
|
refused with TP3's error 66, by design) and `uierror` (a deliberate syntax
|
|
|
error). The gap this replaces was nine fixtures that compiled and were
|
|
|
@@ -1177,11 +1224,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.** 497 bytes, 16 entries, 102
|
|
|
+ invariants are stated rather than implied.** 616 bytes, 18 entries, 105
|
|
|
emitter helpers decoded against their own names across both modules, the
|
|
|
whole code region golden-pinned, all branch targets on instruction
|
|
|
- boundaries — and 42 cases that call the entries one at a time under qemu
|
|
|
- (`rt_exec.py`, 43/43 with its BP pre-flight). What is *not* covered is what a direct call cannot
|
|
|
+ boundaries — and 50 cases that call the entries one at a time under qemu
|
|
|
+ (`rt_exec.py`, 51/51 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; `wrstr` only from its seeded-address cases. (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
|
|
|
@@ -1192,7 +1239,7 @@ Details that are deliberate, not incidental:
|
|
|
hard-coded — but the two `RT_SZ` constants that *were* hard-coded and had
|
|
|
drifted (see the execution section) are the precedent for why this one gets
|
|
|
stated every time.
|
|
|
-- **33 executed fixtures is a small sample of Pascal.** They cover `var`, `const`,
|
|
|
+- **38 executed fixtures is a small sample of Pascal.** They cover `var`, `const`,
|
|
|
`if`, `while`, `for`, `repeat`, `case` over scalars, procedures with value
|
|
|
parameters, `goto`/`label`, string literals, `readln`, all six comparisons,
|
|
|
and the arithmetic and logical operators `* + - div mod and or not` on
|
|
|
@@ -1218,13 +1265,15 @@ Details that are deliberate, not incidental:
|
|
|
emitter has to be revisited. The audit handles them as a documented
|
|
|
*sequence*; if another one appears, the sequence machinery is where it goes,
|
|
|
not a name hack.
|
|
|
-- **String *literals* and string *variables* work; the rest of the string
|
|
|
- language does not.** A literal in a `WRITE`/`WRITELN` argument list is
|
|
|
- emitted inline and needs no runtime support beyond `wrtinl`; a variable
|
|
|
- (`s := 'hi'`, `writeln(s)`) is the `v-TP3-STRVAR` milestone — byte-by-byte
|
|
|
- literal stores and the `wrstr` entry. Still refused: `readln(s)`, assignment
|
|
|
- from one string to another, indexing, and the concatenation/comparison
|
|
|
- operators. The `chr` flag on `ERes` is what keeps
|
|
|
+- **String *literals* and string *variables* work, and strings now read and
|
|
|
+ assign; indexing and the operators do not.** A literal in a
|
|
|
+ `WRITE`/`WRITELN` argument list is emitted inline and needs no runtime
|
|
|
+ support beyond `wrtinl`; a variable (`s := 'hi'`, `writeln(s)`) is the
|
|
|
+ `v-TP3-STRVAR` milestone — byte-by-byte literal stores and the `wrstr` entry;
|
|
|
+ `readln(s)`/`read(s)` and `s := t` are `v-TP3-STRREAD-ASSIGN`, the `rdstr`
|
|
|
+ and `strstore` entries with the limit in `CX`. Still refused: indexing
|
|
|
+ (`s[i]`) and the concatenation/comparison operators. The `chr` flag on
|
|
|
+ `ERes` is what keeps
|
|
|
`writeln('a')` calling the *character* writer instead of the integer writer;
|
|
|
without it the compiler emitted the integer path and printed 97 while the test
|
|
|
still said OK.
|
|
|
@@ -1242,7 +1291,7 @@ Details that are deliberate, not incidental:
|
|
|
and **must not** use `EXIT`, which ICEs gm2 in pass 3 (see the pitfalls
|
|
|
list). Not yet done.
|
|
|
- **Not implemented** (all `ENoLib`): real, set, record, file, string
|
|
|
- *reading*, string-to-string assignment, indexing, the `+`/comparison string
|
|
|
+ *indexing* (`s[i]`), the `+`/comparison string
|
|
|
operators, and any declared type wider than 2 bytes other than `TString`.
|
|
|
`with` is `ENoLib`. `case` *is*
|
|
|
implemented (cascade `CMP`/`JNZ` per label, per RESUME-TP3.md §3.6) but only
|
|
|
@@ -1815,6 +1864,45 @@ reason for existing.
|
|
|
`rdbool` stays in the runtime and `rt_exec` still probes it; the compiler
|
|
|
simply no longer reaches it.
|
|
|
|
|
|
+### Then strings learned to be read and assigned, and three more appeared
|
|
|
+
|
|
|
+39. **`rdstr` stored its first character where the length byte belongs.**
|
|
|
+ `INCsI` — one instruction, `MOV SI,AX` then step past the length byte — was
|
|
|
+ missing, so the first character went to `dest[0]`; the routine stores the
|
|
|
+ length there at the end, overwriting it. `hello` read back as
|
|
|
+ `05 65 6c 6c 6f`. Nothing moved: same sizes, same offsets, the whole
|
|
|
+ compile matrix and every golden green. This is bug 30's family — a
|
|
|
+ one-instruction omission that leaves well-formed bytes — and only
|
|
|
+ `rt_exec.py`'s byte dump can see it, because only there is the *contents*
|
|
|
+ of the string compared and not just its length.
|
|
|
+
|
|
|
+40. **`strstore` copied 65536 characters when the source was empty.** `LOOP`
|
|
|
+ decrements `CX` and branches while it is *non-zero*, so `CX = 0` on entry
|
|
|
+ runs the full 65536 iterations, not zero. `s := ''` therefore wrote 64 KiB
|
|
|
+ from the destination onward, wrapped the segment and walked through the
|
|
|
+ driver's own case record. The guard is a single `JCXZ` before the loop; the
|
|
|
+ compiler emits `MOV CX,imm16` and the entry has to expect that `CX` can be
|
|
|
+ zero because the *source* is empty. `rt_exec.py` reports the case as a
|
|
|
+ malformed record rather than a crash, which is the honest shape of "the
|
|
|
+ entry wrote where it had no business writing".
|
|
|
+
|
|
|
+41. **A local `string` was packed with the integer rule and grew over the
|
|
|
+ frame.** Present since the code generator was written (bug 32's shape: not
|
|
|
+ introduced, merely *reached*). Every local took the pre-decrement `locFree`
|
|
|
+ and every local was assumed to grow *down*; a `string` grows *up* from its
|
|
|
+ base (length byte, then characters), so a `string[5]` local at `BP-2` ran
|
|
|
+ to `BP+3` — over the saved BP at `[BP]` and the return address at `[BP+2]`.
|
|
|
+ Nothing before this milestone could see it, because nothing before this
|
|
|
+ milestone stored *into* a local string; `t39` only ever wrote a literal
|
|
|
+ into a local of three characters (`BP-2..BP+1`, over `[BP]` alone, which
|
|
|
+ the procedure's own `PUSH BP` happened to make survivable). `readln(s)` of
|
|
|
+ five characters stored them over the return address, and `t40` then ran the
|
|
|
+ string's own bytes as an address: Exec86 said `fault at IP=0x6F6C`, which is
|
|
|
+ `l`,`o`. The fix is top-anchored packing that keeps the *integer* offsets
|
|
|
+ identical (`-2`, `-4`, …) and puts a `string[5]` at `BP-6..BP-1`. It moved
|
|
|
+ one fixture's code size (`t39`, `215` → `217`, a disp8 address becoming a
|
|
|
+ disp16 one), which is what the compile matrix pins.
|
|
|
+
|
|
|
## The bug family, stated once
|
|
|
|
|
|
Nine of the bugs recorded here are the *same* bug in different clothes: **loading the
|
|
|
@@ -1894,11 +1982,10 @@ independently-scanned inventory at all.
|
|
|
|
|
|
## Next steps
|
|
|
|
|
|
-1. The rest of the string language, now that `s : string` exists (the
|
|
|
- `v-TP3-STRVAR` milestone covers `s := 'literal'` and `writeln(s)`):
|
|
|
- `readln(s)` (TP3's `xrdstr`), `s := t` between two string variables,
|
|
|
- `s [i]` indexing, and the `+` / comparison operators. All currently
|
|
|
- `ENoLib`.
|
|
|
+1. The rest of the string language. `v-TP3-STRVAR` covers `s := 'literal'`
|
|
|
+ and `writeln(s)`; `v-TP3-STRREAD-ASSIGN` covers `readln(s)`/`read(s)` (TP3's
|
|
|
+ `xrdstr`) and `s := t` between two string variables. Still `ENoLib`:
|
|
|
+ `s [i]` indexing and the `+` / comparison operators.
|
|
|
2. Nested procedures / recursion, `var` parameters (the `SEG:OFF` push from
|
|
|
RESUME-TP3.md §3.11), range/index checks (`TU_RANGE_CHECK`,
|
|
|
`TU_INDEX_CHECK`), typed constants (RESUME-TP3.md §3.14), `array` at its
|