|
|
@@ -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.
|