custom webassembler can now add two numbers
This commit is contained in:
parent
4d192a92d4
commit
dc276fed34
13 changed files with 717 additions and 156 deletions
|
|
@ -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
15
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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("?")) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
663
src/db/wasm.ts
663
src/db/wasm.ts
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
"target": "es2022"
|
||||
"target": "esnext",
|
||||
// "lib": ["es2024"]
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue