--- id: SMH-RESEARCH-WASM0001 type: research title: "Embedded language: wasm components" --- # Embedded language: wasm components ## Question Replace the planned mlua + LuaJIT plugin runtime with wasmtime + WIT components? Requirements: - perf - type safety - tooling - ecosystem reuse - sandbox (RULES: plugins run sandboxed) - determinism (MISSION: CBOR sessions, replay) ## Candidate matrix Scores 0-3, judgement, not measurement. | candidate | perf | types | tooling | reuse | sandbox | determinism | migration cost | | --- | --- | --- | --- | --- | --- | --- | --- | | LuaJIT + LuaLS | 2 | 1 | 1 | 0 | 1 | 2 | 0 | | Luau via mlua | 2 | 2 | 2 | 0 | 3 | 3 | 1 | | wasm components | 2.5 | 3 | 3 | 2 | 3 | 3 | 3 | | native Rust dylibs | 3 | 3 | 3 | 3 | 0 | 3 | 2 | | V8 / TS | 2 | 2 | 3 | 2 | 2 | 1 | 3 | Decision (human): wasm components, Rust guests v1. ## Current state in repo - mlua + mlua-pkg removed from root Cargo.toml (2026 wasm decision). - Zero `.rs` files ever referenced mlua or lua. - Migration cost was dep swap + law text. No code migration. - Migration cost = dep swap + law text. No code migration. ## Runtime: wasmtime vs wasmi Facts: - wasmtime: Cranelift JIT, component model support, WIT bindgen host API in `wasmtime::component`. - wasmi: pure interpreter, 1.0 stable 2025-12, wasmtime-compatible API direction. - Interpreter vs JIT gap: JIT ~4x faster on ARM, ~8x on x86 (wasmi benchmark blog, 2024; wasmi 1.0 thread, 2025). - Both pure Rust → build on aarch64 termux without luajit cross-compile pain. - wasmi lacks full component model support → guest WIT bindings story weaker. Candidates: - wasmtime: primary. Components + determinism knobs + resource limiters. - wasmi: fallback only if binary size or build time becomes blocking; loses component model. To measure on target device (aarch64 termux): - release build time delta, mlua/luajit out vs wasmtime in - binary size delta - layout callback batch latency ## Determinism knobs (wasmtime) From wasmtime docs (Deterministic Wasm Execution): - All imports must be deterministic → smith host API only, no ambient WASI. - `Config::cranelift_nan_canonicalization` → canonical NaN. - `Config::relaxed_simd_deterministic` (slower) or disable relaxed SIMD. - Memory/table growth nondeterminism → `Store::limiter` rejects growth, or require max == min at validation via wasmparser. - Interruption: fuel = deterministic, epoch = non-deterministic → use fuel. - WASI clocks/fs are nondeterministic → if WASI ever needed, virtualize via wasi-virt. Not needed: plugins get explicit host capabilities only. Candidate policy: - fuel metering everywhere (interactive + replay use same mechanism). - NaN canonicalization on, relaxed SIMD off. - modules validated: memories/tables max == min. - time injected as explicit argument by host, never ambient. - host↔guest calls recorded in CBOR session → replay verifiable. ## Sandbox model - wasm isolation: no fs/net/proc unless host grants. - capability grants per plugin: declared in manifest, checked at instantiation, host fns gated per grant. - resource limits: `ResourceLimiter` for memory/tables; fuel for cpu. - zed precedent: extensions run in stricter sandbox with limited capabilities, allowlist discussion in zed#12358. ## Host API sketch (WIT) Not binding. Spec phase decides. ```wit package smith:plugin@0.1.0; world smith-plugin { import host; // log, granted caps: fs-scope, net-scope, secret-proxy export init; // manifest-driven export tools; // list, describe, invoke (recorded io) export layout; // frame batch in, layout batch out export themes; export prompts; export keymap; // predicates + bindings } ``` ## Boundary cost - JIT'd wasm call ≈ ns-scale for entry; cost driver is marshalling. - Layout: batch per frame (one call, dirty regions in, constraints + widgets out) → avoids per-widget calls. - Tools: invoke per tool call, io already event-driven → negligible. - Keymap predicates: called per input event → keep signature tiny. - Perf-critical widget rendering stays Rust (RULES: Rust widgets). ## Guest toolchain - cargo-component (bytecodealliance): cargo subcommand, Rust guests, wit-bindgen generated stubs. - wasm-tools: component tooling, validation. - jco: JS/TS guests later → npm ecosystem. - smith ships thin wrappers only: `smith plugin init/build/check` → wrap cargo + wasm-tools. No own-language tooling. ## Zed precedent - Zed extensions = Rust compiled to wasm component, WIT-defined host API, sandboxed, extension trait guest-side. - Same shape as smith needs: editor callbacks, themes, languages as plugins. - Lesson: keep host API small + versioned; async boundary complexity is real → prefer batched sync calls for v1. ## Open questions - Component API stability: pin wasmtime minor version; WIT world versioning policy. - Hot reload cost: reinstantiate module per change → expected ms-scale, to measure. - Prompt-template heavy plugins: pure compute in wasm → fine; string-heavy marshalling cost to measure. - User config format: decided KDL (human decision, wasm transition). Recorded in SPEC0001. - Data-only theme/keymap packages: rejected by human. All extensions are compiled plugins. ## Candidate conclusions 1. wasmtime + WIT, Rust-only guests v1. → spec SMH-SPEC-WASM0001. 2. No ambient WASI. Host capabilities via WIT imports only. 3. Fuel metering, NaN canonicalization, no relaxed SIMD, max==min memories. 4. Layout API = batched frame interface. 5. Drop mlua, mlua-pkg from root Cargo.toml (needs human approval: root Cargo.toml is human-owned). → applied 2026. 6. Concurrency: defer implementation, pin model now. One instance = single-threaded actor; parallelism = host-orchestrated lanes; intra-guest threads rejected. 7. Versioning: additive-only minors; optional new exports; versioned worlds with deprecation window; manifest declares world version; catch-all unknown case in shared types from v1. 8. Additional guest languages cost a scaffold template + bindgen only; host unchanged. js/ts via jco, go, python are first candidates. 9. Plugin identity = artifact + KDL manifest; artifact content hash pinned in session traces. Runtime never sees source; provenance optional. 10. No recompilation at load time. Old artifacts keep running across additive world updates; EOL artifacts fail with explicit rebuild path. 11. Diagnostics contract: every lifecycle stage names stage, cause, fix. No heuristic errors. 12. Registration is explicit-only: init/build never register; dev, link, install do. Dev links load by reference, override installed plugins, pin artifact hash per load. 13. TypeScript via jco = compatibility canary in CI from v1. Prevents rust coupling: WIT ergonomics, promise interop, fuel ceilings, memory preallocation, language-pluggable tooling. 14. To measure on jco canary: artifact size, quickjs fuel burn ratio, componentize-js memory preallocation vs max==min. ## Links - wasmtime deterministic execution: https://docs.wasmtime.dev/examples-deterministic-wasm-execution.html - wasmtime security: https://docs.wasmtime.dev/security.html - wasmi new engine: https://wasmi-labs.github.io/blog/posts/wasmi-v0.32/ - wasmi 1.0: https://www.reddit.com/r/rust/comments/1pd61oi/ - cargo-component: https://github.com/bytecodealliance/cargo-component - zed extensions: https://zed.dev/blog/zed-decoded-extensions - zed sandbox allowlist: https://github.com/zed-industries/zed/issues/12358