paulmillr/awasm-compiler
GitHub: paulmillr/awasm-compiler
一款极简、可审计的 JavaScript 到 WebAssembly 编译器,通过高层抽象和确定性构建解决原生 WASM 开发门槛高、难以审计的问题。
Stars: 25 | Forks: 1
# AWASM compiler
Auditable js-to-wasm compiler, focusing on ultra-high performance & security.
- 🪶 Small: 2 deps, 12K lines of code, fits into 64KB (zstd-compressed)
- 🏎 Fast: produces JIT-friendly code
- Multi-backend: compile to wasm, larger JS, threaded wasm, or runtime
- Parallel: manages threads and SIMD without hassle
- Stable code ordering: allows deterministic builds
### This library belongs to _awasm_
- **Reproducible builds:** deterministic cross-platform builds
- **Auditable compiler:** reasonably small JS-to-WASM compiler
- **Synchronous execution:** with optional async variant
- Minimal deps, PGP-signed releases and transparent NPM builds
- All libraries:
[awasm-noble](https://github.com/paulmillr/awasm-noble),
[awasm-compiler](https://github.com/paulmillr/awasm-compiler)
- [Check out the homepage](https://paulmillr.com/awasm/)
## Usage
import { Module, array } from '@awasm/compiler/module.js';
import { toWasm, toJs } from '@awasm/compiler/codegen.js';
import * as js from '@awasm/compiler/js.js';
// 1. Define module
const mod = new Module('example')
.mem('data', array('u32', {}, 16))
.fn('sum', [], 'u32', (s) => {
const { u32 } = s.types;
const [total] = s.doN([u32.const(0)], 16, (i, acc) => {
const val = s.memory.data[i].get();
return [u32.add(acc, val)];
});
return total;
});
// 2. Compile
const wasmCode = toWasm(mod); // WebAssembly version
const jsCode = toJs(mod); // Pure JS fallback
// 3. Execute
const instance = js.exec(wasmCode);
// 4. Use
instance.segments['data'].set(
new Uint8Array([
1, 0, 0, 0, 2, 0, 0, 0,
3, 0, 0, 0, 4, 0, 0, 0,
5, 0, 0, 0, 6, 0, 0, 0,
7, 0, 0, 0, 8, 0, 0, 0,
])
);
const result = instance.sum(); // returns sum of data array
Below are example how can awasm compiler be used.
* [Project Structure](#project-structure)
* [Differences from raw WASM](#differences-from-raw-wasm)
* [Quick Start](#quick-start)
* [Module Definition](#module-definition)
+ [Creating a Module](#creating-a-module)
+ [Composing Modules: `.use()`](#composing-modules-use)
+ [Memory: `.mem()` / `.batchMem()`](#memory-mem--batchmem)
+ [Functions: `.fn()`](#functions-fn)
+ [Batched Functions: `.batchFn()`](#batched-functions-batchfn)
+ [Import Functions: `.importFn()`](#import-functions-importfn)
* [Compilation & Execution](#compilation--execution)
+ [Compiling](#compiling)
+ [Executing](#executing)
+ [Writing to Files](#writing-to-files)
+ [Runtime Interpreter](#runtime-interpreter)
+ [Instance Shape](#instance-shape)
+ [Accessing Memory from JS](#accessing-memory-from-js)
+ [Debugging](#debugging)
* [Scope Reference](#scope-reference)
* [Types](#types)
+ [Type Methods](#type-methods)
+ [Type Conversions](#type-conversions)
* [Operations](#operations)
+ [Basic Arithmetic](#basic-arithmetic)
+ [Comparison](#comparison)
+ [Bitwise (Integer Only)](#bitwise-integer-only)
+ [Shifts (Integer Only)](#shifts-integer-only)
+ [Signed Only](#signed-only)
+ [Float Only](#float-only)
+ [SIMD Only](#simd-only)
+ [Generics](#generics)
* [Memory Access](#memory-access)
+ [Basic Access](#basic-access)
+ [Views](#views)
+ [Byte Operations](#byte-operations)
+ [SIMD Lanes](#simd-lanes)
+ [Atomics](#atomics)
+ [Mut (Non-Atomic RMW)](#mut-non-atomic-rmw)
* [Control Flow](#control-flow)
+ [State-Passing Model](#state-passing-model)
+ [Loops](#loops)
+ [Conditionals](#conditionals)
+ [Low-Level Control](#low-level-control)
* [Quick Reference](#quick-reference)
+ [Operations by Type](#operations-by-type)
+ [Memory Quick Reference](#memory-quick-reference)
+ [Control Flow Quick Reference](#control-flow-quick-reference)
## Project structure
The compiler is structured as follows:
- `wasm.ts`: generic binary encoder/decoder for wasm. not full spec (tables/extref missing), but can be used to inspect generated wasm modules
- `js.ts`: wasm ops -> js ops code generation, wasm boilerplate, web workers boilerplate
- `runtime.ts`: small runtime executor/interpreter. **NOTE**: should have minimum amount of dependencies on other stuff
- `module.ts`: small structure that holds functions/memory definitions, user facing types. Used for executor.
- `types.ts`: definitions of operations for various types.
- `memory.ts`:
- `allocateMemSpec`: calculates sizes/alignment of nested memory structures
- `memoryProxy`: user facing API for memory operations
- `memOps`: compiler specific operations for `memoryProxy` (not used in executor!)
- `codegen.ts`
- `toInstr`: collapses `TreeDAG` into stack-based operations for wasm/js code generation, strips types (u32->i32).
- `toWasm`/`toJs`: compiles `Module` into wasm/js code.
- `rewrites.ts`: graph transformation
**NOTE**: it is important that all transformations are stable (we cannot have two transformation that does `a->b` and then `b->a`), since we don't have
compiler passes budgets to enforce reproducible builds. All transformation continuosly applied until there is no changes to graph.
- `lowerSIMD`: lowers SIMD operation to scalar ones
- `lowerU64`: lowers u64/i64 operations into pairs of u32/i32
- `lowerVirtualSIMDPairs`: lowers SIMD virtual types like `u64x4` -> `2xu64x2`
- `lowerVirtualSIMDMask`: lowers SIMD masked virtual types like `u32x2` -> `u32x4`
- `lowerPattern`: merges `pattern` operation (same as SIMD shuffle, but for scalars) into load/store for swapEndianess.
- `lowerU64Arg`: lowers i64/u64 function arguments into two i32/u32. separate from 'lowerU64' because changes API, also because current graph is per function only.
- `lowerWasm`: fixes various unsupported operations in wasm, like missing 'not'/'neg', etc.
- `lowerPatternJS`: lowers 'pattern' that wasn't merged into store/load. Mostly to allow 'swapEndianess' in runtime type modules/tests.
- `optimize`: constant folding and various small optimizations
- `utils.ts`: various small utils.
- `TreeDAG` - core of compiler, data structure that represents tree of directed acyclic graphs. Applies rewrites, removes unused nodes, does topological sort.
- `workers.ts`: helper functions for threading/simd, processes `batchFn`.
## Differences from raw WASM
WASM is designed for encoding compactness, not ergonomics. We provide:
| WASM limitation | AWASM solution |
| ----------------------------------------------------------------- | ----------------------------- |
| No `u32`/`u64` types (only `i32` + unsigned ops) | Proper unsigned types |
| No bitwise ops on `i32x4`/`i64x2` (only `v128`) | Bitwise ops on all SIMD types |
| `not` is SIMD-only | `not` on scalars too |
| No `rotl`/`rotr` in SIMD | Rotation on all types |
| No lane swizzles for `i32x4`/`i64x2` | `shuffleLanes` for all SIMD |
| No `eqz` on SIMD | Added |
| No unsigned comparisons on `i64x2` | Added |
| SIMD compares produce mask vectors that can’t be used with select | Unified via `select` handling |
Plus higher-level conveniences: endianness conversion, unified scalar/SIMD API with automatic interleaving.
## Quick Start
import { Module, array } from '@awasm/compiler/module.js';
import { toWasm, toJs } from '@awasm/compiler/codegen.js';
import * as js from '@awasm/compiler/js.js';
// 1. Define module
const mod = new Module('example')
.mem('data', array('u32', {}, 16))
.fn('sum', [], 'u32', (s) => {
const { u32 } = s.types;
const [total] = s.doN([u32.const(0)], 16, (i, acc) => {
const val = s.memory.data[i].get();
return [u32.add(acc, val)];
});
return total;
});
// 2. Compile
const wasmCode = toWasm(mod); // WebAssembly version
const jsCode = toJs(mod); // Pure JS fallback
// 3. Execute
const instance = js.exec(wasmCode);
// 4. Use
instance.segments['data'].set(
new Uint8Array([
1, 0, 0, 0, 2, 0, 0, 0,
3, 0, 0, 0, 4, 0, 0, 0,
5, 0, 0, 0, 6, 0, 0, 0,
7, 0, 0, 0, 8, 0, 0, 0,
])
);
const result = instance.sum(); // returns sum of data array
## Module Definition
### Creating a Module
import { Module, array } from '@awasm/compiler/module.js';
const mod = new Module('moduleName') // name used in generated code
.mem('data', array('u32', {}, 64)) // define memory region
.batchMem('work', array('u32', {}, 64)) // define batched memory (auto-sized for SIMD/threads)
.fn('sum', [], 'u32', (s) => s.types.u32.const(0)) // define function
.batchFn('process', { lanes: 4 }, ['u32'], () => {}) // define batched/parallel function
.importFn('log', ['u32'], 'void', (value) => console.log(value)) // import external function
.use((m) => m); // compose with another module builder
Methods are chainable and return the module for further definition.
### Composing Modules: `.use()`
`.use(transformer)` applies a function that extends the module. Useful for reusable patterns:
import { Module, array, type FnRegistry, type Segs } from '@awasm/compiler/module.js';
// Define reusable module extension
function addPadding(mod: Module) {
return mod
.mem('padBuffer', array('u32', {}, 64))
.fn('pad', ['u32'], 'void', (s, len) => {
/* ... */
});
}
// Use it
const mod = new Module('hash')
.mem('state', array('u32', {}, 8))
.use(addPadding) // adds padBuffer and pad function
.fn('hash', ['u32'], 'void', (s, len) => {
s.functions.pad.call(len); // can call the added function
});
### Memory: `.mem()` / `.batchMem()`
import { Module, array, struct } from '@awasm/compiler/module.js';
const mod = new Module('memory')
.mem('state', struct({ counter: 'u64' }))
.batchMem('streams', array('u32', {}, 64)); // wraps in array, outer dimension auto-sized
`batchMem` converts the spec to an array if not already one, then adds an outer dimension sized for parallelism (SIMD lanes × thread count). For arrays, it just prepends the dimension; for non-arrays (struct, scalar), it wraps them in an array first.
**Specs:**
| Spec | Example |
| ----------------------------- | -------------------------------- |
| `array(type, opts, ...sizes)` | `array('u32', {}, 64, 64)` |
| `struct({ fields }, opts)` | `struct({ x: 'f32', y: 'f32' })` |
| `scalar(type, opts)` | `scalar('u64')` |
Specs can be nested arbitrarily:
import { array, struct } from '@awasm/compiler/module.js';
// Array of structs
array(struct({ x: 'f32', y: 'f32', z: 'f32' }), {}, 100);
// Struct with nested array
struct({
header: 'u64',
data: array('u32', {}, 256),
checksum: 'u32',
});
// Deeply nested
struct({
meta: struct({ version: 'u32', flags: 'u32' }),
blocks: array(struct({ id: 'u64', payload: array('u32', {}, 16) }), {}, 16),
});
**Options:**
| Option | Description |
| ---------------- | ---------------------------------------------------- |
| `swapEndianness` | Byte-swap on load/store (see note below) |
| `align` | Starting position alignment (default: 16 for arrays) |
| `alignEnd` | End padding alignment |
**Endianness:** Memory defaults to little-endian (WASM behavior). With `swapEndianness: true`, data is read/written as big-endian. Note: not tested on native big-endian systems.
**Fixed size:** Memory size is fixed at compile time — no grow, no shrink.
Types can be nested arbitrarily.
### Functions: `.fn()`
Use `.fn(name, inputs, outputs, callback)` to define an exported function.
- `inputs`: Array of input types `['u32', 'u64', ...]`
- `outputs`: Return type(s) `'u32'` or `['u32', 'u32']` or `'void'`
- `callback`: `(scope, ...args) => returnValue`
import { Module } from '@awasm/compiler/module.js';
const mod = new Module('functions')
.fn('add', ['u32', 'u32'], 'u32', (s, a, b) => {
return s.types.u32.add(a, b);
})
.fn('swap', ['u32', 'u32'], ['u32', 'u32'], (s, a, b) => {
return [b, a]; // multiple returns
});
### Batched Functions: `.batchFn()`
For SIMD/parallel processing:
Use `.batchFn(name, opts, inputs, callback)` for SIMD/parallel processing.
- `opts`: `{ lanes: number, perThread?: number }`
- `callback`: `(scope, lanes, batchPos, perBatchSize, ...args) => void`
**Important:** The callback signature differs from how the function is called:
import { Module } from '@awasm/compiler/module.js';
import { toWasm } from '@awasm/compiler/codegen.js';
import * as js from '@awasm/compiler/js.js';
// Definition: callback receives (scope, lanes, pos, perBatchSize, ...args)
const mod = new Module('batch')
.batchFn(
'process',
{ lanes: 4 },
['u32', 'u32'],
(s, lanes, pos, perBatch, arg1, arg2) => {
// lanes: 1 for scalar, 4 for SIMD
// pos: current batch position
// perBatch: passed through from caller, used for thread work allocation
}
);
// Usage: called as (batchPos, batchLen, perBatchSize, ...args)
const instance = js.exec(toWasm(mod, { useSIMD: false, useThreads: false }));
const arg1Value = 1;
const arg2Value = 2;
instance.process(0, 100, 16, arg1Value, arg2Value);
The `perBatchSize` parameter indicates how much work each batch item represents. It's passed through to the callback and used internally for thread allocation when `perThread` is set.
**Combined example with batchMem and lanes:**
import { Module, array, struct } from '@awasm/compiler/module.js';
const mod = new Module('parallel')
// batchMem: outer dimension auto-sized for parallelism
.batchMem(
'streams',
struct({
state: array('u32', {}, 8),
counter: 'u64',
})
)
.batchFn('process', { lanes: 4 }, ['u32'], (s, lanes, pos, perBatch, rounds) => {
const T = s.getType('u32', lanes);
// .lanes(lanes)[pos] accesses `lanes` parallel streams at once
const stream = s.memory.streams.lanes(lanes)[pos];
// Load state from 4 parallel streams as SIMD vectors
const state = stream.state.get(); // array of u32x4
// Process...
const newState = state.map((v) => T.add(v, T.const(1)));
// Store back to 4 streams
stream.state.set(newState);
});
// Called as: instance.process(batchPos, batchLen, perBatchSize, rounds)
**How batching works:** The `batchLen` parameter controls the internal loop — your callback doesn't see it directly. Instead, the runtime calls your callback multiple times:
- With `lanes=4` (or your configured max) for full SIMD batches
- With `lanes=1` for leftover elements
Example: 17 items with `{ lanes: 4 }` → callback called with `lanes=4` at positions 0, 4, 8, 12, then `lanes=1` at position 16.
### Import Functions: `.importFn()`
Use `.importFn(name, inputs, outputs, callback?, module?)` to declare imports.
Two modes:
1. **With callback**: Function is serialized via `.toString()` and embedded. **Cannot capture closures** — only reference global variables.
import { Module } from '@awasm/compiler/module.js';
const mod = new Module('imports')
.importFn('log', ['u32'], 'void', (value) => {
console.log('Value:', value); // uses global console
});
2. **Without callback**: Function must be provided at runtime via `_imports`. Looks in `_imports.env` by default, or `_imports[module]` if module specified.
import { Module } from '@awasm/compiler/module.js';
import { toJs } from '@awasm/compiler/codegen.js';
import * as js from '@awasm/compiler/js.js';
// Definition
const code = toJs(
new Module('imports')
.importFn('hash', ['u32', 'u32'], 'u32')
.importFn('compress', ['u32'], 'void', undefined, 'crypto')
);
// Usage
js.exec(code, {
env: { hash: (a, b) => a ^ b },
crypto: { compress: (x) => void x }
});
## Compilation & Execution
### Compiling
import { Module } from '@awasm/compiler/module.js';
import { toWasm, toJs } from '@awasm/compiler/codegen.js';
const mod = new Module('compile')
.fn('zero', [], 'u32', (s) => s.types.u32.const(0));
const wasmResult = toWasm(mod); // Compiles to WebAssembly
const jsResult = toJs(mod); // Compiles to pure JavaScript
Both return an object:
type CompileResult = {
raw: string, // IIFE code to execute
typeRaw: string, // TypeScript type definition
modFn: string, // ES module export
modFnType: string, // ES module type export
};
### Executing
import { Module } from '@awasm/compiler/module.js';
import { toJs, toWasm } from '@awasm/compiler/codegen.js';
import * as js from '@awasm/compiler/js.js';
const mod = new Module('exec')
.fn('zero', [], 'u32', (s) => s.types.u32.const(0));
const wasmResult = toWasm(mod);
const jsResult = toJs(mod);
const imports = {};
const pool = undefined;
const wasmInstance = js.exec(wasmResult);
// or
const jsInstance = js.exec(jsResult);
// or
const pooledInstance = js.exec(wasmResult, imports, pool);
### Writing to Files
To avoid `js.exec` (which uses `eval`), write the generated code to files and import:
import { mkdirSync, writeFileSync } from 'fs';
import { Module } from '@awasm/compiler/module.js';
import { toWasm } from '@awasm/compiler/codegen.js';
const mod = new Module('file_example')
.fn('zero', [], 'u32', (s) => s.types.u32.const(0));
const result = toWasm(mod);
// Write as ES module
mkdirSync('./build', { recursive: true });
writeFileSync('./build/myModule.js', result.modFn);
writeFileSync('./build/myModule.d.ts', result.modFnType);
// Then import normally
const { default: myModule } = await import('./build/myModule.js');
const instance = myModule();
### Runtime Interpreter
For debugging or executing without a compilation step (also smaller build size):
import { Module } from '@awasm/compiler/module.js';
import { toJs } from '@awasm/compiler/codegen.js';
import { toRuntime } from '@awasm/compiler/runtime.js';
import { genRuntimeTypeMod, TYPE_MOD_OPTS } from '@awasm/compiler/types.js';
import * as js from '@awasm/compiler/js.js';
// Generate type module once
const typeMod = js.exec(toJs(genRuntimeTypeMod(), TYPE_MOD_OPTS));
// Create interpreter instance
const mod = new Module('runtime')
.fn('zero', [], 'u32', (s) => s.types.u32.const(0));
const instance = toRuntime(() => typeMod, mod)();
### Instance Shape
type Instance = {
// Exported functions
sum(): number;
process(a: number, b: number): void;
// Raw memory buffer
memory: Uint8Array;
// Named memory segment views
segments: {
data: Uint8Array;
'state.counter': Uint8Array;
'state.buffer': Uint8Array;
// ...
};
};
**JS memory views:** All exported segments are `Uint8Array` views (bytes), regardless of element type.
**`_chunks`:** For batched memory, `segments['name']` gives the full region while `segments['name']._chunks` is an array indexing into the outer (batch) dimension. Use `_chunks[i]` to access individual batch slots.
**u64 at JS boundary:** Returns either BigInt or `[lo, hi]` pair depending on compiler options.
### Accessing Memory from JS
import { Module, array } from '@awasm/compiler/module.js';
import { toJs } from '@awasm/compiler/codegen.js';
import * as js from '@awasm/compiler/js.js';
const mod = new Module('memory_access')
.mem('data', array('u8', {}, 16))
.mem('result', array('u8', {}, 16));
const instance = js.exec(toJs(mod));
const inputBytes = new Uint8Array([1, 2, 3]);
const data = new Uint8Array([4, 5, 6]);
const offset = 0;
// Read/write via segments
instance.segments['data'].set(inputBytes);
const output = instance.segments['result'].slice();
// Or via raw memory at specific offsets
instance.memory.set(data, offset);
**Segments vs raw memory:** Segments abstract away internal padding/alignment. The `segments['name']` view gives you exactly the data described by your spec, even if the underlying memory has padding between fields.
### Debugging
Use `s.print()` inside functions to log values at runtime (converted to u32 for display).
To inspect generated code, access `result.raw` — it's a JS string containing either pure JS code or JS boilerplate that instantiates the WASM module:
import { Module } from '@awasm/compiler/module.js';
import { toJs, toWasm } from '@awasm/compiler/codegen.js';
const mod = new Module('debug')
.fn('zero', [], 'u32', (s) => s.types.u32.const(0));
const result = toJs(mod);
console.log(result.raw); // readable JS implementation
const wasmResult = toWasm(mod);
console.log(wasmResult.raw); // JS with embedded WASM base64
## Scope Reference
The first argument to function callbacks is the `Scope`, providing access to everything:
import { Module, array, type UnsignedType } from '@awasm/compiler/module.js';
const mod = new Module('scope')
.mem('buffer', array('u32', {}, 8))
.importFn('helper', ['u32'], 'u32')
.importFn('sideEffect', ['u32'], 'void')
.fn('example', ['u32'], 'void', (s, arg) => {
// Type operations
const { u32, f64, u32x4 } = s.types;
// Dynamic type access
const T = s.getType('u32', 4); // concrete type
// OR
const Generic = s.getTypeGeneric('u32'); // generic
// Memory access
const value = u32.add(arg, u32.const(1));
s.memory.buffer[0].get();
s.memory.buffer[0].set(value);
// Call other functions
const [result] = s.functions.helper.call(arg);
const cond = u32.eq(result, u32.const(0));
s.functions.sideEffect.callIf(cond, arg); // conditional, no return
// Control flow
s.doN([value], 2, (i, cur) => [u32.add(cur, i)]);
s.ifElse(cond, [value], (cur) => [u32.add(cur, arg)], (cur) => [cur]);
// ... see Control Flow section
// Debug
s.print('value =', value);
});
**Important concept:** Values like `arg`, `val`, etc. are compile-time handles (symbolic representations), not actual runtime values. Operations build a computation graph that gets compiled to WASM/JS. You cannot inspect their values at definition time — they only exist at runtime.
## Types
| Base | Description | 2 lanes | 4 lanes | 8 lanes | 16 lanes |
| ------ | ------------------------ | -------- | -------- | -------- | --------- |
| `i8` | 8-bit signed integer | `i8x2` | `i8x4` | `i8x8` | `i8x16` |
| `u8` | 8-bit unsigned integer | `u8x2` | `u8x4` | `u8x8` | `u8x16` |
| `i16` | 16-bit signed integer | `i16x2` | `i16x4` | `i16x8` | `i16x16` |
| `u16` | 16-bit unsigned integer | `u16x2` | `u16x4` | `u16x8` | `u16x16` |
| `i32` | 32-bit signed integer | `i32x2` | `i32x4` | `i32x8` | `i32x16` |
| `u32` | 32-bit unsigned integer | `u32x2` | `u32x4` | `u32x8` | `u32x16` |
| `f32` | 32-bit float | `f32x2` | `f32x4` | `f32x8` | `f32x16` |
| `i64` | 64-bit signed integer | `i64x2` | `i64x4` | `i64x8` | `i64x16` |
| `u64` | 64-bit unsigned integer | `u64x2` | `u64x4` | `u64x8` | `u64x16` |
| `f64` | 64-bit float | `f64x2` | `f64x4` | `f64x8` | `f64x16` |
| `i128` | 128-bit signed integer | `i128x2` | `i128x4` | `i128x8` | `i128x16` |
| `u128` | 128-bit unsigned integer | `u128x2` | `u128x4` | `u128x8` | `u128x16` |
| `i256` | 256-bit signed integer | `i256x2` | `i256x4` | `i256x8` | `i256x16` |
| `u256` | 256-bit unsigned integer | `u256x2` | `u256x4` | `u256x8` | `u256x16` |
**Note:** There are no native 8-bit or 16-bit register types. Like WASM, this operates at register level (32/64 bit) — `i8/u8/i16/u16` are virtual and lowered to `i32/u32`. For byte-level _memory_ access, use views: `.as8()`, `.as16()`, `.as32()`. Lane-count variants are real types (e.g. `u8x4`, `u16x2`); `getType('u8', 4)`/`getType('u16', 2)` is the generic way to select them. `i128/u128/i256/u256` have virtual SIMD lane variants (lowered to scalar ops) and are currently supported via conversions to/from `u32/u64` parts.
### Type Methods
| Method | Description |
| ---------------------- | ----------------------------------------------------------- |
| `const(value)` | Create constant. For SIMD, broadcasts to all lanes. |
| `laneOffsets(offset?)` | Scalar: `0 + offset`. SIMD: `[0, 1, 2, ...]` + offset |
| `select(cond, a, b)` | `cond ? a : b`. For SIMD, accepts vector mask as condition. |
| `swapEndianness(a)` | Reverse byte order within each lane. |
**laneOffsets example:**
import { Module } from '@awasm/compiler/module.js';
const mod = new Module('lane_offsets')
.fn('demo', [], 'void', (s) => {
const { u32, u32x4 } = s.types;
u32.laneOffsets(10); // → 10
u32x4.laneOffsets(10); // → [10, 11, 12, 13]
});
### Type Conversions
| Method | Description |
| ------------------------ | --------------------------------------------------- |
| `to(dstType, value)` | Convert to different type, returns array |
| `from(srcType, values)` | Convert from different type, returns array |
| `toN(dstType, value)` | Same as `to(...)[0]` — returns first element only |
| `fromN(srcType, values)` | Same as `from(...)[0]` — returns first element only |
| `castFrom(srcType, v)` | Bitcast with size checks; no-op for ints |
| `castTo(dstType, v)` | Same as `dstType.castFrom(srcType, v)` |
Use `from`/`to` when conversion changes element count (split u64 → [lo, hi], u16 → [lo, hi] u8). Use `fromN`/`toN` as shorthand when you only need the first result (e.g., low word of u64, first lane of SIMD).
**Conversion behavior:**
| From → To | Behavior |
| -------------------------------- | -------------------------------- |
| `u64` → `u32` | Split: returns `[lo, hi]` |
| `[u32, u32]` → `u64` | Combine lo/hi |
| `u32` → `u64` | Extend (sign/zero based on type) |
| `u32x4` → `u32` | Extract all lanes |
| `[u32, u32, u32, u32]` → `u32x4` | Pack into vector |
| `u32` → `u32x4` | Splat to all lanes |
## Operations
### Basic Arithmetic
Available on **all types**. Operations marked "variadic" accept 2+ arguments.
| Op | Arity | Equivalent | Notes |
| ----- | -------- | ----------- | ----------------------------------------------- |
| `add` | variadic | `a + b` | |
| `sub` | 2 | `a - b` | |
| `mul` | variadic | `a * b` | |
| `div` | 2 | `a / b` | WASM traps on zero; JS returns `Infinity`/`NaN` |
| `rem` | 2 | `a % b` | Floats: `a - trunc(a/b) * b` |
| `min` | variadic | `min(a, b)` | |
| `max` | variadic | `max(a, b)` | |
### Comparison
Available on **all types**. Returns `u32` with 0/1 for scalars, `u32xN`/`u64xN` with bitmask (like `0xffff_ffff`) for SIMD.
| Op | Equivalent |
| ----- | ---------- |
| `eq` | `a == b` |
| `ne` | `a != b` |
| `lt` | `a < b` |
| `gt` | `a > b` |
| `le` | `a <= b` |
| `ge` | `a >= b` |
| `eqz` | `a == 0` |
### Bitwise (Integer Only)
| Op | Arity | Equivalent |
| -------- | -------- | -------------------- |
| `and` | variadic | `a & b` |
| `or` | variadic | `a \| b` |
| `xor` | variadic | `a ^ b` |
| `andnot` | 2 | `a & ~b` |
| `not` | 1 | `~a` |
| `clz` | 1 | Count leading zeros |
| `ctz` | 1 | Count trailing zeros |
| `popcnt` | 1 | Population count |
### Shifts (Integer Only)
Shift amount is `number | Val<'i32'>`. For SIMD, same shift applies to all lanes.
| Op | Equivalent | Notes |
| ------ | ----------------- | ----------------------------------------- |
| `shl` | `a << n` | |
| `shr` | `a >> n` | Arithmetic (signed) or logical (unsigned) |
| `rotl` | Rotate bits left | |
| `rotr` | Rotate bits right | |
**`shr` behavior:** On signed types (`i32`, `i64`) sign-extends (arithmetic shift). On unsigned types (`u32`, `u64`) zero-extends (logical shift).
Shift/rotate behavior matches WebAssembly exactly (including how large shift counts are handled).
### Signed Only
| Op | Equivalent |
| ----- | ---------- |
| `abs` | `\|a\|` |
| `neg` | `-a` |
### Float Only
| Op | Description |
| ---------- | --------------------------------- |
| `sqrt` | Square root |
| `ceil` | Round toward +∞ |
| `floor` | Round toward -∞ |
| `trunc` | Round toward zero |
| `nearest` | Round to nearest, ties to even |
| `copysign` | Magnitude of `a` with sign of `b` |
| `isNaN` | Returns true if NaN |
### SIMD Only
| Op | Description |
| ----------------------------- | -------------------------------------- |
| `extractLane(vec, lane)` | Extract scalar from lane |
| `replaceLane(vec, lane, val)` | Replace value at lane |
| `splat(scalar)` | Broadcast to all lanes |
| `shuffle(a, b, pattern)` | Byte-level shuffle (16 indices, 0..31) |
| `shuffleLanes(a, b, pattern)` | Lane-level shuffle |
| `rol(vec, n)` | Rotate lanes left |
| `ror(vec, n)` | Rotate lanes right |
| `interleave(vecs)` | Interleave for SIMD processing |
| `deinterleave(vecs)` | Reverse interleave |
**`shuffle` vs `shuffleLanes`:**
- `shuffle`: WASM byte-level shuffle. Pattern has 16 elements, indices 0..31 select bytes from concatenated `[a, b]`.
- `shuffleLanes`: Lane-level shuffle. Pattern length = lane count, indices 0..(2×lanes-1).
**shuffleLanes example** (`u32x4`):
a = [A0, A1, A2, A3], b = [B0, B1, B2, B3]
concat = [A0, A1, A2, A3, B0, B1, B2, B3] // indices 0-7
shuffleLanes(a, b, [0, 4, 1, 5]) → [A0, B0, A1, B1]
**`rol`/`ror` vs `rotl`/`rotr`:**
- `rol`/`ror` rotate _lanes_ within a vector
- `rotl`/`rotr` rotate _bits_ within each lane value
**interleave/deinterleave example** (`u32x4`)
Requires: the input length must be a multiple of the lane count (here: multiple of 4).
Input (4 independent streams):
- A = `[A0,A1,A2,A3]`
- B = `[B0,B1,B2,B3]`
- C = `[C0,C1,C2,C3]`
- D = `[D0,D1,D2,D3]`
After `interleave([A,B,C,D])`:
- `[A0,B0,C0,D0]`
- `[A1,B1,C1,D1]`
- `[A2,B2,C2,D2]`
- `[A3,B3,C3,D3]`
`deinterleave` reverses this transformation.
### Generics
Sometimes you want the same algorithm for different types — say, a hash that works on both `u32` and `u64`. The challenge: memory and operations must use the _same_ concrete type, but TypeScript doesn't automatically track that connection.
import { Module, array } from '@awasm/compiler/module.js';
import type { UnsignedType } from '@awasm/compiler/types.js';
// WITHOUT generics — broken: memory is u32, but T could be u64!
function broken(type: T) {
return new Module('oops')
.mem('buf', array('u32', {}, 8)) // hardcoded u32
.fn('test', [], 'void', (f) => {
const U = f.types.u32; // hardcoded u32
// ... what if T was u64?
});
}
broken('u32');
Use `toGeneric` for memory specs and `getTypeGeneric` for operations — both preserve the type parameter `T`:
import { Module, array, toGeneric } from '@awasm/compiler/module.js';
import type { UnsignedType } from '@awasm/compiler/types.js';
function gen(type: T) {
const memType = toGeneric(type);
return new Module('generic')
.mem('buf', array(memType, {}, 8)) // u32 or u64, depending on T
.fn('test', [], 'void', (f) => {
const U = f.getTypeGeneric(type); // matching ops
const x = f.memory.buf[0].get();
f.memory.buf[0].set(U.add(x, U.const(1)));
});
}
// Now both versions are generated correctly:
const mod32 = gen('u32'); // everything is u32
const mod64 = gen('u64'); // everything is u64
The `` part tells TypeScript: "T is some unsigned type, give me operations that work on unsigned types." This keeps type-checking tight while generating code for whichever concrete type you pass in.
## Memory Access
### Basic Access
import { Module, array, struct } from '@awasm/compiler/module.js';
const mod = new Module('access')
.mem('buffer', array('u32', {}, 8))
.mem('matrix', array('u32', {}, 2, 2))
.mem('state', struct({ counter: 'u32', data: array('u32', {}, 2) }))
.fn('demo', ['u32'], 'void', (s, i) => {
const val = s.types.u32.const(1);
// Indexing
s.memory.buffer[i].get(); // load
s.memory.buffer[i].set(val); // store
// Multidimensional
s.memory.matrix[0][1].get();
// Struct fields
s.memory.state.counter.get();
s.memory.state.data[0].set(val);
});
For arrays, `get()` returns nested arrays matching shape.
For structs, `get()` returns a JS object where keys are field names and values are symbolic handles:
import { Module, struct } from '@awasm/compiler/module.js';
const mod = new Module('point')
.mem('point', struct({ x: 'u32', y: 'u32' }))
.fn('sum', [], 'u32', (s) => {
const point = s.memory.point.get(); // { x: , y: }
return s.types.u32.add(point.x, point.y); // use fields in operations
});
Partial struct updates supported.
**Symbolic indexing:** Array indices and sizes can be runtime values (`Val<'u32'>`), not just constants:
import { Module, array } from '@awasm/compiler/module.js';
const mod = new Module('symbolic_index')
.mem('buffer', array('u32', {}, 16))
.fn('read', ['u32', 'u32', 'u32'], 'void', (s, idx, start, len) => {
// Index with runtime value
const val = s.memory.buffer[idx].get(); // idx can be u32 constant or variable
// Range with runtime values
const slice = s.memory.buffer.range(start, len);
});
**No bounds checking:** There are no runtime bounds checks for symbolic/dynamic indices. WASM may trap on significantly out-of-bounds access (page faults), but JS will silently read/write garbage or return undefined. The only guaranteed error is WASM trap on division by zero.
### Views
| Method | Description |
| ---------------------- | ------------------------- |
| `.range(start?, len?)` | Slice to subrange |
| `.reshape(...sizes)` | Reinterpret dimensions |
| `.flat()` | Flatten to 1D |
| `.as(type)` | Reinterpret element type |
| `.as8(type?)` | Byte view (1-byte access) |
| `.as16(type?)` | 16-bit view |
| `.as32(type?)` | 32-bit view |
### Byte Operations
On `.as8()` views:
| Method | Description |
| -------------------------- | ------------------------------ |
| `.copyFrom(src, len?)` | Copy bytes from another region |
| `.fill(value, len?)` | Fill with byte value |
| `.zero(len?)` | Fill with zeros |
| `.read(type, size?)` | Read as type/width |
| `.write(type, val, size?)` | Write as type/width |
### SIMD Lanes
`.lanes(n)` enables strided SIMD access:
import { Module, array } from '@awasm/compiler/module.js';
const mod = new Module('simd_lanes')
.mem('data', array('u32', {}, 8, 8, 8))
.fn('copy', ['u32', 'u32'], 'void', (s, streamIdx, pos) => {
// array[N, M, K]
const view = s.memory.data[streamIdx]; // shape [M, K]
const strided = view.lanes(4)[pos]; // access pos, pos+1, pos+2, pos+3 in M
const vectors = strided.get(); // auto-interleaved for SIMD
strided.set(vectors); // auto-deinterleaved back
});
### Atomics
On scalar integer locations:
import { Module, struct } from '@awasm/compiler/module.js';
const mod = new Module('atomics')
.mem('state', struct({ counter: 'u32' }))
.fn(
'demo',
['u32', 'u32'],
'void',
(s, expected, replacement) => {
const loc = s.memory.state.counter;
const value = s.types.u32.const(1);
loc.atomics.load();
loc.atomics.store(value);
loc.atomics.exchange(value);
loc.atomics.compareExchange(expected, replacement);
loc.atomics.add(value); // also: sub, and, or, xor
// `wait`/`notify` follow standard WebAssembly atomics semantics:
// https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Memory/Wait
loc.atomics.wait(expected, -1);
loc.atomics.notify(1);
loc.atomics.fence();
}
);
### Mut (Non-Atomic RMW)
import { Module, struct } from '@awasm/compiler/module.js';
const mod = new Module('mut')
.mem('state', struct({ counter: 'u32' }))
.fn(
'demo',
['u32', 'u32'],
'void',
(s, expected, replacement) => {
const loc = s.memory.state.counter;
const value = s.types.u32.const(1);
loc.mut.exchange(value);
loc.mut.compareExchange(expected, replacement);
loc.mut.add(value); // val += x, returns old
// ... all type ops available
}
);
## Control Flow
### State-Passing Model
All control flow uses state-passing. State flows through, body transforms it, construct returns final state.
import { Module } from '@awasm/compiler/module.js';
const mod = new Module('state_passing').fn('sum10', [], 'u32', (s) => {
const { u32 } = s.types;
const [sum] = s.doN(
[u32.const(0)], // initial state
10, // iterations
(i, acc) => [u32.add(acc, i)] // body returns new state
);
return sum;
});
**Important:** JS runs at compile time. Don't modify JS variables inside bodies:
import { Module } from '@awasm/compiler/module.js';
// WRONG
const wrong = new Module('wrong_state').fn('wrong', [], 'void', (s) => {
let jsCounter = 0;
s.doN([], 10, () => {
jsCounter++;
return [];
}); // jsCounter++ runs once at compile time!
});
// CORRECT - use state
const correct = new Module('correct_state').fn('correct', [], 'u32', (s) => {
const { u32 } = s.types;
const [counter] = s.doN([u32.const(0)], 10, (i, x) => [u32.add(x, u32.const(1))]);
return counter;
});
### Loops
| Construct | Executes | Condition |
| --------------------------------- | ------------ | ----------- |
| `doN(state, count, body)` | 0 to N times | Before body |
| `doN1(state, count, body)` | 1 to N times | After body |
| `doWhile(state, cond, body)` | 1+ times | After body |
| `forLoop(state, cond, inc, body)` | 0+ times | Before body |
import { Module } from '@awasm/compiler/module.js';
// doN: 0..N iterations
const mod = new Module('loops')
.fn('doNSum', [], 'u32', (s) => {
const { u32 } = s.types;
const [sum] = s.doN([u32.const(0)], 10, (i, acc) => [u32.add(acc, i)]);
return sum;
})
// doWhile: at least once
.fn('doWhileVal', [], 'u32', (s) => {
const { u32 } = s.types;
const [val] = s.doWhile(
[u32.const(1)],
(val) => u32.lt(val, u32.const(100)),
(val) => [u32.mul(val, u32.const(2))]
);
return val;
})
// forLoop: traditional for
.fn('forLoopSum', [], 'u32', (s) => {
const { u32 } = s.types;
const [sum] = s.forLoop(
[u32.const(0), u32.const(0)], // [sum, i]
(sum, i) => u32.lt(i, u32.const(10)), // condition
(sum, i) => [sum, u32.add(i, u32.const(1))], // increment
(sum, i) => [u32.add(sum, i), i] // body
);
return sum;
});
### Conditionals
import { Module } from '@awasm/compiler/module.js';
// With else
const mod = new Module('conditionals')
.fn('withElse', ['u32'], 'u32', (s, initialValue) => {
const { u32 } = s.types;
const condition = u32.gt(initialValue, u32.const(0));
const [result] = s.ifElse(
condition,
[initialValue],
(val) => [u32.add(val, u32.const(1))],
(val) => [u32.const(0)]
);
return result;
})
// Without else (state unchanged if false)
.fn('withoutElse', ['u32'], 'u32', (s, value) => {
const { u32 } = s.types;
const condition = u32.gt(value, u32.const(0));
const [result] = s.ifElse(condition, [value], (val) => [u32.add(val, u32.const(1))]);
return result;
});
### Low-Level Control
import { Module } from '@awasm/compiler/module.js';
// Named blocks for complex control flow
const mod = new Module('control')
.fn('named', ['u32', 'u32', 'u32'], ['u32', 'u32'], (s, a, b, flag) => {
const { u32 } = s.types;
const cond = u32.ne(flag, u32.const(0));
const [x, y] = s.namedBlock('outer', [a, b], (x, y) => {
s.breakIf(cond, 'outer', x, y);
return [u32.add(x, u32.const(1)), u32.add(y, u32.const(1))];
});
return [x, y];
})
// Branch behavior depends on block type:
// - block: br exits (like break)
// - loop: br jumps to start (like continue)
// High-level loop control (inside doN/forLoop/doWhile)
.fn('loopControl', ['u32'], 'u32', (s, limit) => {
const { u32 } = s.types;
const [sum] = s.forLoop(
[u32.const(0), u32.const(0)], // [i, sum]
(i) => u32.lt(i, limit),
(i, sum) => [u32.add(i, u32.const(1)), sum],
(i, sum) => {
const next = u32.add(sum, i);
s.continueIf(u32.eq(i, u32.const(3)), undefined, i, sum);
s.breakIf(u32.gt(i, u32.const(8)), undefined, i, next);
return [i, next];
}
);
return sum;
});
## Quick Reference
### Operations by Type
| Operation | Int | Float | Signed | Unsigned |
| ---------------------------------------------------------------- | --- | ----- | ------ | -------- |
| `add`, `sub`, `mul`, `div`, `rem` | ✓ | ✓ | ✓ | ✓ |
| `min`, `max` | ✓ | ✓ | ✓ | ✓ |
| `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `eqz` | ✓ | ✓ | ✓ | ✓ |
| `and`, `or`, `xor`, `andnot`, `not` | ✓ | | ✓ | ✓ |
| `clz`, `ctz`, `popcnt` | ✓ | | ✓ | ✓ |
| `shl`, `shr`, `rotl`, `rotr` | ✓ | | ✓ | ✓ |
| `abs`, `neg` | | | ✓ | |
| `sqrt`, `ceil`, `floor`, `trunc`, `nearest`, `copysign`, `isNaN` | | ✓ | | |
### Memory Quick Reference
| Operation | On | Description |
| ---------------- | ---------- | -------------------- |
| `[idx]` | array | Index into dimension |
| `.field` | struct | Access field |
| `.get()` | any | Load value(s) |
| `.set(v)` | any | Store value(s) |
| `.range(s,l)` | array | Slice view |
| `.reshape(...s)` | array | Reshape view |
| `.flat()` | array | Flatten to 1D |
| `.as(type)` | array | Reinterpret type |
| `.as8/16/32()` | array | Byte view |
| `.lanes(n)` | array | SIMD strided access |
| `.copyFrom(r)` | bytes | Copy bytes |
| `.fill(v)` | bytes | Fill bytes |
| `.zero()` | bytes | Zero bytes |
| `.atomics.*` | scalar int | Atomic operations |
| `.mut.*` | scalar | Non-atomic RMW |
### Control Flow Quick Reference
| Construct | Executes | Condition Check |
| --------- | ------------ | --------------- |
| `doN` | 0 to N times | Before body |
| `doN1` | 1 to N times | After body |
| `doWhile` | 1+ times | After body |
| `forLoop` | 0+ times | Before body |
| `ifElse` | 0 or 1 time | Before body |
## License
The MIT License (MIT)
Copyright (c) 2026 Paul Miller [(https://paulmillr.com)](https://paulmillr.com)
See LICENSE file.
标签:AI工具, CMS安全, JavaScript, SOC Prime, WebAssembly, 开发工具, 数据可视化, 确定性构建, 编译器, 自动化攻击