An opinionated WebAssembly function platform with Component Model, WIT, and async stepper execution
npm install wasmfuncplatformThis 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.
``bash
npm install
npm test
> Note: component building/transpiling uses
@bytecodealliance/jco and @bytecodealliance/componentize-js.
> Some environments require installing a JS engine for componentize-js. See docs.Layout
-
wit/function.wit — Component interface
- packages/core-ir — canonical IR + stable IDs + lambda lifting
- packages/compiler-component — IR -> guest JS -> jco componentize -> component wasm
- packages/runtime — Node host async stepper loop + effect handlers
- packages/cli — funcctl CLI
- examples/ — example programs
- docs/ — design notes and future directionsLibrary Usage
The platform can be embedded as a library for programmatic compilation and execution.
$3
Use
@wfp/compiler-component to compile programs directly from JavaScript:`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
});
`$3
Use
@wfp/runtime to execute compiled components:`javascript
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 values
`$3
Resources provide object-oriented abstractions with properties and methods:
`javascript
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 value
`$3
Use
@wfp/core-ir for IR manipulation:`javascript
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:
1. run-step is called with context ID, input JSON, state bytes, and optional resume JSON
2. Component returns one of:
-
done(output) — execution complete, return output JSON
- pending(effect, state) — yield effect, save state for resumption
- trap(message) — execution error
3. Host handles the effect (ctx access, messaging, sleep, db query)
4. Host resumes component with effect result
5. Repeat until done or trapThis 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
$3
| 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:
`bash
./examples/run-all.sh
`Run the programmatic resource demo:
`bash
node examples/run-resource-demo.mjs
`Example: Counter Resource
The
examples/program.resource.json demonstrates resource usage with a Counter object:`json
{
"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 value
- increment() adds 1 to the counter and returns the new value
- add(amount) adds the specified amount and returns the new valueRunning with the library:
`javascript
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') === 11
``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.