Skip to content

Latest commit

 

History

History
618 lines (521 loc) · 37.8 KB

runtime-integration.md

File metadata and controls

618 lines (521 loc) · 37.8 KB

CLI and runtime integration

The CLI compiles a required source file to raw binary Wasm or plain WAT and supports explicit export selection. The dependency-free Node.js runtime calls the raw Wasm ABI. The compiler does not execute Wasm: compilation and execution are separate commands, and runtime callbacks must be supplied explicitly.

Run these commands from the repository root. Rust dependencies must already be available locally for --offline; runtime examples require Node.js.

The native adapter is package was-compiler-cli in crates/cli/, depending on was-compiler in crates/compiler/. Root cargo run still selects the single was-compiler executable. For local installation use cargo install --offline --path crates/cli; the virtual root is not installable. See workspace validation and Bacon for development checks.

Quick start

Compile the checked-in array example, exporting only main:

cargo run --offline -- examples/array-sum.was -o target/array-sum.wasm --export main
node runtime/run.cjs target/array-sum.wasm

The runner prints 65. The sum body remains callable by guest code, but is not exported. To call sum from JavaScript, export it and explicitly allocate an array descriptor as shown under Programmatic Node interface. The runner does not turn a native JSON array into a guest array.

To inspect plain WAT or display help:

cargo run --offline -- examples/array-sum.was --emit wat
cargo run --offline -- examples/array-sum.was -o target/array-sum.wat
cargo run --offline -- --help
node runtime/run.cjs --help

Compiler CLI

was-compiler [OPTIONS] <SOURCE>
Option Behavior
SOURCE Required UTF-8 source-file path (PathBuf). Its extension is not significant. - is a literal filename, not stdin.
-o PATH, --output PATH Write an artifact. Parent directories must already exist. - means text stdout.
--emit wasm Produce raw binary Wasm; requires a filesystem output path.
--emit wat Produce plain WAT. Defaults to stdout without -o.
--export NAME Export only these user-function names; repeat for more names.
--no-exports Export no user functions. Cannot be combined with --export.
--host DECL Declare a host import and its language signature; repeat for more imports.
-O0 … -O3, -O LEVEL -O0 is the default baseline; -O1–-O3 share experimental #39 literal/add-sub specialization.
-- Treat remaining arguments as source paths, including a source filename beginning with -.
-h, --help Print usage and exit 0.

#39 specializes exact parsed signed-i32 literals and proven add/sub subtrees, converting back to f64 at every unspecialized consumer. Numeric locals (including never-reassigned lets), comparisons, unary operations, calls, loads and captured reads retain existing lowering/unknown facts. Public AST and raw ABI are unchanged. Enabled bytes may differ from -O0; require semantic/diagnostic parity across levels and library/CLI/current-source browser byte parity within each level. Direct codegen APIs retain baseline policy. Contracts and status separate this subset from #38's broader future scope. Measurements show size benefit and noisy material timing regressions pending explicit user acceptance. #39 correctness gates passed, including 1,261 workspace tests with zero failed/ignored and 25 rebuilt-browser groups, not release acceptance or a default change. Review against 09dcb5c is complete.

The CLI uses clap derive for argument parsing, generated help, and usage errors. Long options with values also accept --option=value, such as --emit=wasm. No arguments or invalid arguments exit 2 with an error: message and help hint. There is no built-in example fallback or stdin support. Host registration and compiler validation remain separate from argument parsing.

Without --emit, an output filename ending in the exact lowercase extension .wasm implies binary Wasm; all other cases default to WAT. Explicit --emit overrides extension inference. Binary stdout is intentionally refused so terminal output remains readable text. The CLI rejects multiple source files, conflicting/repeated singleton options, unknown options, unknown selected exports, and duplicate selected names.

Host declarations use:

NAME=MODULE.FIELD(PARAMETER_TYPES)->RESULT_TYPE

Host type spellings are number, bool, string, number[], bool[], string[] and void; void is only legal as a result. #37 CLI integration is implemented with release gates passed. range is recognized but passed to core host registration for unsupported-ABI rejection, with CLI guidance to use supported host types; it is not an unknown type or a public range ABI. Empty parentheses mean no parameters. The declaration's source name still follows host-registration identifier/reserved-name rules. This compact notation is not an escaping format for arbitrary import names; the Rust API is available when module/field names need characters that collide with delimiters.

