No C entry stub for wasm32
Measured 2026-09-04 by franks-ab, checking whether its crtl signal bridge
(baec9c0bd) could be reached from C on wasm32. It cannot be reached from C on
wasm32 at all, and neither can anything else.
Repro
int main(void) { return 0; }
$ ./compiler/pascal26 --target=wasm32 t.c t.wasm
pascal26:5: error: C program entry stub not implemented for this target yet
cparser.inc:11666. Independently reproduced by this lane on a larger C
program (a time/exit probe) before the ticket was written -- same message, same
line, and it is the FIRST error, so nothing after it has ever been exercised.
Why it is worth more than its size
The goal is languages x platforms, and this is an empty cell in the product, not a missing feature in one axis. Everything C-shaped that has been made to work on wasm32 has been made to work through Pascal.
It also silently scopes other people's numbers. This lane's wasm32 gap census runs over a Pascal-only source list, because a C list would fail before codegen on every row and land in the census's own "never measured" bucket. So "518 IR_SYSCALL refusals" and every figure derived from it is a claim about the Pascal frontend on wasm32 and says nothing about the C one. That is not a defect in the census; it is what the census can physically see.
The precedent
devdocs/progress/done/bug-cfront-no-entry-stub-for-xtensa is the same shape on
the other odd target and was solved. Read that first: whoever takes this should
be checking whether the xtensa stub generalises rather than writing a third one,
because a per-target entry stub written three times is the shape that grows a
fourth.
wasm32 differs from xtensa in one way that matters to the design: a wasm module
has no ELF _start, it has a WASI _start export, and argv/envp come from
args_get/environ_get rather than off the stack. The Pascal driver already
does all of that -- WasmEmitEntry and the wasi backend's argv/environ path --
so the material exists; the question is whether the C driver can reuse it or
whether the two entry paths need normalising first
(devdocs/dev/normalise-dont-special-case.md).
What would close it
int main(void){return 0;} exits 0 under wasmtime, and a C program that calls
printf prints. Then re-run the wasm32 gap census over a C source list and
report the denominator, because the current one cannot cover a single C source.
Parked 2026-09-04 by frankA — four walls measured, and the fourth is not mine to fix here
Taken because it is the same shape as the xtensa stub, which I had just closed. It is not: xtensa needed five per-target machine-code arms, and this needs the wasm32 backend to change an assumption about what a function IS. Parking with the walls named, so the next person starts at wall four rather than wall one.
Measured by putting a no-op TARGET_WASM32: arm in the entry-stub case at
cparser.inc and compiling int main(void){return 0;}, then removing each
refusal in turn. Every arm below was REVERTED — the tree is unchanged except for
the diagnostic in (3), which stands on its own.
Wall 1 — EmitCSetjmpStubs: unsupported target. Raw machine-code stubs; a
wasm module has no stack to longjmp across. The xtensa split guard is already
there (if TargetArch <> TARGET_XTENSA) and wasm32 joins it. Two identical
copies, both must change — the comment above them says why.
Wall 2 — EmitCReturnZeroStub: unsupported target, reached through
EmitCFenvStubs. Same reason: it emits bytes. wasm32 skips fenv entirely, and
that is an honest gap — a C program calling fesetround will not link.
Wall 3 — wasm: duplicate export "main$463". The message named a collision
and left two very different causes indistinguishable: two bodies whose names
collided, or ONE body exported twice. I first fixed the wrong one — the chunk
namer builds main$<chunk index> and the body exporter disambiguates by
appending $<slot>, so the two namespaces genuinely overlap — and it changed
nothing, because that was not what was happening. That namespace overlap is
real, is NOT what this was, and I could not construct a case that reaches it, so
I reverted the rename rather than leave a guard whose necessity was never
shown. What DID land is wasmenc.inc separating the two causes in the
message; it is what turned an hour of guessing into one reading.
Wall 4 — the real one.
[[bug-a-wasm32-emits-a-separate-function-per-compileast-call-so-a-proc-built-in-two-calls-loses-a-body]].
C's main is built in THREE CompileAST calls (CompilePendingGlobalInits,
CEmitDeferredCAggInits, then the body) and wasm32 makes one wasm function per
call, all claiming main's slot. That ticket is filed at prio 70 because it is
NOT about C at all: procedure Fill(out s: string) already prints [KEEPXY] on
wasm32 against [XY] on the five register targets, silently, today.
Landing this stub before wall 4 would produce a C program that builds, runs,
and silently drops its global initialisers. That is worse than the refusal it
replaces, so this ticket is blocked-by it rather than merely later than it.
Not investigated past wall 4, and it should not be assumed there are only four:
each was found only by removing the one in front of it, which is the same
counting problem the xtensa ticket had. argc/argv is a known fifth — WASI has no
initial stack, WasmEmitArgvFetch exists for the Pascal side and depends on
PXXAlloc, which a C program does not necessarily link.
Unclaimed again; owner: cleared.
Parked 2026-09-04
blocked on the wasm32 one-function-per-CompileAST bug: four walls measured, the fourth needs the backend to change what a function is, and landing the stub before it would silently drop C global initialisers
Before resuming: read the reason above, then the ticket body. If the reason does not tell you what would make this worth picking up again, establishing that is the first step -- a park is a handoff to a stranger who may be you.
2026-09-06 (frankC): wall 4 IS gone — verified against its own symptom — and walls 5 and 6 are named
Wall 4 was resolved by frankwasm (460f439f6, cf23cfaf0). Confirmed here
rather than inherited, because a resolved blocker is the thing most often read
as more progress than it was. Checked against wall 4's OWN symptom, the one its
ticket names, not against "the C program gets further":
procedure Fill(out s: string);
begin s := s + 'X'; s := s + 'Y'; end;
... t := 'KEEP'; Fill(t); writeln('[', t, ']');
| native | [XY] |
| wasm32, before | [KEEPXY] (the ticket's recorded value) |
| wasm32, now | [XY] |
The probe is chosen so the fixed answer DIFFERS from the broken one and from an
empty result; [XY] cannot be produced by the bug.
The walls now, re-walked with frankA's method (a no-op TARGET_WASM32: arm, reverted)
Walls 1 and 2 are as frankA left them and remain honest gaps: EmitCSetjmpStubs
and EmitCFenvStubs emit machine code, wasm32 skips both, and a C program
calling fesetround or setjmp will not link. Both copies of the guard must
move together — the comment above them says so, and the probe's assertion
caught me editing one (count == 2, not 1).
Wall 3 is gone with wall 4. With the no-op arm the program COMPILES.
Wall 5 — there is no entry export, and the failure is SILENT. The module
exports main (C's own function) and no _start. Measured:
$ wasmtime <module exporting only "main"> -> rc=0, runs nothing
$ ./pascal26 --target=wasm32 t42.c t42.wasm && wasmtime t42.wasm
rc=0 # the program says return 42
int main(void){return 0;} exiting 0 is exactly what a module that never ran
also does — the ticket's own first acceptance criterion cannot tell them
apart, and I nearly recorded the no-op arm as a success on it. Any test for this
must return NONZERO. wasm _start is [] -> [] while C's main is
(i32,i32) -> i32, so exporting main twice is not available; a wrapper is
required, as WasmEmitMainWrapper already is for Pascal.
The material for it exists and is located: WasmEmitArgvFetch (WASI
args_sizes_get/args_get, allocates via PXXAlloc), WasmSlotForProc,
WasmAddExport, and the proc_exit import. The shape is a [] -> [] function
that runs initialisers, fetches argv, calls main, runs finalizers and
proc_exits the result — the same five steps every machine-code arm does.
Wall 6 — va_arg is unsupported on wasm32, and it blocks the ticket's SECOND
acceptance criterion. #include <stdio.h> pulls crtl, and lib/crtl/src/fcntl.c
uses va_arg for open/openat:
pascal26:32: error: variadic C functions (va_arg) are not yet supported on this cross target
in: ./compiler/../lib/crtl/src/fcntl.c
This is [[bug-c-the-32-bit-va-arg-set-is-complete-only-because-two-targets-cannot-compile-c-yet]], and wasm32 is one of the two targets that ticket is named for. So it is not a new wall — it is the one that ticket predicted would surface exactly here.
And it is already guarded. tools/c_va_arg_every_target.sh lists wasm32,
examines 7 targets, and separates "wasm32 refused at the C entry stub" (expected,
outside the check by construction) from "wasm32 refused for a NEW reason". With
the no-op arm in place it failed by name:
FAIL - wasm32 refused for a reason that is NOT the C entry stub, so this check
silently stopped covering it: ... va_arg ... not yet supported
That ticket's stated defence was "a written instruction to whoever implements the missing entry stub". It is not a written instruction any more; it fires. Whoever lands wall 5 will be told the same day, and the check going red at that moment is correct — it must be answered by implementing wasm32 va_arg or by re-stating its expectation against that ticket, not by lowering the floor.
What this changes for whoever takes it
Start at wall 5, not wall 1. Wall 5 alone buys freestanding C on wasm32 —
honest, and enough to empty the "no C program can target it" claim — but it does
NOT buy printf, because wall 6 sits in front of crtl. The two acceptance
criteria in this ticket are therefore separately blocked and only the first
is reachable without touching the va_arg lowering.
Probe reverted; tree unchanged. Binary back to f2f11cd439e7, refusal
reproduced, c_va_arg_every_target.sh green at 6 built, 1 awaiting a C entry stub, 7 examined.
Log
- 2026-09-06 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit be2c87890.