← board

C on the ESP profile cannot reach crtl

The 2×2, which is the whole diagnosis

Source is #include <stdio.h> plus int main(void) { printf("hello\n"); return 0; }.

--platform=posix ESP profile (default and --esp-profile=bare)
--emit-obj BUILDS pascal26:11: error: compiler error: PXXMemZero not found
executable BUILDS refuses earlier, at the C entry stub (correct, by design)

The discriminator is the PROFILE, not the output mode. That matters because the obvious reading — "the object path is less complete than the executable path" — is wrong, and it is the reading someone will reach for.

A freestanding C source is fine on both ESP profiles: int main(void) with no includes builds to an object exporting GLOBAL app_main. So the entry machinery works and it is specifically the crtl reach that does not.

Why "not found" is misleading, and what it is not

PXXMemZero is declared at compiler/builtin/builtinheap.pas:468 and defined at :4561. The body is unconditional — only its fast paths carry {$ifdef CPUX86_64} — and it sits after the {$ifndef PXX_ESP} block (3671–4356) closes, so it is not guarded out on ESP.

So the symbol exists and the lookup is not reaching it. This is not a missing implementation; it is the builtin heap unit not being pulled into a C compilation on PLATFORM_ESP. The compiler error: spelling is an internal assertion — one of fifteen identical PXXMemZero not found sites across six backends (symtab.inc plus five ir_codegen_*), so the message says which symbol and nothing about which of the six asked.

Do not "fix" this by defining PXXMemZero somewhere. It is defined. The question is unit reachability on this profile, and a second definition would make the symptom go away while leaving every other builtin in the same unit just as unreachable — the next one would surface as a different name and read as an unrelated bug.

Where this came from, and what it bounds

decide-should-a-c-main-exist-on-the-esp-profile-at-all (decided today) established that --emit-obj is how C ships to an ESP32 and that the standalone refusal is correct. That is true for freestanding C and I wrote it without the bound, which frankC caught by noticing its own c_va_arg_every_target.sh --emit-obj row builds only a trivial main and a stdarg program and never reaches printf. The diagnostic in cparser.inc now carries the bound and names this ticket.

Next step for whoever takes it

The cheap first measurement is which of the six backends raises it here — PXXDBG or a temporary distinct string per site — because symtab.inc raising it means the symbol is absent from the table entirely, while an ir_codegen_* site raising it means the table was built and the unit was still not in it. Those are different bugs with the same message.

Resolution (2026-09-23, frank) — IDF fixed, bare is by design

The fix

One predicate, cparser.inc:

- cPullsBuiltinHeap := (not NoDefaultRtl) and (TargetPlatform <> PLATFORM_ESP);
+ cPullsBuiltinHeap := (not NoDefaultRtl) and (not TargetIsEspClass);

TargetIsEspClass is the tree's profile-aware predicate for this exact question and the Pascal driver asks it at 24 sites. It differs from the platform spelling on xtensa, which is PLATFORM_ESP unconditionally — so the gate was permanently False there and no xtensa C source could reach crtl on any profile.

Two things in the tree already said so:

The neighbouring pxxcio gate is correctly spelled over TargetPlatform — it asks "are there posix syscalls under this target", a different question with its own note — which is why two adjacent gates disagreeing read as deliberate.

Correction — the 2×2's conclusion was wrong, and it is what hid the cause

The ticket says "the discriminator is the PROFILE, not the output mode" and flags the alternative as the reading someone would wrongly reach for. The profile is not the discriminator either:

default (IDF) --esp-profile=bare
xtensa / esp32 / esp32s3 refused refused
esp32c3 / esp32c6 BUILT ALL ALONG refused

esp32c3 is the ESP platform on riscv32 and built this the whole time, because riscv32 is posix unless the profile says otherwise. So the failing set is xtensa on any profile, plus either ISA under bare — an ISA×profile shape, not a profile one. Measuring only the primary target made an ISA effect look like a profile effect, and the fix a profile reading suggests (widen the platform test) would have dragged the whole RTL into bare-metal images.

The ticket's own next-step question, answered

"The cheap first measurement is which of the six backends raises it." It is an ir_codegen_* site, never symtab.inc — so, in the ticket's own terms, the table was built and the unit was still not in it. Measured by making all fifteen sites name their file, which is kept: the message now reads PXXMemZero not found (xtensa). Nothing asserted on the old string.

Verified

All IDF profiles build (xtensa, esp32, esp32s3, esp32c3, riscv32), and the xtensa object is sound rather than merely accepted: ET_REL/Tensilica, PXXMemZero a defined local, app_main the global entry, and every undefined symbol IDF- or newlib-provided (calloc, free, putchar, vTaskDelete, read, write, fopen…).

Seven rows in test-emit-obj, of which three fail under pinned v416 — both xtensa compiles and the xtensa nm row. The esp32c3 rows pass under the pin too, deliberately: they are there to record that a riscv32-only row would have passed throughout. nm ... ' t PXXMemZero' is asserted separately from the compile because "the compiler accepted it" and "the unit got pulled" are two claims and only the second was broken.

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

Residual — owned, not left silent

Bare metal deliberately gets no default RTL, so bare refusing this source is correct and the negative-control row pins it. What is still wrong there is only the diagnostic: it is an internal assertion (compiler error:) naming a symbol, with no remedy a C programmer could act on, because C has no uses clause to add. Filed as [[bug-s-the-bare-esp-refusal-for-c-that-needs-the-rtl-is-an-internal-assertion]]. Low prio, and lower still under the owner's 2026-09-23 ruling that IDF is the assumed profile and bare is a test vehicle.

Log