Compilation finishes before an output file is touched. Files are written to an exclusively created sibling temporary file, synced, then renamed into place; failed writes clean up that temporary file. Output resolving to the source's canonical path is refused. This preserves existing artifacts on compilation errors and avoids publishing a partially written file. It does not promise directory-sync crash durability or race-proof source-file identity checks.

Function-result inference and raw calls

Native CLI and browser compilation accept omitted function results through acyclic chains and diamonds, including named/default calls:

cargo run --offline -- examples/inferred-defaults.was -o target/inferred-defaults.wasm --export main
node runtime/run.cjs target/inferred-defaults.wasm

This prints 584. Parameters still require types and named functions require explicit returns. Reachable returns must agree exactly; incomplete nonvoid paths are errors. No value-return syntax infers void, while Never-only value-return syntax requires an annotation. Every member of a recursive component needs an explicit result, including calls in unreachable/skipped source and across annotated barriers. These are initial inference limitations, not a new runtime or polymorphic ABI.

The concrete result ABI is identical to an explicit annotation: number is f64, bool is i32 with normalized host boundaries, strings/arrays are instance-local i32 descriptor handles, and void has no result. Range signatures remain private. Source defaults do not reduce raw export arity. For the example, exporting total requires all three machine arguments; use a source wrapper to apply defaults. String/array marshaling remains an explicit host operation using the generated instance's memory, not compiler memory or automatic JSON conversion.

Inference/binding/default errors use the existing diagnostic renderer and original byte spans. Annotated-source bytes, ordinary body-literal error stages and existing first-error ordering remain unchanged. See the raw-AST migration and release validation.

Function preconditions (WP8)

Source functions may declare ordered requires clauses after the optional result annotation and before the body; see syntax and the shared pure whitelist. Source invocation order is written explicit arguments, omitted defaults in formal order, all callee parameter validation, ordered contracts, then body. Validation of string/array/range parameters precedes even a constant false contract or one that never reads those parameters. Internal and recursive calls enforce the same checks as selected raw exports. Raw exports retain full arity: the host supplies every machine argument; neither source defaults nor contracts add host wrappers.

A false condition executes standard Wasm unreachable; JavaScript observes a WebAssembly.RuntimeError, without portable custom messages, contract metadata, or rollback of prior argument/default effects. Declaration errors use expression-only UTF-8 spans through the existing renderer. Contracts must be bool, read only formals, and use the narrow default whitelist; all declarations and skipped operands are checked before inference/body analysis. False contracts do not relax return rules or change inference. No optimization level removes checks. requires is reserved as a source/host-language name, while arbitrary nonempty host module/field names remain valid.

