A frontend cannot see that a backend calls library routines
The two mechanisms, with their measured target sets
| routine | who calls it | targets |
|---|---|---|
PXXWriteDecW |
every ordinal write | riscv32 only (ir_codegen_riscv32.inc, the Is64BitRISCV32(tk) or TypeIsOrdinal(tk) arm). i386 and arm32 reach it only for a WIDTH on a 64-bit value; x86-64 and aarch64 never. |
PXXMemMove |
the aggregate-result epilogue | aarch64, arm32, riscv32 measured; cparser.inc's hosted branch names xtensa for the same reason. |
The sets OVERLAP without one containing the other, and that is the whole
lesson. The first version of the predicate was called
TargetWritePathNeedsHeapRuntime and it was too narrow within the hour: with
only the write mechanism covered, test_rust_option.rs — a record return, no
printing at all — failed on aarch64 and arm32 while passing on i386 and
riscv32. A guard named after the routine you have in hand answers correctly
for the case you have in hand. It is now named for the question,
TargetCodegenCallsHeapRuntime, and the union is
aarch64/arm32/riscv32/xtensa.
Why it was invisible until 2026-09-06
Both frontends refused every non-x86-64 target, and the refusal ran before a
compile could be attempted. It came out of the entry-stub extraction
(bug-a-three-frontend-drivers-hand-write-an-x86-64-program-tail-and-a-target-refusal-is-what-hides-it),
and one of its faces is worth carrying: test_zig_skeleton.zig failing to
compile for riscv32 was written down as a Zig frontend defect, and it was this.
A per-target exclusion written from a failing compile records the target the
defect was VISIBLE on, not the target it is about.
Fixed for two frontends, and what is left
rparser.inc and zparser.inc now pull builtinheap + builtin when the
predicate says so (and softfloat first on riscv32/xtensa non-bare, mirroring
cparser.inc's hosted branch — pulling the two units alone moved the error to
the soft-float kernel __pxx_l2d is not linked rather than clearing it, which
is measured and not predicted). Zig pulled no unit at all before this.
Residual, none of it fixed here:
eparser.incpulls nothing — FIXED, along with the two defects in front of it; Erlang now matches its native output on all four cross targets. But the census that closed it found SIX MORE DRIVERS in the same state, see below.- The predicate is a hand-maintained union. A third mechanism, or a new backend that lowers a construct onto a library routine, will not announce itself here. What would retire this ticket is the backend DECLARING its library dependencies rather than a frontend guessing them.
- The xtensa entry is unexercised — both frontends that ask still refuse
xtensa, and the entry comes from
cparser.inc's comment, not from a run.
Acceptance
test-skeleton-frontends-cross-target covers the fixed part: the same program
on every accepted target, compared whole against its own native run, no
expected text anywhere in the row. Positive control taken and RED — with the
Rust unit pull reverted, four rows report does not COMPILE and name the right
targets. A compile failure is a failing row and not a skip in that recipe, on
purpose: a skip would have recorded this defect as "not applicable".
2026-09-07 (frankA) — THE CENSUS: six more drivers, and one of them already crosses
Ran the adoption matrix the eparser finding implied — every frontend driver
against every shared per-target emitter — and then widened it to the frontends
nobody names. compiler/ holds nine skeleton drivers, not three:
| driver | language | refuses non-x86-64 | hand-written x86-64 | pulls units |
|---|---|---|---|---|
rparser |
Rust | narrowed to 5 | none left | yes |
zparser |
Zig | narrowed to 5 | none left | yes |
eparser |
Erlang | narrowed to 5 | none left | yes |
aparser |
Ada | x86-64 only | none | no |
fparser |
Fortran | x86-64 only | none | no |
gparser |
Algol | x86-64 only | none | no |
lparser |
LOLCODE | x86-64 only | none | no |
wparser |
Whitespace | x86-64 only | none | no |
bparser |
BASIC | NO REFUSAL AT ALL | none | no |
BASIC is the control and it cost nothing to read, because it has no refusal:
test_basic_comprehensive.bas is already IDENTICAL to its native output on
i386, aarch64 and arm32, and fails on riscv32 with
the soft-float kernel __pxx_l2d is not linked — this ticket's defect, exactly.
So two things follow, and the second is the one worth acting on:
- BASIC has crossed to three targets for some time and nothing measures it. Not a defect; an unrecorded capability, which is how a capability gets lost.
- The five remaining refusals are probably stale. None of those drivers hand-writes a byte of machine code — which was the whole reason the Rust, Zig and Erlang refusals were load-bearing — and their sibling with no refusal works. Probably, not certainly: unmeasured until each is run, and that is the ordering this group has already been burned by once.
AND THE PULL BLOCK IS NOW ON ITS THIRD COPY, heading for its ninth. Three
drivers carry the same six lines (softfloat, then builtinheap, then builtin,
guarded by TargetCodegenCallsHeapRuntime), and six more need it. Two is a
smell and three is a design flaw, so the next commit folds it into one shared
routine beside EmitProgramPrologue — which all nine already call — rather than
adding a fourth copy. A minimal fix to a duplication bug adds a copy, and
this ticket exists because a mechanism nobody could see from a frontend was
spelled out per frontend.
2026-09-18 (frankS) — THIRD MECHANISM, AND IT IS THE PASCAL DRIVER ITSELF
read / readln / Eof lower onto PXXReadLine / PXXReadVar* /
PXXReadDiscard / PXXStdinEof, and nothing told the Pascal driver's own
need-detector that a read token implies builtinheap. Under
-uPXX_MANAGED_STRING that broke readln on all five cross targets plus
x86-64, three different messages, one cause:
| target | message | since |
|---|---|---|
| i386 / arm32 / aarch64 / riscv32 / xtensa | PXXReadLine not found |
predates 2026-09-18; reproduces on the pin |
| x86-64, any target type | PXXLineEnsure not found in builtin unit |
0ab100740, when its asm reader started sharing the builtin buffer |
| x86-64, frozen-string target | call to a runtime stub that was never emitted |
reproduces on the pin |
Fixed in 295bcceb9 by adding the pull to DetectPascalRuntimeNeeds, with a
fixture (test_readln_in_a_frozen_string_build.pas, x86-64 and i386 rows) whose
positive control is the pinned compiler refusing it on both.
This is not a fourth ticket because it is not a fourth mechanism — it is the
same one arriving in the driver that looked immune. DetectPascalRuntimeNeeds
already carries two paragraphs saying, in those exact words, the dependency was
moved and this is where it has to be paid — once for floats, once for a frozen
string written with a field width. read is the third, and why nobody paid it
is the better argument for this ticket's prio than any count of instances:
x86-64 emitted its own self-contained reader, so the pull looked
cross-target-only, and the five targets that actually needed it were failing in
a build mode no row covers. A hand-maintained union of known mechanisms grows a
new member every time a construct is shimmed onto a helper, and the member is
invisible until someone compiles in the one mode that exercises it.
A CROSS-TARGET CENSUS IS NOT A CROSS-BUILD-MODE CENSUS
The census that cleared the readln de-duplication ran eight fixtures across five
--target= flags and found fifteen shapes byte-identical. It was right about
agreement and wrong about completeness, and the reason is the axis it was
given: it varied TARGET and never varied BUILD MODE, so it compared two readers
on programs that BUILD — and there was an entire mode in which one of them does
not. -uPXX_MANAGED_STRING is not an exotic corner: it is the model
compiler.pas itself is compiled with. The population excluded the build the
compiler uses on itself.
This is the house rule with a new axis attached — a census answers honestly about whatever it enumerates, and that applies to whoever drew the boundary, not only to whoever ran it. Wherever stdin, strings or the heap are involved, the frozen-string model deserves a row, because it is the one mode where the managed-string runtime is absent and every dependency that was quietly riding on it comes due.
2026-09-22 (frankb-8e) — BASIC fixed, and the size of the surface measured
Landed e7e925193. bparser.inc was the last skeleton driver with no
PullTargetRuntimeUnits call, so every BASIC program that printed anything
refused on riscv32 and xtensa. Four constructs x six targets before the fix:
a program with no PRINT built on all six; PRINT of a number and of a string
literal built on x86-64/i386/arm32/aarch64 and refused on the other two. After:
24/24 build, and riscv32's runtime output under qemu is byte-identical to the
x86-64 oracle on all four.
Why the suite could not have caught it — the target list, not the fixtures
Five .bas fixtures ALREADY ran cross, over i386 aarch64 arm32, which is
exactly the set of cross targets on which this defect does not reproduce.
The list now reads i386 aarch64 arm32 riscv32 xtensa and both additions
run rather than merely building — qemu-xtensa executes a hosted .bas
today. Nothing about the fixtures needed to change.
The number this ticket should be ranked on
| instrument | population | result |
|---|---|---|
five .bas fixtures vs pinned fda77c48b8ee, riscv32 |
one frontend | 5 distinct missing routines |
FindProc('PXX…'|'__pxx…') census, five non-x86 backends |
riscv32 83, arm32 56, aarch64 54, xtensa 36, wasm32 5 | 374 sites, 276 erroring, 71 distinct routines |
| this ticket's own table | — | 2 |
Tree for both: e7e925193. Quote 71, not 374 — a count of call sites is not
a count of routines, and the two get confused precisely because 374 is the
bigger number.
The five from one frontend: PXXWriteCharW, PXXWriteNL,
PXXWriteFrozen[B]W, PXXWriteDecW, and the softfloat kernel __pxx_l2d.
The residual on this ticket predicted exactly this — "a third mechanism, or
a new backend that lowers a construct onto a library routine, will not announce
itself here" — and three arrived at once, from the frontend that happened to be
next.
Position of the pull is load-bearing in TWO directions
Recorded because the shared routine's own header sends you to the first wall:
| placement | outcome |
|---|---|
after EmitExit |
PXXWriteNL not found, unchanged, from a binary that already had the fix. This is what PullTargetRuntimeUnits' header prescribes; that header was written from the CALL-STUB drivers. |
after ParseBBlock |
invalid IR symbol reference in store_sym on all four non-x86 targets — parsing a Pascal unit moves the symbol table under an AST already holding indices into it. 10 LET A = 1 trips it. |
| before the prescan | correct — no BASIC symbol allocated, no node built. |
aparser.inc and gparser.inc already carried a measured warning about the
first wall. I hit it anyway, because I read the shared routine's header and
not the sibling call site. That is this repo's own sibling rule failing in the
direction it usually fails: I grepped for the routine, not for the other
spelling's handler.
Residual, for whoever takes the class
eparser still pulls nothing. The xtensa entry in the predicate is still
inherited from a comment. And the thing that would retire this ticket is not
a bigger list — it is removing the need for one. The cheapest honest version:
have every skeleton driver pull unconditionally and delete
TargetCodegenCallsHeapRuntime, paying the unit cost on x86-64 to buy the
whole class. That trade has NOT been measured and is the next thing to measure.