← board

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:

  1. eparser.inc pulls 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.
  2. 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.
  3. 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:

  1. 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.
  2. 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.