custom webassembler can now add two numbers

This commit is contained in:
Jeremy Penner 2025-02-09 01:08:31 -05:00
parent 4d192a92d4
commit dc276fed34
13 changed files with 717 additions and 156 deletions

View file

@ -2,8 +2,7 @@
"type": "module",
"dependencies": {
"@types/node": "^22.10.2",
"js-sha256": "^0.11.0",
"wasmati": "^0.2.4"
"js-sha256": "^0.11.0"
},
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387",
"devDependencies": {

15
pnpm-lock.yaml generated
View file

@ -14,9 +14,6 @@ importers:
js-sha256:
specifier: ^0.11.0
version: 0.11.0
wasmati:
specifier: ^0.2.4
version: 0.2.4
devDependencies:
'@tsconfig/recommended':
specifier: ^1.0.8
@ -365,9 +362,6 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
js-sha256@0.11.0:
resolution: {integrity: sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==}
@ -516,9 +510,6 @@ packages:
jsdom:
optional: true
wasmati@0.2.4:
resolution: {integrity: sha512-cJl05zeUhYM5Vx6A8p4z8S9H9HVJPlLK4HO5RFoXswy5ZoWjd2zN0FX1amOs3vkshxbiKFQpJsefkK9OBgyYjQ==}
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
@ -767,8 +758,6 @@ snapshots:
fsevents@2.3.3:
optional: true
ieee754@1.2.1: {}
js-sha256@0.11.0: {}
loupe@3.1.3: {}
@ -908,10 +897,6 @@ snapshots:
- tsx
- yaml
wasmati@0.2.4:
dependencies:
ieee754: 1.2.1
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0

View file

@ -1,10 +1,8 @@
import { f32, f64, func, i32, i64, Type, ValueType, type Func } from 'wasmati';
import { memo, pure } from './querydb';
import { getChildren, getParent, getParentHandle, getTree, moveHandle, moveNode, type Language, type SrcHandle, type SrcNode, type ValTree } from './node';
import { match, MatchVars } from './pattern';
import { topLevel } from './sexpr';
import { openHandle, Repo } from '../repo';
import { WasmSignature } from './wasm';
import { memo, pure } from './querydb.js';
import { getChildren, getParent, getParentHandle, getTree, moveHandle, moveNode, type Language, type SrcHandle, type SrcNode, type ValTree } from './node.js';
import { match, MatchVars } from './pattern.js';
import { topLevel } from './sexpr.js';
import assert from 'node:assert/strict';
export const getName = pure((tree: ValTree): string | undefined => {
@ -178,41 +176,41 @@ const getWasmType = (dumbType: DumbType): Type<ValueType> => {
throw new Error('no matching wasm type definition');
}
const getFunctionSignature = (node: Node): WasmSignature<any, any> => {
const match = matchForm(getTree(node));
if (match && match.form === 'defn') {
const rettypeNode = match.match.getNode('rettype', node);
assert(rettypeNode);
const rettype = getWasmType(parseTypeLiteral(rettypeNode));
const defs = match.def.scopeDefs?.(match.match, node);
assert(defs);
const paramtypes = defs
.filter(d => d.place === 'parameter')
.map(d => getWasmType(typeFromNode(moveNode(node, d.handle.id), d.place)));
return { in: paramtypes, out: [rettype] };
}
throw new Error("tried to get function signature for non-function");
};
// const getFunctionSignature = (node: Node): WasmSignature<any, any> => {
// const match = matchForm(getTree(node));
// if (match && match.form === 'defn') {
// const rettypeNode = match.match.getNode('rettype', node);
// assert(rettypeNode);
// const rettype = getWasmType(parseTypeLiteral(rettypeNode));
// const defs = match.def.scopeDefs?.(match.match, node);
// assert(defs);
// const paramtypes = defs
// .filter(d => d.place === 'parameter')
// .map(d => getWasmType(typeFromNode(moveNode(node, d.handle.id), d.place)));
// return { in: paramtypes, out: [rettype] };
// }
// throw new Error("tried to get function signature for non-function");
// };
export const compileFunc = memo((node: Node): Func<any, any> => {
const signature = getFunctionSignature(node);
const match = matchForm(getTree(node));
// export const compileFunc = memo((node: Node): Func<any, any> => {
// const signature = getFunctionSignature(node);
// const match = matchForm(getTree(node));
return func(signature, (args, locals) => {
const emit = (node: Node) => {
const form = matchForm(getTree(node));
if (form) {
if (form.def.emit) {
form.def.emit(form.match, node, emit, args, locals);
return;
}
}
throw new Error(`Don't know how to emit code for node`);
};
for (const child of matchForm(getTree(node))?.match?.getNodes('body', node) || []) {
emit(child);
}
});
});
// return func(signature, (args, locals) => {
// const emit = (node: Node) => {
// const form = matchForm(getTree(node));
// if (form) {
// if (form.def.emit) {
// form.def.emit(form.match, node, emit, args, locals);
// return;
// }
// }
// throw new Error(`Don't know how to emit code for node`);
// };
// for (const child of matchForm(getTree(node))?.match?.getNodes('body', node) || []) {
// emit(child);
// }
// });
// });
export default DumbLang;

View file

@ -83,7 +83,7 @@ export function freeze<T>(obj: T, cycleMapping?: Map<any, [string, any]>): T {
}
});
}
} else if (typeof obj === 'string' || typeof obj === 'undefined' || typeof obj === 'boolean' || typeof obj === 'number' || (typeof(obj) === 'symbol' && Symbol.keyFor(obj))) {
} else if (typeof obj === 'string' || typeof obj === 'undefined' || typeof obj === 'boolean' || typeof obj === 'number' || (typeof(obj) === 'symbol' && Symbol.keyFor(obj)) || typeof obj === 'bigint') {
return obj;
} else if (typeof obj === 'function') {
(obj as any)[hashedValue] = hashcode('f', crypto.randomUUID());
@ -107,6 +107,9 @@ export function getHash(obj: any): string {
if (typeof obj === 'symbol') {
prefix = 'S';
hash.update(Symbol.keyFor(obj) as string);
} else if (typeof obj === 'bigint' || typeof obj === 'number') {
prefix = 'v';
hash.update(obj.toString());
} else {
prefix = 'v';
hash.update(typeof obj === 'undefined' ? 'undefined' : JSON.stringify(obj));

View file

@ -1,5 +1,5 @@
import type { Syntax } from './parse';
import { memo, pure } from './querydb';
import type { Syntax } from './parse.js';
import { memo, pure } from './querydb.js';
export class InvalidPathError extends Error {
path: number[];

View file

@ -1,8 +1,8 @@
import type { Query } from './querydb';
import type { SrcNode, Tree } from './node';
import { memo, pure, query } from './querydb';
import { freeze } from "./hash";
import { getPath, getSubtree, InvalidPathError } from './node';
import type { Query } from './querydb.js';
import type { SrcNode, Tree } from './node.js';
import { memo, pure, query } from './querydb.js';
import { freeze } from "./hash.js";
import { getPath, getSubtree, InvalidPathError } from './node.js';
export class NeedsMoreInput extends Error {}

View file

@ -1,7 +1,7 @@
import { descendNode, moveNode, siblingNode, SrcNode, type Tree, type ValTree } from './node';
import type { Syntax } from './parse';
import * as sexpr from './sexpr';
import { contentsFromText } from './parse';
import { descendNode, moveNode, siblingNode, SrcNode, type Tree, type ValTree } from './node.js';
import type { Syntax } from './parse.js';
import * as sexpr from './sexpr.js';
import { contentsFromText } from './parse.js';
function varSymbol(pattern: Syntax): string | undefined {
if (typeof pattern.value === 'symbol' && Symbol.keyFor(pattern.value)?.startsWith("?")) {

View file

@ -1,4 +1,4 @@
import { freeze, getHash, memoize } from './hash';
import { freeze, getHash, memoize } from './hash.js';
export function pure<T extends Function>(f: T): T {
return ((...args: any) => freeze(f(...freeze(args)))) as unknown as T;

View file

@ -1,6 +1,6 @@
import type { Syntax } from './parse';
import { memo, pure } from './querydb';
import * as parse from './parse';
import type { Syntax } from './parse.js';
import { memo, pure } from './querydb.js';
import * as parse from './parse.js';
export const expr = memo((contents: String[]): Syntax => {
return parse.choice(contents,

View file

@ -1,50 +1,629 @@
import { func, Func, i32, ToTypeTuple, ValueType } from "wasmati";
import { SrcHandle } from "./node";
import { memo } from "./querydb";
import { Tuple } from "wasmati/build/util";
import { getHash } from "./hash";
import assert from "node:assert";
import { freeze, getHash } from "./hash.js";
import { SrcHandle } from "./node.js";
export type WasmSignature<Args extends Tuple<ValueType>, Results extends Tuple<ValueType>> = {
in: ToTypeTuple<Args>,
out: ToTypeTuple<Results>
};
class WasmBuffer {
private readonly buffers: ArrayBuffer[];
const stubHandle = Symbol('stubHandle');
export const getFunctionStub = memo(<A extends Tuple<ValueType>, R extends Tuple<ValueType>>(handle: SrcHandle, signature: WasmSignature<A, R>): Func<A, R> => {
const f = func(signature, () => {});
(f as any)[stubHandle] = handle;
return f;
});
export const replaceStubs = <T>(v: T, getFunc: (handle: SrcHandle) => Func<any, any>, stubCache?: Map<string, Func<any, any>>): T => {
if (!stubCache) {
stubCache = new Map();
constructor() {
this.buffers = [];
}
if (typeof v === 'object' && v) {
if (stubHandle in v) {
const srcHandle = v[stubHandle] as SrcHandle;
const srcHash = getHash(srcHandle);
let f = stubCache.get(srcHash);
if (!f) {
f = Object.assign({}, getFunc(srcHandle)); // needs to be unfrozen
stubCache.set(srcHash, f);
for (const [k, v] of Object.entries(f)) {
(f as any)[k] = replaceStubs(v, getFunc, stubCache);
}
}
return f as T;
get length(): number {
return this.buffers.reduce((sum, b) => sum + b.byteLength, 0);
}
grow(bytes: number): DataView {
let buffer = this.buffers.length > 0 ? this.buffers[this.buffers.length - 1] : null;
if (buffer && buffer.byteLength + bytes <= buffer.maxByteLength) {
buffer.resize(buffer.byteLength + bytes);
} else {
if (Array.isArray(v)) {
return v.map(child => replaceStubs(child, getFunc, stubCache)) as T;
} else {
const o = {};
for (const [k, child] of Object.entries(v)) {
(o as any)[k] = replaceStubs(child, getFunc, stubCache);
buffer = new ArrayBuffer(bytes, { maxByteLength: Math.max(1024, bytes) });
this.buffers.push(buffer);
}
return new DataView(buffer, buffer.byteLength - bytes, bytes);
}
byte(b: number) { this.grow(1).setUint8(0, b); }
u32(v: number | bigint) { this.unsigned(BigInt(v), 32); }
u64(v: bigint) { this.unsigned(v, 64); }
f32(v: number) { this.grow(4).setFloat32(0, v, true); }
f64(v: number) { this.grow(8).setFloat64(0, v, true); }
unsigned(int: bigint, bits: number) {
assert(int >= 0n && int < (2n << BigInt(bits + 1)));
while (int > 0x7f) {
this.byte(0x80 | Number(int & 0x7fn));
int = int >> 7n;
}
this.byte(Number(int));
}
signed(int: bigint, bits: number) {
assert(int >= -(2n << BigInt(bits)) && int < (2n << BigInt(bits)));
while ((int >= 0 && int < 0x3f) || int < -0x7f) {
this.byte(0x80 | Number(int & 0x7fn));
int = int >> 7n;
}
this.byte(Number(int & 0x7fn));
}
appendBuffer(buf: WasmBuffer) {
const totalLength = buf.length;
const view = this.grow(totalLength);
const dstArray = new Uint8Array(view.buffer, view.buffer.byteLength - totalLength, totalLength);
let index = 0;
for (const buffer of buf.buffers) {
const srcArray = new Uint8Array(buffer, 0, buffer.byteLength);
dstArray.set(srcArray, index);
index += buffer.byteLength;
}
}
finish(): ArrayBuffer {
const tempBuf = new WasmBuffer();
tempBuf.appendBuffer(this);
return tempBuf.buffers[0];
}
}
class IdxAllocator<T> {
private readonly map: Map<string, number> = new Map();
private handles: T[] = [];
getIdx(handle: T): number {
handle = freeze(handle);
const key = getHash(handle);
let idx = this.map.get(key);
if (idx === undefined) {
idx = this.handles.length;
this.map.set(key, idx);
this.handles.push(handle);
}
return idx;
}
[Symbol.iterator]() {
return this.handles[Symbol.iterator]();
}
get length() {
return this.handles.length;
}
}
interface FuncCtx {
bufferIndex: number;
locals: Map<string, number>;
}
export type Compiler = (handle: any) => Code;
export type WasmBuild = {
compiler: (handle: any) => Code,
exports: Export[]
}
function vecsection(): WasmBuffer[] { return [new WasmBuffer(), new WasmBuffer()]; }
export class Wassembler {
readonly funcHandles: IdxAllocator<any> = new IdxAllocator();
readonly signatures: IdxAllocator<Signature> = new IdxAllocator();
private sections: { [K in keyof typeof SectionIDs]: WasmBuffer[] } = {
type: [],
import: [],
func: vecsection(),
table: [],
mem: [],
global: [],
export: [],
start: [],
elem: [],
datacount: [],
code: vecsection(),
data: []
}
private activeSection: keyof typeof SectionIDs = 'func';
private handleStack: SrcHandle[] = [];
private funcStack: FuncCtx[] = [];
private builddef: WasmBuild;
constructor(builddef: WasmBuild) {
this.builddef = builddef;
}
get buf(): WasmBuffer {
const bufs = this.sections[this.activeSection];
if (bufs.length === 0) {
bufs.push(new WasmBuffer());
}
return bufs[bufs.length - 1];
}
switch(section: keyof typeof SectionIDs) {
this.activeSection = section;
}
term(term: Term) {
if (term.src) { this.handleStack.push(term.src); }
TermEncoders[term.term](this, term as any);
if (term.src) { this.handleStack.pop(); }
}
op(op: Op) {
if (typeof op === 'string') {
this.buf.byte(PlainOpEncodings[op]);
} else if ('term' in op) {
this.term(op);
} else if (Array.isArray(op)) {
const opcode = ParameterizedOpEncodings[op[0]];
this.buf.byte(opcode);
// todo: handle other stuff
assert(typeof op[1] === 'object' && 'term' in op[1]);
this.term(op[1]);
}
}
vec<T extends Term>(vec: T[]) {
this.buf.u32(vec.length);
for (const term of vec) {
this.term(term);
}
}
name(name: string) {
const bytes = new TextEncoder().encode(name);
this.buf.u32(bytes.length);
for (const byte of bytes) {
this.buf.byte(byte);
}
}
withFuncCtx(locals: Map<string, number>, f: () => void) {
this.buf;
const section = this.sections[this.activeSection];
const bufferIndex = section.length;
section.push(new WasmBuffer());
this.funcStack.push({ bufferIndex, locals });
f();
this.funcStack.pop();
section[bufferIndex - 1].u32(this.sectionLength(section.slice(bufferIndex)));
}
sectionLength(bufs: WasmBuffer[]) {
return bufs.reduce((len, buf) => len + buf.length, 0);
}
localIdx(handle: any): number {
const idx = this.funcStack[this.funcStack.length - 1].locals.get(getHash(handle));
if (idx === undefined) {
throw new Error(`unknown local ${handle}`);
}
return idx;
}
prefixSectionLength(length: number) {
this.sections[this.activeSection][0].u32(length);
}
build() {
assert(this.funcHandles.length === 0);
this.switch('export');
this.vec(this.builddef.exports);
// writing the export section populates this.funcHandles. if we iterate over it to generate the code section, any references
// to new functions will be appended to this.funcHandles and picked up in a later iteration.
for (const funcHandle of this.funcHandles) {
const code = this.builddef.compiler(funcHandle);
this.switch('func');
this.term({ term: 'TypeIdx', type: code.signature } as TypeIdx);
this.switch('code');
this.term(code);
}
this.switch('func');
this.prefixSectionLength(this.funcHandles.length);
this.switch('code');
this.prefixSectionLength(this.funcHandles.length);
// now that all code has been written, all relevant function types have been referenced, and thus we can generate the type section
this.switch('type');
this.vec([...this.signatures].map(s => ({ term: 'FuncType', signature: s })));
}
getSections(): [keyof typeof SectionIDs, WasmBuffer[]][] {
const sectionOrder: (keyof typeof SectionIDs)[] = [
'type', 'import', 'func', 'table', 'mem', 'global', 'export', 'start', 'elem', 'code', 'data', 'datacount'
];
return sectionOrder.map(id => [id, this.sections[id]]);
}
toBuffer(): ArrayBuffer {
const buf = new WasmBuffer();
for (const b of [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]) {
buf.byte(b);
}
for (const [id, section] of this.getSections()) {
const len = this.sectionLength(section);
if (len > 0) {
buf.byte(SectionIDs[id]);
buf.u32(len);
for (const sectionbuf of section) {
buf.appendBuffer(sectionbuf);
}
return o as T;
}
}
} else {
return v;
return buf.finish();
}
};
toStream() {
// TODO; should return a Response object that can be passed to WebAssembly.instantiateStreaming()
}
}
export interface Term {
term: keyof typeof TermEncoders,
src?: SrcHandle
}
export type TermEncoder<T extends Term> = (wasm: Wassembler, term: T) => void;
export const SectionIDs = {
type: 1,
import: 2,
func: 3,
table: 4,
mem: 5,
global: 6,
export: 7,
start: 8,
elem: 9,
code: 10,
data: 11,
datacount: 12
}
export const ValTypeEncodings = {
i32: 0x7f,
i64: 0x7e,
f32: 0x7d,
f64: 0x7c,
v128: 0x7b,
funcref: 0x70,
externref: 0x6f
}
export interface ValType extends Term {
term: 'ValType'
type: keyof typeof ValTypeEncodings
}
export interface Signature {
parameterTypes: ValType["type"][],
returnTypes: ValType["type"][]
}
export interface FuncType extends Term {
term: 'FuncType',
signature: Signature
}
export interface Limits extends Term {
term: 'Limits',
min: number,
max?: number
}
export interface FuncIdx extends Term { term: 'FuncIdx', handle: any }
export interface TypeIdx extends Term { term: 'TypeIdx', type: Signature }
export interface MemIdx extends Term { term: 'MemIdx', idx: number }
export interface Export extends Term {
term: 'Export',
name: string,
index: FuncIdx | MemIdx
}
export const PlainOpEncodings = {
'unreachable': 0x00,
'nop': 0x01,
'return': 0x0f,
'ref.isnull': 0xd1,
'drop': 0x1a,
'select': 0x1c,
'i32.eqz': 0x45,
'i32.eq': 0x46,
'i32.ne': 0x47,
'i32.lt_s': 0x48,
'i32.lt_u': 0x49,
'i32.gt_s': 0x4a,
'i32.gt_u': 0x4b,
'i32.le_s': 0x4c,
'i32.le_u': 0x4d,
'i32.ge_s': 0x4e,
'i32.ge_u': 0x4f,
'i64.eqz': 0x50,
'i64.eq': 0x51,
'i64.ne': 0x52,
'i64.lt_s': 0x53,
'i64.lt_u': 0x54,
'i64.gt_s': 0x55,
'i64.gt_u': 0x56,
'i64.le_s': 0x57,
'i64.le_u': 0x58,
'i64.ge_s': 0x59,
'i64.ge_u': 0x5a,
'f32.eq': 0x5b,
'f32.ne': 0x5c,
'f32.lt': 0x5d,
'f32.gt': 0x5e,
'f32.le': 0x5f,
'f32.ge': 0x60,
'f64.eq': 0x61,
'f64.ne': 0x62,
'f64.lt': 0x63,
'f64.gt': 0x64,
'f64.le': 0x65,
'f64.ge': 0x66,
'i32.clz': 0x67,
'i32.ctz': 0x68,
'i32.popcnt': 0x69,
'i32.add': 0x6a,
'i32.sub': 0x6b,
'i32.mul': 0x6c,
'i32.div_s': 0x6d,
'i32.div_u': 0x6e,
'i32.rem_s': 0x6f,
'i32.rem_u': 0x70,
'i32.and': 0x71,
'i32.or': 0x72,
'i32.xor': 0x73,
'i32.shl': 0x74,
'i32.shr_s': 0x75,
'i32.shr_u': 0x76,
'i32.rotl': 0x77,
'i32.rotr': 0x78,
'i64.clz': 0x79,
'i64.ctz': 0x7a,
'i64.popcnt': 0x7b,
'i64.add': 0x7c,
'i64.sub': 0x7d,
'i64.mul': 0x7e,
'i64.div_s': 0x7f,
'i64.div_u': 0x80,
'i64.rem_s': 0x81,
'i64.rem_u': 0x82,
'i64.and': 0x83,
'i64.or': 0x84,
'i64.xor': 0x85,
'i64.shl': 0x86,
'i64.shr_s': 0x87,
'i64.shr_u': 0x88,
'i64.rotl': 0x89,
'i64.rotr': 0x8a,
'f32.abs': 0x8b,
'f32.neg': 0x8c,
'f32.ceil': 0x8d,
'f32.floor': 0x8e,
'f32.trunc': 0x8f,
'f32.nearest': 0x90,
'f32.sqrt': 0x91,
'f32.add': 0x92,
'f32.sub': 0x93,
'f32.mul': 0x94,
'f32.div': 0x95,
'f32.min': 0x96,
'f32.max': 0x97,
'f32.copysign': 0x98,
'f64.abs': 0x99,
'f64.neg': 0x9a,
'f64.ceil': 0x9b,
'f64.floor': 0x9c,
'f64.trunc': 0x9d,
'f64.nearest': 0x9e,
'f64.sqrt': 0x9f,
'f64.add': 0xa0,
'f64.sub': 0xa1,
'f64.mul': 0xa2,
'f64.div': 0xa3,
'f64.min': 0xa4,
'f64.max': 0xa5,
'f64.copysign': 0xa6,
'i32.wrap_i64': 0xa7,
'i32.trunc_f32_s': 0xa8,
'i32.trunc_f32_u': 0xa9,
'i32.trunc_f64_s': 0xaa,
'i32.trunc_f64_u': 0xab,
'i64.extend_i32_s': 0xac,
'i64.extend_i32_u': 0xad,
'i64.trunc_f32_s': 0xae,
'i64.trunc_f32_u': 0xaf,
'i64.trunc_f64_s': 0xb0,
'i64.trunc_f64_u': 0xb1,
'f32.convert_i32_s': 0xb2,
'f32.convert_i32_u': 0xb3,
'f32.convert_i64_s': 0xb4,
'f32.convert_i64_u': 0xb5,
'f32.demote_f64': 0xb6,
'f64.convert_i32_s': 0xb7,
'f64.convert_i32_u': 0xb8,
'f64.convert_i64_s': 0xb9,
'f64.convert_i64_u': 0xba,
'f64.promote_f32': 0xbb,
'i32.reinterpret_f32': 0xbc,
'i64.reinterpret_f64': 0xbd,
'f32.reinterpret_i32': 0xbe,
'f64.reinterpret_i64': 0xbf,
'i32.extend8_s': 0xc0,
'i32.extend16_s': 0xc1,
'i64.extend8_s': 0xc2,
'i64.extend16_s': 0xc3,
'i64.exend32_s': 0xc4
}
export type PlainOp = keyof typeof PlainOpEncodings;
export interface Ops extends Term {
term: 'Ops',
ops: Op[]
}
export interface Expr extends Term {
term: 'Expr',
ops: Ops
}
export const ParameterizedOpEncodings = {
'block': 0x02,
'loop': 0x03,
'if': 0x04,
'br': 0x0c,
'br_if': 0x0d,
'br_table': 0x0e,
'call': 0x10,
'call_indirect': 0x11,
'ref.null': 0xd0,
'ref.func': 0xd2,
'select_t': 0x1c,
'local.get': 0x20,
'local.set': 0x21,
'local.tee': 0x22,
'global.get': 0x23,
'global.set': 0x24,
'table.get': 0x25,
'table.set': 0x26,
'i32.load': 0x28,
'i64.load': 0x29,
'f32.load': 0x2a,
'f64.load': 0x2b,
'i32.load8_s': 0x2c,
'i32.load8_u': 0x2d,
'i32.load16_s': 0x2e,
'i32.load16_u': 0x2f,
'i64.load8_s': 0x30,
'i64.load8_u': 0x31,
'i64.load16_s': 0x32,
'i64.load16_u': 0x33,
'i64.load32_s': 0x34,
'i64.load32_u': 0x35,
'i32.store': 0x36,
'i64.store': 0x37,
'f32.store': 0x38,
'f64.store': 0x39,
'i32.store8': 0x3a,
'i32.store16': 0x3b,
'i64.store8': 0x3c,
'i64.store16': 0x3d,
'i64.store32': 0x3e,
'memory.size': 0x3f,
'memory.grow': 0x40,
'i32.const': 0x41,
'i64.const': 0x42,
'f32.const': 0x43,
'f64.const': 0x44
}
export const FCOpEncodings = {
'table.init': 12,
'elem.drop': 13,
'table.copy': 14,
'table.grow': 15,
'table.size': 16,
'table.fill': 17,
'memory.init': 8,
'data.drop': 9,
'memory.copy': 10,
'memory.fill': 11
}
export type ParameterizedOp =
['local.get' | 'local.set' | 'local.tee', LocalIdx] |
['call' | 'ref.func', FuncIdx] |
['i32.const', number | bigint] | ['i64.const', bigint] |
['f32.const' | 'f64.const', number]
export type Op = PlainOp | ParameterizedOp | Ops;
export interface LocalIdx extends Term {
term: 'LocalIdx',
handle: any
}
export interface LocalHandle {
idx: LocalIdx,
type: ValType
}
export interface Code extends Term {
term: 'Code',
signature: Signature,
locals: LocalHandle[],
expr: Expr
}
export function assertExhaustive(value: never, message: string = 'Reached unexpected case in exhaustive switch'): never {
throw new Error(message);
}
export const TermEncoders = {
ValType(wasm: Wassembler, valtype: ValType) { wasm.buf.byte(ValTypeEncodings[valtype.type]); },
FuncType(wasm: Wassembler, functype: FuncType) {
wasm.buf.byte(0x60);
wasm.vec(functype.signature.parameterTypes.map(t => ({ term: 'ValType', type: t })));
wasm.vec(functype.signature.returnTypes.map(t => ({ term: 'ValType', type: t })));
},
Limits(wasm: Wassembler, limits: Limits) {
if (limits.max) {
wasm.buf.byte(0x01);
wasm.buf.u32(limits.min);
wasm.buf.u32(limits.max);
} else {
wasm.buf.byte(0x00);
wasm.buf.u32(limits.min);
}
},
FuncIdx(wasm: Wassembler, idx: FuncIdx) { wasm.buf.u32(wasm.funcHandles.getIdx(idx.handle)); },
TypeIdx(wasm: Wassembler, idx: TypeIdx) { wasm.buf.u32(wasm.signatures.getIdx(idx.type)); },
LocalIdx(wasm: Wassembler, idx: LocalIdx) { wasm.buf.u32(wasm.localIdx(idx.handle)); },
MemIdx(wasm: Wassembler, idx: MemIdx) { wasm.buf.u32(idx.idx); },
Export(wasm: Wassembler, exp: Export) {
wasm.name(exp.name);
switch(exp.index.term) {
case 'FuncIdx': wasm.buf.byte(0x00); break;
case 'MemIdx': wasm.buf.byte(0x02); break;
default: assertExhaustive(exp.index);
}
wasm.term(exp.index);
},
Ops(wasm: Wassembler, ops: Ops) {
for (const op of ops.ops) {
wasm.op(op);
}
},
Expr(wasm: Wassembler, expr: Expr) {
wasm.term(expr.ops);
wasm.buf.byte(0x0b);
},
Code(wasm: Wassembler, code: Code) {
const localHandles = code.locals.toSorted((a, b) => a.type.type.localeCompare(b.type.type));
const locals = new Map<string, number>();
const paramCount = code.signature.parameterTypes.length;
for (let i = 0; i < paramCount; i ++) {
locals.set(getHash(i), i);
}
for (const [i, localHandle] of localHandles.entries()) {
locals.set(getHash(localHandle.idx.handle), i + paramCount);
}
wasm.withFuncCtx(locals, () => {
const packedLocals: { type: ValType, count: number }[] = [];
for (const localHandle of localHandles) {
if (packedLocals.length === 0 || packedLocals[packedLocals.length - 1].type.type !== localHandle.type.type) {
packedLocals.push( { type: localHandle.type, count: 0 });
}
packedLocals[packedLocals.length - 1].count ++;
}
wasm.buf.u32(packedLocals.length);
for (const { type, count } of packedLocals) {
wasm.buf.u32(count);
wasm.term(type);
}
wasm.term(code.expr);
});
},
}

View file

@ -1,10 +1,10 @@
import {test, expect} from 'vitest';
import { freeze, getHash } from '../db/hash';
import { memo } from '../db/querydb';
import { consume, contentsFromText, getLocation } from '../db/parse';
import { rewrite } from '../db/pattern';
import DumbLang, * as Dumb from '../db/dumb';
import * as Node from '../db/node';
import { freeze, getHash } from '../db/hash.js';
import { memo } from '../db/querydb.js';
import { consume, contentsFromText, getLocation } from '../db/parse.js';
import { rewrite } from '../db/pattern.js';
import DumbLang, * as Dumb from '../db/dumb.js';
import * as Node from '../db/node.js';
const { moveNode } = Node;

View file

@ -1,41 +1,35 @@
import {test, expect} from 'vitest';
import { freeze } from "../db/hash.js";
import { call, Dependency, func, i32, if_, local, Module } from 'wasmati';
// import { call, Dependency, func, i32, if_, local, Module } from 'wasmati';
import { Code, Compiler, FuncIdx, WasmBuild, Wassembler } from '../db/wasm.js';
async function compileFunc<T extends Dependency.Export>(f: T) {
return (await Module({ exports: { f } }).instantiate()).instance.exports.f;
async function compileFunc(compiler: Compiler, handle: any): Promise<(...args: any) => any> {
const build: WasmBuild = { compiler, exports: [{ term: 'Export', name: "f", index: { term: 'FuncIdx', handle } }]};
const wasm = new Wassembler(build);
wasm.build();
const module = await WebAssembly.instantiate(wasm.toBuffer());
return module.instance.exports.f as any;
}
test('freeze a wasm function', async () => {
const f = func({ in: [i32, i32], out: [i32] }, ([l, r]) => {
i32.add(l, r);
})
expect((await compileFunc(f))(1, 2)).toBe(3);
expect((await compileFunc(freeze(f) as typeof f))(1, 2)).toBe(3);
const inc1 = func({ in: [i32], out: [i32]}, ([val]) => {
call(freeze(f) as typeof f, [val, 1]);
})
expect((await compileFunc(freeze(inc1)))(5)).toBe(6);
});
test('mutually recursive functions', async () => {
let double = func({ in: [i32, i32], out: [i32]}, (_) => { i32.const(0); });
let decrement = func({ in: [i32, i32], out: [i32]}, (_) => { i32.const(0); });
Object.assign(double, func({ in: [i32, i32], out: [i32]}, ([val, count]) => {
i32.eqz(count);
if_({ out: [i32] },
() => { local.get(val) },
() => {
call(decrement, [i32.add(val, val), i32.sub(count, 1)]);
});
}));
Object.assign(decrement, func({ in: [i32, i32], out: [i32]}, ([val, count]) => {
i32.eqz(count);
if_({ out: [i32] },
() => { local.get(val) },
() => {
call(double, [i32.sub(val, 1), i32.sub(count, 1)]);
});
}));
expect((await compileFunc(freeze(double)))(5, 3)).toBe( ((5 * 2) - 1) * 2 );
const compiler = (_: any): Code => {
return {
term: 'Code',
locals: [],
signature: { parameterTypes: ['i32', 'i32'], returnTypes: ['i32'] },
expr: {
term: 'Expr',
ops: {
term: 'Ops',
ops: [
['local.get', { term: 'LocalIdx', handle: 0 }],
['local.get', { term: 'LocalIdx', handle: 1 }],
'i32.add'
]
}
}
}
};
const f = await compileFunc(compiler, 'f');
expect(f(1,2)).toBe(3);
});

View file

@ -2,6 +2,9 @@
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"outDir": "build",
"target": "es2022"
"target": "esnext",
// "lib": ["es2024"]
"module": "nodenext",
"moduleResolution": "nodenext"
}
}