[fathom (gale) — cross-arch driver seam]
#1013 fixed the aarch64 ELF builder's panic, and both the issue and the fix rest
on the premise that "the ARM and RISC-V backends already do the right thing on
the identical module" / "matching the sibling backends."
On v0.60.0 the RISC-V backend does not, when the declined target is an
internal function rather than a requested export. It exits 0 and ships
an object that cannot be linked.
Repro
Any of gale's thin-seam drivers. gpio-thin is a 13-export real-world wasm:
$ synth compile gpio-thin.wasm -b riscv --target esp32c3 --all-exports --relocatable -o out.o
warning: skipping function 'func_18': backend 'riscv' failed: compilation failed:
RISC-V selector: immediate 1048588 too large for memory offset
warning: 1 of 18 functions were skipped (not in output): func_18
Compiled 17 functions to out.o
Total code size: 1696 bytes
Relocations: 22 (requires linking with Kiln bridge)
$ echo $?
0
Exit 0, and the decline is only a warning. But the object references the
declined function:
$ nm out.o
...
0000039c T func_10
000003b0 T func_11
00000518 T func_16
00000520 T func_17
00000000 T func_2
U read32
U synth_func_18 <-- declined func_18, referenced anyway
U write32
read32/write32 are the legitimate seam imports. synth_func_18 is not — it
is the function the backend just declined, and nothing defines it:
$ ld.lld out.o -o firmware
ld.lld: error: undefined symbol: synth_func_18
>>> referenced by out.o:(gust:hal/gpio@0.1.0#set)
>>> referenced by out.o:(gust:hal/gpio@0.1.0#read)
Two exported entry points reference it, so this is not dead code that a
--gc-sections pass would drop.
Why the export guard does not catch it
The #952 guard fires only when a requested export is skipped. On the same
corpus, mpu-thin skips the export gust:mpu/iso@0.1.0#size-field and synth
correctly exits 1 with no object. gpio-thin skips only the internal func_18,
so nothing fires — even though the resulting object is equally unusable.
Secondary: the dangling name does not match the definition namespace
Definitions in the object are func_N; the dangling reference is
synth_func_N. So the symbol could not be satisfied by another object either —
even a hand-written func_18 would not resolve it.
Scope on our corpus (v0.60.0, 13 thin-seam driver wasms)
|
rv32 result |
| 12 of 13 |
exit 0, object emitted, 1–10 dangling synth_func_N |
1 of 13 (mpu-thin) |
exit 1, no object — correct, caught by the export guard |
switch-thin is the worst: 13 of 30 functions declined, 10 dangling symbols.
The same 13 modules lower cleanly on cortex-m3 — every object's undefined set
is exactly its wasm imports, and internals are emitted as local t func_N. The
divergence is specific to the RISC-V path.
Not a regression: 0.52.0 / 0.53.0 / 0.54.0 / 0.55.0 / 0.57.0 / 0.58.0 / 0.60.0
all produce identical output on these inputs.
Two underlying selector gaps (separate from the guard)
These are what triggers the declines; the guard issue stands regardless of
whether they are fixed:
RISC-V selector: immediate 1048588 too large for memory offset — RV32 I-type
offsets are 12-bit signed; this needs lui/addi materialization. Hit by 12
of 13 drivers.
unsupported wasm op for RV32 skeleton: GlobalGet(0) — mpu-thin, 8 of 20
functions.
What we would expect
Exit non-zero with a message naming the declined symbol, the way #952 does for
exports and the way #1013 made aarch64 behave — rather than shipping an object
whose link is guaranteed to fail. If a partial object is sometimes wanted, an
opt-in flag (mirroring --allow-skipped-exports) would fit the existing shape.
Why this mattered to us
Our cross-arch gate asserted undefined_count > 0 — a rule written against the
0.49 truncation shape, where declined functions took their call sites with them.
A dangling synth_func_N satisfies > 0 just as well as a real seam symbol, so
the gate read green across every one of these objects. We have since changed the
rule to undefined set == wasm imports, which is what surfaced this.
Minor, while here: the success output suggests Link with: arm-none-eabi-ld for
a RISC-V object.
[fathom (gale) — cross-arch driver seam]
#1013 fixed the aarch64 ELF builder's panic, and both the issue and the fix rest
on the premise that "the ARM and RISC-V backends already do the right thing on
the identical module" / "matching the sibling backends."
On v0.60.0 the RISC-V backend does not, when the declined target is an
internal function rather than a requested export. It exits 0 and ships
an object that cannot be linked.
Repro
Any of gale's thin-seam drivers.
gpio-thinis a 13-export real-world wasm:Exit 0, and the decline is only a warning. But the object references the
declined function:
read32/write32are the legitimate seam imports.synth_func_18is not — itis the function the backend just declined, and nothing defines it:
Two exported entry points reference it, so this is not dead code that a
--gc-sectionspass would drop.Why the export guard does not catch it
The
#952guard fires only when a requested export is skipped. On the samecorpus,
mpu-thinskips the exportgust:mpu/iso@0.1.0#size-fieldand synthcorrectly exits 1 with no object.
gpio-thinskips only the internalfunc_18,so nothing fires — even though the resulting object is equally unusable.
Secondary: the dangling name does not match the definition namespace
Definitions in the object are
func_N; the dangling reference issynth_func_N. So the symbol could not be satisfied by another object either —even a hand-written
func_18would not resolve it.Scope on our corpus (v0.60.0, 13 thin-seam driver wasms)
synth_func_Nmpu-thin)switch-thinis the worst: 13 of 30 functions declined, 10 dangling symbols.The same 13 modules lower cleanly on
cortex-m3— every object's undefined setis exactly its wasm imports, and internals are emitted as local
t func_N. Thedivergence is specific to the RISC-V path.
Not a regression: 0.52.0 / 0.53.0 / 0.54.0 / 0.55.0 / 0.57.0 / 0.58.0 / 0.60.0
all produce identical output on these inputs.
Two underlying selector gaps (separate from the guard)
These are what triggers the declines; the guard issue stands regardless of
whether they are fixed:
RISC-V selector: immediate 1048588 too large for memory offset— RV32 I-typeoffsets are 12-bit signed; this needs
lui/addimaterialization. Hit by 12of 13 drivers.
unsupported wasm op for RV32 skeleton: GlobalGet(0)—mpu-thin, 8 of 20functions.
What we would expect
Exit non-zero with a message naming the declined symbol, the way
#952does forexports and the way #1013 made aarch64 behave — rather than shipping an object
whose link is guaranteed to fail. If a partial object is sometimes wanted, an
opt-in flag (mirroring
--allow-skipped-exports) would fit the existing shape.Why this mattered to us
Our cross-arch gate asserted
undefined_count > 0— a rule written against the0.49 truncation shape, where declined functions took their call sites with them.
A dangling
synth_func_Nsatisfies> 0just as well as a real seam symbol, sothe gate read green across every one of these objects. We have since changed the
rule to undefined set == wasm imports, which is what surfaced this.
Minor, while here: the success output suggests
Link with: arm-none-eabi-ldfora RISC-V object.