← board

A BASIC program cannot print anything on riscv32 or xtensa

The measurement

Binary 81bf5f94fb6f, --platform=posix, one row per target. Every cell is a build, not an inference.

source riscv32 xtensa arm32 aarch64 i386 x86-64
10 LET A = 7 / 20 PRINT A BREAK BREAK ok ok ok ok
10 PRINT "hi" BREAK BREAK ok ok ok ok
10 LET A = 7 / 20 IF A = 0 THEN END (no PRINT) ok ok ok ok ok ok

compiler error: PXXWriteDecW not found. The third row is the one that keeps this narrow: the frontend is fine on those targets until it writes.

Pre-existing, and the control is why that is stated rather than assumed

The pinned compiler refuses identically on both targets. So this is not 523833fde (the needsAnsiRuntime evidence scan), even though bparser.inc:731 records that commit retiring the unconditional flag BASIC used to ride on — which is exactly the story a reader would construct, and it is wrong. One pinned-versus-HEAD run settles it, and CLAUDE.md's rule is to attribute a delta to a RANGE before attributing it to yourself.

Cause

TargetCodegenCallsHeapRuntime (emit.inc:1705) owns the target set and names both mechanisms: riscv32 routes every ordinal write through builtinheap's PXXWriteDecW, and the aggregate-result epilogue lowers onto PXXMemMove on aarch64/arm32/riscv32/xtensa. PullTargetRuntimeUnits (frontend_prologue.inc:162) is the one routine that pulls builtinheap for a driver whose language has no uses clause to say so.

compiler/bparser.inc never calls it. Every other skeleton frontend does:

frontend call site
Fortran fparser.inc:388
Ada / Algol aparser.inc:389
(g) gparser.inc:402
Zig zparser.inc:2063
BASIC absent

Five drivers, one missing, and the routine's own header says it exists because the block had reached three identical copies — so this is a driver that was never wired to the fix rather than one that regressed out of it.

Why the one-line call is not obviously the fix

BSourceUsesAUnit (bparser.inc:710) decides whether the AnsiString shims may be emitted, and its contract is explicit:

BASIC pulls builtinheap through exactly one door — USES <unit> during the parse — so "will builtinheap be here" and "does this source say USES" are the SAME question, and it is the only honest predicate for whether the shims can be emitted.

PullTargetRuntimeUnits calls ParseUsesUnitAmbient('builtinheap'), which adds no tkUses token — so on riscv32 and xtensa builtinheap would be present while BSourceUsesAUnit still answers False. The two stop being the same question on exactly the two targets this ticket is about, and that function's comment argues its premise at length and ends "Do not replace this scan with that flag".

That is a live contradiction, not a style point: the predicate would be a FALSE NEGATIVE there (shims withheld although the bodies arrived), which costs nothing today and is a trap the next person steps in.

So the acceptance is not "PRINT compiles". It is: PRINT builds and RUNS on riscv32 (qemu is available for it; xtensa has no runner here, so say that rather than claiming it), a BASIC program with no PRINT still pulls nothing on those targets, and BSourceUsesAUnit's comment says what it means afterwards — in the same commit, because a premise falsified silently is how this class survives.

How it was found

Applying "fixed one arm of a double case? grep for the sibling" after 23fcd326f (the Pascal record/PXXMemMove break). The sibling search was for other frontends that could reach a builtinheap routine without saying so, and it turned up a third instance of the class — the first being bug-a-cfront-riscv32-byval-record-result-pxxmemmove (C, done, p70, rooted an 18-job cascade), the second the Pascal record arm. Three frontends, three causes, one shape, which is the argument for [[feature-a-pull-builtinheap-on-demand-instead-of-predicting-it]] rather than a fourth prediction site.