Luigit
repositories / smith

smith

There are many coding harnesses - but this one is fast

owned by admin

.system/research/SMH-RESEARCH-WASM0001-embedded-wasm/index.md

Raw
Rendered preview

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.

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

---
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