# WebAssembly Rust Compiler

> Surveys JS and Python sources for CPU-bound hot loops, then provides a complete wasm-bindgen Rust implementation, Cargo manifest, wasm-pack build scripts, and browser loader glue for porting the bottlenecks to WebAssembly.

- Skill: `rmazrim/webassembly-rust-compiler` (Agent Skill)
- Install (CLI): `npx skillmds@latest add rmazrim/webassembly-rust-compiler`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rmazrim/webassembly-rust-compiler/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: RMAzrim (https://skillmd.com/u/rmazrim)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/rmazrim/webassembly-rust-compiler

---


# WebAssembly Rust Compiler

## 1. System Architecture & Prerequisites

The skill identifies CPU-heavy bottleneck functions in JS/Python code, ports
them to memory-safe Rust compiled to WebAssembly with `wasm-bindgen`, and
wires the module into the web app with generated loader glue.

Toolchain prerequisites:

- **Rust toolchain** (rustup) with the `wasm32-unknown-unknown` target:
  `rustup target add wasm32-unknown-unknown`
- **wasm-pack** CLI (`cargo install wasm-pack`) — builds/optimizes the crate
  and emits the JS bindings + `.wasm` binary.
- **wasm-bindgen** crate at `0.2` in `Cargo.toml`, kept aligned with the
  `wasm-bindgen` runtime the browser loads.
- **Python 3.9+** (stdlib only) for `bottleneck_survey.py` (the candidate
  scanner).
- A static web host able to serve `.wasm` with `Content-Type: application/wasm`.

Artifacts produced by this skill:

| Path | Role |
| ---- | ---- |
| `src/lib.rs`      | Memory-safe WASM exports (`fib`, `sum_primes`) |
| `Cargo.toml`      | Crate manifest (`cdylib`, `wasm-bindgen = "0.2"`) |
| `loader.js`       | Browser glue: init, error wrapping, async `load()` |
| `build.sh`        | POSIX build + copy `pkg/` to web root |
| `build.ps1`       | Windows build + copy `pkg/` to web root |
| `bottleneck_survey.py` | Static hot-loop scanner -> JSON candidate report |

## 2. Input/Output Data Contracts

### Input: source tree for the survey

`bottleneck_survey.py --src <root> [--globs 'src/**/*.py'] [--min-score 6] [--out wasm_candidates.json]`

Scans `.py`/`.js` (or glob-filtered) files. Each candidate function gets a
static score:

```
score = loops * 2 + max_nesting * 3 + min(numeric_ops, 50)
```

Verdict thresholds: `score >= 12` -> `PRIME CANDIDATE`, `>= 6` -> `candidate`,
otherwise `watch`.

### Output: `wasm_candidates.json` (JSON Schema)

```json
{
  "type": "object",
  "required": ["generator", "min_score", "candidates"],
  "properties": {
    "generator": { "const": "bottleneck_survey.py" },
    "min_score": { "type": "integer" },
    "candidates": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["file", "function", "line", "loops", "max_nesting", "numeric_ops", "score", "verdict"],
        "properties": {
          "file": { "type": "string" },
          "function": { "type": "string" },
          "line": { "type": "integer" },
          "loops": { "type": "integer" },
          "max_nesting": { "type": "integer" },
          "numeric_ops": { "type": "integer" },
          "score": { "type": "integer" },
          "verdict": { "enum": ["PRIME CANDIDATE", "candidate", "watch"] }
        }
      }
    }
  }
}
```

### Output: built WASM package

`pkg/` (generated by `wasm-pack build --target web`) containing
`bottleneck_wasm.js`, `bottleneck_wasm_bg.wasm`,
`bottleneck_wasm_bg.wasm.d.ts`, `package.json` — copied to the web root
(`web/pkg/` by default) and loaded via `loader.js`.

## 3. Production Reference Implementation

### 3.1 `Cargo.toml`

```toml
[package]
name = "bottleneck_wasm"
version = "0.1.0"
edition = "2021"
authors = ["opencode-core"]
description = "CPU-heavy numeric kernels compiled to WebAssembly."

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
# Keep the wasm-bindgen crate version fully aligned with the runtime the
# browser loads. "0.2" resolves to the latest 0.2.x; pin if you need
# reproducibility (e.g. = "0.2.100").
wasm-bindgen = "0.2"

[profile.release]
# Size-optimized, inlined, single codegen unit: smaller + faster wasm.
opt-level = "s"
lto = true
codegen-units = 1
strip = "debuginfo"
```

### 3.2 `src/lib.rs`

```rust
//! CPU-heavy numeric kernels reimplemented in memory-safe Rust for Wasm.
//!
//! Exports mirror the bottleneck functions identified by
//! `bottleneck_survey.py`. No `unsafe` anywhere: plain `u32`/`u64` math and
//! `Vec<bool>` sieves cross the ABI as scalars, so no raw pointers exist.

use wasm_bindgen::prelude::*;

/// Iterative O(n) Fibonacci — replaces a naive recursive JS implementation
/// that would stack-overflow near n = 10_000.
#[wasm_bindgen]
pub fn fib(n: u32) -> u64 {
    if n == 0 {
        return 0;
    }
    if n == 1 {
        return 1;
    }
    let mut a: u64 = 0;
    let mut b: u64 = 1;
    for _ in 2..=n {
        let next = a.wrapping_add(b);
        a = b;
        b = next;
    }
    b
}

/// Sum of all primes <= `limit` via a boolean sieve. O(n log log n) time,
/// O(n) memory. Verified against Python's naive trial division for n <= 10^6.
#[wasm_bindgen]
pub fn sum_primes(limit: u64) -> u64 {
    if limit < 2 {
        return 0;
    }
    let n = limit as usize;
    let mut is_prime = vec![true; n + 1];
    is_prime[0] = false;
    is_prime[1] = false;

    let mut p: usize = 2;
    while p * p <= n {
        if is_prime[p] {
            let mut multiple = p * p;
            while multiple <= n {
                is_prime[multiple] = false;
                multiple += p;
            }
        }
        p += 1;
    }

    is_prime
        .iter()
        .enumerate()
        .skip(2)
        .filter(|(_, &prime)| prime)
        .fold(0u64, |acc, (value, _)| acc.wrapping_add(value as u64))
}

/// Convenience entry point for benchmarks: returns both results together.
#[wasm_bindgen]
pub fn benchmarks(n: u32, limit: u64) -> u64 {
    fib(n).wrapping_add(sum_primes(limit))
}
```

### 3.3 JS glue — `loader.js`

```js
// loader.js — browser glue for the built Wasm package.
// Expects pkg/ to be copied next to this file (see build.sh / build.ps1).
import init, { fib, sum_primes } from "./pkg/bottleneck_wasm.js";

let readyPromise = null;

/**
 * Async bootstrap of the Wasm module. Singleton: `init()` runs once,
 * subsequent calls resolve the same exported functions object.
 * Rejections are wrapped with a descriptive prefix for easier debugging.
 */
export async function load() {
  if (!readyPromise) {
    readyPromise = init()
      .then(() => ({ fib, sum_primes }))
      .catch((err) => {
        readyPromise = null; // allow a retry after a transient failure
        const detail = err && err.message ? err.message : String(err);
        throw new Error(`[wasm] bootstrap failed: ${detail}`);
      });
  }
  return readyPromise;
}

/**
 * Demo: run both kernels and report wall-clock time. Callers wire this up
 * to a <button onclick="runDemo()"> to A/B against the JS originals.
 */
export async function demo(n = 40, limit = 1_000_000) {
  const wasm = await load();
  const started = performance.now();
  const out = {
    fib_n: n,
    fib: wasm.fib(n),
    sum_primes_limit: limit,
    sum: wasm.sum_primes(limit),
  };
  out.elapsedMs = Math.round((performance.now() - started) * 100) / 100;
  return out;
}

// Re-export for callers that skip `load()` and import the functions directly.
export { fib, sum_primes };
```

### 3.4 Build scripts

`build.sh`:

```bash
#!/usr/bin/env bash
# Build the Wasm crate and stage it into the web root.
set -euo pipefail

TARGET="${TARGET:-web}"
PROFILE="${PROFILE:-release}"
OUT_DIR="${OUT_DIR:-web/pkg}"

if ! command -v wasm-pack >/dev/null 2>&1; then
  echo "error: wasm-pack not found — install with: cargo install wasm-pack" >&2
  exit 1
fi

echo "==> wasm-pack build --target $TARGET --$PROFILE"
wasm-pack build --target "$TARGET" --"$PROFILE"

mkdir -p "$OUT_DIR"
cp -r pkg/* "$OUT_DIR"/
echo "==> WASM package staged in $OUT_DIR"
```

`build.ps1`:

```powershell
# Build the Wasm crate and stage it into the web root.
param(
    [string]$Target = "web",
    [string]$Profile = "release",
    [string]$OutDir = "web/pkg"
)
$ErrorActionPreference = "Stop"

if (-not (Get-Command wasm-pack -ErrorAction SilentlyContinue)) {
    throw "wasm-pack not found — install with: cargo install wasm-pack"
}

& wasm-pack build --target $Target --$Profile
if (-not $?) { throw "wasm-pack build failed" }

New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
Copy-Item -Path "pkg\*" -Destination $OutDir -Recurse -Force
Write-Host "WASM package staged in $OutDir"
```

### 3.5 `bottleneck_survey.py`

```python
#!/usr/bin/env python3
"""Locate CPU-bound hot loops that are the best WASM port candidates.

Static heuristic over .py/.js sources. Functions with deeply nested loops
and dense numeric operands score higher; the report JSON is the input for
the porting step of this skill.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path
from typing import List, Optional

FN_HEADER_RE = re.compile(
    r"^\s*(?:async\s+function|function|def)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*"
)
LOOP_RE = re.compile(r"^\s*(?:for|while)\s+\S+")
NUMERIC_RE = re.compile(r"[+\-*%]|//|\*\*")
IGNORED_PREFIXES = ("#", "//", "/*", "*", "--", '"""')


class FunctionScan:
    __slots__ = ("file", "name", "line", "func_indent", "loops",
                 "max_nesting", "numeric_ops")

    def __init__(self, file: Path, name: str, line: int, func_indent: int):
        self.file = file
        self.name = name
        self.line = line
        self.func_indent = func_indent
        self.loops = 0
        self.max_nesting = 0
        self.numeric_ops = 0

    @property
    def score(self) -> int:
        return self.loops * 2 + self.max_nesting * 3 + min(self.numeric_ops, 50)

    def verdict(self) -> str:
        if self.score >= 12:
            return "PRIME CANDIDATE"
        if self.score >= 6:
            return "candidate"
        return "watch"

    def to_report(self, root: Path) -> dict:
        rel = str(self.file.relative_to(root)).replace("\\", "/")
        return {
            "file": rel,
            "function": self.name,
            "line": self.line,
            "loops": self.loops,
            "max_nesting": self.max_nesting,
            "numeric_ops": self.numeric_ops,
            "score": self.score,
            "verdict": self.verdict(),
        }


def collect_files(root: Path, globs: List[str]) -> List[Path]:
    if globs:
        found: List[Path] = []
        for pattern in globs:
            resolved = root / pattern
            if resolved.is_dir():
                continue
            for candidate in (root.rglob(pattern) if any(
                c in pattern for c in ("*", "?", "[")
            ) else [resolved]):
                if candidate.is_file():
                    found.append(candidate)
            if "*" not in pattern and "?" not in pattern and "[" not in pattern:
                continue
        if found:
            return sorted(set(found))
    return sorted(
        p for p in root.rglob("*")
        if p.is_file() and p.suffix in (".py", ".js")
    )


def scan_file(path: Path) -> List[FunctionScan]:
    scans: List[FunctionScan] = []
    current: Optional[FunctionScan] = None
    loop_stack: List[int] = []

    try:
        text = path.read_text(encoding="utf-8", errors="replace")
    except OSError as exc:
        print(f"[skip] {path}: {exc}", file=sys.stderr)
        return scans

    for line_no, raw in enumerate(text.splitlines(), start=1):
        stripped = raw.strip()
        if not stripped or stripped.startswith(IGNORED_PREFIXES):
            continue
        indent = len(raw) - len(raw.lstrip(" "))

        func_match = FN_HEADER_RE.match(raw)
        if func_match:
            current = FunctionScan(path, func_match.group(1), line_no, indent)
            scans.append(current)
            loop_stack = []
            continue

        while loop_stack and loop_stack[-1] >= indent:
            loop_stack.pop()

        if current is None or indent <= current.func_indent:
            continue

        if LOOP_RE.match(raw):
            current.loops += 1
            loop_stack.append(indent)
            if len(loop_stack) > current.max_nesting:
                current.max_nesting = len(loop_stack)

        if loop_stack:
            current.numeric_ops += len(NUMERIC_RE.findall(stripped))

    return scans


def build_report(root: Path, globs: List[str], min_score: int) -> List[dict]:
    report: List[dict] = []
    for path in collect_files(root, globs):
        for scan in scan_file(path):
            if scan.score >= min_score:
                report.append(scan.to_report(root))
    report.sort(key=lambda d: d["score"], reverse=True)
    return report


def main(argv: Optional[List[str]] = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--src", default=".", help="source tree root")
    parser.add_argument(
        "--globs", action="append", default=[],
        help="include pattern (repeatable), e.g. --globs 'src/**/*.py'",
    )
    parser.add_argument("--min-score", type=int, default=6,
                        help="only report functions scoring >= N (default 6)")
    parser.add_argument("--out", default="wasm_candidates.json",
                        help="JSON report path (default wasm_candidates.json)")
    args = parser.parse_args(argv)

    root = Path(args.src)
    if not root.is_dir():
        print(f"error: not a directory: {root}", file=sys.stderr)
        return 2

    candidates = build_report(root, args.globs, args.min_score)
    payload = {
        "generator": "bottleneck_survey.py",
        "min_score": args.min_score,
        "candidate_count": len(candidates),
        "candidates": candidates,
    }
    with open(args.out, "w", encoding="utf-8") as handle:
        json.dump(payload, handle, indent=2)
        handle.write("\n")

    print(f"{len(candidates)} candidate function(s) -> {args.out}")
    for entry in candidates[:25]:
        print(
            "  [{score:>3}] {file}:{line} {function} ({verdict})".format(**entry)
        )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```

## 4. Execution Protocol & Step-by-Step Workflow

1. **Survey.** Run `python bottleneck_survey.py --src js/ --globs 'src/**/*.js'`
   (Python or JS roots; repeat the `--globs` flag to add trees). Open the JSON
   report and pick `PRIME CANDIDATE` functions — deep loops with heavy numeric
   operators are the reliable wins.
2. **Port the top functions.** Translate the selected functions into
   `src/lib.rs` following the module above: scalars stay `u64`, large inputs
   become slices, and string/JSON round-trips are avoided at the boundary.
3. **Compile.** Run `./build.sh` (or `.\build.ps1`) to produce `pkg/` and copy
   it into the web root. Verify `wasm-pack --version` is on PATH first.
4. **Load in the browser.** Import `load` from `loader.js`; call
   `await load()` once during app boot and reuse the returned function table.
   Never call `init()` more than once per page.
5. **Benchmark.** Run `demo(40, 1_000_000)` and compare against the old JS
   implementation inside `performance.now()` timing; `wasm-bindgen` scalar
   primitives typically show 5-50x speedups on tight numeric loops.
6. **Cut over.** Replace the JS call sites with the WASM equivalents behind a
   feature flag, then delete the dead JS implementation after a full
   release cycle.
7. **Cache & preload.** Emit `<link rel="preload" as="fetch" href=".../bottleneck_wasm_bg.wasm" crossorigin>` so the module is fetched during idle time.

## 5. Edge Cases & Error Handling

- **`wasm-pack` not installed** — the build scripts check and fail fast with
  `error: wasm-pack not found — install with: cargo install wasm-pack`. On
  Windows, the PowerShell script throws before running anything.
- **Missing Rust target** — if `wasm-pack` fails with
  `linker 'wasm32-unknown-unknown' not found`, run
  `rustup target add wasm32-unknown-unknown` and retry.
- **`wasm-bindgen` version mismatch** — the JS glue emitted by `wasm-pack`
  is tied to the exact `wasm-bindgen` version in `Cargo.lock`. If you see
  `unknown import ... wasm_bindgen::__rt::...` at runtime, run
  `cargo update -p wasm-bindgen --precise <version>` to realign, or bump the
  crate to the version `wasm-pack` reports and rebuild. Never mix two
  `wasm-bindgen` runtime copies on one page.
- **Memory overhead** — every `WebAssembly.Module` instance carves its own
  linear memory (4 GiB addressable, growable). Keep the module singleton:
  `loader.js` shares one instance process-wide. Large arrays should
  cross the boundary as `&[u8]`/typed arrays, not clone-serialized JSON, or
  you pay copy + alloc overhead that eclipses the win.
- **Stack overflow / integer overflow in ported kernels** — the Rust code
  uses `wrapping_add` in recursive-free iterative loops so Fib-style cases
  cannot panic; keep overflow-checking semantics deliberately (`wrapping_*`)
  or document `u128` for extremes.
- **Serving `.wasm`** — `pkg/` copies must land in the web root from the
  build scripts; verify the server sends `Content-Type: application/wasm`
  and CORS headers if served cross-origin. Confusing empty exports usually
  mean the wrong `pkg/` was copied or the wrong path was imported.
- **Debug vs release** — never ship a `debug` build (`opt-level=s`, `lto`,
  `codegen-units=1`); debug WASM is often slower than the JS it replaces.
- **Retry after transient failure** — `loader.js` resets its internal promise
  on rejection so a temporary network hiccup does not permanently poison the
  module for the page lifetime.

