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.
Installs into .claude/skills of the current project.
Are you the author of Wasm Rust Compiler?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/rmazrim-wasm-rust-compiler)
---
name: WebAssembly Rust Compiler
description: 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.
metadata:
source: skills/wasm-rust-compiler/wasm-rust-compiler.md
---
# 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.