Skip to content

Improve instruction selector: explicit WASM value stack tracking for deterministic register assignment #72

@avrabe

Description

@avrabe

Overview

The InstructionSelector in crates/synth-synthesis/src/instruction_selector.rs is designed as a transpiler (WASM → readable ARM assembly), not a full AOT compiler. This is a great architectural choice for safety-critical embedded targets — it keeps the output deterministic, traceable, and auditable. This issue captures a set of improvements that would make the implementation fully consistent with that transpiler design intent.


Current Situation

The select_default path allocates registers via a round-robin counter (next_reg % 10) independent of the WASM value stack state. This means:

  • The registers assigned to rd, rn, rm have no defined relationship to actual WASM stack operands
  • Register assignment is non-deterministic across calls (depends on next_reg state)
  • The select_with_stack path does the right thing, but select_default is used as the fallback for all unmatched ops

Additionally, two Replacement variants produce silent Nop output instead of an explicit error:

Replacement::Var(_var_name) => Ok(vec![ArmOp::Nop]), // Placeholder
Replacement::Inline => Ok(vec![ArmOp::Nop]),          // Placeholder

Proposed Improvements

1. Explicit WASM Value Stack Tracker

Since WASM is a typed stack machine with statically known maximum stack depth (enforced by validation), each stack slot can be mapped to a fixed ARM register deterministically:

struct ValueStack {
    slots: Vec<Reg>, // index = WASM stack depth, value = assigned ARM register
}

impl ValueStack {
    fn pop(&mut self) -> Reg { self.slots.pop().expect("stack underflow") }
    fn push(&mut self, r: Reg) { self.slots.push(r) }
    fn peek(&self) -> Reg { *self.slots.last().expect("empty stack") }
}

This makes every generated ARM instruction's register choice directly traceable to a WASM stack position — exactly what a transpiler output should provide.

2. Static Stack-Slot-to-Register Mapping

For shallow WASM stacks (depth ≤ 10, which covers the vast majority of real WASM functions), a simple deterministic mapping suffices:

fn stack_depth_to_reg(depth: usize) -> Reg {
    ALLOCATABLE_REGS[depth] // bounded by WASM validation
}

No graph coloring, no linear scan needed — the transpiler goal means correctness and traceability matter more than optimal register reuse.

3. Explicit Errors for Unimplemented Replacement Variants

Replace silent Nop placeholders with proper errors:

Replacement::Var(name) => Err(Error::unimplemented(
    format!("Replacement::Var({name}) is not yet implemented")
)),
Replacement::Inline => Err(Error::unimplemented(
    "Replacement::Inline is not yet implemented"
)),

This ensures unimplemented paths fail loudly rather than silently producing incorrect output.

4. Unify select and select_with_stack

Once the value stack is tracked explicitly, select_default can use the same stack state as select_with_stack, eliminating the dual-path divergence.


Why This Matters

As a transpiler targeting safety-critical Cortex-M systems, each line of generated ARM assembly should have a clear, verifiable origin in the WASM source. Deterministic register assignment is a prerequisite for output auditability — which in turn supports the formal verification goals of the project (Rocq proofs, contract annotations).


Acceptance Criteria

  • select_default reads operand registers from an explicit ValueStack rather than from alloc_reg()
  • Register assignment for a given WASM op sequence is deterministic (same input → same output, independent of prior state)
  • Replacement::Var and Replacement::Inline return Err(...) instead of Nop
  • Existing tests continue to pass
  • New test: verify register assignment for I32Add sequence matches expected stack-slot mapping

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions