|
|
@@ -26,6 +26,7 @@ manual, not guessed.
|
|
|
| 8086-legal conditional branches and `SETcc` | `v-TP3-8086-LOWERING` | done, **the emitted code no longer contains an opcode the 8086 lacks** |
|
|
|
| `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** |
|
|
|
|
|
|
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
|
|
|
@@ -119,11 +120,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.
|
|
|
|
|
|
-**33 of 36 fixtures compile clean**, up from 1 (the empty program) when the
|
|
|
+**34 of 37 fixtures compile clean**, up from 1 (the empty program) when the
|
|
|
direct harness was first built.
|
|
|
|
|
|
```
|
|
|
-compile matrix: 36 passed, 0 failed (of 36)
|
|
|
+compile matrix: 37 passed, 0 failed (of 37)
|
|
|
```
|
|
|
|
|
|
Compiling: `t01` minimal · `t04` var+assign+`writeln` · `t06` two args ·
|
|
|
@@ -138,16 +139,17 @@ supplied input file · `t30` a counted `for` whose bounds come from an
|
|
|
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.
|
|
|
+`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.
|
|
|
|
|
|
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 33 that compile** to a real `.COM`
|
|
|
+`run_com_tests.sh` additionally links **all 34 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: 33 checked, 0
|
|
|
+the layout instead of restating it: `independent .COM check: 34 checked, 0
|
|
|
failed`. That last part was itself a bug fix — see "the two restated
|
|
|
constants" below.
|
|
|
|
|
|
@@ -264,16 +266,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: 33 fixtures are compiled to `.COM`, put on a floppy, booted,
|
|
|
+finds the most: 34 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: 33 passed, 0 failed (of 33)
|
|
|
+execution: 34 passed, 0 failed (of 34)
|
|
|
```
|
|
|
|
|
|
This is no longer the only execution oracle: `run_exec86.py` below runs the
|
|
|
-same 33 images a second time, in a different machine, and requires the two to
|
|
|
+same 34 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
|
|
|
@@ -320,11 +322,11 @@ The only check that asks a question about the *target* rather than about the
|
|
|
compiler: **does the 8086 have this instruction at all?**
|
|
|
|
|
|
```
|
|
|
-8086 check: 35 comparison sites, 31 lowered to a Boolean value, 13 lowered to a branch
|
|
|
- value conditions : = x6 <> x2 < x5 >= x3 <= x2 > x13
|
|
|
+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: 30 of 33 fixtures swept end to end, 2897 bytes
|
|
|
+ program code: 31 of 34 fixtures swept end to end, 3373 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
|
|
|
```
|
|
|
@@ -378,7 +380,7 @@ check runs every fixture through it as well as through qemu and requires the
|
|
|
two to agree **byte for byte**:
|
|
|
|
|
|
```
|
|
|
-exec86: 33 passed, 0 failed (of 33), cross-checked against qemu
|
|
|
+exec86: 34 passed, 0 failed (of 34), cross-checked against qemu
|
|
|
```
|
|
|
|
|
|
Three assertions per fixture, in order of how much they are worth: the output
|
|
|
@@ -446,13 +448,25 @@ the `[BP+off]` rule, the behavioural bugs, the emitter-name audit of
|
|
|
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.
|
|
|
+
|
|
|
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
|
|
|
was **run**. Two are behavioural (below), two come from the new interpreter, two
|
|
|
pin the two new grammar rows the helper audit gained for `EmXorAl01`, and two
|
|
|
are the `R` key's mutation and its restored green.
|
|
|
|
|
|
-Ten arrived with this one, and they are a different shape: **a check whose own
|
|
|
+Ten arrived with `v-TP3-IMAGELAYOUT`, and they are a different shape: **a check whose own
|
|
|
input had never been verified.** `check_framedisp` swept a region built from a
|
|
|
literal that had drifted, and the region is now two independent readings of the
|
|
|
file required to agree — so the first three cases break each reading in turn:
|
|
|
@@ -1054,21 +1068,13 @@ Details that are deliberate, not incidental:
|
|
|
|
|
|
## Honest limitations
|
|
|
|
|
|
-- **A multi-argument call whose earlier argument is a computed value is still
|
|
|
- wrong, and is not claimed to be fixed.** `SaveLeft` parks a kind-2 operand
|
|
|
- (a value that exists only in `AX`) across the parse of the *other* operand of
|
|
|
- a binary operator, which is what fixed `(p > q) or (q > p)`. The three call
|
|
|
- parsers never park an argument that has already been parsed, so in
|
|
|
- `f (a > b, x)` the parse of `x` overwrites `a > b`'s value before the call is
|
|
|
- emitted, and `f (a > b, c > d)` passes the second comparison twice.
|
|
|
- Reproduced exactly as written here; the fix belongs to the call path and was
|
|
|
- out of scope for the operator fix.
|
|
|
-- **`runtest.py` runs one fixture through `R`, not 33.** It drives `t34_arith`
|
|
|
+- **`runtest.py` runs one fixture through `R`, not the whole suite.** It drives
|
|
|
+ `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 32 are covered by
|
|
|
+ which it does with eight assertions. The other 33 are covered by
|
|
|
`run_exec86.py`, which calls the same interpreter directly.
|
|
|
-- **Every fixture that compiles is now also run.** 33 of 36 execute; the 3 that
|
|
|
+- **Every fixture that compiles is now also run.** 34 of 37 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,
|
|
|
@@ -1083,13 +1089,15 @@ Details that are deliberate, not incidental:
|
|
|
execution check to go red. Finding them cost four fixes; see the milestone
|
|
|
section.
|
|
|
- **A call takes at most 16 arguments, and says "compiler overflow" if you
|
|
|
- exceed it.** `args` is `ARRAY [0..15] OF ERes` in the three call parsers, and
|
|
|
- the guard raises `ECompOvf` = 99. So `far (1, 2, ... , 70)` is rejected with
|
|
|
+ exceed it.** Each of the three call parsers counts as it parses and raises
|
|
|
+ `ECompOvf` = 99 at the seventeenth — there is no `args` array left to
|
|
|
+ overflow, since an argument is pushed the moment it has been parsed. So
|
|
|
+ `far (1, 2, ... , 70)` is rejected with
|
|
|
error 99, whose text in TP3 means the compiler's own table overflowed — an
|
|
|
error about the compiler, for a program that merely has a long argument
|
|
|
list. This is why `t28_farparam` can *declare* 70 parameters (which is what
|
|
|
- puts `p63` at `[BP+128]` and exercises the disp16 encoding) but can only
|
|
|
- *pass* 16, and why its body reads `p63`/`p70` into a variable whose value is
|
|
|
+ puts `p8` at `[BP+128]` and exercises the disp16 encoding) but can only
|
|
|
+ *pass* 16, and why its body reads `p8`/`p1` into a variable whose value is
|
|
|
deliberately absent from the `.out`: those two slots hold stack garbage, and a
|
|
|
fixture that printed them would be testing the harness, not the compiler.
|
|
|
- **Parameters are separated by `,` and only by `,`.** `procedure two (a : integer ; b : integer)`
|
|
|
@@ -1598,10 +1606,12 @@ reason for existing.
|
|
|
shape table *is* the fix — an inline "just reload it" at one call site
|
|
|
would not survive the next operator.
|
|
|
|
|
|
- This bug is also why the fix has a stated boundary: the **call** path has
|
|
|
- the same hole and does not have it fixed (`f (a > b, x)`). A fix that
|
|
|
- claims "computed operands" while only covering binary operators is worse
|
|
|
- than one that writes its edge down; see Honest limitations.
|
|
|
+ This bug is also why the fix had a stated boundary: the **call** path had
|
|
|
+ the same hole (`f (a > b, x)`) and was written down as unfixed rather than
|
|
|
+ quietly left out of a claim about "computed operands". A fix that claims
|
|
|
+ the general case while covering one call site is worse than one that writes
|
|
|
+ its edge down. That boundary is closed by bug 36 below, and the fixture
|
|
|
+ that closes it is `t36_argclobber`.
|
|
|
|
|
|
35. **`not` was lowered identically for booleans and integers, so every
|
|
|
boolean negation was wrong.** TPSRC9's `neglevel` picks the instruction
|
|
|
@@ -1627,9 +1637,68 @@ reason for existing.
|
|
|
claims — moving the `34H` to `35H` fails with *"no name pattern accepts
|
|
|
it"*, so the emitter cannot silently become a different instruction.
|
|
|
|
|
|
+### Then a computed argument met the parse of the next one, and one more appeared
|
|
|
+
|
|
|
+36. **Every call parser read the whole argument list before pushing any of it,
|
|
|
+ so an argument that existed only in `AX` did not survive the parse of the
|
|
|
+ argument after it — and the callee numbered its parameter slots the opposite
|
|
|
+ way round from the pushes.** `t36_argclobber` exists because no fixture had
|
|
|
+ ever called anything with a computed argument anywhere but last.
|
|
|
+
|
|
|
+ Three parsers deferred the pushes: `ParseCallArgs` (a procedure call as a
|
|
|
+ statement), `ParseCall` (a function called inside an expression) and
|
|
|
+ `IoCall` (`write`/`writeln`/`read`). A `kind 2` result means the value is in
|
|
|
+ `AX` and nowhere else, and `LoadAtom` deliberately does nothing to it — so by
|
|
|
+ the time a deferred loop reached argument *i*, `AX` held whatever argument
|
|
|
+ *i+1* had computed. `p2 (a + b, x)` printed `0 0`, `b2 (a > b, x > c)`
|
|
|
+ printed `FALSE FALSE`, and `writeln (add2 (c * 2, a))` printed `10` instead
|
|
|
+ of `19`. `IoCall` carried a second loss underneath that one: it pushed after
|
|
|
+ the *whole* list had been parsed, so an argument was also pushed after the
|
|
|
+ runtime call made for an inline string literal in between —
|
|
|
+ `writeln (b > a, ' ', x + 1)` printed `TRUE 544`, the first argument handed
|
|
|
+ over as the third one's value and the third read out of an `AX` that a call
|
|
|
+ had already had. `read`/`readln` could not hit any of this, because their
|
|
|
+ arguments are always plain names and a name emits nothing while it is
|
|
|
+ parsed.
|
|
|
+
|
|
|
+ The other half was the callee. Parameter offsets were handed out in
|
|
|
+ declaration order from `BP+4`, so the *first* declared parameter sat at
|
|
|
+ `BP+4`; but an argument pushed first ends up farthest from `BP`, so the two
|
|
|
+ halves disagreed about which parameter was where, and a one-argument call
|
|
|
+ only ever worked because one slot and one push cannot disagree. Both halves
|
|
|
+ now follow the original: TPSRC8 `cproc`/`cprlp1`/`cprlp2` emits
|
|
|
+ `CALL exprsave` then `CALL epushax` for each argument *as it is read* and
|
|
|
+ only then the `CALL`, and RESUME-TP3.md §3.11 states that the last declared
|
|
|
+ parameter is the one at `BP+4` — which is exactly where the first-pushed
|
|
|
+ argument ends up. `Runtime.mod` needed no change: every entry it has takes
|
|
|
+ one stack argument, and one push and one slot are order-independent.
|
|
|
+
|
|
|
+ So each argument is loaded and pushed the moment it has been parsed, in
|
|
|
+ parse order; `IoCall`'s parse loop and its emit loop are one loop; and once
|
|
|
+ a procedure's parameter list has been read, its symbols are remapped with
|
|
|
+ `off := parmOff + 2 - off`, which sends `4 + 2*(k-1)` to `4 + 2*(n-k)` and
|
|
|
+ invents no offset in between. The range that remap sweeps is exactly the
|
|
|
+ parameters: the parameter's own `NewSym` inside the loop is the only symbol
|
|
|
+ created there, `ParseType` creates none, the procedure's own name predates
|
|
|
+ `nestMark`, and a function's result variable comes after it.
|
|
|
+
|
|
|
+ No expectation row moved, and that is worth as much as the fix: the reorder
|
|
|
+ emits the same bytes in a different order, and the remap hands the same set
|
|
|
+ of displacements to different names — `t28`'s two disp16 reads are still at
|
|
|
+ `+128` and `+142`, now read through `p8` and `p1` where they were `p63` and
|
|
|
+ `p70`, and its printed sum still comes from the two slots the sixteen
|
|
|
+ pushed arguments land in. So nothing was re-baselined; the `t36` row was
|
|
|
+ written while the compiler was still broken, and its numbers simply held.
|
|
|
+
|
|
|
+ The red was seen before the fixture was encoded, not after: the pre-fix
|
|
|
+ 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 bug family, stated once
|
|
|
|
|
|
-Nine of the thirty-five are the *same* bug in different clothes: **loading the
|
|
|
+Nine of the bugs recorded here are the *same* bug in different clothes: **loading the
|
|
|
address where the value was wanted, or picking the register one byte or one
|
|
|
letter away from the right one.** `EmPushVarAddr` had the right bytes for the
|
|
|
wrong register. `LdAlBx` and `MovAlBl` are one letter apart. `MovAh0` and
|
|
|
@@ -1706,11 +1775,14 @@ independently-scanned inventory at all.
|
|
|
|
|
|
## Next steps
|
|
|
|
|
|
-1. **The multi-argument kind-2 clobber.** `f (a > b, x)` passes a wrong first
|
|
|
- value, and `f (a > b, c > d)` passes the second comparison twice.
|
|
|
- `SaveLeft` parks a computed operand across the parse of the *other* operand
|
|
|
- of a binary operator; the three call parsers park nothing. Fixture first, so
|
|
|
- the bug is red before the fix (see "Honest limitations").
|
|
|
+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.
|
|
|
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
|