← board

riscv32 and xtensa accept a ShortString SysOpen path and open nothing

Measured

One program, 2026-09-04. It creates /tmp/pxx_sysopen_shortstring_path.tmp through a managed AnsiString path (so the setup does not depend on the branch under test), then reopens it through a ShortString path.

target managed rows SysOpen(sp, 0) on a file that exists
x86-64 pass TRUE, reads back PXX26
i386 pass compile error: target i386: SysOpen expects a managed AnsiString path
aarch64 pass compile error, same wording
arm32 pass compile error, same wording
riscv32 pass short open FALSE — compiles, opens nothing
xtensa pass short open FALSE — compiles, opens nothing

The managed rows passing on every target is what makes this a statement about the ShortString branch rather than about /tmp, the runner, or the file.

Two separate things, and the second is the one to decide first

  1. riscv32 and xtensa are wrong. They accept the shape and return a negative fd for a file that opens fine one line earlier. No diagnostic.
  2. Three targets refuse and two accept. Refusing is defensible — the arm is genuinely not written — but a shape that is a compile error on three targets and a silent wrong answer on two is the worst of both. Whoever takes this should settle which it is before writing code: either riscv32 and xtensa grow the refusal (cheap, honest, and matches the majority), or all five grow the arm.

x86-64 is the only target that implements it. Its emitter pair is EmitTerminateString + EmitLeaStrDataRdi in symtab.inc, both of which follow the frozen-string prefix width -- EmitTerminateString did not until regression-test-threads-test-loadfile-shortstring was fixed, and reading it now is the cheapest description of what the other targets would need.

Guard

test/test_sysopen_shortstring_path.pas exists and is wired NATIVE ONLY, with this slug in the Makefile comment beside it saying why. Wire the cross rows when this closes; three of them will be refusal-assertions rather than value rows unless the second question above is settled the other way.

Resolved 2026-09-05 (frankS)

The fix

SysPathFrozenSym(argNode) (compiler/symtab.inc, beside TypeIsFrozenString) answers one question for both backends: is this argument a LOAD_SYM/LEA of a scalar frozen-string symbol? It returns the symbol index or -1, and it refuses IsArray deliberately -- declining a case it cannot lower rather than lowering it to the wrong address.

EmitSysBuiltinRISCV32 and EmitSysBuiltinXtensa each gained a pathIsArg0: Boolean parameter, true at exactly one of their five call sites (SysOpen). When it fires on arg 0 and the helper returns a symbol, the arg loop emits slot-addr / load-len / lea-data / add / store-zero / move instead of the generic node evaluation. Every other syscall through those routines is byte-unchanged -- pathIsArg0 is False at read, write, close and fchmod.

The buffer address comes from the SYMBOL rather than from the generic value evaluation on purpose: what a generic load leaves in a register for a frozen string is a per-backend convention, while the slot address is not. Both arms share FrozenStrPrefixSize through EmitLoadStrLen*/EmitLeaStrData*, so the change states no new layout fact on either target.

Evidence — and what KIND of evidence it is

A local qemu RUN on this machine, not a cross-compile-and-inspect. Recorded explicitly because cross-target claims in the fleet are otherwise stale, and a built-and-eyeballed object is a different claim from an executed one.

Compiler 0d8884ee2e9a, converged after 1 round(s) (a real recompute, not the stamp path). One source, seven targets:

target before after
x86-64 short open TRUE / short read 5 PXX26 / short miss TRUE unchanged
riscv32 short open FALSE identical to the x86-64 run, exit 0
xtensa short open FALSE identical to the x86-64 run, exit 0
i386 / aarch64 / arm32 refuse by name unchanged, now ASSERTED
wasm32 -- cannot build the test at all (raw syscall in sysutils)

Positive control. The pre-fix binaries printed short open FALSE on both targets while x86-64 printed TRUE; the wired rows compare the qemu output against the x86-64 run of the same source, and expect_same.sh was checked to reject exactly that string pair (MISMATCH, rc=1). The control is drawn from the population the question is about -- the literal output the defect produced. The rows assert RELATIONS (does the open succeed, do the bytes come back), carry no per-target constant, and so would still be right if a target changed a width.

Wired

test-core gains riscv32 and xtensa value rows and, for i386/aarch64/arm32, refusal-assertions matching the diagnostic by message. The refusals are asserted rather than omitted because "refuses" silently becoming "answers wrong" is precisely the transition riscv32 and xtensa had already made while nothing was watching them.

The wasm32 claim in the original body was never run

This ticket said wasm32 "would implement it". Measured: wasm32 cannot compile this test for an unrelated reason, so its position is UNMEASURED and that sentence should not travel.

Banked, not folded in

Both share one root cause seen from two sides: every backend re-derives the path address from the symbol index, discarding the lvalue the parser already built. That is the thing worth fixing once, and it is bigger than this ticket.