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