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-unknowntarget:rustup target add wasm32-unknown-unknown - wasm-pack CLI (
cargo install wasm-pack) — builds/optimizes the crate and emits the JS bindings +.wasmbinary. - wasm-bindgen crate at
0.2inCargo.toml, kept aligned with thewasm-bindgenruntime the browser loads. - Python 3.9+ (stdlib only) for
bottleneck_survey.py(the candidate scanner). - A static web host able to serve
.wasmwithContent-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)
{
"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
[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
//! 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
// 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 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:
#!/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:
# 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
#!/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
- Survey. Run
python bottleneck_survey.py --src js/ --globs 'src/**/*.js'(Python or JS roots; repeat the--globsflag to add trees). Open the JSON report and pickPRIME CANDIDATEfunctions — deep loops with heavy numeric operators are the reliable wins. - Port the top functions. Translate the selected functions into
src/lib.rsfollowing the module above: scalars stayu64, large inputs become slices, and string/JSON round-trips are avoided at the boundary. - Compile. Run
./build.sh(or.\build.ps1) to producepkg/and copy it into the web root. Verifywasm-pack --versionis on PATH first. - Load in the browser. Import
loadfromloader.js; callawait load()once during app boot and reuse the returned function table. Never callinit()more than once per page. - Benchmark. Run
demo(40, 1_000_000)and compare against the old JS implementation insideperformance.now()timing;wasm-bindgenscalar primitives typically show 5-50x speedups on tight numeric loops. - 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.
- 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-packnot installed — the build scripts check and fail fast witherror: wasm-pack not found — install with: cargo install wasm-pack. On Windows, the PowerShell script throws before running anything.- Missing Rust target — if
wasm-packfails withlinker 'wasm32-unknown-unknown' not found, runrustup target add wasm32-unknown-unknownand retry. wasm-bindgenversion mismatch — the JS glue emitted bywasm-packis tied to the exactwasm-bindgenversion inCargo.lock. If you seeunknown import ... wasm_bindgen::__rt::...at runtime, runcargo update -p wasm-bindgen --precise <version>to realign, or bump the crate to the versionwasm-packreports and rebuild. Never mix twowasm-bindgenruntime copies on one page.- Memory overhead — every
WebAssembly.Moduleinstance carves its own linear memory (4 GiB addressable, growable). Keep the module singleton:loader.jsshares 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_addin recursive-free iterative loops so Fib-style cases cannot panic; keep overflow-checking semantics deliberately (wrapping_*) or documentu128for extremes. - Serving
.wasm—pkg/copies must land in the web root from the build scripts; verify the server sendsContent-Type: application/wasmand CORS headers if served cross-origin. Confusing empty exports usually mean the wrongpkg/was copied or the wrong path was imported. - Debug vs release — never ship a
debugbuild (opt-level=s,lto,codegen-units=1); debug WASM is often slower than the JS it replaces. - Retry after transient failure —
loader.jsresets its internal promise on rejection so a temporary network hiccup does not permanently poison the module for the page lifetime.