Skip to content

-b aarch64 silently miscompiles f32 ops (emits 'mov w0,w1' / drops constants) instead of erroring like other unsupported milestone-1 ops #554

Description

@avrabe

Filed by the PulseEngine challenge harness (synth 137b773, aarch64 backend). Confirmed by disassembling the (now-valid, post-#546) ELF64/EM_AARCH64 output.

Defect: -b aarch64 SILENTLY miscompiles f32 ops instead of rejecting them

milestone-1 correctly rejects most unimplemented ops with an explicit error, e.g.:

$ synth compile i32shl.wat -b aarch64 ...
Error: aarch64 selector: unsupported wasm op for aarch64 milestone-1: I32Shl

(same honest failure for I32DivS, I64Add, I64Mul, I32Load.) But f32 ops compile "successfully" and emit wrong code:

input emitted A64 correct?
f32.add(a,b) mov w0, w1 ; ret ❌ returns the 2nd operand, no add
f32.sub(a,b) mov w0, w1 ; ret
f32.mul(a,b) mov w0, w1 ; ret
f32.const 2.5 ret ❌ constant never materialized

No FP instruction (fadd/fmul/fmov/scvtf) is ever emitted, and f32 args live in s0/s1 (not w0/w1) under AAPCS64 anyway. A program doing f32 arithmetic compiled with -b aarch64 gets silently wrong results — strictly worse than the honest "unsupported milestone-1" error the other ops produce.

Repro

printf '(module (func (export "f") (param f32 f32)(result f32)(f32.add (local.get 0)(local.get 1))))\n' > f.wat
synth compile f.wat -b aarch64 -n f -o f.o
llvm-objdump -d f.o     # -> add? no:  mov w0, w1 ; ret

Root cause

The op stream handed to the aarch64 backend has the f32 op dropped:

INFO WASM operations: [LocalGet(0), LocalGet(1), End]      # F32Add is gone

So F32Add never reaches the selector's guard at crates/synth-backend-aarch64/src/selector.rs:97 (other => Err("unsupported wasm op...")) that would otherwise reject it. The End handler then just does mov w0, <top-of-stack=w1>; ret.

Fix

Ensure f32/f64 ops are NOT silently dropped before the aarch64 selector — either implement them (fadd/fsub/fmul on s/d regs, args in v0..), or let them fall through to the milestone-1 unsupported error so the failure is honest. Silent wrong-code must never be the outcome.

Related observation (not claiming a bug here)

The same op stream [LocalGet, LocalGet, End] (F32Add dropped) is logged for the ARM backend on the --relocatable/direct-selector path. The optimized ARM path may handle f32 separately (VFP), but the direct-selector path is worth checking for the same silent drop.

Note

Regression probe added: harness/synth_aarch64_f32_probe.sh (asserts f32 ops either error or emit an FP instruction — never silently return an operand).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions