wasmfuncplatform
v0.1.0
Published
An opinionated WebAssembly function platform with Component Model, WIT, and async stepper execution
Maintainers
Readme
wasm-func-platform-node (Component Model + WIT + async stepper)
This repo is an opinionated function platform:
- A user program is a set of functions with a fixed main signature.
- We compile to a WebAssembly Component defined by WIT.
- Execution is always asynchronous via a stepper interface: the component returns
pending(effect, state)and the Node host performs the effect (DB, messages, timers, ctx) and resumes. - Nested/inline functions are compiled into separate, referencable functions (stable IDs) via lambda lifting (foundation for "DB query + callback" fan-out).
- Object-oriented abstractions via WIT resources for natural method/property access.
Quick start
npm install
npm test
# Compile example into a component
node packages/cli/dist/main.js compile examples/program.simple.json --out /tmp/f.component.wasm
# Run it (will auto-transpile using jco if available)
node packages/cli/dist/main.js run /tmp/f.component.wasm --ctx n=41 --ctx k=1Note: component building/transpiling uses
@bytecodealliance/jcoand@bytecodealliance/componentize-js. Some environments require installing a JS engine for componentize-js. See docs.
Layout
wit/function.wit— Component interfacepackages/core-ir— canonical IR + stable IDs + lambda liftingpackages/compiler-component— IR -> guest JS ->jco componentize-> component wasmpackages/runtime— Node host async stepper loop + effect handlerspackages/cli—funcctlCLIexamples/— example programsdocs/— design notes and future directions
Library Usage
The platform can be embedded as a library for programmatic compilation and execution.
Programmatic Compilation
Use @wfp/compiler-component to compile programs directly from JavaScript:
import { compileProgram, compileProgramJson } from '@wfp/compiler-component';
// Option 1: Compile from a file path
await compileProgramJson('./examples/program.simple.json', {
outWasm: './dist/func.wasm',
entry: 'main' // optional, defaults to 'main'
});
// Option 2: Compile a Program object directly
const program = {
version: 1,
functions: [{
id: '', // auto-generated stable ID
name: 'main',
parent: null,
params: [],
body: [
{ op: 'let', name: 'n', expr: { op: 'ctx_get_i64', key: 'n' } },
{ op: 'let', name: 'one', expr: { op: 'lit_i64', value: 1 } },
{ op: 'let', name: 'result', expr: { op: 'add', a: { op: 'var', name: 'n' }, b: { op: 'var', name: 'one' } } },
{ op: 'return', expr: { op: 'json', value: { result: { op: 'var', name: 'result' } } } }
]
}]
};
await compileProgram(program, {
outWasm: './dist/func.wasm',
outIrJson: './dist/ir.json' // optional: save processed IR
});Programmatic Execution
Use @wfp/runtime to execute compiled components:
import { makeEnv, runComponent } from '@wfp/runtime';
// Create a host environment
const env = makeEnv();
// Set context values (accessible via ctx_get_i64 in the program)
env.ctx.set('n', 41);
env.ctx.set('k', 1);
// Run the component
const { output, env: finalEnv } = await runComponent('./dist/func.wasm', env);
console.log(output); // '{"result":42}'
console.log(finalEnv.messages); // Messages sent via msg_send
console.log(finalEnv.ctx.get('n')); // Updated context valuesWorking with Resources
Resources provide object-oriented abstractions with properties and methods:
import { makeEnv, runComponent, createResourceInstance, getResourceInstance } from '@wfp/runtime';
// Create environment and resource instance
const env = makeEnv();
const handle = createResourceInstance(env, 'Counter', { value: 0 });
// Run a method on the resource
const { output } = await runComponent('./dist/counter.wasm', env, {
resourceHandle: handle,
maxSteps: 1000 // optional step limit
});
// Access updated resource properties
const instance = getResourceInstance(env, handle);
console.log(instance.properties.get('value')); // Updated counter valueCore IR Transformations
Use @wfp/core-ir for IR manipulation:
import { assignMissingIds, lambdaLift, hashObject } from '@wfp/core-ir';
// Assign stable content-hash IDs to functions
const withIds = assignMissingIds(program);
console.log(withIds.functions[0].id); // e.g., 'a1b2c3d4e5f67890'
// Lift nested functions to top-level with explicit captures
const lifted = lambdaLift(withIds);
// Compute deterministic hash of any object
const hash = hashObject({ name: 'test', value: 42 });Async Stepper Model
The platform uses an async stepper execution model where components yield effects and the host resumes them:
- run-step is called with context ID, input JSON, state bytes, and optional resume JSON
- Component returns one of:
done(output)— execution complete, return output JSONpending(effect, state)— yield effect, save state for resumptiontrap(message)— execution error
- Host handles the effect (ctx access, messaging, sleep, db query)
- Host resumes component with effect result
- Repeat until
doneortrap
This model enables:
- Deterministic replay — state can be persisted and resumed
- Effect isolation — all I/O happens in the host
- Step limiting — prevent infinite loops with max step count
Supported Effects
| Effect | Description | Resume Data |
|--------|-------------|-------------|
| ctx-get-i64 | Read i64 from context map | { i64: number } |
| ctx-set-i64 | Write i64 to context map | none |
| msg-send | Send message to topic | none |
| sleep-ms | Wait for duration | none |
| db-query | Database query (MVP placeholder) | { rows: [] } |
Examples
The examples/ directory contains several example programs with run scripts:
| Example | Description | Run Script |
|---------|-------------|------------|
| program.simple.json | Basic: reads context, computes sum, sends message | run-simple.sh |
| program.nested.json | Lambda lifting: nested function captures parent variable | run-nested.sh |
| program.converter.json | Unit conversion: reads Celsius, outputs Fahrenheit | run-converter.sh |
| program.accumulator.json | Multi-input: reads 3 values, stores sum and doubled | run-accumulator.sh |
| program.resource.json | Counter resource with increment/getValue methods | run-resource-demo.mjs |
| program.bank-account.json | Complex resource: balance, transactions, transfers | run-resource-demo.mjs |
| program.state-machine.json | State machine with transitions and error tracking | run-resource-demo.mjs |
Run all CLI examples:
./examples/run-all.shRun the programmatic resource demo:
node examples/run-resource-demo.mjsExample: Counter Resource
The examples/program.resource.json demonstrates resource usage with a Counter object:
{
"version": 1,
"functions": [
{
"id": "",
"name": "getValue",
"resource": "Counter",
"params": [],
"body": [
{"op": "return", "expr": {"op": "self_get", "property": "value"}}
]
},
{
"id": "",
"name": "increment",
"resource": "Counter",
"params": [],
"body": [
{"op": "let", "name": "current", "expr": {"op": "self_get", "property": "value"}},
{"op": "let", "name": "one", "expr": {"op": "lit_i64", "value": 1}},
{"op": "let", "name": "newValue", "expr": {"op": "add", "a": {"op": "var", "name": "current"}, "b": {"op": "var", "name": "one"}}},
{"op": "expr", "expr": {"op": "self_set", "property": "value", "value": {"op": "var", "name": "newValue"}}},
{"op": "return", "expr": {"op": "self_get", "property": "value"}}
]
}
],
"resources": [
{
"name": "Counter",
"properties": [{"name": "value", "type": "i64"}],
"methods": ["getValue", "increment", "add"]
}
]
}Expected behavior:
getValue()returns the current counter valueincrement()adds 1 to the counter and returns the new valueadd(amount)adds the specified amount and returns the new value
Running with the library:
import { makeEnv, runComponent, createResourceInstance } from '@wfp/runtime';
const env = makeEnv();
const handle = createResourceInstance(env, 'Counter', { value: 10 });
// After running increment method:
// output: '{"result":11}'
// instance.properties.get('value') === 11Philosophy
Wasm is the execution artifact. The canonical IR (and metadata like stable IDs and nesting) is the source of truth for pretty-printing in different languages.
