Przeglądaj źródła

Summary: document inline string literals, the milestone's real state

Brings SUMMARY.md up to date with 42402a0 and 13a99c7, and corrects several
claims in it that had gone stale or, on inspection, were never right.

New section on the string-literal encoding, because it is the milestone's
whole content and the reason is not obvious from the code: the literal is
emitted INTO the code stream (TPSRC8 pwrinlin / TPSRC10 estring, made
self-delimiting by TPSRC4 xwrtinl's POP BX ... JMP BX), so it costs its own
characters in code and nothing at all in the data segment.  t26's data= stays
260, unchanged from a program with no strings, which is the arithmetic that
confirms the design rather than an assumption about it.

Also recorded:

- A string literal is a value in exactly one place, a WRITE/WRITELN argument,
  enforced in one place (LoadAtom) because every other operand path reaches it
  there.  Stated explicitly because the alternative - letting a TString through
  as a machine word - yields a silently wrong program, and t25 exists to pin
  the error instead.

- The runtime is 385 bytes / 14 entries, not 366/13, with the wrtinl row added
  to the offset table and to the calling-convention table (it takes no stack
  argument at all, which is the whole point).

- The compile matrix now ASSERTS rather than being counted by a human:
  expected.tsv pins verdict and numbers per fixture.  25 of 27 compile.

- Three more bugs that only running or dumping found: a string literal eating
  the rest of the source (every error position was exactly the buffer length),
  every program silently losing 6 bytes to an uninitialised flag, and
  writeln('hi') emitting 02 69 00 because StrNew did not advance strTop.

Corrections to what the file previously claimed:

- "three of its own bugs were caught by running it under a broken emulator" was
  wrong.  Two were found by decoding the hex dump, one by running.  Overstated
  because it flattered the harness, which is the opposite of what this file is
  for.  wrtinl is now called out as the one entry no test touches at all -
  rt_exec.py has 33 checks and no wrtinl case, because its argument is not a
  stack word and the caller must place the length and characters at the return
  address.

- rt_exec.py needs /home/eric/.venvs/tp3emu/bin/python; unicorn is not in the
  system Python, so the file appears to be missing rather than failing.

- wrreal is a deliberate ?REAL? stub, not a formatting routine.

- The TU_* placeholders stay on the ladder even though the real offsets are now
  known (wrinl is really 142/8EH, the placeholder says 70H), because the
  compiler cannot yet call the runtime and using true offsets would only make
  it look like it works.

"String runtime" as a next step is replaced by "string variables", which is
the actual remaining work and is blocked on EmPushVarAddr: IoCall raises ENoLib
for a string variable rather than emitting the bad address.

Verified at this commit: make clean && make rc=0, tpshell 125136 bytes,
compile matrix 27/27 exit 0, uitest 10/10, runtime probe 385 bytes with the
14 entry offsets quoted above.

Still nothing executable.  The runtime is not yet copied into the compiler's
code buffer, there is no linker, CmdRun is a stub, and no 8086 executor on this
machine has yet proved trustworthy.
Eric Streit 1 tydzień temu
rodzic
commit
87d8112b99
1 zmienionych plików z 199 dodań i 48 usunięć
  1. 199 48
      SUMMARY.md

+ 199 - 48
SUMMARY.md

@@ -17,6 +17,7 @@ manual, not guessed.
 | Parser `Skip` bug class (9 sites) | `v-TP3-PARSER-FIXES` | done |
 | Standard procedures + `rel16` fix | `v-TP3-STDPROCS` | done |
 | Runtime library + 8086 execution harness | `v-TP3-RUNTIME-BLOB` | assembled, **never run** |
+| Inline string literals (`writeln('hi')`) | `v-TP3-STRLITERAL` | done, **never run** |
 | Linker + `CmdRun` | — | **not started** |
 
 ## Build
@@ -36,7 +37,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` **120792 bytes**.
+Current clean build: `make clean && make` → rc=0, `tpshell` **125136 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/build_tpshell.sh` and `shell/Makefile` are authoritative.
@@ -58,20 +59,32 @@ cd shell && tests/run_compile_tests.sh /some/dir  # another fixture set
 printf '@dump\ntests/fixtures/t19_int1.pas\n' | ./compiletest   # hex-dump the image
 ```
 
-**17 of 23 fixtures compile**, up from 1 (the empty program) when the
-direct harness was first built.
+**The matrix asserts; it does not just count.** `tests/fixtures/expected.tsv`
+pins the verdict *and the numbers* per fixture — verdict plus code size plus
+data size, or error number plus position — and the runner compares. A wrong
+error position or a program that lost six bytes now fails the suite instead of
+needing a squint. It was checked for vacuousness by reverting the string
+scanner fix: 19/23 and exit 1, restored: 23/23 and exit 0.
+
+**25 of 27 fixtures compile**, up from 1 (the empty program) when the direct
+harness was first built.
+
+```
+compile matrix: 27 passed, 0 failed (of 27)
+```
 
 Compiling: `t01` minimal · `t04` var+assign+`writeln` · `t06` two args ·
 `t07` 10 assignments · `t08` const · `t09` if/then/else · `t10` while ·
 `t11` for/to · `t12` repeat/until · `t13` procedure + value param ·
 `t15` label + goto · `t16` `writeln('a')` · `t18` bare `writeln` ·
 `t19` `writeln(1)` · `t20` 3 string args · `t21` mixed args ·
-`t22` `case` with two labels.
+`t22` `case` with two labels · `t23` `writeln('')` · `t24` `writeln('don''t')` ·
+`t26` mixed scalar/string args · `t02`/`t03`/`t05`/`t17` multi-char literals.
 
-Failing, all deliberately: `t02/t03/t05/t17` multi-char string literals and
-`t14` `array [1..5] of integer` → `ENoLib` (102), the original's
-"not implemented" path; `uierror` is a deliberate syntax error used by the
-UI test.
+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 used by the UI test.
 
 ### Shell + editor UI — `shell/tests/uitest.py`
 
@@ -96,9 +109,10 @@ expectations drift with it stops being a test.
 
 ## The executor problem (blocking everything downstream)
 
-The whole point of a Pascal→8086 compiler is that the output *runs*. Until this
-milestone no compiled image had ever been executed, so the plan was: get a real
-CPU emulator, run the image, assert the exact stdout bytes.
+The whole point of a Pascal→8086 compiler is that the output *runs*, and it
+still has not: no compiled image and no runtime entry has ever been executed
+on a correct CPU. So the plan stands: get a real CPU emulator, run the image,
+assert the exact stdout bytes.
 
 **Unicorn 2.1.4 cannot be used for this.** `UC_MODE_16` mis-decodes 16-bit
 ModRM memory operands. The measurement, by loading a byte-pattern image so a
@@ -150,6 +164,24 @@ currently **fails**, and the failures are the emulator's, not the library's —
 `wrint` prints `-` for every value because `MOV AX,[BP+4]` reads the wrong
 address. Do not read those results as a verdict on the runtime.
 
+It holds **33 checks** (7 pass, 33 fail — the 7 are the `stackchk` returns and
+`rdln` cases that happen not to touch memory) covering `initmem`, `wrint`
+(10 values incl. both `INT16` extremes), `wrchar`, `wrbool`, `wrln`,
+`stackchk`, a composed `writeln(42) writeln TRUE` sequence, and the read
+entries against supplied input including EOF. It needs the venv that has the
+package — `unicorn` is **not** in the system Python:
+
+```sh
+/home/eric/.venvs/tp3emu/bin/python tests/rt_exec.py     # → 33 FAILURE(S), rc=1
+```
+
+It has **no `wrtinl` case yet**. That entry needs a harness change, not just a
+machine change, because its argument is not a stack word: the caller must place
+a length byte and the characters at the *return address*, which is precisely
+the property the compiler relies on — so testing it also tests the encoding
+contract between `Compiler.IoCall` and `Runtime.EmitWrInl`. Add it when the
+executor exists, and expect all 33 current checks to pass first.
+
 ## Components
 
 ### Shell — `shell/Shell.mod`, `Term.mod`, `Posix.c`, `TextBuf.mod`
@@ -191,11 +223,12 @@ for forward references, TP3-style error reporting (number + relative
 position), and code/data size accounting. Emitted image is a byte array
 (mode word, CS/DS, size words, `CALL initmem`, `MOV BP,SP`, generated code).
 
-Working subset (v0.4): integer/char/boolean/byte scalars, constants with
+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,
-`GOTO`/`EXIT`, and the standard procedures `WRITE`, `WRITELN`, `READ`,
-`READLN`, `HALT`.
+`GOTO`/`EXIT`, the standard procedures `WRITE`, `WRITELN`, `READ`,
+`READLN`, `HALT`, and **inline string literals** as `WRITE`/`WRITELN`
+arguments.
 
 **Standard procedures** are `KBuiltin`, not `KProc`, because they are not
 called generically. TP3 (TPSRC8 `pwriteln`/`pwrloop`/`prdtyped`) does *not*
@@ -205,16 +238,23 @@ runtime only ever sees a value. `IoCall` mirrors that — one call per
 argument, then a final call for the line break:
 
 ```
-writeln(1)    MOV AX,1     ; PUSH AX ; CALL 20H ; ADD SP,2 ; CALL 40H
-writeln('a')  MOV AX,'a'   ; PUSH AX ; CALL 28H ; ADD SP,2 ; CALL 40H
-readln(x)     LEA AX,[0104]; PUSH AX ; CALL 48H ; ADD SP,2 ; CALL 60H
+writeln(1)      MOV AX,1     ; PUSH AX ; CALL 20H ; ADD SP,2 ; CALL 40H
+writeln('a')    MOV AX,'a'   ; PUSH AX ; CALL 28H ; ADD SP,2 ; CALL 40H
+writeln('hi')                 ; CALL 70H ; 02 'h' 'i' ; CALL 40H
+readln(x)       LEA AX,[0104]; PUSH AX ; CALL 48H ; ADD SP,2 ; CALL 60H
 ```
 
+(Those are the `TU_*` **placeholders** — the real runtime is not wired in yet,
+see the limitations. The third line is the 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.)
+
 `READ`/`READLN` push the *address* so the runtime can store
 (`EmPushVarAddr`: `8D 46 disp` / `8D 06 off`); a non-variable argument is
 `ETypeErr` (56), as in TP3. `TU_WrInt/Char/Bool/Real`, `TU_WrLn`,
-`TU_RdInt/Char/Bool`, `TU_RdLn`, `TU_Halt` continue the existing `TU_*`
-image-base space (`TU_InitMem=8H`, `TU_ProgEnd=10H`, `TU_StackChk=18H`).
+`TU_RdInt/Char/Bool`, `TU_RdLn`, `TU_WrInl`, `TU_Halt` continue the existing
+`TU_*` image-base space (`TU_InitMem=8H`, `TU_ProgEnd=10H`, `TU_StackChk=18H`,
+`TU_WrInl=70H`).
 
 Detail: `TP3-COMPILER.md`.
 
@@ -235,16 +275,16 @@ data therefore lives at fixed low offsets and needs no relocation, and because
 both sides of every `CALL` shift by the same amount, `EmCall`'s displacement
 arithmetic is unaffected by the runtime being prepended.
 
-Current blob: **366 bytes**, 13 entries, offsets read back out of the
+Current blob: **385 bytes**, 14 entries, offsets read back out of the
 assembled bytes by `tests/RtProbe.mod`:
 
 | entry | offset | entry | offset | entry | offset |
 |---|---|---|---|---|---|
-| `initmem` | 0 | `wrint` | 36 | `rdint` | 142 |
-| `progend` | 28 | `wrchar` | 97 | `rdchar` | 243 |
-| `stackchk` | 35 | `wrbool` | 106 | `rdbool` | 264 |
-| `halt` | 28 | `wrreal` | 126 | `rdln` | 310 |
-| | | `wrln` | 134 | | |
+| `initmem` | 0 | `wrint` | 36 | `rdint` | 161 |
+| `progend` | 28 | `wrchar` | 97 | `rdchar` | 262 |
+| `stackchk` | 35 | `wrbool` | 106 | `rdbool` | 283 |
+| `halt` | 28 | `wrreal` | 126 | `rdln` | 329 |
+| | | `wrln` | 134 | `wrtinl` | 142 |
 
 `progend` and `halt` deliberately share one address (`XOR AX,AX / MOV AH,4C /
 INT 21h / RET`): the compiler already zeroes AX before `progend` and discards
@@ -259,6 +299,7 @@ Conventions, matching `Compiler.IoCall` exactly:
 | `WrLn/RdLn/StackChk` | nothing | |
 | `InitMem` | `AX` = offset of the program header | a *register*, not a stack word |
 | `ProgEnd/Halt` | nothing | exits, code 0 |
+| `WrInl` | **nothing** — reads its own text via `POP BX` | see below |
 
 `InitMem` reads the data base and end out of the header words at `+2`/`+6` and
 zeroes that range, because Pascal leaves globals undefined. `StackChk` is a
@@ -271,6 +312,63 @@ displacement.
 
 **It has never executed.** See the emulator finding below.
 
+### String literals — `writeln('hi')`
+
+`writeln('toto')` was the last thing standing between the front end and a
+hello-world: string literals had no encoding at all, so anything past one
+character was `ENoLib`. The encoding is not invented — it is the original's.
+
+TPSRC8 `pwrinlin` peeks at the character after the literal: if it is `,` or
+`)` the literal is a *WRITE argument*, not an expression, and it emits (TPSRC10
+`estring`) the length byte and the characters **into the code stream** right
+behind the call:
+
+```
+CALL wrtinl   <length byte> <character>...
+```
+
+TPSRC4 `xwrtinl` is what makes that self-delimiting: `POP BX` takes the return
+address — which *is* the address of the length byte — and the entry ends with
+`JMP BX`, returning to just past the last character. So the literal needs no
+terminator, no length table, and **nothing at all in the data segment**. The
+arithmetic confirms it: `t26` emits `02 68 69` inline and its `data=` stays
+**260**, unchanged from a program with no strings at all.
+
+`wrtinl` is 19 bytes at offset 142, hand-checked against the 8086 table
+(`5B` POP BX · `31 C9` XOR CX,CX · `8A 0F` MOV CL,[BX] · `43` INC BX ·
+`B4 02` MOV AH,2 · `E3 07` JCXZ to the end label · `8A 07` MOV AL,[BX] ·
+`CD 21` · `43` · `E2 F9` LOOP · `FF E3` JMP BX).
+
+**A string literal is a value in exactly one place: a `WRITE`/`WRITELN`
+argument.** Everywhere else it is a hard error, and it is enforced in a single
+place — `LoadAtom` — because assignment, `IF`, `WHILE`, `FOR`, `REPEAT`,
+`CASE`, array subscripts and every operator all reach their operand through
+`LoadAtom`, and none of them can use a counted string where a 16-bit word is
+expected. `ParseFactor` therefore *marks* a literal (`kind = 3`) rather than
+rejecting it, and `IoCall` handles it before `LoadAtom` is ever reached. The
+alternative — letting it through and producing a machine word that happens to
+be a pointer — would be a silently wrong program; `t25` pins the error
+instead.
+
+Literal text has to survive from the scan to `IoCall`, since the parser does
+not yet know it is writing rather than computing, so it is collected into a
+pool as it is read: `strPool[0..4095]`, `strOff`/`strLen[0..255]`, `strTop`,
+`strCnt`, plus `StrNew`/`StrPut`. The pool is reset in `Inittur`, so it is
+per-compilation.
+
+Details that are deliberate, not incidental:
+
+- **`''` is a zero-length literal**, reaching the runtime's `JCXZ` path. It
+  used to be the scalar 39, so `writeln('')` printed a quote mark.
+- **`'don''t'`** — the doubled quote becomes one character; `t24` pins `len 5`.
+- **A literal of 256 characters or more is `EConstRange` (45), not truncated.**
+  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, because `EmPushVarAddr`'s local form is still broken
+  (see bug 4 below) and a bad address prints garbage rather than failing.
+
 
 ## Honest limitations
 
@@ -278,32 +376,45 @@ displacement.
   linker is unwritten, and no 8086 executor on this machine has yet proved
   trustworthy (above). The emitted code is verified *byte by byte* against the
   offsets the compiler intends, but nothing has ever run it.
-- **The runtime is written but unproven.** `Runtime.mod` assembles to 366 bytes
+- **The runtime is written but unproven.** `Runtime.mod` assembles to 385 bytes
   and its entry offsets are derived from the emitted bytes, but it has never
-  been executed on a correct CPU, so treat every encoding in it as unverified
-  even though three of its own bugs were caught by running it under a broken
-  emulator.
+  been executed on a correct CPU, so treat every encoding in it as unverified.
+  Two of its own bugs were found by decoding the hex dump, one by running it
+  under the broken emulator; `wrtinl` is the newest and the only entry no test
+  touches even in principle.
+- **`wrreal` is a deliberate stub.** It writes the literal text `?REAL?` — the
+  string lives in the runtime's own data block at `D_REAL=24`, which is what
+  makes it a real 9-byte routine rather than a trap. Reals are not formatted
+  yet, so `writeln(1.5)` "works" and prints nonsense. A trap would be louder;
+  this was chosen because the runtime is not yet reachable, and neither choice
+  is a real answer.
 - **The compiler is not yet wired to the runtime.** `Compiler.mod` still
   carries the hardcoded placeholder `TU_*` constants (`TU_InitMem=8H`,
-  `TU_ProgEnd=10H`, …) and still starts `pc` at 0, so emitted images do not
-  contain the runtime and those offsets are still wrong. `Runtime.mod` is not
-  in `make` or `run_compile_tests.sh` yet for the same reason. Note the
+  `TU_ProgEnd=10H`, `TU_WrInl=70H`, …) and still starts `pc` at 0, so emitted
+  images do not contain the runtime and those offsets are still wrong — note
+  the real `wrtinl` is at 142 (8EH) and the placeholder is 70H. `Runtime.mod`
+  is not in `make` or `run_compile_tests.sh` yet for the same reason. Note the
   prologue's `CALL TU_InitMem` targets offset 8, which is currently the
   `hdrMax` header word — coherent only once the blob is really prepended.
   *(Correction to the earlier note in this file: the `TU_InitMem=8` "collision"
   was a false alarm. Per TPSRC7 the runtime is copied to the *front* of the
   code buffer and `pc` starts past it, so `TU_*` offsets are runtime-relative,
   not image-absolute, and no rebasing of the displacement arithmetic is
-  needed.)*
-- **No string runtime.** `RdConst` gives a 1-character literal as `TScalar`
-  (its char code) and only longer literals as `TString`, so multi-char
-  literals raise `ENoLib`. `writeln('a')` works via a `chr` flag on `ERes`;
-  without it the compiler emitted the *integer* writer and would have printed
-  97 while the test still said OK.
-- **Not implemented** (all `ENoLib`): real, set, record, file, string, and
-  any type wider than 2 bytes. `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.
+  needed. The placeholders stay on the ladder even though the real offsets are
+  now known, because the compiler cannot yet call the runtime — using the true
+  offsets would only make it look like it works.)*
+- **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, and `EmPushVarAddr` is wrong for locals. 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.
+- **Not implemented** (all `ENoLib`): real, set, record, file, string
+  *variables*, and any type wider than 2 bytes. `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
   NUL* and leaves the tail untouched, so NUL-terminated tables are safe —
   this was checked, not assumed.
@@ -336,9 +447,11 @@ hand — code sizes looked perfectly plausible throughout.
 
 ## Bugs found by actually running code
 
-Executing the hand-assembled runtime — even under a broken emulator — paid for
-itself immediately, because a wrong encoding *executes* rather than failing to
-assemble. Four, none of which a compiler diagnostic would ever have reported.
+Executing the hand-assembled runtime — even under a broken emulator — and
+running the emitted images back through the harness paid for itself
+immediately, because a wrong encoding *executes* rather than failing to
+assemble. Seven so far, none of which a compiler diagnostic would ever have
+reported.
 
 1. **`B()` silently truncated multi-byte opcodes.** `PROCEDURE B` emits exactly
    one byte and masks with `MOD 100H`, so `B (8BE4H)` — a two-byte opcode passed
@@ -360,6 +473,36 @@ assemble. Four, none of which a compiler diagnostic would ever have reported.
    with `off MOD 100H`, losing displacements above 255. The global form
    `8D 06 off` (`LEA AX,[disp16]`) is correct. Found while writing the runtime's
    read entries, which needed the same encoding to be right.
+5. **A string literal was eating the rest of the source.** Every multi-character
+   literal reported its error at *exactly* `Length()` — one past the last
+   character of the buffer — so the editor landed past the final `.` of the
+   program. The scanner loop tested `CurCh # quote`, but its "closing quote
+   detected" branch consumed *two* characters (the content character **and** the
+   quote), so the cursor moved past the quote and the next condition test saw
+   the character *after* the literal, was satisfied, and scanned on to
+   end-of-buffer. The `IF` after the loop that was meant to consume the closing
+   quote was unreachable for any string of two or more characters — which is
+   exactly why `writeln('a')` always worked and `writeln('hi')` never did.
+   Worse than a bad caret: it destroyed the parse, so anything after a literal
+   was consumed as string contents and a genuine later error was misattributed
+   to end-of-file.
+6. **Every program lost 6 bytes to an uninitialised flag.** The `FOR` over
+   `IoCall`'s argument list needed a "did this argument push a value" flag, and
+   it was never set, so the first argument's `CALL` and its `ADD SP,2` were
+   skipped. Nothing looked wrong — a slightly smaller image looks *more*
+   plausible, not less. Only `expected.tsv` pinning code sizes caught it.
+7. **`writeln('hi')` emitted `02 69 00`** — `i` then NUL. `StrNew` recorded the
+   first character of a literal but did not advance `strTop`, so the first
+   `StrPut` landed on top of the seeded character and overwrote it. `t17_two_str`
+   is what pinned it down: its third emitted character was `e`, the *second*
+   literal's character, which had been written into that slot.
+
+The last three were each found by a different means — (5) by noticing that every
+error position was exactly the buffer length, (6) by `expected.tsv`, (7) by
+hex-dumping the image — and it is worth being precise about why all three were
+invisible to a check that only asks "does it compile": (5) still produced a
+plausible error *number*, (6) a plausible code *size*, and (7) a plausible
+*character*. Each is precisely the shape of bug a compile-only fixture ships.
 
 ## gm2 / ISO Modula-2 pitfalls hit along the way
 
