Tag: v-TP3-STRCAT Commit: (the commit that adds this file) Covers: v-TP3-STRCAT
This is the resume point for the session that completed "Next work" item 1c —
string concatenation + — in full, with the test harness extensions that the
change forced. Every claim below is pinned by fixtures in
shell/tests/fixtures/expected.tsv and by the gate suite.
a + b, writeln(a + b), s := a + b
(v-TP3-STRCAT).TP3's xconcat (TPSRC2:963) builds the result in a scratch string, copies the
left operand into it, then appends the right (addstr/addch). The compiler
now does the same:
StrConcat allocates a fresh 256-byte global scratch STRING per + in
the source (NewScratchStr: @@SC of length 256, created at dc; dc +=
256). The scratch is a KVar symbol of class TString (size 256).ERes becomes a string variable (kind 1), pointing at that
scratch (res.idx := dest). This matches how Assignment, IoCall,
ParseCmp already handle string operands; no new ERes kind was added.EmPushStrOperand (left) → for TString calls
EmPushStrAddr; for a CHAR (one-character literal, char variable, or
s[i] element) it builds a two-byte <len><char> on the stack in a
per-operand 2-byte scratch slot (dc += 2) and pushes its address. This is
how TP3 allows s + 'a' to be legal while s = 'a' is refused.strstore(dest, left, 255) (existing selector 17) copies left into
the scratch. Then strcat(dest, right, 255) (new selector 19) appends
right. Both share the same argument order as strstore (dest at [BP+4],
src at [BP+6], CX = maximum).strcat branches to concerr and
leaves through INT 21h AH=4Ch with AL=16 (TP3's "string too long").
The exit code is now asserted by the execution harness via a per-fixture
.exit sidecar (default 0).+.The hook at the head of BinOpEmit was originally:
IF (left.cls = TString) OR (right.cls = TString) THEN
StrConcat (...)
END
That sent 'a' + 'b' down the integer path (both are TScalar+chr,
cls != TString) producing CHR(195) — a silently wrong program, not an
error. It now tests the same StrOperand predicate that StrConcat guards
with:
IF (StrOperand (left) OR StrOperand (right)) THEN
StrConcat (...)
END
where StrOperand(r) := (r.cls = TString) OR (r.cls = TChar) OR
((r.cls = TScalar) AND r.chr). This is a forward-declared function next
to StrConcat (added at line 1916). With this hook, two CHARs, or a string
plus a number, reach StrConcat and are rejected by its guard as
ETypeErr 56. t53_strcat_type ('a' + 'b') pins the refusal; it was
red before the hook change (ran through integer path) and is green after.
StrConcat's error path now leaves a well-formed non-string placeholder
(res.kind = 0, res.cls = TScalar, res.imm = 0) rather than a half-built
kind-2/TString with an uninitialised idx. That is the invariant: if Err()
was called (and abortFac set), the returned ERes must not claim to be a
string, because string consumers dereference symtab[res.idx] for any
TString.
The compiler's idiom is: after ParseExpr, check IF NOT OK () THEN RETURN
END before dereferencing any symbol from the returned ERes. That guard was
missing in IoCall's argument loop: when a failed concat returned (kind-2/
TString, idx garbage) the write arm did
EmPushVarAddr (symtab [arg.idx].local, symtab [arg.idx].off). With the
hook now routing error cases into StrConcat, writeln ('a' + 'b') became a
compiler segfault. Adding the OK () guard after ParseExpr (arg) in
IoCall (mirroring ParseSub) stops consuming a failed argument and lets the
statement loop terminate via abortFac. With the safe placeholder in
StrConcat, nested cases such as ('a' + 'b') + s also report the first
error cleanly.
strcat (selector 19).Runtime.mod gains EmitStrCat which appends the source string to the
destination. The implementation preserves AX,BX,CX,DX,SI,DI (pushes all
six) because the compiler may hold a string element address in DI across the
call (e.g. s := s + s[i] in effect, or expressions that push next operands
after a string operation). The path is:
CX (max) <= 255lenD from [BP+4] (destination length byte), lenS from [BP+6]newLen = lenD + lenS; if newLen > CX → concerr (exit 16)lenS chars from src[1].. to dst[1 + lenD].., set dst[0] =
newLen, returnconcerr: MOV AL,16H; MOV AH,4CH; INT 21H (6 bytes)The runtime's entry table grows to 20 entries; RT_Entry/RT_Build register
entry 19 as strcat. RtProbe.mod extends the name table to index 19.
check_runtime.py and runtime.golden were reblessed accordingly.
Runtime.mod's MaxLbl was 64 → 96, MaxFix 400 → 512. The
procedures M (add a label) and AddFix (add a fixup) now call HALT when
full instead of silently dropping, so a blown table produces a clean abort
rather than a subtly wrong runtime blob.
bootcom.s's INT 21h AH=09h handler kept its cursor at 0702h (the first
byte of the loaded program image at 0602h in the load window), so writing a
CRLF overwrote the start of the loaded code (turning E8 1A into a wild
jump). The handler now uses SI as its cursor and only writes to the video
buffer via ES/DI; this removes the code-window limit and fixes the hang seen
during bring-up.
tests/run_com_exec.py now reads a .exit sidecar (decimal integer, default
0) per fixture (stem + ".exit") via cases(), and asserts
exit_code_of(rc) == c["exit"] after the output comparison. The module
docstring documents the convention. This is how t55_strcat_overflow asserts
exit code 16 while still checking its partial output (read\r\n).
New fixtures:
t51_strcat.pas — all operand pairings (var+var, literal+var, var+literal,
var+char, char+var, nested concatenation in arguments/assignment), covering
multiple consumers (writeln, assignment, comparison context). Output committed
byte-for-byte.t52_strcat_nest.pas — nested a + b + '!', concatenation compared to a
string, and a string element as a + operand. Output committed.t53_strcat_type.pas — error: 'a' + 'b' (two CHARs). Expected ERR 56 663.t54_strcat_strnum.pas — error: s + 1 (string + number). Expected ERR 56
463.t55_strcat_overflow.pas — readln 128 characters, then s := s + s (length
becomes 256, >255) → runtime exit 16. Input 128×x+LF in .in, output
read\r\n in .out, .exit 16.expected.tsv rows for t51–t55 with their verdicts, code/data bytes, and error
positions (the pos is the 0-based offset of the lookahead token when Err
fires).
Gates: run_compile_tests.sh 56/56; run_com_tests.sh 48/48; run_com_exec.py
48/48 (t55 has exit 16 asserted); rt_exec.py 56/56; check_runtime.py PASS;
check_8086.py PASS; nonvacuity.sh 78 ok/0 failed; run_all.sh OVERALL ALL
PASS.
cd shell && make # tpshell rebuilt, clean
cd shell && tests/run_compile_tests.sh # 56 passed, 0 failed
cd shell && tests/run_com_exec.py # 48 passed, 0 failed
cd shell && tests/rt_exec.py # 56 passed, 0 failed
cd shell && tests/check_runtime.py # PASS
cd shell && tests/check_8086.py # PASS
cd shell && tests/nonvacuity.sh # 78 ok, 0 failed
cd shell && tests/run_all.sh # OVERALL: ALL PASS
At this tag the suite reports:
compile matrix: 56 passed, 0 failed (of 56)
independent .COM check: 48 checked, 0 failed
execution: 48 passed, 0 failed (of 48) (t55 asserts exit 16 via .exit)
exec86: 48 passed, 0 failed (of 48), cross-checked against qemu
runtime entries: 56 passed, 0 failed (of 56) (55 cases + BP pre-flight)
runtime: 770 bytes, code 0..738 (739), 20 entries, 65 branches checked
check_8086: runtime 770/420, program code 48/48 (9692 bytes), clause H 11/11
nonvacuity: 78 ok, 0 failed
OVERALL: ALL PASS
-fgen-module-list ... || true link step prints a roll-up error from
Compiler.mod during the final link of tpshell; this is the expected pass-3
summary and does not indicate a compilation failure (Compiler.o was
built successfully).compiletest is rebuilt when its inputs are newer than the binary (the harness
checks staleness). After modifying Compiler.mod, run_compile_tests.sh will
rebuild compiletest.bootcom.s
AH=09h cursor fix (use SI), the Runtime.mod MaxLbl/MaxFix HALT guards
and the table increases, the BinOpEmit hook change to use StrOperand
(fixing the 'a' + 'b' silent wrong program), the IoCall OK() guard and
the StrConcat safe-placeholder fix (eliminating the compiler segfault), the
new runtime entry strcat (selector 19) and register preservation, the
run_com_exec.py .exit sidecar convention, and the new fixtures t51–t55.