The plan is the translator shim's output: everything the TS runtime needs to
instantiate and link one component, derived deterministically from the component
binary. This document is the interface between crates/translator-shim
(producer) and runtime/ (consumer); see also
descriptor-ir.md and intrinsics.md.
Current formatVersion: 6. Loaders require strict equality. Schema changes
bump the version and update producer and consumer together; editorial changes to
this document do not change the wire format.
Format 6 versions the changed FACT enter-sync-call core signature:
(calleeAsync, calleeInstance), replacing format 5's
(callerInstance, calleeAsync, calleeInstance). Re-translate old deploy-time
envelopes with the matching translator; do not rewrite only the version number.
Old runtimes reject new plans and new runtimes reject old plans before any
initializer runs. The cancellable fields remain false-only wire slots:
the producer emits false, the loader rejects true, and execution no longer
uses them. Removed nonzero builtin immediates are validation errors.
- Own schema, not wasmtime's. The shim maps wasmtime's unstable internal API to this versioned format. Only the shim depends on both shapes.
- JSON encoding. Inspectable and deterministic, without a separate binary-format implementation.
- No duplicate bytes. Embedded core modules are referenced as
[offset, offset + len)byte ranges into the original component binary — the executor slices them itself. Only FACT adapter modules (bytes that don't exist in the input) ship as separate artifacts. - Types, not precomputed lanes. The plan carries component-level types (descriptor IR); flattening is computed in the runtime by shared, reference-tested rules. See descriptor-ir.md "Flattening".
A translation produces:
plan.json this document's schema
adapters/<idx>.wasm FACT-generated core modules (kilobytes each)
The original component binary is the third input at instantiation time; the plan never embeds it.
The artifact cache keys by component hash, shim binary hash, and feature
flags. producer.shimVersion alone is not a build identity: two shim builds
with that version can produce different adapters.
Notes on specific entries:
- Type exports index into
resourceTables, not theResourceIndexspace. An export'stype: {"kind": "resource", "resource": n}carries a resource-table index (TypeResourceTableIndex, the same space as descriptor-IRown/borrow), not aResourceIndex. Consequence: one resource type can be reachable through several distinct table indices — e.g. a type export pointing at table 1 while the functions' handles use table 0, both resolving to the sameResourceIndexviaresourceTables[n].resource. Implementation/destructor state is keyed by the resolvedResourceIndex. Guest handle checks must retain the table index: two abstract types inside one nested instance may resolve to the same origin without being interchangeable there.TypeResourceTableIndexis not reconstructible from the pair(ResourceIndex, instance); preserve the emitted table entries. - Module exports: the executor surfaces the export as the platform's
compiled-module value —
WebAssembly.Modulein the JS runtime — reusing the compilation the instantiation path already performs. Module exports are excluded from the canonical world digest (digest.md's item rule: only functions and resources contribute as export/import items; a module export is not WIT-expressible and does not affect positional-calling ABI shape, so a digest match stays ABI-sound). The WIT-shaped conventions facade skips them (the type-export precedent); they are available on the raw executor export surface only. Export::ModuleImport(re-export of an imported module) is rejected at translation as unsupported, as is imported-module instantiation.- Structured error envelope: translation failures emit
{"error": "<message>", "errorDetail": {"phase": "validation" | "unsupported" | "internal", "message", "detail"?}}.errorDetailis additive (consumers tolerate its absence); onlyphase: "validation"may be scored as a correctassert_invalid/assert_malformedverdict. Body-validation failures in FACT-generated (non-embedded) modules classify asinternal, nevervalidation. - Runtime instance/memory/realloc counts are derivable, not carried; executors create state lazily.
- Adapter naming = static-module index; embedded
wasm_module_offsetequals slice position (shim-asserted);NameMap/IndexMapiteration is insertion-ordered (determinism holds).
Byte-identical plan.json + adapters for identical
(component bytes, shim build, features). JSON emission must use stable key
order and no floats-as-locale. This property is what makes the artifact cache
(docs/architecture.md §10) a pure content-address lookup — treat any
nondeterminism as a bug.
- Validate
formatVersion(strict equality) and fail fast on mismatch. - Execute
initializersstrictly in order; each op's semantics follow wasmtime-environ's documented behavior for the correspondingGlobalInitializervariant. - Referenced unsupported trampolines, intrinsics, and operations fail during
instantiation. Unreferenced table entries need not be materialized.
Unsupported runtime operations must report capability errors, not guest traps.
In particular, a blocking path reached without JSPI can fail at call time with
NeedsJspi; streams, futures, and error-context values themselves are implemented. - Verify the canonical world digest when typed bindings are in play (docs/architecture.md §9, digest.md).
- The shim must fail translation with a clear error on any wasmtime-environ construct not representable in this format (never silently drop).
-
Declared-import checking (#372):
importscontains runtime-used leaves from environ'scomponent.imports, not the completecomponent.import_typessurface. Unused imports and equality-bound resource aliases can disappear, so the current executor cannot check their presence, kind, or supplied resource identity. This is a linking gap, not evidence that those declarations impose no constraints.The next design must retain the declared import tree and its type constraints while keeping runtime import indices stable for initializer references. Resource aliases must refer to the existing resource identity, not allocate a second token. Validate supplied aliases against that identity before running initializers. Under the Wasmtime-compatible host policy, an omitted equality-bound alias or recursively empty instance can be synthesized; a supplied value must still match. See the pinned Explainer's type bounds and resource substitution rules, and Wasmtime
component/matching.rsfor this host policy.Core-module matching also needs typed import/export metadata: the JS module reflection API exposes names and kinds, not signatures or limits. Do not claim to check module subtyping from that reflection alone. Named-instance wiring in the WAST harness must preserve exported resource identities and module metadata; missing providers must not satisfy a type-mismatch assertion. Cover matching, mismatching, unused, and omitted imports, and verify rejection before start-function side effects. This is a follow-up design constraint, not an optional field or a change to formatVersion 6; the schema and its version transition must be reviewed together with the implementation.
-
valuessection (the component-level value-definition feature): out of scope (wasmtime parity, docs/architecture.md §7). -
Imported-module instantiation (
InstantiateModule::Import) and re-export remain unsupported. -
The memory-identity half of
canon_task_return's options-equality check remains a named open gap:prepare-call.memoryis the adapter's second-hand view and wasmtime's own check is one-sided — re-justified at the site (intrinsics/fact_calls.ts / async_builtins.ts CONTRACT notes).
{ "formatVersion": 6, "producer": { "shimVersion": "…", // crates/translator-shim crate version "wasmtimeEnviron": "50.0.0-dev+cc546ee", // crate version + pinned git rev "features": ["cm-async", "…"] // wasmparser feature set used // (incl. cm-fixed-length-lists, cm-map, // cm-implements, cm-threading) — // artifact-cache key input }, "component": { "sha256": "…", "len": 123 }, // Static module index space: embedded modules first, then FACT adapters, // exactly as wasmtime-environ returns them (PrimaryMap<StaticModuleIndex>). "modules": [ { "kind": "embedded", "offset": 10, "len": 52 }, { "kind": "adapter", "file": "adapters/2.wasm", "len": 290, "intrinsics": [/* see intrinsics.md: required imports, categorized */] } ], // Ordered instantiation program. One entry per // wasmtime_environ::component::GlobalInitializer, tag-for-tag: // instantiate-module | lower-import | extract-memory | extract-realloc | // extract-callback | extract-post-return | extract-table | resource "initializers": [ { "op": "instantiate-module", "module": 0, "instance": 0, // RuntimeComponentInstanceIndex; null = adapter "args": [/* CoreDef */] }, { "op": "lower-import", "index": 0, "import": 0 }, { "op": "extract-memory", "index": 0, "export": {/* CoreExport */} }, { "op": "extract-realloc", "index": 0, "def": {/* CoreDef */} }, { "op": "extract-callback", "index": 0, "def": {/* CoreDef */} }, { "op": "extract-post-return", "index": 0, "def": {/* CoreDef */} }, { "op": "extract-table", "index": 0, "export": {/* CoreExport */} }, { "op": "resource", "index": 0, "rep": "i32", "dtor": {/* CoreDef? */}, "instance": 0 } ], // CoreDef encoding (wasmtime_environ::component::CoreDef, tag-for-tag): // { "kind": "export", "instance": n, "item": {…} } core instance export // { "kind": "instance-flags", "instance": n } i32 flags global // { "kind": "trampoline", "index": n } host trampoline // { "kind": "unsafe-intrinsic", "intrinsic": "<symbol>" } // The unsafe-intrinsic symbol is wasmtime's stable UnsafeIntrinsic::name() // ("context-get-i32-0", …), never the #[repr(u32)] ordinal (unstable // internal). All 21 variants are wire-representable. Executor obligation: // implement context-{get,set}-i32-{0,1} as canonical context.{get,set} // over per-thread storage (definitions.py Thread.storage); refuse the 17 // raw-host-memory symbols at instantiate time. // // CoreExport.item encoding (pinned): wasmtime's ExportItem is // Index(EntityIndex) | Name(String); a JS embedder can only address core // exports by *name*, so the shim resolves Index via Module::exports // inversion and always emits: // { "name": "…", "space": "func" | "table" | "memory" | "global" | "tag" } // Adapter modules import `unsafe-intrinsic` CoreDefs too (FACT saves, // clears and restores the task's `context.{get,set}` slots around // `realloc` and `post-return` calls); the executor wires them exactly as // it does for embedded modules. // Host trampolines (ComponentTranslation::trampolines), one per // wasmtime_environ::component::Trampoline variant. Executors must fail // during instantiation on referenced unimplemented kinds. See "Executor // obligations" for call-time capability errors. Full wire declarations: // TrampolineDecl in the shim and WireTrampoline in runtime/src/plan/format.ts. "trampolines": [ { "kind": "lower-import", "index": 0, "lowered": 0, /* LoweredIndex */ "options": 0, /* -> canonicalOptions */ "type": 0 /* -> types */ }, { "kind": "resource-drop", "index": 1, "instance": 0, "resource": 0 }, // FACT `runtime.trap<code>` import, nullary: the trap code is static per // import site (wasmtime `Trampoline::Trap(Trap)`), so it rides in the // plan rather than as a call argument. `code` is wasmtime's `Trap` // discriminant (`trap_encoding.rs`). { "kind": "trap", "index": 2, "code": 24 }, // Cooperative-threading built-ins: `thread-index` and // `thread-resume-later` carry `instance`; // `thread-suspend`, `thread-yield`, `thread-suspend-then-resume`, // `thread-yield-then-resume`, `thread-suspend-then-promote`, // `thread-yield-then-promote` carry `instance` + `cancellable`. { "kind": "thread-yield", "index": 3, "instance": 0, "cancellable": false }, { "kind": "task-return", "index": 4, "instance": 0, "results": 0, // RAW wasmtime TypeTupleIndex — the FACT lookup key // prepare-call passes at runtime "resultType": 0, // interned plan.types index | null (null accepted on // the wire; the producer always emits a tuple — a // no-result task carries the empty tuple) "options": 0 } // … ], // The loader builds the raw→interned task-return dictionary and rejects // contradictory mappings; the executor runs canon_task_return's // result-type check for FACT tasks (structural comparison against the // task's declared result type, definitions.py canon_task_return). // Canonical options table (Component::options), referenced by index from // trampolines and exports. Mirrors wasmtime_environ CanonicalOptions; // memory/realloc are flattened from wasmtime's // data_model: CanonicalOptionsDataModel::LinearMemory{memory, realloc} // (the Gc data model is rejected per descriptor-ir.md): "canonicalOptions": [ { "instance": 0, "stringEncoding": "utf8", // utf8|utf16|latin1+utf16 "memory": 0, // RuntimeMemoryIndex | null "realloc": 0, // RuntimeReallocIndex | null "postReturn": null, // RuntimePostReturnIndex | null "callback": null, // RuntimeCallbackIndex | null "async": false, "cancellable": false, "coreType": { "params": ["i32", "i32"], "results": ["i32"] } } ], // Component-level type table: descriptor-ir.md ValType/FuncType JSON. // Referenced by index from trampolines, imports and exports. Carries two // families: ValTypes *and* function types tagged {"kind":"func", // "params": [{label,type}], "results": [...], "async": bool} — "func" is // not a ValType kind; consumers must discriminate. "types": [/* descriptor IR */], // Resource tables, referenced by descriptor-IR own/borrow indices. // Index space = wasmtime TypeResourceTableIndex. "resourceTables": [ { "kind": "concrete", "resource": 0, "instance": 0 }, { "kind": "abstract", "id": 0 } ], // Imported resources, in ResourceIndex order; optional on the wire // (absent ⇒ empty). Executor obligation: // ResourceIndex = importedResources.length + DefinedResourceIndex. "importedResources": [{ "import": 0 /* RuntimeImportIndex */ }], // Stream/future tables: index spaces = wasmtime TypeStreamTableIndex / // TypeFutureTableIndex. Stream/future trampolines carry table indices; // these sections are what lets a consumer size and lift a copy buffer. // Digest-neutral: table sections do not enter the world digest (element // types reach it only via function types on the world surface). "streamTables": [{ "element": /* ValType | null */ null, "instance": 0 }], "futureTables": [{ "element": /* ValType | null */ null, "instance": 0 }], // Error-context tables: index space = // wasmtime TypeComponentLocalErrorContextTableIndex, emitted from // environ's ComponentTypes.error_context_tables. The // error-context-transfer trampoline's srcTable/dstTable resolve through // this section via a dedicated errorContextTableInstance(i) accessor — // never through resourceTables (loud PlanError on out-of-range, no ?? 0 // defaults). Digest-neutral. "errorContextTables": [{ "instance": 0 }], // World surface. Import names use the component's exact import strings; // runtime import indices match wasmtime's RuntimeImportIndex order. // Import entries carry "path": string[] — wasmtime's RuntimeImportIndex // is (ImportIndex, Vec<String>) walking into instance imports. "imports": [{ "name": "…", "kind": "func", "type": 0, "path": [] }], "exports": [ { "kind": "lifted-func", "name": "greet", "coreDef": {/* CoreDef */}, "options": 0, "type": 0 }, { "kind": "instance", "name": "ns:pkg/interface", "exports": [/* recursive */] }, { "kind": "type", "name": "resource-name", "type": { "kind": "resource", "resource": 0 } }, { "kind": "type", "name": "value-name", "type": { "kind": "value", "type": 0 } }, // A component exporting one of its own embedded core modules // (wasmtime Export::ModuleStatic); n indexes plan.modules and names an // *embedded* entry by construction (FACT adapters are appended after // translation and are never component exports). { "kind": "module", "name": "…", "module": 0 } ], // Legacy shim-emitted digest, retained for wire compatibility; nothing // may depend on it. The normative digest is cewd:1 per digest.md, // computed by consumers from the plan's types/imports/exports at load // time. "worldDigest": "sha256:…" }