diff --git a/CHANGELOG.md b/CHANGELOG.md
index dab518b..f452403 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,23 +5,50 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [Unreleased]
-
-### Changed
-- Reorganized documentation structure - consolidated 41 root docs into organized `docs/` subdirectories
-- Documentation now organized by category: status, sessions, analysis, planning, research, build-systems, validation, architecture
+## [0.1.0] - 2026-03-21
### Added
-- GitHub project infrastructure: 40 labels, 4 milestones, 24 issues
-- Project board for tracking reorganization progress
-## [0.1.0] - TBD
+#### Compiler
+- WebAssembly-to-ARM Cortex-M AOT compiler
+- 197+ WASM opcodes supported (i32, i64, f32, f64, SIMD/Helium)
+- Full control flow (block, loop, if/else, br, br_if, br_table, call)
+- Sub-word memory operations (load8/16, store8/16)
+- memory.size / memory.grow
+- Globals, select, i64 register pairs
+- ARM Thumb-2 instruction encoding
+- VFP/FPU support (f32, f64)
+- WASM SIMD to ARM Helium MVE (Cortex-M55)
+
+#### Output
+- ELF binary output with vector table and startup code
+- Linker script generation (STM32, nRF52840, generic)
+- MPU region configuration
+- `synth compile --link` cross-compilation pipeline via arm-none-eabi-gcc
+
+#### CLI
+- `synth compile` -- compile WASM/WAT to ARM ELF
+- `synth disasm` -- disassemble ARM ELF binaries
+- `synth verify` -- Z3 SMT translation validation
+- `synth backends` -- list available compilation backends
+- Target profiles: cortex-m3, cortex-m4, cortex-m4f, cortex-m7, cortex-m7dp, cortex-m55
+
+#### Verification
+- Rocq mechanized proofs (188 Qed / 52 Admitted)
+- All i32 operations (arithmetic, division, comparison, bit-manip, shift/rotate) have T1 result-correspondence proofs
+- Z3 SMT translation validation (53 verification tests)
+- STPA safety analysis (losses, hazards, UCAs, constraints)
+- Rivet SDLC artifact traceability (250+ artifacts)
+
+#### Testing
+- 851 tests, all passing
+- 227/257 WebAssembly spec test files compile
+- Renode ARM Cortex-M4 emulation tests via Bazel
-### Planned
-- Working CLI: `synth compile input.wasm -o output.elf`
-- Calculator demo running on Zephyr/QEMU
-- Complete CI/CD pipeline
-- Comprehensive documentation
+#### Examples & Integrations
+- Zephyr RTOS integration examples
+- Anti-pinch window controller (automotive safety demo)
+- OSxCAR WASM compilation test
---
diff --git a/Cargo.toml b/Cargo.toml
index 402357a..905f58c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,9 @@ rust-version = "1.88"
authors = ["PulseEngine Team"]
license = "Apache-2.0"
repository = "https://github.com/pulseengine/synth"
+homepage = "https://github.com/pulseengine/synth"
+keywords = ["webassembly", "arm", "cortex-m", "compiler", "embedded"]
+categories = ["compilers", "embedded", "wasm", "no-std"]
[workspace.dependencies]
# WebAssembly tooling
diff --git a/README.md b/README.md
index 0702086..965a91d 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# Synth
-WebAssembly-to-ARM compiler with mechanized correctness proofs
+WebAssembly-to-ARM Cortex-M AOT compiler with mechanized correctness proofs
@@ -29,7 +29,7 @@
-Synth compiles WebAssembly to native ARM Cortex-M machine code, producing bare-metal ELF binaries for embedded targets. Pattern-based instruction selection, AAPCS calling conventions, and ELF generation -- with mechanized correctness proofs in [Rocq](https://rocq-prover.org/) (formerly Coq) and SMT-based translation validation via Z3.
+Synth compiles WebAssembly to native ARM Cortex-M machine code, producing bare-metal ELF binaries for embedded targets. It supports 197+ WASM opcodes (i32, i64, f32, f64, SIMD/Helium), full control flow, memory operations, and globals. Pattern-based instruction selection, AAPCS calling conventions, and ELF generation -- with mechanized correctness proofs in [Rocq](https://rocq-prover.org/) (formerly Coq) and SMT-based translation validation via Z3.
Part of [PulseEngine](https://github.com/pulseengine) -- a WebAssembly toolchain for safety-critical systems with mechanized verification:
@@ -44,7 +44,7 @@ Part of [PulseEngine](https://github.com/pulseengine) -- a WebAssembly toolchain
### From source (Cargo)
-Requires Rust 1.85+ (edition 2024).
+Requires Rust 1.88+ (edition 2024).
```bash
git clone https://github.com/pulseengine/synth.git
@@ -62,6 +62,38 @@ Bazel 8.x builds Rust, Rocq proofs, and Renode emulation tests hermetically via
bazel build //crates:synth
```
+## Quick Start
+
+```bash
+# Compile a WAT file to a Cortex-M ELF binary
+synth compile examples/wat/simple_add.wat --cortex-m -o firmware.elf
+
+# Disassemble the result
+synth disasm firmware.elf
+
+# Formally verify the translation
+synth verify examples/wat/simple_add.wat firmware.elf
+```
+
+## Features
+
+| Category | Status |
+|----------|--------|
+| i32 arithmetic, bitwise, comparison, shift/rotate | Supported |
+| i64 arithmetic (register pairs) | Supported |
+| f32/f64 via VFP | Supported |
+| WASM SIMD via ARM Helium MVE | Supported (Cortex-M55) |
+| Control flow (block, loop, if/else, br, br_if, br_table) | Supported |
+| Function calls (direct, indirect) | Supported |
+| Memory (load/store, sub-word, size/grow) | Supported |
+| Globals, select, unreachable, nop | Supported |
+| ELF output with vector table | Supported |
+| Linker scripts (STM32, nRF52840, generic) | Supported |
+| Cross-compilation via arm-none-eabi-gcc | Supported |
+| Rocq mechanized proofs | 188 Qed / 52 Admitted |
+| Z3 translation validation | 53 tests passing |
+| WebAssembly spec test suite | 227/257 files compile |
+
## Usage
### Compile WASM/WAT to ARM ELF
@@ -120,7 +152,7 @@ graph LR
**Pipeline stages:**
1. **Parse** -- decode WASM binary or WAT text via `wasmparser`/`wat` crates
-2. **Instruction selection** -- pattern-match WASM ops to ARM instruction sequences (i32 integer ops; f32/f64/i64 rejected at compile time)
+2. **Instruction selection** -- pattern-match WASM ops to ARM instruction sequences (i32, i64, f32, f64, SIMD)
3. **Peephole optimization** -- redundant-op elimination, NOP removal, instruction fusion, constant propagation (0-25% code reduction)
4. **ARM encoding** -- emit 32-bit ARM / Thumb-2 machine code
5. **ELF builder** -- produce ELF32 with `.text`, `.isr_vector`, `.data`, `.bss`, symbol table; optional vector table and reset handler for Cortex-M
@@ -134,12 +166,14 @@ Synth employs two complementary verification strategies.
Mechanized proofs in Rocq 9 show that `compile_wasm_to_arm` preserves WASM semantics for each operation. The proof suite lives in `coq/Synth/` and covers ARM instruction semantics, WASM stack-machine semantics, and per-operation correctness theorems.
```
-154 Qed / 71 Admitted / 12 axioms
- T1: 19 result-correspondence (ARM output = WASM result)
+188 Qed / 52 Admitted
+ T1: 39 result-correspondence (ARM output = WASM result)
T2: 95 existence-only (ARM execution succeeds)
- T3: 71 admitted (VFP, flags, shifts)
+ T3: 52 admitted (VFP/float semantics)
```
+All i32 operations (arithmetic, division, comparison, bit-manip, shift/rotate) have full T1 proofs.
+
Build the proofs:
```bash
@@ -173,7 +207,7 @@ The `synth-verify` crate encodes WASM and ARM semantics as Z3 formulas and check
## Testing
```bash
-# Run all Rust tests (521 tests across workspace)
+# Run all Rust tests (851 tests across workspace)
cargo test --workspace
# Lint
diff --git a/crates/synth-abi/Cargo.toml b/crates/synth-abi/Cargo.toml
index da958db..2bf9044 100644
--- a/crates/synth-abi/Cargo.toml
+++ b/crates/synth-abi/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "WebAssembly Component Model ABI (lift/lower) for the Synth compiler"
[dependencies]
synth-wit = { path = "../synth-wit" }
diff --git a/crates/synth-analysis/Cargo.toml b/crates/synth-analysis/Cargo.toml
index 2323352..867230e 100644
--- a/crates/synth-analysis/Cargo.toml
+++ b/crates/synth-analysis/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "SSA and control flow analysis for the Synth compiler"
[dependencies]
synth-core = { path = "../synth-core" }
diff --git a/crates/synth-backend-awsm/Cargo.toml b/crates/synth-backend-awsm/Cargo.toml
index 4266f26..4bb6eef 100644
--- a/crates/synth-backend-awsm/Cargo.toml
+++ b/crates/synth-backend-awsm/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "aWsm backend integration for the Synth compiler"
[dependencies]
synth-core = { path = "../synth-core" }
diff --git a/crates/synth-backend-wasker/Cargo.toml b/crates/synth-backend-wasker/Cargo.toml
index 8dde712..f259e27 100644
--- a/crates/synth-backend-wasker/Cargo.toml
+++ b/crates/synth-backend-wasker/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "Wasker backend integration for the Synth compiler"
[dependencies]
synth-core = { path = "../synth-core" }
diff --git a/crates/synth-backend/Cargo.toml b/crates/synth-backend/Cargo.toml
index e14e2b6..0cd47f2 100644
--- a/crates/synth-backend/Cargo.toml
+++ b/crates/synth-backend/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "ARM encoder, ELF builder, vector table, linker scripts, and MPU configuration"
[features]
default = ["arm-cortex-m"]
diff --git a/crates/synth-cfg/Cargo.toml b/crates/synth-cfg/Cargo.toml
index 9d67526..b19d061 100644
--- a/crates/synth-cfg/Cargo.toml
+++ b/crates/synth-cfg/Cargo.toml
@@ -5,5 +5,6 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "Control flow graph representation for the Synth compiler"
[dependencies]
diff --git a/crates/synth-cli/Cargo.toml b/crates/synth-cli/Cargo.toml
index b23b6d7..4b2ba8f 100644
--- a/crates/synth-cli/Cargo.toml
+++ b/crates/synth-cli/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "CLI for Synth, the WebAssembly-to-ARM Cortex-M AOT compiler"
[[bin]]
name = "synth"
diff --git a/crates/synth-cli/src/main.rs b/crates/synth-cli/src/main.rs
index 9f00c30..a95cc7f 100644
--- a/crates/synth-cli/src/main.rs
+++ b/crates/synth-cli/src/main.rs
@@ -1,6 +1,7 @@
-//! Synth CLI - WebAssembly Component Synthesizer
+//! Synth CLI - WebAssembly-to-ARM Cortex-M AOT Compiler
//!
-//! Command-line interface for the Synth synthesizer.
+//! Command-line interface for the Synth compiler. Compiles WASM/WAT files
+//! to bare-metal ARM Cortex-M ELF binaries with optional formal verification.
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
@@ -23,7 +24,17 @@ use wast::{Wast, WastDirective};
#[derive(Parser)]
#[command(name = "synth")]
-#[command(about = "WebAssembly Component Synthesizer for Embedded Systems", long_about = None)]
+#[command(about = "WebAssembly-to-ARM Cortex-M AOT compiler")]
+#[command(
+ long_about = "Synth compiles WebAssembly (WASM/WAT) to native ARM Cortex-M machine code,\n\
+producing bare-metal ELF binaries for embedded targets.\n\n\
+Examples:\n \
+synth compile input.wat -o output.elf\n \
+synth compile input.wat --cortex-m -o firmware.elf\n \
+synth compile input.wat --cortex-m --link -o firmware.elf\n \
+synth disasm firmware.elf\n \
+synth verify input.wat firmware.elf"
+)]
#[command(version)]
struct Cli {
#[command(subcommand)]
@@ -90,7 +101,7 @@ enum Commands {
target: String,
},
- /// Compile WASM/WAT to ARM ELF
+ /// Compile WASM/WAT to ARM ELF (e.g., synth compile input.wat --cortex-m -o fw.elf)
Compile {
/// Input WASM or WAT file (optional, use --demo for built-in demos)
#[arg(value_name = "INPUT")]
@@ -155,7 +166,7 @@ enum Commands {
builtins: Option,
},
- /// Disassemble an ARM ELF file
+ /// Disassemble an ARM ELF file (e.g., synth disasm output.elf)
Disasm {
/// Input ELF file
#[arg(value_name = "INPUT")]
@@ -165,7 +176,7 @@ enum Commands {
/// List available compilation backends and their status
Backends,
- /// Verify compilation correctness (translation validation)
+ /// Verify compilation correctness via Z3 (e.g., synth verify input.wat output.elf)
Verify {
/// Input WASM or WAT file (source)
#[arg(value_name = "WASM")]
diff --git a/crates/synth-core/Cargo.toml b/crates/synth-core/Cargo.toml
index d12131b..b226628 100644
--- a/crates/synth-core/Cargo.toml
+++ b/crates/synth-core/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "Core types, error handling, and backend trait for the Synth compiler"
[dependencies]
serde.workspace = true
diff --git a/crates/synth-frontend/Cargo.toml b/crates/synth-frontend/Cargo.toml
index 7572423..0e48fb8 100644
--- a/crates/synth-frontend/Cargo.toml
+++ b/crates/synth-frontend/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "WASM/WAT parser and module decoder frontend for the Synth compiler"
[dependencies]
synth-core = { path = "../synth-core" }
diff --git a/crates/synth-memory/Cargo.toml b/crates/synth-memory/Cargo.toml
index a05dda0..3ff04d6 100644
--- a/crates/synth-memory/Cargo.toml
+++ b/crates/synth-memory/Cargo.toml
@@ -2,8 +2,10 @@
name = "synth-memory"
version.workspace = true
edition.workspace = true
+authors.workspace = true
license.workspace = true
-description = "Portable memory management for Synth WASM-to-ARM compiler"
+repository.workspace = true
+description = "Portable memory abstraction (Zephyr, Linux, bare-metal) for the Synth compiler"
[dependencies]
bitflags = "2.4"
diff --git a/crates/synth-opt/Cargo.toml b/crates/synth-opt/Cargo.toml
index 040ef4f..4cd93e0 100644
--- a/crates/synth-opt/Cargo.toml
+++ b/crates/synth-opt/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "Peephole optimization passes for the Synth compiler"
[dependencies]
synth-cfg = { path = "../synth-cfg" }
diff --git a/crates/synth-qemu/Cargo.toml b/crates/synth-qemu/Cargo.toml
index 4ed65c1..3aa5361 100644
--- a/crates/synth-qemu/Cargo.toml
+++ b/crates/synth-qemu/Cargo.toml
@@ -5,5 +5,6 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "QEMU integration utilities for testing Synth-compiled ARM binaries"
[dependencies]
diff --git a/crates/synth-synthesis/Cargo.toml b/crates/synth-synthesis/Cargo.toml
index e5ce850..5534c99 100644
--- a/crates/synth-synthesis/Cargo.toml
+++ b/crates/synth-synthesis/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "WASM-to-ARM instruction selection and peephole optimizer"
[dependencies]
synth-core = { path = "../synth-core" }
diff --git a/crates/synth-test/Cargo.toml b/crates/synth-test/Cargo.toml
index 0e429a2..ada0b9c 100644
--- a/crates/synth-test/Cargo.toml
+++ b/crates/synth-test/Cargo.toml
@@ -4,7 +4,8 @@ version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
-description = "WAST-driven test runner for Synth"
+repository.workspace = true
+description = "WAST-to-Robot Framework test generator for Renode emulation"
[dependencies]
wast = { workspace = true }
diff --git a/crates/synth-verify/Cargo.toml b/crates/synth-verify/Cargo.toml
index 1fae55a..1fcedbb 100644
--- a/crates/synth-verify/Cargo.toml
+++ b/crates/synth-verify/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "Z3 SMT translation validation for the Synth compiler"
[features]
default = ["z3-solver", "arm"]
diff --git a/crates/synth-wit/Cargo.toml b/crates/synth-wit/Cargo.toml
index 2ab044c..5b11120 100644
--- a/crates/synth-wit/Cargo.toml
+++ b/crates/synth-wit/Cargo.toml
@@ -5,5 +5,6 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
+description = "WIT (WebAssembly Interface Types) support for the Synth compiler"
[dependencies]
diff --git a/docs/design/meld-kiln-integration-instructions.md b/docs/design/meld-kiln-integration-instructions.md
new file mode 100644
index 0000000..0c6082b
--- /dev/null
+++ b/docs/design/meld-kiln-integration-instructions.md
@@ -0,0 +1,266 @@
+# Meld & Kiln Integration Instructions for Synth Pipeline
+
+## Context
+
+Synth (WASM→ARM Cortex-M AOT compiler) can now compile real-world WASM to ARM ELF binaries. The next step is the full cross-compilation chain:
+
+```
+WASM Components → meld fuse → Core WASM → synth compile --link → firmware.elf → Cortex-M
+```
+
+We've verified that the **OSxCAR anti-pinch window controller** (3 WASM components from https://osxcar.de/insights/demonstration/) compiles through synth individually. The goal is to fuse them via meld, then compile+link the fused output into a single firmware.elf that runs on Renode.
+
+---
+
+## What Synth Provides Today
+
+- `synth compile -o output.elf --cortex-m --all-exports` — AOT compiles core WASM to ARM Cortex-M ELF
+- `synth compile ... --link --builtins ` — Compiles AND links into firmware.elf
+- Relocatable ELF output with `__meld_dispatch_import` undefined symbol + `.meld_import_table` section
+- Linker script generation with Meld integration sections
+- 197+ WASM opcodes, 227/257 spec files compile, 851 tests
+
+---
+
+## What Meld Needs To Do
+
+### Step 1: Fuse the OSxCAR Components
+
+The 3 OSxCAR WASM modules are already-lowered core modules (not full P2 components). They were lowered by wit-component before being base64-embedded in JavaScript. Meld should be able to fuse them:
+
+```bash
+meld fuse anti_pinch_v2.wasm motor_driver_v2.wasm soft_start_stop.wasm -o fused_antipinch.wasm --memory shared
+```
+
+**Key requirement:** The output must be a **single-memory core module** (not multi-memory). Synth targets single-memory ARM Cortex-M. Options:
+1. `--memory shared` with `--address-rebase` (preferred if stable)
+2. OR synth adds multi-memory support (more work)
+
+### Step 2: Verify the Fused Output
+
+```bash
+meld inspect fused_antipinch.wasm --types --interfaces
+```
+
+Expected output should show:
+- Single core module (not component)
+- Merged exports from all 3 components
+- Unresolved WASI imports listed as core imports
+- Single memory
+
+### Step 3: Import Map (nice-to-have)
+
+Add `--emit-import-map imports.json` flag that produces:
+
+```json
+{
+ "imports": [
+ {"index": 0, "module": "wasi:cli/stderr@0.2.6", "name": "get-stderr"},
+ {"index": 1, "module": "wasi:io/streams@0.2.6", "name": "[method]output-stream.blocking-write-and-flush"}
+ ]
+}
+```
+
+This tells synth (and kiln-builtins) exactly which import index maps to which WASI function, enabling the dispatcher to route calls correctly.
+
+### Step 4: Test with Synth
+
+After fusing, synth should be able to compile the output:
+
+```bash
+synth compile fused_antipinch.wasm -o antipinch.elf --cortex-m --all-exports
+```
+
+If this fails, the error message will tell us what WASM opcodes or features the fused module uses that synth doesn't support yet.
+
+---
+
+## What Kiln Needs To Do
+
+### Step 1: Create `kiln-builtins` Crate
+
+A new `no_std` crate in the kiln workspace. This is the **C ABI bridge** between synth-compiled ARM code and kiln's runtime services.
+
+**Location:** `kiln-builtins/`
+
+**Cargo.toml:**
+```toml
+[package]
+name = "kiln-builtins"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+crate-type = ["staticlib"] # Produces libkiln_builtins.a
+
+[dependencies]
+kiln-foundation = { path = "../kiln-foundation", default-features = false }
+
+[features]
+default = []
+std = ["kiln-foundation/std"]
+```
+
+**Target:** `thumbv7em-none-eabi` (Cortex-M4F) and `thumbv7em-none-eabihf` (hard-float)
+
+### Step 2: Implement Core Functions
+
+The minimum viable set (3 functions):
+
+```rust
+#![no_std]
+
+/// Called by synth-compiled code: MOV R0, #import_index; BL __meld_dispatch_import
+/// Routes to the appropriate WASI/host function based on the import index.
+#[unsafe(no_mangle)]
+pub extern "C" fn __meld_dispatch_import(import_index: u32) -> u32 {
+ // Read import table from .meld_import_table section (linker-provided symbols)
+ // Match module/name to handler
+ // Call handler with args from linear memory
+ // Return result
+ 0 // stub for Phase 1
+}
+
+/// Returns the base address of WASM linear memory.
+#[unsafe(no_mangle)]
+pub extern "C" fn __meld_get_memory_base() -> *mut u8 {
+ extern "C" { static __wasm_memory_start: u8; }
+ unsafe { &raw const __wasm_memory_start as *mut u8 }
+}
+
+/// Canonical ABI allocator for linear memory.
+/// Called by adapter code to allocate space for strings, lists, records.
+/// For embedded: bump allocator within linear memory bounds.
+#[unsafe(no_mangle)]
+pub extern "C" fn cabi_realloc(
+ old_ptr: u32,
+ old_size: u32,
+ align: u32,
+ new_size: u32,
+) -> u32 {
+ // Bump allocator: maintain a pointer at a known memory location
+ // Allocate new_size bytes at current pointer, advance pointer
+ // If old_ptr != 0, this is a realloc (copy old data, return new ptr)
+ // Return offset from memory base (not absolute address)
+ 0 // stub for Phase 1
+}
+```
+
+### Step 3: WASI Embedded Dispatcher
+
+For the anti-pinch demo, the minimum WASI needed is:
+- `wasi:cli/stderr` — error output (can stub to UART/semihosting)
+- `wasi:io/streams` — blocking write (route to UART)
+- `wasi:clocks/monotonic-clock` — timer (read SysTick)
+
+Source material exists in `kiln-wasi/src/dispatcher.rs` — extract the dispatch logic into no_std-compatible form.
+
+### Step 4: Build for ARM Target
+
+```bash
+cargo build --target thumbv7em-none-eabi -p kiln-builtins --release
+# Produces: target/thumbv7em-none-eabi/release/libkiln_builtins.a
+```
+
+### Step 5: Test with Synth
+
+```bash
+# Compile WASM to relocatable object
+synth compile fused_antipinch.wasm -o module.o --cortex-m --all-exports
+
+# Link with kiln-builtins
+arm-none-eabi-gcc -nostartfiles -nostdlib -mcpu=cortex-m4 -mthumb \
+ -T synth_generated.ld module.o libkiln_builtins.a -o firmware.elf
+
+# OR use synth's --link flag:
+synth compile fused_antipinch.wasm -o firmware.elf \
+ --cortex-m --all-exports --link --builtins libkiln_builtins.a
+```
+
+---
+
+## The OSxCAR Test Case
+
+### What We Have
+
+Three WASM modules extracted from https://osxcar.de/insights/demonstration/:
+
+| Component | WASM Size | ARM Code | Functions |
+|-----------|-----------|----------|-----------|
+| anti_pinch_v2 | 13,294 bytes | 8,392 bytes | initialize, step, terminate |
+| motor_driver_v2 | 2,574 bytes | 1,844 bytes | initialize, step, terminate |
+| soft_start_stop | 1,785 bytes | 1,728 bytes | step, terminate |
+
+WIT interface: `aptiv:antipinch/anti-pinch-v2@0.1.0`, `aptiv:antipinch/motor-driver-v2@0.1.0`, `aptiv:antipinch/soft-start-stop@0.1.0`
+
+### Test Script
+
+`tests/integration/fetch_osxcar_wasm.sh` fetches these from the website, compiles each individually through synth (all 3 pass).
+
+### End-to-End Goal
+
+```bash
+# 1. Fetch WASM components
+bash tests/integration/fetch_osxcar_wasm.sh # → /tmp/osxcar_*.wasm
+
+# 2. Fuse with meld (MELD TODO)
+meld fuse /tmp/osxcar_antipinch_0.wasm /tmp/osxcar_motor_driver_0.wasm \
+ /tmp/osxcar_soft_start_0.wasm -o /tmp/fused.wasm --memory shared
+
+# 3. Compile + link with synth (SYNTH READY)
+synth compile /tmp/fused.wasm -o /tmp/firmware.elf \
+ --cortex-m --all-exports --link --builtins libkiln_builtins.a
+
+# 4. Run on Renode (verification)
+renode --execute "machine LoadPlatformDescription @synth_cortex_m.repl; \
+ sysbus LoadELF @/tmp/firmware.elf; start"
+```
+
+---
+
+## Synth's Expectations (Contract)
+
+### From Meld:
+1. Output is a **single core WASM module** (not a component, not multi-memory)
+2. Unresolved imports have human-readable module/name strings
+3. Exports are the merged set of all component exports
+4. Memory is single, shared (OR synth needs to add multi-memory — tell us)
+5. `cabi_realloc` is either exported by the fused module or provided externally
+
+### From Kiln-builtins:
+1. `__meld_dispatch_import(u32) -> u32` — C ABI, AAPCS calling convention
+2. `__meld_get_memory_base() -> *mut u8` — returns `__wasm_memory_start` linker symbol
+3. `cabi_realloc(u32, u32, u32, u32) -> u32` — bump allocator in linear memory
+4. Compiled as `libkiln_builtins.a` for `thumbv7em-none-eabi`
+5. All symbols have `#[unsafe(no_mangle)]` / `extern "C"`
+
+### Linker Symbols Synth Provides:
+- `__wasm_memory_start` / `__wasm_memory_end` — linear memory bounds
+- `__meld_import_table_start` / `__meld_import_table_end` — import metadata
+- All exported WASM functions as global symbols (with Thumb bit set)
+
+### Linker Symbols Synth Expects (undefined):
+- `__meld_dispatch_import` — provided by kiln-builtins.a
+- `__meld_get_memory_base` — provided by kiln-builtins.a
+
+---
+
+## Priority Order
+
+1. **Kiln: Create kiln-builtins stub** (even returning 0 for all imports — enables linking)
+2. **Meld: Test fusing OSxCAR components** (verify single-memory output works)
+3. **Kiln: Implement cabi_realloc** (bump allocator — enables canonical ABI)
+4. **Meld: Add --emit-import-map** (enables intelligent dispatch)
+5. **Kiln: WASI embedded dispatcher** (clocks, stdout — enables output verification)
+6. **All: End-to-end Renode test** (the goal)
+
+## Rivet Artifacts
+
+All requirements are traced in synth's rivet artifacts:
+- `artifacts/kiln-builtins-api.yaml` — KB-001..005 (API requirements)
+- `artifacts/compilation-chain.yaml` — CC-001..008 (pipeline stages)
+- `artifacts/static-linking.yaml` — SL-001..007 (linker requirements)
+- `artifacts/e2e-verification.yaml` — E2E-VER-001..010 (test plan)
+- `artifacts/cross-compilation.yaml` — XC-001..009 (toolchain requirements)
+
+Cross-link with `kiln:` and `meld:` prefixes in rivet externals.
diff --git a/tests/wast/BUILD.bazel b/tests/wast/BUILD.bazel
index e839316..aab4a03 100644
--- a/tests/wast/BUILD.bazel
+++ b/tests/wast/BUILD.bazel
@@ -188,6 +188,26 @@ wast_multi_func_test(
tags = ["control", "loop", "factorial"],
)
+# =============================================================================
+# Example application tests
+# =============================================================================
+
+# --- Calculator example (arithmetic, bitwise, accumulator, comparison) ---
+
+wast_multi_func_test(
+ name = "calculator",
+ wast = "calculator.wast",
+ tags = ["example", "calculator"],
+)
+
+# --- Anti-pinch controller example (jam detection, PWM ramping, safety) ---
+
+wast_multi_func_test(
+ name = "anti_pinch",
+ wast = "anti_pinch.wast",
+ tags = ["example", "anti-pinch", "safety"],
+)
+
# =============================================================================
# Official WebAssembly Spec Tests
# =============================================================================
diff --git a/tests/wast/anti_pinch.wast b/tests/wast/anti_pinch.wast
new file mode 100644
index 0000000..ebafd50
--- /dev/null
+++ b/tests/wast/anti_pinch.wast
@@ -0,0 +1,608 @@
+;; Anti-Pinch Window Controller -- WAST test suite
+;;
+;; Safety-critical automotive use case: prevents window motor from injuring
+;; occupants by detecting jam conditions via motor current monitoring.
+;;
+;; Derived from examples/anti-pinch-controller/anti_pinch.wat
+;;
+;; Note: Each test runs in a fresh machine with zero-initialized memory.
+;; Stateful sequences (init -> set_target -> tick) cannot be tested across
+;; separate assertions. Tests focus on:
+;; - Pure functions: check_jam (parameter-only logic)
+;; - Getter functions: verify zero-initialized state readback
+;; - ramp_pwm: reads ramp_rate from memory[28] (0 when not init'd, snaps to target)
+;; - init + getter combos via acc_add-style single-invocation tests
+
+(module
+ ;; Linear memory: 1 page (64KB) for controller state
+ (memory (export "memory") 1)
+
+ ;; =========================================================================
+ ;; init -- Initialize controller to safe defaults
+ ;; =========================================================================
+ (func (export "init")
+ ;; window_position = 0 (fully open)
+ i32.const 0
+ i32.const 0
+ i32.store
+
+ ;; motor_current_ma = 0
+ i32.const 4
+ i32.const 0
+ i32.store
+
+ ;; pwm_duty = 0
+ i32.const 8
+ i32.const 0
+ i32.store
+
+ ;; direction = 0 (stopped)
+ i32.const 12
+ i32.const 0
+ i32.store
+
+ ;; jam_detected = 0
+ i32.const 16
+ i32.const 0
+ i32.store
+
+ ;; current_threshold_ma = 800 (800mA default threshold)
+ i32.const 20
+ i32.const 800
+ i32.store
+
+ ;; target_position = 0 (fully open)
+ i32.const 24
+ i32.const 0
+ i32.store
+
+ ;; pwm_ramp_rate = 20 (2.0% per tick for soft start/stop)
+ i32.const 28
+ i32.const 20
+ i32.store
+
+ ;; consecutive_over = 0
+ i32.const 32
+ i32.const 0
+ i32.store
+
+ ;; jam_debounce_limit = 3 (3 consecutive ticks to confirm jam)
+ i32.const 36
+ i32.const 3
+ i32.store
+ )
+
+ ;; =========================================================================
+ ;; set_target -- Set desired window position (0=open, 1000=closed)
+ ;; =========================================================================
+ (func (export "set_target") (param $target i32)
+ (local $clamped i32)
+
+ ;; Clamp target to 0-1000
+ local.get $target
+ local.set $clamped
+
+ ;; if target < 0, clamp to 0
+ local.get $clamped
+ i32.const 0
+ i32.lt_s
+ if
+ i32.const 0
+ local.set $clamped
+ end
+
+ ;; if target > 1000, clamp to 1000
+ local.get $clamped
+ i32.const 1000
+ i32.gt_s
+ if
+ i32.const 1000
+ local.set $clamped
+ end
+
+ ;; Store clamped target
+ i32.const 24
+ local.get $clamped
+ i32.store
+
+ ;; Clear jam flag when new target is set (allows retry)
+ i32.const 16
+ i32.const 0
+ i32.store
+
+ ;; Reset debounce counter
+ i32.const 32
+ i32.const 0
+ i32.store
+ )
+
+ ;; =========================================================================
+ ;; update_current -- Feed motor current sensor reading (milliamps)
+ ;; =========================================================================
+ (func (export "update_current") (param $current_ma i32)
+ i32.const 4
+ local.get $current_ma
+ i32.store
+ )
+
+ ;; =========================================================================
+ ;; check_jam -- Check if current exceeds threshold (jam detection)
+ ;; Returns: 1 if jam detected, 0 otherwise
+ ;; =========================================================================
+ (func (export "check_jam") (param $current_ma i32) (param $threshold_ma i32) (result i32)
+ local.get $current_ma
+ local.get $threshold_ma
+ i32.gt_s
+ if (result i32)
+ i32.const 1
+ else
+ i32.const 0
+ end
+ )
+
+ ;; =========================================================================
+ ;; ramp_pwm -- Soft start/stop: ramp current PWM toward target PWM
+ ;; Returns: new PWM value after ramping by one step
+ ;; =========================================================================
+ (func (export "ramp_pwm") (param $current_pwm i32) (param $target_pwm i32) (result i32)
+ (local $ramp_rate i32)
+ (local $diff i32)
+ (local $result i32)
+
+ ;; Load ramp rate from memory
+ i32.const 28
+ i32.load
+ local.set $ramp_rate
+
+ ;; Calculate difference: target - current
+ local.get $target_pwm
+ local.get $current_pwm
+ i32.sub
+ local.set $diff
+
+ ;; If diff > ramp_rate, step up by ramp_rate
+ local.get $diff
+ local.get $ramp_rate
+ i32.gt_s
+ if
+ local.get $current_pwm
+ local.get $ramp_rate
+ i32.add
+ local.set $result
+ else
+ ;; If diff < -ramp_rate, step down by ramp_rate
+ local.get $diff
+ i32.const 0
+ local.get $ramp_rate
+ i32.sub
+ i32.lt_s
+ if
+ local.get $current_pwm
+ local.get $ramp_rate
+ i32.sub
+ local.set $result
+ else
+ ;; Close enough -- snap to target
+ local.get $target_pwm
+ local.set $result
+ end
+ end
+
+ ;; Clamp result to 0-1000
+ local.get $result
+ i32.const 0
+ i32.lt_s
+ if
+ i32.const 0
+ local.set $result
+ end
+ local.get $result
+ i32.const 1000
+ i32.gt_s
+ if
+ i32.const 1000
+ local.set $result
+ end
+
+ local.get $result
+ )
+
+ ;; =========================================================================
+ ;; tick -- Main control cycle (called at 100Hz)
+ ;; Returns: PWM duty cycle (0-1000)
+ ;; =========================================================================
+ (func (export "tick") (result i32)
+ (local $position i32)
+ (local $target i32)
+ (local $current_ma i32)
+ (local $threshold i32)
+ (local $direction i32)
+ (local $pwm i32)
+ (local $target_pwm i32)
+ (local $new_pwm i32)
+ (local $position_delta i32)
+ (local $consecutive i32)
+ (local $debounce_limit i32)
+ (local $ramp_rate i32)
+ (local $diff i32)
+
+ ;; Load state from memory
+ i32.const 0
+ i32.load
+ local.set $position
+
+ i32.const 24
+ i32.load
+ local.set $target
+
+ i32.const 4
+ i32.load
+ local.set $current_ma
+
+ i32.const 20
+ i32.load
+ local.set $threshold
+
+ i32.const 8
+ i32.load
+ local.set $pwm
+
+ i32.const 32
+ i32.load
+ local.set $consecutive
+
+ i32.const 36
+ i32.load
+ local.set $debounce_limit
+
+ ;; Check if jam already flagged -- if so, keep motor stopped
+ i32.const 16
+ i32.load
+ i32.const 1
+ i32.eq
+ if
+ ;; Jam flagged: force PWM to 0, direction to stopped
+ i32.const 8
+ i32.const 0
+ i32.store
+ i32.const 12
+ i32.const 0
+ i32.store
+ i32.const 0
+ return
+ end
+
+ ;; Determine direction: compare position to target
+ local.get $target
+ local.get $position
+ i32.gt_s
+ if
+ ;; target > position: closing (moving toward fully closed)
+ i32.const 1
+ local.set $direction
+ ;; target PWM = 800 (80% duty for normal closing)
+ i32.const 800
+ local.set $target_pwm
+ else
+ local.get $target
+ local.get $position
+ i32.lt_s
+ if
+ ;; target < position: opening (moving toward fully open)
+ i32.const 2
+ local.set $direction
+ ;; target PWM = 800 (80% duty for normal opening)
+ i32.const 800
+ local.set $target_pwm
+ else
+ ;; target == position: stop motor
+ i32.const 0
+ local.set $direction
+ i32.const 0
+ local.set $target_pwm
+ end
+ end
+
+ ;; Store direction
+ i32.const 12
+ local.get $direction
+ i32.store
+
+ ;; --- JAM DETECTION (only while closing, direction==1) ---
+ local.get $direction
+ i32.const 1
+ i32.eq
+ if
+ ;; Check if current exceeds threshold
+ local.get $current_ma
+ local.get $threshold
+ i32.gt_s
+ if
+ ;; Increment consecutive over-threshold counter
+ local.get $consecutive
+ i32.const 1
+ i32.add
+ local.set $consecutive
+
+ ;; Store updated counter
+ i32.const 32
+ local.get $consecutive
+ i32.store
+
+ ;; Check if debounce limit reached
+ local.get $consecutive
+ local.get $debounce_limit
+ i32.ge_s
+ if
+ ;; JAM CONFIRMED: emergency stop
+ ;; Set jam flag
+ i32.const 16
+ i32.const 1
+ i32.store
+ ;; Stop motor
+ i32.const 8
+ i32.const 0
+ i32.store
+ ;; Set direction to stopped
+ i32.const 12
+ i32.const 0
+ i32.store
+ ;; Reset debounce counter
+ i32.const 32
+ i32.const 0
+ i32.store
+ ;; Return 0 PWM immediately
+ i32.const 0
+ return
+ end
+ else
+ ;; Current below threshold: reset debounce counter
+ i32.const 32
+ i32.const 0
+ i32.store
+ i32.const 0
+ local.set $consecutive
+ end
+ end
+
+ ;; --- PWM RAMPING (soft start/stop) ---
+ ;; Ramp current PWM toward target PWM
+ ;; Inline ramp logic (same as ramp_pwm function)
+ i32.const 28
+ i32.load
+ local.set $ramp_rate
+
+ local.get $target_pwm
+ local.get $pwm
+ i32.sub
+ local.set $diff
+
+ local.get $diff
+ local.get $ramp_rate
+ i32.gt_s
+ if
+ local.get $pwm
+ local.get $ramp_rate
+ i32.add
+ local.set $new_pwm
+ else
+ local.get $diff
+ i32.const 0
+ local.get $ramp_rate
+ i32.sub
+ i32.lt_s
+ if
+ local.get $pwm
+ local.get $ramp_rate
+ i32.sub
+ local.set $new_pwm
+ else
+ local.get $target_pwm
+ local.set $new_pwm
+ end
+ end
+
+ ;; Clamp new_pwm to 0-1000
+ local.get $new_pwm
+ i32.const 0
+ i32.lt_s
+ if
+ i32.const 0
+ local.set $new_pwm
+ end
+ local.get $new_pwm
+ i32.const 1000
+ i32.gt_s
+ if
+ i32.const 1000
+ local.set $new_pwm
+ end
+
+ ;; Store new PWM
+ i32.const 8
+ local.get $new_pwm
+ i32.store
+
+ ;; --- POSITION UPDATE ---
+ ;; Estimate position change based on direction and PWM
+ ;; position_delta = new_pwm / 100 (scale: at 100% PWM, ~10 units/tick = 1%/tick)
+ local.get $new_pwm
+ i32.const 100
+ i32.div_s
+ local.set $position_delta
+
+ local.get $direction
+ i32.const 1
+ i32.eq
+ if
+ ;; Closing: position increases
+ local.get $position
+ local.get $position_delta
+ i32.add
+ local.set $position
+ ;; Clamp to target (don't overshoot)
+ local.get $position
+ local.get $target
+ i32.gt_s
+ if
+ local.get $target
+ local.set $position
+ end
+ else
+ local.get $direction
+ i32.const 2
+ i32.eq
+ if
+ ;; Opening: position decreases
+ local.get $position
+ local.get $position_delta
+ i32.sub
+ local.set $position
+ ;; Clamp to target (don't undershoot)
+ local.get $position
+ local.get $target
+ i32.lt_s
+ if
+ local.get $target
+ local.set $position
+ end
+ end
+ end
+
+ ;; Store updated position
+ i32.const 0
+ local.get $position
+ i32.store
+
+ ;; Check if position reached target
+ local.get $position
+ local.get $target
+ i32.eq
+ if
+ ;; Reached target: stop motor
+ i32.const 12
+ i32.const 0
+ i32.store
+ i32.const 8
+ i32.const 0
+ i32.store
+ i32.const 0
+ return
+ end
+
+ ;; Return current PWM duty cycle
+ local.get $new_pwm
+ )
+
+ ;; =========================================================================
+ ;; Getter functions for reading controller state
+ ;; =========================================================================
+
+ (func (export "get_position") (result i32)
+ i32.const 0
+ i32.load
+ )
+
+ (func (export "get_pwm") (result i32)
+ i32.const 8
+ i32.load
+ )
+
+ (func (export "get_direction") (result i32)
+ i32.const 12
+ i32.load
+ )
+
+ (func (export "is_jam_detected") (result i32)
+ i32.const 16
+ i32.load
+ )
+
+ ;; =========================================================================
+ ;; clear_jam -- Reset jam flag for retry
+ ;; =========================================================================
+ (func (export "clear_jam")
+ i32.const 16
+ i32.const 0
+ i32.store
+
+ ;; Reset debounce counter
+ i32.const 32
+ i32.const 0
+ i32.store
+ )
+)
+
+;; =============================================================================
+;; Jam detection tests -- check_jam is a pure function (no memory side effects)
+;; =============================================================================
+
+;; Current exceeds threshold: jam detected
+(assert_return (invoke "check_jam" (i32.const 1200) (i32.const 800)) (i32.const 1))
+
+;; Current well above threshold
+(assert_return (invoke "check_jam" (i32.const 5000) (i32.const 800)) (i32.const 1))
+
+;; Current below threshold: no jam
+(assert_return (invoke "check_jam" (i32.const 500) (i32.const 800)) (i32.const 0))
+
+;; Current exactly at threshold: no jam (gt_s, not ge_s)
+(assert_return (invoke "check_jam" (i32.const 800) (i32.const 800)) (i32.const 0))
+
+;; Current just above threshold
+(assert_return (invoke "check_jam" (i32.const 801) (i32.const 800)) (i32.const 1))
+
+;; Zero current, zero threshold: no jam (0 is not > 0)
+(assert_return (invoke "check_jam" (i32.const 0) (i32.const 0)) (i32.const 0))
+
+;; Zero current, positive threshold: no jam
+(assert_return (invoke "check_jam" (i32.const 0) (i32.const 800)) (i32.const 0))
+
+;; Different threshold values
+(assert_return (invoke "check_jam" (i32.const 400) (i32.const 300)) (i32.const 1))
+(assert_return (invoke "check_jam" (i32.const 100) (i32.const 500)) (i32.const 0))
+
+;; =============================================================================
+;; PWM ramping tests
+;;
+;; ramp_pwm reads ramp_rate from memory[28]. In a fresh machine, memory is
+;; zero-initialized so ramp_rate = 0. With ramp_rate = 0:
+;; diff > 0 is never > 0 (ramp_rate), and diff < -0 is never < 0,
+;; so it always snaps to target_pwm (the else branch).
+;; =============================================================================
+
+;; With ramp_rate=0 (uninit'd memory), ramp_pwm snaps directly to target
+(assert_return (invoke "ramp_pwm" (i32.const 0) (i32.const 100)) (i32.const 100))
+(assert_return (invoke "ramp_pwm" (i32.const 0) (i32.const 500)) (i32.const 500))
+(assert_return (invoke "ramp_pwm" (i32.const 500) (i32.const 0)) (i32.const 0))
+(assert_return (invoke "ramp_pwm" (i32.const 100) (i32.const 100)) (i32.const 100)) ;; already at target
+(assert_return (invoke "ramp_pwm" (i32.const 800) (i32.const 1000)) (i32.const 1000))
+(assert_return (invoke "ramp_pwm" (i32.const 1000) (i32.const 800)) (i32.const 800))
+
+;; Edge: target at clamping boundaries
+(assert_return (invoke "ramp_pwm" (i32.const 0) (i32.const 0)) (i32.const 0))
+(assert_return (invoke "ramp_pwm" (i32.const 0) (i32.const 1000)) (i32.const 1000))
+(assert_return (invoke "ramp_pwm" (i32.const 1000) (i32.const 0)) (i32.const 0))
+
+;; =============================================================================
+;; Getter functions with zero-initialized memory
+;; Each runs in a fresh machine so memory is all zeros
+;; =============================================================================
+(assert_return (invoke "get_position") (i32.const 0))
+(assert_return (invoke "get_pwm") (i32.const 0))
+(assert_return (invoke "get_direction") (i32.const 0))
+(assert_return (invoke "is_jam_detected") (i32.const 0))
+
+;; =============================================================================
+;; Tick with zero-initialized memory
+;;
+;; With all memory = 0:
+;; position=0, target=0, current_ma=0, threshold=0, pwm=0
+;; jam_detected=0 (no early return)
+;; target(0) == position(0) -> direction=0, target_pwm=0
+;; ramp_rate=0, diff=0-0=0, not > 0, not < -0 -> snap: new_pwm=0
+;; position_delta = 0/100 = 0
+;; direction=0: no position update
+;; position(0) == target(0) -> reached target, return 0
+;; =============================================================================
+(assert_return (invoke "tick") (i32.const 0))
diff --git a/tests/wast/calculator.wast b/tests/wast/calculator.wast
new file mode 100644
index 0000000..54b9ae1
--- /dev/null
+++ b/tests/wast/calculator.wast
@@ -0,0 +1,256 @@
+;; Calculator -- comprehensive WAST test suite
+;;
+;; Exercises the calculator example's i32 arithmetic, bitwise ops,
+;; accumulator (memory load/store), and comparison/selection logic.
+;; Derived from examples/zephyr-calculator/calculator.wat
+
+(module
+ ;; One page of linear memory for accumulator state
+ (memory (export "memory") 1)
+
+ ;; ---------------------------------------------------------------
+ ;; Basic arithmetic
+ ;; ---------------------------------------------------------------
+
+ ;; add(a, b) -> a + b
+ (func (export "add") (param $a i32) (param $b i32) (result i32)
+ local.get $a
+ local.get $b
+ i32.add
+ )
+
+ ;; sub(a, b) -> a - b
+ (func (export "sub") (param $a i32) (param $b i32) (result i32)
+ local.get $a
+ local.get $b
+ i32.sub
+ )
+
+ ;; mul(a, b) -> a * b
+ (func (export "mul") (param $a i32) (param $b i32) (result i32)
+ local.get $a
+ local.get $b
+ i32.mul
+ )
+
+ ;; div_safe(a, b) -> a / b, returns 0 when b == 0
+ (func (export "div_safe") (param $a i32) (param $b i32) (result i32)
+ (if (result i32) (i32.eqz (local.get $b))
+ (then (i32.const 0))
+ (else (i32.div_s (local.get $a) (local.get $b)))
+ )
+ )
+
+ ;; ---------------------------------------------------------------
+ ;; Accumulator operations (use linear memory at offset 0)
+ ;; ---------------------------------------------------------------
+
+ ;; acc_set(v) -- store v to memory[0..3]
+ (func (export "acc_set") (param $v i32)
+ i32.const 0
+ local.get $v
+ i32.store
+ )
+
+ ;; acc_get() -> value at memory[0..3]
+ (func (export "acc_get") (result i32)
+ i32.const 0
+ i32.load
+ )
+
+ ;; acc_add(v) -> acc += v, return new accumulator value
+ (func (export "acc_add") (param $v i32) (result i32)
+ (local $new i32)
+ ;; new = memory[0] + v
+ i32.const 0
+ i32.load
+ local.get $v
+ i32.add
+ local.set $new
+ ;; memory[0] = new
+ i32.const 0
+ local.get $new
+ i32.store
+ ;; return new
+ local.get $new
+ )
+
+ ;; ---------------------------------------------------------------
+ ;; Bitwise operations
+ ;; ---------------------------------------------------------------
+
+ ;; bit_and(a, b) -> a & b
+ (func (export "bit_and") (param $a i32) (param $b i32) (result i32)
+ local.get $a
+ local.get $b
+ i32.and
+ )
+
+ ;; bit_or(a, b) -> a | b
+ (func (export "bit_or") (param $a i32) (param $b i32) (result i32)
+ local.get $a
+ local.get $b
+ i32.or
+ )
+
+ ;; bit_xor(a, b) -> a ^ b
+ (func (export "bit_xor") (param $a i32) (param $b i32) (result i32)
+ local.get $a
+ local.get $b
+ i32.xor
+ )
+
+ ;; bit_not(a) -> ~a (implemented as a XOR 0xFFFFFFFF)
+ (func (export "bit_not") (param $a i32) (result i32)
+ local.get $a
+ i32.const -1
+ i32.xor
+ )
+
+ ;; ---------------------------------------------------------------
+ ;; Comparison / selection
+ ;; ---------------------------------------------------------------
+
+ ;; max(a, b) -> the larger value (signed)
+ (func (export "max") (param $a i32) (param $b i32) (result i32)
+ (if (result i32) (i32.gt_s (local.get $a) (local.get $b))
+ (then (local.get $a))
+ (else (local.get $b))
+ )
+ )
+
+ ;; min(a, b) -> the smaller value (signed)
+ (func (export "min") (param $a i32) (param $b i32) (result i32)
+ (if (result i32) (i32.lt_s (local.get $a) (local.get $b))
+ (then (local.get $a))
+ (else (local.get $b))
+ )
+ )
+
+ ;; abs(x) -> |x|
+ (func (export "abs") (param $x i32) (result i32)
+ (if (result i32) (i32.lt_s (local.get $x) (i32.const 0))
+ (then (i32.sub (i32.const 0) (local.get $x)))
+ (else (local.get $x))
+ )
+ )
+)
+
+;; =============================================================================
+;; Addition tests
+;; =============================================================================
+(assert_return (invoke "add" (i32.const 5) (i32.const 3)) (i32.const 8))
+(assert_return (invoke "add" (i32.const 0) (i32.const 0)) (i32.const 0))
+(assert_return (invoke "add" (i32.const -1) (i32.const 1)) (i32.const 0))
+(assert_return (invoke "add" (i32.const 100) (i32.const 200)) (i32.const 300))
+(assert_return (invoke "add" (i32.const -5) (i32.const -3)) (i32.const -8))
+(assert_return (invoke "add" (i32.const 0x7FFFFFFF) (i32.const 1)) (i32.const 0x80000000)) ;; overflow wraps
+
+;; =============================================================================
+;; Subtraction tests
+;; =============================================================================
+(assert_return (invoke "sub" (i32.const 10) (i32.const 3)) (i32.const 7))
+(assert_return (invoke "sub" (i32.const 0) (i32.const 0)) (i32.const 0))
+(assert_return (invoke "sub" (i32.const 5) (i32.const 10)) (i32.const -5))
+(assert_return (invoke "sub" (i32.const -1) (i32.const -1)) (i32.const 0))
+(assert_return (invoke "sub" (i32.const 1000) (i32.const 1)) (i32.const 999))
+
+;; =============================================================================
+;; Multiplication tests
+;; =============================================================================
+(assert_return (invoke "mul" (i32.const 6) (i32.const 7)) (i32.const 42))
+(assert_return (invoke "mul" (i32.const 0) (i32.const 100)) (i32.const 0))
+(assert_return (invoke "mul" (i32.const -2) (i32.const 3)) (i32.const -6))
+(assert_return (invoke "mul" (i32.const -2) (i32.const -3)) (i32.const 6))
+(assert_return (invoke "mul" (i32.const 1) (i32.const 42)) (i32.const 42))
+(assert_return (invoke "mul" (i32.const 100) (i32.const 100)) (i32.const 10000))
+
+;; =============================================================================
+;; Safe division tests (returns 0 on divide-by-zero)
+;; =============================================================================
+(assert_return (invoke "div_safe" (i32.const 10) (i32.const 3)) (i32.const 3))
+(assert_return (invoke "div_safe" (i32.const 10) (i32.const 0)) (i32.const 0)) ;; safe div by zero
+(assert_return (invoke "div_safe" (i32.const 0) (i32.const 5)) (i32.const 0))
+(assert_return (invoke "div_safe" (i32.const 100) (i32.const 10)) (i32.const 10))
+(assert_return (invoke "div_safe" (i32.const -10) (i32.const 3)) (i32.const -3)) ;; signed division
+(assert_return (invoke "div_safe" (i32.const 7) (i32.const 2)) (i32.const 3)) ;; truncation
+(assert_return (invoke "div_safe" (i32.const -7) (i32.const 2)) (i32.const -3)) ;; signed truncation
+(assert_return (invoke "div_safe" (i32.const 1) (i32.const 1)) (i32.const 1))
+
+;; =============================================================================
+;; Accumulator tests
+;; Memory starts at zero, each test runs in a fresh machine so acc starts at 0
+;; =============================================================================
+
+;; acc_get reads memory[0] which starts at 0
+(assert_return (invoke "acc_get") (i32.const 0))
+
+;; acc_add adds to memory[0] (starts at 0): 0 + 42 = 42
+(assert_return (invoke "acc_add" (i32.const 42)) (i32.const 42))
+
+;; acc_add with 0: 0 + 0 = 0
+(assert_return (invoke "acc_add" (i32.const 0)) (i32.const 0))
+
+;; acc_add with negative: 0 + (-10) = -10
+(assert_return (invoke "acc_add" (i32.const -10)) (i32.const -10))
+
+;; =============================================================================
+;; Bitwise AND tests
+;; =============================================================================
+(assert_return (invoke "bit_and" (i32.const 0xFF) (i32.const 0x0F)) (i32.const 0x0F))
+(assert_return (invoke "bit_and" (i32.const 0x12345678) (i32.const 0x0000FFFF)) (i32.const 0x00005678))
+(assert_return (invoke "bit_and" (i32.const 0) (i32.const 0xFFFFFFFF)) (i32.const 0))
+(assert_return (invoke "bit_and" (i32.const -1) (i32.const -1)) (i32.const -1))
+
+;; =============================================================================
+;; Bitwise OR tests
+;; =============================================================================
+(assert_return (invoke "bit_or" (i32.const 0xF0) (i32.const 0x0F)) (i32.const 0xFF))
+(assert_return (invoke "bit_or" (i32.const 0) (i32.const 0)) (i32.const 0))
+(assert_return (invoke "bit_or" (i32.const 0xAAAA0000) (i32.const 0x0000BBBB)) (i32.const 0xAAAABBBB))
+
+;; =============================================================================
+;; Bitwise XOR tests
+;; =============================================================================
+(assert_return (invoke "bit_xor" (i32.const 0xFF) (i32.const 0xFF)) (i32.const 0))
+(assert_return (invoke "bit_xor" (i32.const 0xAAAA) (i32.const 0x5555)) (i32.const 0xFFFF))
+(assert_return (invoke "bit_xor" (i32.const 0) (i32.const 0)) (i32.const 0))
+(assert_return (invoke "bit_xor" (i32.const 123) (i32.const 0)) (i32.const 123))
+
+;; =============================================================================
+;; Bitwise NOT tests (~a via XOR -1)
+;; =============================================================================
+(assert_return (invoke "bit_not" (i32.const 0)) (i32.const -1))
+(assert_return (invoke "bit_not" (i32.const -1)) (i32.const 0))
+(assert_return (invoke "bit_not" (i32.const 0x0000FFFF)) (i32.const 0xFFFF0000))
+(assert_return (invoke "bit_not" (i32.const 1)) (i32.const -2))
+
+;; =============================================================================
+;; Max tests (signed comparison)
+;; =============================================================================
+(assert_return (invoke "max" (i32.const 5) (i32.const 10)) (i32.const 10))
+(assert_return (invoke "max" (i32.const 10) (i32.const 5)) (i32.const 10))
+(assert_return (invoke "max" (i32.const 0) (i32.const 0)) (i32.const 0))
+(assert_return (invoke "max" (i32.const -5) (i32.const 5)) (i32.const 5))
+(assert_return (invoke "max" (i32.const -1) (i32.const -10)) (i32.const -1))
+(assert_return (invoke "max" (i32.const 7) (i32.const 7)) (i32.const 7)) ;; equal values
+
+;; =============================================================================
+;; Min tests (signed comparison)
+;; =============================================================================
+(assert_return (invoke "min" (i32.const 5) (i32.const 10)) (i32.const 5))
+(assert_return (invoke "min" (i32.const 10) (i32.const 5)) (i32.const 5))
+(assert_return (invoke "min" (i32.const 0) (i32.const 0)) (i32.const 0))
+(assert_return (invoke "min" (i32.const -5) (i32.const 5)) (i32.const -5))
+(assert_return (invoke "min" (i32.const -1) (i32.const -10)) (i32.const -10))
+(assert_return (invoke "min" (i32.const 7) (i32.const 7)) (i32.const 7)) ;; equal values
+
+;; =============================================================================
+;; Abs tests
+;; =============================================================================
+(assert_return (invoke "abs" (i32.const -5)) (i32.const 5))
+(assert_return (invoke "abs" (i32.const 5)) (i32.const 5))
+(assert_return (invoke "abs" (i32.const 0)) (i32.const 0))
+(assert_return (invoke "abs" (i32.const -1)) (i32.const 1))
+(assert_return (invoke "abs" (i32.const -100)) (i32.const 100))
+(assert_return (invoke "abs" (i32.const 1)) (i32.const 1))