|
|
@@ -29,6 +29,7 @@ manual, not guessed.
|
|
|
| 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** |
|
|
|
| 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)** |
|
|
|
|
|
|
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
|
|
|
@@ -122,11 +123,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.
|
|
|
|
|
|
-**35 of 39 fixtures compile clean**, up from 1 (the empty program) when the
|
|
|
+**36 of 40 fixtures compile clean**, up from 1 (the empty program) when the
|
|
|
direct harness was first built.
|
|
|
|
|
|
```
|
|
|
-compile matrix: 39 passed, 0 failed (of 39)
|
|
|
+compile matrix: 40 passed, 0 failed (of 40)
|
|
|
```
|
|
|
|
|
|
Compiling: `t01` minimal · `t04` var+assign+`writeln` · `t06` two args ·
|
|
|
@@ -143,18 +144,21 @@ expression · `t31` a value parameter *and* a call across statements ·
|
|
|
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. ·
|
|
|
-`t37` `readln (b)` of a `byte` sitting next to an `integer`
|
|
|
+`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
|
|
|
|
|
|
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`
|
|
|
+and `t25` a string literal assigned to an *integer* (`i := 'ab'`, the reverse
|
|
|
+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; and
|
|
|
`uierror` is a deliberate syntax error (41) used by the UI test.
|
|
|
|
|
|
-`run_com_tests.sh` additionally links **all 35 that compile** to a real `.COM`
|
|
|
+`run_com_tests.sh` additionally links **all 36 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: 35 checked, 0
|
|
|
+the layout instead of restating it: `independent .COM check: 36 checked, 0
|
|
|
failed`. That last part was itself a bug fix — see "the two restated
|
|
|
constants" below.
|
|
|
|
|
|
@@ -193,13 +197,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 — **101/101**, 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 — **102/102**, 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 (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_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_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, 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 |
|
|
|
+| `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 |
|
|
|
|
|
|
Two of these deserve the detail, because the reason they exist is the reason
|
|
|
they are hard.
|
|
|
@@ -303,7 +307,7 @@ signature (`hdrFlag = 1`, `hdrDS == hdrOff + 1000h + bias`, `hdrHeap > hdrDS`,
|
|
|
`prologAt` and `dataBase` per file:
|
|
|
|
|
|
```
|
|
|
-measured runtime size: 465 bytes (header at image offset 468)
|
|
|
+measured runtime size: 497 bytes (header at image offset 500)
|
|
|
```
|
|
|
|
|
|
A restated constant that has drifted is worse than a derived one, and the two
|
|
|
@@ -330,8 +334,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: 465 bytes, 235 swept, 0 0F-prefixed
|
|
|
- program code: 32 of 35 fixtures swept end to end, 3496 bytes
|
|
|
+ runtime: 497 bytes, 256 swept, 0 0F-prefixed
|
|
|
+ program code: 34 of 36 fixtures swept end to end, 3720 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
|
|
|
```
|
|
|
@@ -557,7 +561,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 33/33 now.
|
|
|
+that tag and 36/36 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
|
|
|
@@ -670,13 +674,19 @@ 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.
|
|
|
|
|
|
-40 cases, one fresh qemu boot each, plus a pre-flight that runs before any of
|
|
|
+42 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, and `wrtinl`.
|
|
|
+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
|
|
|
+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.
|
|
|
|
|
|
Three properties are load-bearing, and each of them was chosen against the
|
|
|
easier alternative:
|
|
|
@@ -923,22 +933,24 @@ 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: **465 bytes**, 15 entries, 41 branch targets, offsets read back
|
|
|
+Current blob: **497 bytes**, 16 entries, 42 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 | `rdbyte` | 278 |
|
|
|
-| `stackchk` | 35 | `wrbool` | 111 | `rdchar` | 308 |
|
|
|
-| `halt` | 28 | `wrreal` | 136 | `rdbool` | 331 |
|
|
|
-| | | `wrln` | 144 | `rdln` | 379 |
|
|
|
+| `initmem` | 0 | `wrint` | 36 | `rdint` | 203 |
|
|
|
+| `progend` | 28 | `wrchar` | 97 | `rdbyte` | 310 |
|
|
|
+| `stackchk` | 35 | `wrbool` | 111 | `rdchar` | 340 |
|
|
|
+| `halt` | 28 | `wrreal` | 136 | `rdbool` | 363 |
|
|
|
+| | | `wrln` | 144 | `rdln` | 411 |
|
|
|
| | | `wrtinl` | 152 | | |
|
|
|
+| | | `wrstr` | 171 | | |
|
|
|
|
|
|
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 — six of the offsets in an earlier version of this table were
|
|
|
+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
|
|
|
@@ -958,6 +970,7 @@ 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 |
|
|
|
| `WrLn/RdLn/StackChk` | nothing | |
|
|
|
| `InitMem` | `AX` = offset of the program header | a *register*, not a stack word |
|
|
|
| `ProgEnd/Halt` | nothing | exits, code 0 |
|
|
|
@@ -1000,7 +1013,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` — 41 cases, all passing — with the register contract each
|
|
|
+`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
|
|
|
(`v-TP3-BP-CONTRACT`) is the reason this paragraph no longer says the runtime
|
|
|
@@ -1033,9 +1046,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` (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,
|
|
|
+**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,
|
|
|
because the
|
|
|
original has the same fixed-offset behaviour.
|
|
|
|
|
|
@@ -1092,10 +1105,10 @@ Details that are deliberate, not incidental:
|
|
|
The length is one byte, so 300 characters would go out behind a length of
|
|
|
44 and the runtime would print 44 of them and silently drop the rest. TP3
|
|
|
strings are at most 255 characters, so refusing is the faithful answer.
|
|
|
-- **A string *variable* is `ENoLib`, not wrong code.** `IoCall` knows the
|
|
|
- difference and refuses. `EmPushVarAddr`'s local form is fixed now (see bug 4
|
|
|
- below), so the blocker is no longer the encoding — it is that there is no
|
|
|
- `string` type, no length word, no assignment path and no `WrStr` entry.
|
|
|
+- **A string *variable* is a real store now, not a refusal.** `s := 'hi'` and
|
|
|
+ `writeln(s)` compile (see `v-TP3-STRVAR`); what is still refused is
|
|
|
+ `readln(s)`, `s := another_string`, `s [i]` indexing and a literal assigned
|
|
|
+ to a non-string — all `ENoLib`, because none of them has an encoding yet.
|
|
|
|
|
|
|
|
|
## Honest limitations
|
|
|
@@ -1106,7 +1119,7 @@ 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 34 are covered by
|
|
|
`run_exec86.py`, which calls the same interpreter directly.
|
|
|
-- **Every fixture that compiles is now also run.** 35 of 39 execute; the 4 that
|
|
|
+- **Every fixture that compiles is now also run.** 36 of 40 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
|
|
|
@@ -1164,13 +1177,13 @@ 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.** 465 bytes, 15 entries, 102
|
|
|
+ invariants are stated rather than implied.** 497 bytes, 16 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 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
|
|
|
+ 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
|
|
|
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
|
|
|
+ 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
|
|
|
runtime at all also reaches it *through generated code*.)
|
|
|
- **The pushback slot's address is a moving target.** It sits at
|
|
|
@@ -1205,11 +1218,13 @@ 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* work; string *variables* do not.** A literal in a
|
|
|
- `WRITE`/`WRITELN` argument list is emitted inline and needs no runtime
|
|
|
- support beyond `wrtinl`. Declaring `s : string`, assigning to it and
|
|
|
- printing it are all `ENoLib` — there is no `string` type, no length word, no
|
|
|
- assignment path, no `WrStr` entry. The `chr` flag on `ERes` is what keeps
|
|
|
+- **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
|
|
|
`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.
|
|
|
@@ -1227,7 +1242,9 @@ 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
|
|
|
- *variables*, and any type wider than 2 bytes. `with` is `ENoLib`. `case` *is*
|
|
|
+ *reading*, string-to-string assignment, indexing, 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
|
|
|
over scalar labels — subrange labels and label lists are untested.
|
|
|
- gm2 string-literal → `ARRAY OF CHAR` assignment copies the literal *plus a
|
|
|
@@ -1877,10 +1894,11 @@ independently-scanned inventory at all.
|
|
|
|
|
|
## Next steps
|
|
|
|
|
|
-1. **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
|
|
|
- refuses with `ENoLib`.
|
|
|
+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`.
|
|
|
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
|