One construct, two spellings, and only the literal was ever tested
Measured 2026-09-02, one program, five backends, FPC as the oracle:
var s: ShortString; a: AnsiString;
s := 'abcdef'; a := 'abcdef';
WriteLn('[', s:9, ']'); { ShortString variable }
WriteLn('[', a:9, ']'); { AnsiString variable }
WriteLn('[', 'abcdef':9, ']'); { literal }
| target | s:9 |
a:9 |
'abcdef':9 |
c:5 i:5 b:8 |
|---|---|---|---|---|
| x86-64 | [ abcdef] |
[ abcdef] |
[ abcdef] |
correct |
| riscv32 | [ abcdef] |
[ abcdef] |
[ abcdef] |
correct |
| i386 | [abcdef] |
[abcdef] |
[ abcdef] |
correct |
| aarch64 | [abcdef] |
[abcdef] |
[ abcdef] |
correct |
| arm32 | [abcdef] |
[abcdef] |
[ abcdef] |
correct |
| FPC | [ abcdef] |
[ abcdef] |
[ abcdef] |
correct |
The literal arm reads wid and pads; the variable arms do not read it at all.
That is why this survived
bug-a-x86-64-write-ignores-a-field-width-on-a-char, which swept the same
question for Char and left a test whose string rows all use literals or an
AnsiString on x86-64.
What the fix has to do
The literal arm knows the length at compile time and emits a constant-length
pad. The variable arms cannot: the pad is max(0, wid - len) computed at
runtime, which is the shape x86-64 already has (ir_codegen.inc, the
sub rax, rcx / jump-over-the-pad sequence, and its AnsiString sibling forty
lines below).
Landmine in the aarch64 file: ir_codegen_aarch64.inc:4530 assigns
wid := CodeLen inside the tyAnsiString arm and patches with it. wid is the
field width everywhere else in the same procedure. Rename that to a pDone
before adding a pad, or the pad will read a code offset as a column count.
Both of the two x86-64 arms this mirrors carry their own history --
bug-a-hand-written-literal-short-jumps-span-emitters-that-can-grow found the
ShortString one jumping eight bytes past its target -- so copy the shape, not
the constants.
Verification
qemu-aarch64, qemu-arm and qemu-riscv32 are on plexus, so all five
backends can be diffed against FPC from one box; the program above is the whole
harness. riscv32 and x86-64 are the positive controls: they must not move.
Resolved 2026-09-02 — and it was three defects, not one
The ticket was filed on defect (1) alone. Writing the cross-target test found the other two, both x86-64-only, both in the arms the first fix had just made the other four backends stop duplicating.
| # | defect | wrong on | how it was found |
|---|---|---|---|
| 1 | field width dropped on a string VARIABLE, honoured on a LITERAL | i386, aarch64, arm32 | five-backend FPC diff |
| 2 | empty/nil AnsiString with a width prints nothing | x86-64 only | a row in the new test |
| 3 | width > 40 writes past the spaces buffer | x86-64 only | a row in the new test |
(3) is the one worth remembering. WriteLn(s:60) for a one-character
string emitted a single write(fd, spaces, 59) against a 40-byte buffer, so
the line had exactly the right LENGTH and 19 bytes of the wrong CONTENT —
adjacent process data on stdout. Every length- or column-counting assertion
passes on it. Only a byte compare against FPC sees it, which is why the test
compares bytes and the ticket says so.
The shape all three share
The pad rule — max(0, wid - len), runtime because the length is — had five
copies: three inline in x86-64, one each inline in i386/arm32/aarch64 (which
had none, hence defect 1), and one in PXXWritePad, called by riscv32 and
wasm32. The copy that was exercised by every test was right; the others drifted.
Now: the four non-x86-64 backends call the portable helper, and x86-64's three
arms share one EmitPadSpacesFromRax. Two implementations instead of six, and
x86-64 keeps its own because PXXWrite*W hardcodes fd 1 while x86-64 honours
CurWriteFd (WriteLn(StdErr, ...)). The other four already hardcoded stdout
in their string arms, so calling the helper changed nothing there — checked
before the change, not assumed.
Not done, and deliberately: folding x86-64 onto the portable helper too. That
needs PXXWritePad/PXXWriteStrMW to take an fd, which is an RTL signature
change with its own blast radius.
Log
- 2026-09-02 — resolved, commit 6ae8af59e. Fixes all three defects; the ticket was filed on the first of them.