Tag: v-TP3-STRIDX-VAR Commit: (the commit that adds this file) Covers: v-TP3-STRIDX-VAR
This is the resume point for the session that did "Next work" item 1a — the
variable string subscript s [i] — in full. It is written so the next session
can pick up without re-deriving anything: every claim below is pinned by a
fixture in shell/tests/fixtures/expected.tsv and by the gate suite.
s [i] where i is a run-time value
(v-TP3-STRIDX-VAR).v-TP3-STRINDEX accepted a constant index only: it folds into boff
(off+i) at compile time, and anything else was ENoLib. A variable index
is a run-time address, so it needs a register. TP3 forms it in TPSRC9
indstr / ialoop (1100-1111, 1200-1229); ours does the same, in
ParseSub:
LoadAtom (index) ; (* AX := i *)
EmLeaDi (local, off+boff) ; (* LEA DI,[base] -- the base's byte 0 *)
EmAddDiAx () ; (* DI := &s[i] (01 C7: add di,ax) *)
r.kind := 5 ; (* the address lives in DI *)
r.cls := TChar ; (* one byte, exactly as a constant index *)
kind = 5 is the new invariant. ERes.kind already meant:
0 const · 1 var · 2 value in AX · 3 string literal · 4 on the
stack. 5 slots in as "a string element whose address is in DI",
avoiding a new boolean field that could go stale. The five consumers:
LoadAtom (kind 5) emits MOV AL,[DI] (8A 05) and zeroes AH, i.e.
the same single byte the A0 off form loads, then drops to kind 2.SaveLeft materialises and pushes a kind-5 left operand the moment
the operator is recognised, before the right-hand side is parsed. DI
cannot survive that parse: the right operand may contain a subscript of its
own (which reuses DI) or a call (which may clobber it). Turning kind 5
into kind 2 → push → kind 4 makes a kind-5 left operand converge on the
path a kind-2 one already takes.Assignment needs the destination address after the right-hand side, so
it PUSH DIes it first, restores it with POP DI, and stores MOV [DI],AL
(88 05). g[i] := h[j] therefore stores where it read. This is exactly
TPSRC8 asnscal2/asnscal3 (758-760, 786-789), which set flgpshdi
around the expression for the same reason.IoCall needs only the address, so for a kind-5 argument it PUSH DIes it
(57) instead of LEA AX,[off]; readln (s[i]) still classifies as
TChar and picks rdchar, reading one byte.No new runtime entry: the arithmetic is emitted inline, so the runtime image is byte-for-byte unchanged (616 bytes, 18 entries, 51 branches).
There is no type check and no range check on the index, matching the
constant-index path (which also does neither). {$R+} / {$I+} are a
separate milestone.
Four one-line emitters and one parameterised:
AddDiAx 01 C7 add di, ax
LdAlDi 8A 05 mov al, byte ptr [di]
StDiAl 88 05 mov byte ptr [di], al
PushDi 57 push di
PopDi 5F pop di
LeaDi 8D 7E d8 lea di, [bp + disp8]
8D BE d16 lea di, [bp + disp16]
8D 3E off lea di, [off]
EmLeaDi reuses the address forms EmPushVarAddr already had, minus the push.
Every opcode was checked with objdump -b binary -m i8086 and the project's
own tests/disasm16.py before it was emitted. The four one-liners are audited
by name by audit_helpers.py; LeaDi has a parameter list, so it is the one
entry that must be listed in NON_ONE_LINE (as COMP) rather than name-audited.
DI is a safe place to keep the address: a .COM has DS=SS=CS=ES and neither
the compiler nor the runtime ever loads a segment register. The runtime
routines that use DI are all CALLed (and some already save it — rdstr
does PushDi/PopDi), and Assignment saves it across the only other thing
that runs between the two uses.
EmBpDisp gained a regf parameter (the register field of the ModR/M byte),
so LeaDi can reuse it. All five of its old call sites pass regf = 0, which
emits the identical bytes, so check_framedisp.py (which examines t27/t28)
is unaffected. This is the only edit to a previously-working path.
.in fixtures must be LF-terminatedNot a compiler bug, but a fixture-authoring trap worth writing down. rdln
(EmitRdLn) exits the moment it reads a CR, an LF or a ^Z,
consuming that one byte and nothing more. An .in file written with CRLF
terminators therefore leaves the LF pending, and the next character read
picks up a stray 0x0A. That is what made t47's second readln (g[i]) store
a newline: the first readln ate only the CR.
The convention is therefore: .in is LF-terminated (the runtime's own comment
says "LF because our input files are LF-terminated"), .out is CRLF, because
the guest emits CRLF. t45_strindex_read.in already followed this; t47 was
written CRLF first and was wrong. The compiler was right both times.
t46_strindex_var OK 578 31 s[i] with a variable index, both
directions; global + local, a computed
index (i+1), a subscript as an
argument, and both-indexed g[i]:=h[j]
t47_strindex_var_read OK 188 15 readln (g[i]) with the index in a
variable (input: 'X' then 'Y')
Both were red before the change — ERR 102 (ENoLib), no expected.tsv row —
and green after. Data sizes: t46 = 31 is g + h (string[10], 11 each) +
i + j (4) + c (1) + two @@T temporaries (4); t47 = 15 is the global
string[8] (9) + i (2) + two @@T (4).
DI, not BX. The sketch in v-TP3-DECL-WINDOW-STRIDX.md item 1a
proposed LEA BX and parking the index ERes on r until LoadAtom /
Assignment runs, which would have needed ERes to carry a nested index.
Storing the address in DI instead is simpler and matches TP3's own shape
more closely: there is no index to park, because ParseSub spends it on the
spot, and the only thing that must outlive the parse is one register, which
PUSH DI/POP DI handle. kind = 5 is the whole change to ERes — no new
field.SaveLeft is the one place the
address must be spent before the right side is parsed, for the reason in the
list above. A subscript on the right is fine: it sets DI after SaveLeft
and LoadPair reads it immediately, with no parse in between.NON_ONE_LINE[mod] or the coverage
check fails. LeaDi is COMP; the other five are audited by name. This is
the second time the rule has bitten, and it is documented in
audit_helpers.py's header.Item 1 (the rest of the string language), in the order that keeps each step self-contained:
b. String comparison = <> < <= > >= (cmpstr, TPSRC2:890-924, and
`vcompare`, TPSRC9:133). A new runtime entry taking two addresses and
leaving flags set is enough because `EmSetcc` reads live flags; the open
question is the SIGNEDNESS and the prefix rule (is "ab" < "abc"?) — read it
off `cmpstr` before trusting the `SETL`/`SETG` opcodes. This adds a
runtime entry, so `runtime.golden`, `check_runtime.py`, `rt_exec.py` (next
selector is 18) and the branch count all move with it.
c. Concatenation + (xconcat, TPSRC2; addstr -> xconcat,
TPSRC9:194). Hardest: TP3 pushes strings inline on the stack, this model
addresses them, so a result needs a scratch/result string and a length rule
(TP3 caps at 255).
Item 2 (the large one) is untouched: nested procedures / recursion, var
parameters (the SEG:OFF push, RESUME-TP3.md 3.11), range/index checks
(TU_RANGE_CHECK, TU_INDEX_CHECK), typed constants (3.14), array at its
point of use (t14), case with subrange labels.
Also still open from before this session:
d. Harden the program-header parameter loop against non-advancing input
(`program p(1;)`) with a `BOOLEAN` flag — NOT `EXIT`, which ICEs gm2.
e. FreeDOS (freedos.qcow2, FD14-LiveCD) is still untried; it is the only
third opinion on the `INT 21h` shim.
cd shell && make clean && make # tpshell 191272 bytes, rc=0
cd shell && tests/run_all.sh # expect: OVERALL: ALL PASS
At this tag the suite reports:
compile matrix: 48 passed, 0 failed (of 48)
independent .COM check: 43 checked, 0 failed
execution: 43 passed, 0 failed (of 43)
exec86: 43 passed, 0 failed (of 43), cross-checked against qemu
audit_helpers: 110/110 agree (30 in Compiler.mod, was 25)
runtime entries: 51 passed, 0 failed (of 51)
check_8086: 43 of 43 fixtures swept end to end, 5697 bytes
runtime: 616 bytes, 18 entries, 51 branches (unchanged)
OVERALL: ALL PASS
Each new behaviour was proven red before green: t46 and t47 both report
ERR 102 (ENoLib, "not implemented") with the old ParseSub, and a stray
0x0A — not a compiler fault — is the only thing a CRLF .in puts wrong.