← board

The bare-ESP refusal for C that needs the RTL is an internal assertion

Split out of [[bug-s-c-on-the-esp-profile-cannot-reach-crtl]] on 2026-09-23, whose IDF half is fixed. This half is not a contract bug. Bare metal getting no default RTL is the design, and test-emit-obj now carries a negative-control row that pins it.

What a user sees

$ pascal26 --target=xtensa --esp-profile=bare --emit-obj hello.c hello.o
pascal26:11: error: compiler error: PXXMemZero not found (xtensa)
  near: >>>   malloc

Line 11 is inside stdio.h. PXXMemZero is a compiler-private heap helper the user never wrote and cannot reference. compiler error: is the spelling this tree uses for internal faults, so the message reads as "the compiler is broken" when the truth is "this profile excludes the unit that provides it".

Why the obvious remedies are both wrong

The shape of a fix

Raise the refusal where the policy is decided — beside cPullsBuiltinHeap in cparser.inc, which already knows both that the profile excludes the RTL and that this translation unit reaches for it — rather than letting fifteen ir_codegen_* sites each discover a missing symbol. The message should name the profile and what it excludes, and say that freestanding C is the supported shape here.

The Pascal driver answers the same situation by naming a uses clause. C has no uses, so the C wording cannot be borrowed and has to say something a C programmer can act on: use the IDF profile, or do not call into crtl.

Ranking

Low, and lower after the owner's 2026-09-23 ruling that IDF is the assumed profile and bare is a test vehicle for niche cases. It costs a confusing hour to whoever meets it on bare and nothing to anyone else. It is a diagnostic-only change, so it carries no risk to the contract the parent's control row pins.

Umbrella

[[meta-a-pxx-produces-linkable-code]]


FIXED 2026-09-24 (frank)

The ticket's proposed location does not work, and the reason is worth keeping

This ticket says to raise the refusal beside cPullsBuiltinHeap in cparser.inc, "which already knows both that the profile excludes the RTL and that this translation unit reaches for it". The second half of that is false. That gate knows the POLICY and cannot know the DEMAND:

A refusal at that gate would therefore have to be unconditional on the bare profile, and that breaks freestanding bare C — the supported shape, and the thing --esp-profile=bare exists for. So the proposed fix would have traded a bad message for a broken contract.

Where it went instead

FindHeapHelperOrRefuse in symtab.inc, immediately after FindProc. Fifteen codegen sites across six backends plus symtab now route through it instead of each spelling if procIdx < 0 then Error('compiler error: PXXMemZero not found (<arch>)'). That is the one place demand is known, and the ticket's real complaint — fifteen sites each discovering a missing symbol — is answered by having one of them rather than by moving the question somewhere it cannot be asked.

THE BRANCH IS THE FIX, NOT THE WORDING. Where NoDefaultRtl or TargetIsEspClass holds, the absence is a decision someone took, and the reader gets a refusal naming the profile and two actions. Where neither holds, builtinheap should be there and its absence really is an internal fault — so that arm keeps compiler error: exactly as it was. Softening both into one friendly message would have destroyed a real signal, which is the trap in a "make the message nicer" ticket.

Before and after, bare ESP, both ISAs

- pascal26:11: error: compiler error: PXXMemZero not found (xtensa)
+ pascal26:11: error: PXXMemZero is provided by the builtinheap unit, and this
+ build excludes the default RTL ... Either build it for the ESP-IDF profile
+ (drop --esp-profile=bare), where the RTL is pulled and crtl is reachable, or
+ keep the unit freestanding and do not call into crtl (in C, that means no
+ <stdio.h>/<stdlib.h> calls such as printf or malloc). Adding a `uses` of the
+ builtin unit is NOT an option on a bare ESP boot today.

It deliberately does NOT advise uses builtinheap, and that was checked rather than assumed: uses builtin; on a bare ESP boot does not compile, by design — 22 arms of needsBuiltin carry (not TargetIsEspClass), and [[feature-bare-esp-supports-uses-builtin]] is open precisely because making it work is an open-ended sequence of judgement calls, measured four steps deep. The obvious advice would have sent the reader into that.

Controls

result
bare ESP + C needing crtl, xtensa AND riscv32 refused, actionable message
freestanding bare C, xtensa AND riscv32 builds — the row an unconditional gate refusal would have broken
IDF profile + C needing crtl, both ISAs still builds (the parent's contract)
bare Pascal, both ISAs still builds (the 24 TargetIsEspClass sites)
x86-64 C, --emit-obj and exe builds; exe prints hello

The pre-existing bare row survived the rewrite because it greps the SYMBOL, not the wording — which is what its own comment said it was for. But it was not sufficient: asserting only that bare REFUSES was satisfied by the bad message too. Two rows now pin the message itself — it must name esp-profile=bare, and it must NOT contain compiler error: — and both were verified to FAIL against the old wording, so they discriminate rather than decorate.

test/c_freestanding_on_bare_esp_needs_no_rtl.c is new and carries the reason it must stay freestanding, so nobody turns the control into the other test by adding a printf.

Not controlled: the compiler error: arm has no positive control, because reaching it needs builtinheap absent with no policy explaining it, which is not constructible without breaking the tree. It is the unchanged pre-existing behaviour, not new code, but it is untested and this says so rather than implying otherwise.

make test-emit-obj GREEN, gate.sh quick GREEN, fixedpoint converged.

Log