WP8 is implemented and release gates passed, including rebuilt-browser and full ignored runtime coverage. See WP8 suites and frozen contracts. WP9 match statements/values are implemented with release gates passed, including bounded Node execution and current-source rebuilt-browser/full ignored validation. crates/compiler/tests/match_runtime.rs covers effects, first-match overlaps, strings, fractional/NaN cases and transfers; crates/cli/tests/match_adapters.rs, crates/browser/tests/match_adapters.rs covers native/browser compatibility. Node/browser cases retain their default ignored policy. Match uses the existing raw ABI and explicit marshaling, with no new host callback or helper kind; its scrutinee runs once and only the first matching arm executes. See the frozen match rules. WP10 named-only pipelines are implemented (#27), with release gates passed on 2026-10-14. WP11 implements capture-free local callables (#29) and immediate-creator shared parameter/let captures (#30), plus captured numeric compounds (#31). #32 adds nested parameter/let forwarding and live callable contexts; #33 adds read-only iteration captures, with validation passed. Nested read-only range/array captures and transitive live callable contexts compose with captured compounds and invocation-local lifetime behavior. WP11 is implemented; full native/browser release gates passed on 2026-10-15 under #35. Issue #44 implements known lexical callable pipeline stages; integrated release validation remains pending. WP12 placeholder locals and immediate stages remain deferred. #30 subset validation passed, including rebuilt-browser and full ignored runtime coverage before #31. #31 validation passed. WP13 optimization requires separate authorization.

Local callables and shared captures (WP11)

The CLI and core accept let twice = (x: number) => x * 2; followed by twice(21). Expression and tail-valued block bodies infer concrete results; zero-argument and Void helpers work too. Shared captures of ancestor parameters and ordinary lets of type number, bool, string, number[] or range are supported. Reassignment from either side and ordinary indexed array mutation are supported. #32 forwards original ancestor parameter/let cells and earlier live callable contexts. #33 permits read-only range/array iteration captures; ordinary and all numeric compound writes remain rejected even when skipped/unreachable. Each yielded captured binding receives a fresh eight-byte cell before the body, even if the closure declaration is skipped; empty/exhausted/failed yields allocate no cell. Cells are monotonic, never reused/reclaimed; environments allocate only at executed declarations. Nested helpers forward the same original read-only iteration cell; transitive dependencies retain its live callable environment. Mutable shadowing lets remain distinct. See the #33 contract. #31 supports all five numeric compounds on captured scalar/indexed places, including creator-side promoted bindings. The checked target and old value are retained before RHS effects, even if the RHS rebinds the array or grows memory. Fixed-storage indexed assignments preserve their pre-RHS checked element address. For opt-in resizable arrays, #66 writes instead capture handle/index and revalidate current authority/storage after the RHS, preserving the old numeric compound value; receiver rebinding does not redirect them. See the relocation-safe write contract and the subset, diagnostics and AST migration.

Helpers are private direct-call functions after existing runtime helpers, not exports or host callables. Context-free declarations allocate no environment and add no hidden argument. Capturing helpers receive a private hidden first i32 environment argument. Captured parameter cells allocate in formal order after validation/contracts, captured let cells after completing initializers, and nonempty environments only at executed declarations. Structural cell promotion can allocate even when a later capturing declaration is skipped.

Private uniform eight-byte cells/slots use monotonic allocation: calls can grow memory or trap without rollback of prior effects or allocation. There is no scope/return/trap reclamation or general heap rewind; returned or host-retained arrays/strings must remain valid. Refresh JavaScript views after guest calls or host allocations that may grow memory. Normal handle validation, explicit marshaling and external range restrictions remain unchanged. No public callable ABI, table or implicit callback is added.

Recursion, host reentry and traps (#34)

A synchronous host callback may reenter creator exports on the same instance. Each recursive/reentrant invocation has distinct live parameter/local cells and nested callable environments; helpers within one invocation still share original bindings. Supply every registered import, even unused ones. Host registration only declares signatures; the embedding still supplies the actual callbacks.

A host callback that catches an inner Wasm trap may resume the suspended outer invocation. A trap escaping to the embedding also leaves the instance available for later calls. Neither path rolls back completed guest writes, host effects or successful allocations, nor runs automatic frame cleanup. Retained arrays and host-created strings returned through capture-allocating functions keep their instance-local handles across later calls, traps and memory growth. Fixed-storage payload offsets retain their lifetime rights; opt-in resizable payloads may relocate and must be reacquired after possible relocation, including reentry.

Keep descriptor offsets rather than long-lived DataView or typed-array objects. After a potentially growing guest call or __alloc, obtain exports.memory.buffer again and recreate views before reading or writing through those offsets. Do this inside callbacks too: reentry can grow memory before the original callback returns. A view from before growth can be detached; it is not a persistent reference.

Repeated bounded invocations can accumulate storage without bound until the instance's memory limit or engine allocation failure causes a trap. The compiler provides neither general reclamation nor isolation from a host that corrupts memory. See the #34 coverage and validation.

Callables remain local lexical names, not first-class values: no aliasing, reassignment, argument/return storage, immediate invocation, or callable pipeline stages. Closure defaults/contracts/result annotations are unsupported. WP12 and separately authorized WP13 optimization are outside this contract. Full WP11 release gates passed on 2026-10-15 under #35, including current-source rebuilt-browser and full ignored runtime coverage. See the separate #30 execution evidence for shared-storage validation before #31, not captured compounds. Historical #29 results cover only capture-free callables.

Named-only pipelines (WP10; release gates passed)

Native compilation supports user-function stages with positional-prefix/named arguments and omitted trailing defaults. Input binds formal 0 before written arguments and defaults; bare and empty stages are equivalent. Evaluation is once only and ordered, followed by callee parameter validation and ordered contracts. Host imports and builtin len cannot be stages; wrap host effects in a user function.

A final declared-Void stage requires a discarded result: direct expression statements and propagated discarded block tails/if branches/match arms, with transparent parentheses. Outer discard does not authorize operands, call arguments, pipeline inputs or value-producing contexts. Nonfinal declared-Void stages remain rejected even after Never; expression if/match structural joins are unchanged. See normative §12.

There is no pipeline runtime API, callback registration shortcut, typed discard wrapper or public discard flag. Ordinary binding/defaults, imports, exports and explicit marshaling retain the raw full-arity ABI; hosts still supply all registered imports, and raw exported invocations do not synthesize defaults. Source-semantic discard is not wholly reconstructible from forged typed input; backend injection ownership, type and flow checks remain independent. See migration. The plan's #25/#26 validation records remain historical subset evidence, not full-release evidence. #27 release validation passed, including current-source rebuilt-browser and full ignored runtime coverage.

Export selection in Rust

use was_compiler::{Compiler, ExportOptions};

let mut compiler = Compiler::new();
compiler.set_exports(ExportOptions::Only(vec!["main".into()]));

let wasm = compiler.compile_source(
    "fn main() -> number { return helper(); } fn helper() -> number { return 7; }",
)?;

Compiler::exports() exposes the current policy. ExportOptions::All is the default; None exports no user functions. This policy is separate from the existing, copyable CompilerOptions and its optimization level.

The lower-level API exposes both codegen::compile(program) (all user exports) and codegen::compile_with_exports(program, &policy). Export entries retain source order even when the requested list uses another order. Selection removes no function body, signature, registered import, static data, or runtime helper; indices and validation of unexported bodies remain intact.

Memory-using modules still export memory and __alloc, including with None. Selecting user functions with those names in a memory-backed module conflicts with mandatory ABI exports; unexported functions may use those source names. Scalar-only modules may export functions with those names. The Node runtime does not reserve function names or hide callable ABI helpers.

Internal ranges and numeric membership

The CLI/library support number in range returning bool, with comparison-tier precedence and existing left associativity. Both operands evaluate once, left-to-right; exact number/range types allow Never compatibility and all children remain checked. in is reserved for source/host-language names, not host module/field names. Membership validates the consumed record before the candidate guard: malformed records trap even for invalid candidates. Nonfinite, fractional, or out-of-safe-domain candidates return false; both signed zeros are accepted. Exact signed i64 interval/congruence checks require no host callback or floating remainder helper. Optional Contains follows Construct/Validate only when membership exists, preserving construction-only bytes.

Range handles remain private: reject all range host imports and selected range user exports. Use --export main when helpers have internal range signatures; default export-all rejects those signatures. A scalar membership function may be exported normally; its bool result uses raw i32 0/1. There is no public range marshaling API.

Statement for name in iterable { body } accepts range or any supported array with Never compatibility and needs no new host callback. Ranges use the same private records; arrays use the existing public descriptors. for is reserved for source/host-language names, not host module/field names. Its iterable evaluates once in the enclosing context before the loop/binding is active; iterable break/continue targets the outer nearest loop, and return/divergence is preserved. The body has a read-only scalar element binding (number for ranges; a distinct inner shadowing let remains mutable), permits bare breaks, and rejects value breaks even when unreachable. Continue reaches advancement before retesting. For ranges, cached i64 fields and signed endpoint checks before exact f64 yield avoid rounding stalls; yielded zero is +0, and a valid advance past bounds exhausts normally even outside the safe domain. For is a void statement, not a value expression, and adds no public range ABI.

For arrays, complete typed outer-storage validation (no element scan) precedes caching the handle and unsigned u32 length once, including for an empty array. Unsigned index iteration uses typed checked loads/consumed-value checks before binding/capture promotion/body effects, not an element snapshot or cached raw payload pointer. Alias mutations to unvisited elements are visible; source reassignment does not replace the captured handle. Continue exits the body latch before index advancement/retest. Old-type-only ABI/helper order is unchanged, and array-only iteration needs no range helpers. Issue #5 changed no parser or AST shapes; #37 adds typed For.element_type.

CLI and browser compilation support both iterable types through the shared core. WP5 implementation is complete and all release gates passed. See extension plan §5 for exact commands/results, including the full ignored-test run. Issue #6 adds adapter/cross-feature coverage, including private host-signature rejection and export selection across optimization levels. Range host declarations fail with exit status 1 and supported-type guidance; registration failures leave existing output files unchanged. Browser validation requires rebuilt bindings. crates/compiler/tests/array_for_loops.rs is the array-for suite; cross-feature execution coverage is detailed in the WP5 checkpoint. crates/compiler/tests/for_frontend.rs covers frontend contracts, crates/compiler/tests/for_lowering.rs covers forged input and decoded lowering, and crates/compiler/tests/for_loops.rs covers deterministic layout/imports/exports and bounded Node execution at all optimization levels. The runtime cases check exact yields/exhaustion, effects, transfers, record validation and caching. They are ignored by default; default Cargo success alone does not establish runtime coverage. See the private record contract.

Host callbacks

Imports are raw callbacks, not typed marshaling wrappers. A CommonJS provider can export an object of module namespaces, for example { env: { log_number } }, or a factory getInstance => rawImports. The factory can use createAbi(getInstance) for explicit string/array conversion inside callbacks. The getter returns the instantiated WebAssembly.Instance once available, but returns undefined during Start callbacks; those callbacks cannot use the instance's memory ABI. Compiler-generated modules have no Start function.

No implicit env, I/O, or WASI implementation is installed. Provider paths resolve from the current working directory. Every registered import must be provided, even if unused by the selected entry. Providers are trusted Node.js code and retain Node's host capabilities.

Language type Raw host argument/result
number JavaScript number representing f64, including NaN and infinities.
bool i32 number; generated host boundaries normalize nonzero values to 1.
string, number[], bool[], string[] i32 descriptor handle in the current instance's memory.
void No Wasm result.

Source named/default calls are compiler-side conveniences only. A declaration such as fn f(a: number, b: number = a + 1) -> number still exports two f64 parameters: raw Node/browser callers must supply both. No default metadata or host-side argument generator is emitted. runtime.call('f', [3]) rejects the missing raw argument; runtime.call('f', [3, 4]) supplies the full signature. Direct JavaScript Wasm calls do not run source defaults either; JavaScript's conversion of omitted arguments is not the language's omission semantics. Use a source wrapper such as fn main() -> number { return f(3); } to invoke an omitted default from the embedding.

Explicit source arguments run once in written order, followed by omitted defaults in formal order on every invocation. Scalars are captured values; string/array/range handles are shared, not deep copies. Later explicit array mutations can therefore be observed by permitted default equality/len operations. A transfer or trap in an explicit argument prevents later defaults and the callee body from executing; normalization adds no function/loop context. Range import/selected-export restrictions are unchanged.

Named arguments are user-function-only; host declarations carry no formal-name or default contract, and builtin len remains positional. See the default whitelist. WP6 named/default parameters are implemented, and #13 release validation passed, including CLI execution and freshly rebuilt browser/runtime coverage. WP7 result inference is also available through the CLI/library/browser adapters; see #16 release evidence.

Hosts must obey the declared signatures. Host callbacks receive descriptor handles, not native strings or arrays, and must return handles for memory-backed results. Use the instance's memory and __alloc, optionally via explicit createAbi conversion. Decoding an array produces a copy; mutate guest memory directly when reference mutation is intended. See memory-abi.md for layouts, bounds, lifetime, and a complete raw host roundtrip example.

The checked-in examples/host-provider.cjs exports a factory using createAbi(getInstance) to explicitly decode incoming string/array handles and encode return handles. Compile and run its source example:

cargo run --offline -- examples/host-calls.was -o target/host-calls.wasm --export main --host 'echo=env.echo(string)->string' --host 'print=env.print(string)->void' --host 'adjust=env.adjust(number[])->number[]'
node runtime/run.cjs target/host-calls.wasm --imports examples/host-provider.cjs

This prints Hello from host followed by 12.

Different source names may target one import module/field. One JavaScript callback must then satisfy every declared raw signature; the runtime does not infer language types or build signature-specific wrappers. Prefer distinct properties for incompatible contracts. Calls are synchronous; Promises are not awaited and are not substitutes for declared scalar or descriptor results.

Typed-array host integration (#37)

Core, Rust host registration/selected exports, CLI/browser host-signature adapters and explicit Node marshaling implement number[], bool[] and string[]. Final #37 release gates passed after the accessor fix: 1,251 workspace tests passed with none ignored, including six marshaling tests and CLI/browser adapter coverage; 25 browser smoke groups and all build/Clippy gates passed. See typing and public Rust migration. Existing WP release records retain their earlier scope.

CLI --host and browser registration accept bool[]/string[] for parameters and results, with the same core checks and raw i32 ABI as numeric arrays. Range host restrictions and full-arity raw calls remain unchanged. No callbacks, signature metadata or native-value conversions are installed automatically.

Descriptors remain untagged: bool[] has one-byte canonical 0/1 elements; string[] has four-byte little-endian string handles, not inline UTF-8 bytes. String descriptors still count bytes with width 1; array descriptors count elements. Entry/length checks validate complete outer storage without scans. Consumed bool bytes must be 0/1 (not scalar host normalization); consumed string handles receive structural checks. Equality/iteration may leave invalid unvisited elements unconsumed. Replacing an element validates the destination before RHS but does not consume the old value. UTF-8 validity, immutable string bytes and published descriptor metadata remain trusted-host obligations. See the authoritative memory ABI.

The explicit createAbi contract extends encode(type, value)/decode(type, value) to bool[] and string[]. Encoding requires plain JavaScript arrays of booleans or strings, not typed arrays/coercible values. Both directions copy: later native input/decoded-array mutations do not alter guest storage. Use direct memory access for deliberate shared mutation. Refresh views after allocations/calls that may grow memory, including per-string allocations; retaining Wasm offsets is not retaining valid JavaScript views. Encoding still creates fixed-storage arrays; decoding copies current logical elements, not shared payload borrows.

New-array encoding captures initial length/each scalar once, validates its type and accounts UTF-8 bytes against that snapshot, then copies only captured values. Changing getters cannot bypass validation/budgets or change the copied count. Invalid/oversized captured input fails before guest access/allocation; getter side effects are not rolled back. Both accessor regressions passed in the final full run.

Each new-array operation has a checked 64 MiB aggregate logical-copy budget: N bytes for bool[], 4*N + sum(per-occurrence UTF-8 bytes) for string[]. Repeated occurrences count repeatedly, even when handles/strings repeat. This limits logical copied bytes, not JS object overhead, RSS, guest allocation or execution. Standalone string and numeric-array 64 MiB payload limits are unchanged. See final release evidence for exact commands, counts, tools and post-fix source hashes. Historical M4 measurements and handoff predate the runtime accessor fix; their hashes are preserved and are not claimed to match the final runtime.

Opt-in resizable storage (#65–#69)

resizable(source) creates a fresh resize-authorized copy for number[], bool[] and string[]. identifier.reserve(additional) and relocation-safe indexed writes are implemented, as are push(item), pop() and remove(index) for all three array types. Push returns void; pop/remove return the removed scalar and trap for an empty pop or invalid index. Neither removal reduces capacity. Reserve returns void, preserves length and the stable eight-byte descriptor handle, and exposes no capacity or authority query. It captures and validates receiver authority before the argument: fixed-storage receivers trap even for reserve(0), before argument effects. After evaluating the argument once, it revalidates the captured object's current state; rebinding cannot redirect it. Additional must be finite, integral and nonnegative (-0 accepted), with checked sizing within the 2^31-byte request ceiling.

Growth acquires full selected capacity, raw-copies live slots without scanning otherwise unconsumed bool bytes/string handles, and publishes replacement storage before releasing only the displaced owned payload. It adds no general free or host ownership-transfer API. Completed argument effects survive later failure. See the narrow reserve contract.

Hosts may retain opt-in handles, but payload pointer/length borrows must be reacquired from the descriptor after any call or reentrant callback that may resize, including a call that traps after completed mutations. This applies even without memory growth and is separate from refreshing DataView/typed-array views after possible growth. Fixed-storage host lifetime guarantees and ordinary host encoding remain unchanged. See host borrows and relocation-safe writes.

The explicit Node createAbi encoder still produces fixed-storage arrays isolated from later JavaScript mutation. All four resize methods reject those handles; reserve/push/remove reject them before argument effects. Obtain a movable handle by calling an exported guest function that constructs resizable(source), not by fabricating descriptors or registering host ownership. decode(type, handle) copies the current logical elements and does not expose spare capacity or a live view. Existing accessor snapshots and 64 MiB copy accounting are unchanged.

resizable is newly reserved as a global function/registered host-language name; rename prior global uses. Host module/field names may still be resizable, and normal lexical shadowing remains valid. #69 adapter/raw-host evidence covers this implemented subset, not final M4 acceptance. No invocation wrappers, cleanup APIs or allocator-management exports are required or introduced. Historical #37/#65–#68 records retain their original scope.

Programmatic Node interface

Compile an artifact exporting sum:

cargo run --offline -- examples/array-sum.was -o target/array-sum.wasm --export main --export sum

Explicitly marshal its array argument:

const fs = require('node:fs');
const { instantiate } = require('./runtime/index.cjs');
const { createAbi } = require('./runtime/abi.cjs');

const runtime = instantiate(fs.readFileSync('target/array-sum.wasm'), {});
const abi = createAbi(() => runtime.instance);
const handle = abi.encode('number[]', [1, 2, 3]);
console.log(runtime.call('sum', [handle]));

This prints 6. instantiate(bytes, imports) returns { instance, call }. call(name, args) checks the exported arity and accepts numeric raw arguments, then invokes the callable export and returns its raw result. It does not infer language signatures or marshal native values. instance is the underlying WebAssembly.Instance. The embedding must know whether an i32 means a boolean or a descriptor handle. Use abi.decode('string', handle) or abi.decode('number[]', handle) explicitly for memory-backed results. Hosts can use the same helper with an instance getter to decode incoming handles and encode raw return handles.

The runner's --entry defaults to main; --args defaults to [] and accepts a JSON array of numeric raw arguments. Use numbers for f64, i32 booleans, and valid descriptor handles, not native strings or nested arrays. Handles must already refer to valid storage in that instance; the CLI does not allocate payloads for them. --imports PROVIDER.cjs supplies raw callbacks. Successful void calls print nothing. JSON cannot spell NaN or infinities; programmatic raw calls can pass them.

Binary compatibility

CLI binary output and library output are the same raw Wasm bytes for the same source, host declarations, and compiler/export options. There is no interface custom section, package metadata, or replacement interface format. The Node runtime accepts either output; it needs binary Wasm, not WAT. WAT must be assembled externally before being passed to JavaScript's binary WebAssembly API.

The generated module has no automatic start function and no WASI _start. Invoking exported main is an embedding convention. Keep application-known signatures and provider bindings consistent with the compiled module.

Diagnostics and failures

Compiler failures go to stderr with an exit status of 1; usage errors use 2. Where a source span exists, diagnostics include the source path, line/character column, stage, source excerpt, and caret underline. UTF-8 byte spans, CRLF, tabs, EOF, and multiline statements are handled. Semantic/codegen expression locations still cover the containing statement, not individual expression nodes. Unlocated export/layout errors report their stage without inventing line numbers.

The Node runner exits 1 for failures and reports these stages:

Stage Example / remedy
file Missing Wasm/provider path; verify cwd and paths.
compilation Invalid binary or WAT supplied as bytes; compile binary Wasm.
instantiation Missing/noncallable provider import; supply every named module/field with the declared raw signature.
invocation Missing/noncallable entry, invalid runner arguments, host exception, or guest trap; inspect raw inputs and the reported call context.

A same-thread timer cannot interrupt a synchronous nonterminating guest. Run untrusted or potentially unbounded work under worker/process or engine resource controls. Neither traps nor host exceptions roll back prior effects. See limitations.md for current workarounds and future choices.