Repository navigation
arm optimizer: dynamic i32.load/store address constant-folded to a fixed 0x100 — pointer params ignored (--no-optimize correct) #178
Description
Activity
Kernel-side / host-ABI context (we own the host — let's design the contract together)
Flagging the broader picture, since PulseEngine owns both sides here: gale is the kernel that hosts and calls this code, so we're not a sandboxed guest stuck with a fixed wasm memory model — we control
__linear_memory_base, the value offp/r11 at the call site, the linker script, and the call site itself. We can meet synth halfway. Here's what we see and what we can offer, so the fix lands in a shape that actually works on-target.What synth does today (from the emitted code)
- Memory is addressed as
[fp + offset],fp(r11) = linear-memory base, initialized from the linker symbol__linear_memory_base(synth's own linker script:PROVIDE(__linear_memory_base = _bss_end)). --no-optimizeis correct: a pointer-param deref(local.get 0)(i32.load)→ldr [fp, r0](base + param).- Optimized path is wrong (this issue): the dynamic operand is constant-folded away →
ldr [0x20000100], param ignored.
The on-target problem for a host-pointer-dereferencing primitive
Our shim's
z_impl_k_sem_give(struct k_sem *sem)dereferences a nativek_sempointer the kernel passes inr0. Withfp = __linear_memory_base = <some RAM region>,[fp + sem]≠ the real sem. For the deref to be correct, either the host pointer must be a linmem offset, or the base must be 0 for that access.What the kernel side can provide (pick whichever fits synth's model)
-
Native-pointer ABI /
__linear_memory_base = 0. If synth supports (or documents)fp/__linear_memory_base = 0in--relocatablemode, then[fp + r0]=[r0]= a correct native deref. We will set__linear_memory_base = 0in the Zephyr link and, if needed, emit a 3-instruction trampoline that forcesr11 = 0around the call (r11 is callee-saved, so it survives the imported kernel calls). The catch we hit: this only works if the wasm module has no internal mutable statics at low linmem offsets (they'd land in flash at base 0). Our shim's only static is its spinlock — we made itextern(kernel-providedgale_wasm_shim_lock) to remove it. But then--no-optimizez_implfails with the arm regalloc: register exhaustion (no consecutive i64 pair) hard-fails on compiler_builtins::float::div::div #168 i64 regalloc exhaustion — so this path also needs the regalloc work, and ideally a way for&extern_datato lower to anR_ARM_ABS32/MOVW-MOVT data relocation (we didn't get to confirm synth emits one). -
Marshalling trampoline (works today, but costs). The kernel copies
sem->count/limitinto linmem before the call andnew_countback after. Correct on-target immediately, but the marshalling cost is in the measurement — so it won't show LTO-parity (it'd land ~GALE_USE_SYNTHclass). Fine as a first real number, not the end state. -
A dedicated "native pointer" param attribute. Cleanest long-term: synth marks selected pointer params as native (dereferenced at base 0 / directly), while module-internal data keeps the linmem base. That lets the optimized path emit
ldr [r0]for host pointers and still place statics in linmem. This is the one that gives a true native drop-in and optimized codegen.
Concrete asks to sync on
- Fix this issue (optimized dynamic load must use the operand) — needed regardless.
- Document/parameterize the
fp/__linear_memory_basecontract for--relocatableoutput so a host can set the base (e.g., 0 for native pointers). - Confirm whether
&extern_datalowers to a data relocation (R_ARM_ABS32/R_ARM_MOVW_ABS_NC+MOVT) — we need it for option 1. - The arm regalloc: register exhaustion (no consecutive i64 pair) hard-fails on compiler_builtins::float::div::div #168 i64 regalloc exhaustion resurfaces on
z_implonce the lock isextern(--no-optimize); relevant if option 1 is the chosen path.
Happy to provide the linker symbol, the trampoline, a reserved register, or a relocated
gale_wasm_shim_lock— whatever shape works best for synth's codegen. Tell us the contract and we'll meet it on the kernel side.- Memory is addressed as
- added a commit that references this issue
on May 30, 2026 Thanks for the detailed host-ABI context — really useful that you own both sides.
The bug itself is fixed in PR #179 (merging): the optimized path was dropping the load/store address operand for both dynamic and constant addresses (not just pointer params). Until the optimized memory codegen is properly repaired (tracked in #180),
optimize_fulldeclines memory-using modules and falls back toselect_with_stack, which is correct — so the optimized (default) output now byte-matches--no-optimize(ldr [fp, r0]). That's the "optimized dynamic load must use the operand — needed regardless" item.On the broader contract (separate from the bug) — your three options map cleanly onto a real design axis, and since it sets the native-drop-in ABI it deserves its own design issue rather than being decided inside a bugfix. I've captured it as a discussion thread and will pull in #34 (Meld host model), #170 (
$tmapping + standalone resolution), and #171 (i64 regalloc, which you correctly note resurfaces onz_implonce the lock isextern). Quick reactions:- Option 1 (
__linear_memory_base = 0/ native-pointer mode) is the cleanest path to true LTO-parity and the one I'd want to land. The catch you hit (module-internal statics at low linmem offsets landing in flash at base 0) is real; the right shape is probably Option 3 — a per-param "native pointer" attribute so host pointers deref at base 0 while internal data keeps the linmem base. That gives native drop-in and keeps the optimized path. - Option 2 (marshalling trampoline) is a fine first real number; agreed it won't show LTO-parity.
&extern_data→ data relocation: needs verifying on the synth side (whether it emitsR_ARM_MOVW_ABS_NC+MOVT/R_ARM_ABS32for an address-of-extern). I'll confirm and report in the design issue rather than guess.
Will open the design issue and tag it here. For now, PR #179 unblocks the correctness side (optimized pointer derefs are correct), shipping as v0.11.3.
- Option 1 (
- added 2 commits that reference this issue
on May 30, 2026
Summary
On synth
8f213351(post-v0.11.2), the optimized path miscompilesi32.load/i32.storeof a dynamic address (an address fromlocal.get/ a function parameter): it lowers to a load/store from a fixed constant address (linmem_base + 0x100), ignoring the dynamic operand entirely.--no-optimizeis correct (ldr [fp, r0]— base register + param), so this is an optimizer bug. Any wasm function that dereferences a pointer parameter, compiled with optimization on (the default), reads/writes the wrong memory.This is the on-target blocker for the cross-language-LTO-via-wasm route: the C shim's
z_impl_k_sem_give(struct k_sem *sem)readssem->countfrom0x20000100instead of from thesempointer the kernel passes inr0, so it cannot work as a drop-in native kernel primitive.(The relocation/encoding issues #167/#173/#174 are all fixed and verified — calls now resolve correctly. This is a separate, memory-addressing bug. gale's verified functions like
gale_k_sem_give_decide(count, limit, has_waiter)are unaffected because they take values, not pointers — which is whyGALE_USE_SYNTHworked. The bug surfaces the moment a wasm function dereferences a pointer arg.)Minimal reproduction
--no-optimizeis correct — it uses the param via the linear-memory base registerfp:So the optimizer is dropping the dynamic operand and substituting the constant
0x100. (The deadadds r4, r5, r1in the optimized output, withr5/r1uninitialized, looks like the remains of the intendedbase+offsetcomputation after the operand was constant-folded away.)Real-module manifestation
merged.both.wasm'sz_impl_k_sem_give(the wasm shim hosting the C↔Rust seam):The kernel calls
z_impl_k_sem_give(real_sem_ptr), but the code reads/writes0x20000100— wrong RAM — so the semaphore is never actually updated.Impact
Blocker for any optimized synth
armoutput that dereferences pointer parameters / does dynamic memory access — i.e., any wasm function operating on host-provided memory. Functions that take and return only scalar values are unaffected (which is whygale_k_sem_give_decide(count, limit, has_waiter)and theGALE_USE_SYNTHvalue-only path work).Secondary / architectural note (separate from the optimizer bug)
Even the correct
--no-optimizeform usesfpas the linear-memory base ([fp, r0]). For a--relocatableobject dropped into a native host (Zephyr kernel) where the caller passes a real pointer inr0via a plain AAPCS call, the effective address becomeslinmem_base + real_ptr, which is wrong unlesslinmem_base == 0. So making a pointer-dereferencing wasm function callable as a native drop-in also needs a way to set the linmem base to 0 (or otherwise treat pointer params as native addresses). Possibly already covered by the Meld/kiln host model (#34); flagging in case a--relocatable/native-ABI mode should zero the linmem base.Environment
8f213351(post-v0.11.2),armbackend, targetcortex-m4f--no-optimizeis correct