@@ -412,7 +555,10 @@ assemble. Four, none of which a compiler diagnostic would ever have reported.
    boot-sector loader and an `INT 21h` shim is the plan; keep `rt_exec.py`'s
    expectations and change only the machine behind them. (Do *not* reach for
    Unicorn's 16-bit mode again, and do not re-derive the `rm` table by hand
-   again — check it against a real decoder.)
+   again — check it against a real decoder.) A last resort is writing the 8086
+   interpreter `CmdRun` needs anyway, cross-checked against Unicorn only on
+   `disp16`-only code, which is the one addressing form Unicorn 2.1.4 gets
+   right.
 2. **Fix `EmPushVarAddr`** — `8D 45`/`8D 85` for locals, full `disp16`, per
    bug 4 above. Two lines, and `read` into a local is wrong until it is done.
 3. **Wire the runtime in and write the linker**: `pc := RT_Size`,
@@ -426,9 +572,14 @@ assemble. Four, none of which a compiler diagnostic would ever have reported.
    stdout bytes (`writeln('hi')` → `hi`). That single assertion is what turns
    this from "assembles" into "works".
 5. **`CmdRun`** — run the emitted image from the `R` menu key.
-6. **String runtime** — unlocks the last 4 real fixture failures.
+6. **String *variables*** — `s : string`, `s := 'hi'`, `writeln(s)`. Blocked on
+   item 2: `IoCall` deliberately raises `ENoLib` for a string variable rather
+   than emitting the wrong address, so the moment `EmPushVarAddr` is fixed this
+   needs a length word, an assignment path, and a `WrStr` entry (TPSRC4
+   `xwrtstr`).
 7. 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).
+   `TU_INDEX_CHECK`), typed constants (RESUME-TP3.md §3.14), `array` at its
+   point of use (`t14`).
 8. Harden the program-header parameter loop against non-advancing input
    (`program p(1;)`) with a `BOOLEAN` flag — **not** `EXIT`, which ICEs gm2.