├── .gitignore ├── LICENSE.md ├── README.md ├── examples ├── blog │ ├── .gitignore │ ├── add-with-class.ts │ ├── add.ts │ ├── greater.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── deno.ts ├── example.ts ├── example.wat ├── fma.ts └── simple.ts ├── package-lock.json ├── package.json ├── src ├── binable.ts ├── dependency.ts ├── export.ts ├── func-types.ts ├── func.ts ├── immediate.ts ├── immediate.unit-test.ts ├── index.ts ├── instruction │ ├── atomic.ts │ ├── base.ts │ ├── binable.ts │ ├── const.ts │ ├── control.ts │ ├── memory.ts │ ├── numeric.ts │ ├── opcodes.ts │ ├── stack-args.ts │ ├── variable-get.ts │ ├── variable.ts │ └── vector.ts ├── local-context.ts ├── memory-binable.ts ├── memory.ts ├── module-binable.ts ├── module.ts ├── types.ts ├── util-node.ts └── util.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gregor Mitscha-Baude 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasmati 🍚   [![npm version](https://img.shields.io/npm/v/wasmati.svg?style=flat)](https://www.npmjs.com/package/wasmati) 2 | 3 | _Write low-level WebAssembly, from JavaScript_ 4 | 5 | **wasmati** is a TS library that lets you create Wasm modules by writing out their instructions. 6 | 7 | - 🥷 You want to create low-level, hand-optimized Wasm libraries? wasmati is the tool to do so effectively. 8 | - 🚀 You want to sprinkle some Wasm in your JS app, to speed up critical parts? wasmati gives you a JS-native way to achieve that. 9 | - ⚠️ You want to compile Wasm modules from a high-level language, like Rust or C? wasmati is not for you. 10 | 11 | ```sh 12 | npm i wasmati 13 | ``` 14 | 15 | ```ts 16 | // example.ts 17 | import { i64, func, Module } from "wasmati"; 18 | 19 | const myMultiply = func({ in: [i64, i64], out: [i64] }, ([x, y]) => { 20 | i64.mul(x, y); 21 | }); 22 | 23 | let module = Module({ exports: { myMultiply } }); 24 | let { instance } = await module.instantiate(); 25 | 26 | let result = instance.exports.myMultiply(5n, 20n); 27 | console.log({ result }); 28 | ``` 29 | 30 | ``` 31 | $ node --experimental-strip-types example.ts 32 | { result: 100n } 33 | ``` 34 | 35 | ## Features 36 | 37 | - Works in all modern browsers, `node` and `deno` 38 | 39 | - **Parity with WebAssembly.** The API directly corresponds to Wasm opcodes, like `i32.add` etc. All opcodes and language features of the [latest WebAssembly spec (2.0)](https://webassembly.github.io/spec/core/index.html) are supported. 40 | In addition, wasmati supports the following extensions which are not part of the spec at the time of writing: 41 | 42 | - [threads and atomics](https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md) 43 | - [relaxed simd](https://github.com/WebAssembly/relaxed-simd/blob/main/proposals/relaxed-simd/Overview.md) 44 | 45 | - **Readability.** Wasm code looks imperative - like writing WAT by hand, just with better DX: 46 | 47 | ```ts 48 | const myFunction = func({ in: [i32, i32], out: [i32] }, ([x, y]) => { 49 | local.get(x); 50 | local.get(y); 51 | i32.add(); 52 | i32.const(2); 53 | i32.shl(); 54 | call(otherFunction); 55 | }); 56 | ``` 57 | 58 | - Optional syntax sugar to reduce boilerplate assembly like `local.get` and `i32.const` 59 | 60 | ```ts 61 | const myFunction = func({ in: [i32, i32], out: [i32] }, ([x, y]) => { 62 | i32.add(x, y); // local.get(x), local.get(y) are filled in 63 | i32.shl($, 2); // $ is the top of the stack; i32.const(2) is filled in 64 | call(otherFunction); 65 | }); 66 | 67 | // or also 68 | 69 | const myFunction = func({ in: [i32, i32], out: [i32] }, ([x, y]) => { 70 | let z = i32.add(x, y); 71 | call(otherFunction, [i32.shl(z, 2)]); 72 | }); 73 | ``` 74 | 75 | - **Type-safe.** Example: Local variables are typed; instructions know their input types: 76 | 77 | ```ts 78 | const myFunction = func( 79 | { in: [i32, i32], locals: [i64], out: [i32] }, 80 | ([x, y], [u]) => { 81 | i32.add(x, u); // type error: Type '"i64"' is not assignable to type '"i32"'. 82 | } 83 | ); 84 | ``` 85 | 86 | - **Great debugging DX.** Stack traces point to the exact line in your code where an invalid opcode is called: 87 | 88 | ``` 89 | Error: i32.add: Expected i32 on the stack, got i64. 90 | ... 91 | at file:///home/gregor/code/wasmati/examples/example.ts:16:9 92 | ``` 93 | 94 | - **Easy construction of modules.** Just declare exports; dependencies and imports are collected for you. Nothing ends up in the module which isn't needed by any of its exports or its start function. 95 | 96 | ```ts 97 | let mem = memory({ min: 10 }); 98 | 99 | let module = Module({ exports: { myFunction, mem } }); 100 | let instance = await module.instantiate(); 101 | ``` 102 | 103 | - **Excellent type inference.** Example: Exported function types are inferred from `func` definitions: 104 | 105 | ```ts 106 | instance.exports.myFunction; 107 | // ^ (arg_0: number, arg_1: number) => number 108 | ``` 109 | 110 | - **Atomic import declaration.** Imports are declared as types along with their JS values. Abstracts away the global "import object" that is separate from "import declaration". 111 | 112 | ```ts 113 | const consoleLog = importFunc({ in: [i32], out: [] }, (x) => 114 | console.log("logging from wasm:", x) 115 | ); 116 | 117 | const myFunction = func({ in: [i32, i32], out: [i32] }, ([x, y]) => { 118 | call(consoleLog, [x]); 119 | i32.add(x, y); 120 | }); 121 | ``` 122 | 123 | - Great composability and IO 124 | - Internal representation of modules / funcs / etc is a readable JSON object 125 | - close to [the spec's type layout](https://webassembly.github.io/spec/core/syntax/modules.html#modules) (but improves readability or JS ergonomics where necessary) 126 | - Convert to/from Wasm bytecode with `module.toBytes()`, `Module.fromBytes(bytes)` 127 | 128 | ### Features that aren't implemented yet 129 | 130 | _PRs welcome!_ 131 | 132 | - **Wasmati build.** We want to add an optional build step which takes as input a file that exports your `Module`, and compiles it to a file which doesn't depend on wasmati at runtime. Instead, it hard-codes the Wasm bytecode as base64 string, correctly imports all dependencies (imports) for the instantiation like the original file did, instantiates the module (top-level await) and exports the module's exports. 133 | 134 | ```ts 135 | // example.ts 136 | let module = Module({ exports: { myFunction, mem } }); 137 | 138 | export { module as default }; 139 | ``` 140 | 141 | ```ts 142 | import { myFunction } from "./example.wasm.js"; // example.wasm.js does not depend on wasmati at runtime 143 | ``` 144 | 145 | - **Experimental Wasm opcodes.** We want to support opcodes from recently standardized or in-progress feature proposals ([like this one](https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md)) which haven't yet made it to the spec. The eventual goal is to support proposals as soon as they are implemented in at least one JS engine. 146 | 147 | - **Custom module sections.** We want to support creation and parsing of "custom sections" like the [name section](https://webassembly.github.io/spec/core/appendix/custom.html#name-section) 148 | 149 | ### Some ideas that are a bit further out: 150 | 151 | - **Decompiler**: take _any_ Wasm file and create wasmati TS code from it -- to modify it, debug it etc 152 | - **Source maps**, so you can look at the culprit JS code when Wasm throws an error 153 | - Optional JS interpreter which can take DSL code and execute it _in JS_ 154 | - could enable even more flexible debugging -- inspect the stack, global/local scope etc 155 | -------------------------------------------------------------------------------- /examples/blog/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/blog/add-with-class.ts: -------------------------------------------------------------------------------- 1 | import { 2 | i32, 3 | func, 4 | Local, 5 | if_, 6 | return_, 7 | Module, 8 | memory, 9 | local, 10 | block, 11 | br, 12 | Input, 13 | } from "wasmati"; 14 | 15 | let p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001n; 16 | 17 | let w = 29; // limb size 18 | let wordMax = (1 << w) - 1; 19 | let n = Math.ceil(p.toString(2).length / w); // number of limbs 20 | 21 | console.log({ n }); 22 | 23 | // bigints 24 | let wn = BigInt(w); 25 | let wordMaxn = BigInt(wordMax); 26 | 27 | type Field = { 28 | get(i: number): Input; 29 | }; 30 | 31 | class ConstantField implements Field { 32 | limbs: Uint32Array; 33 | 34 | constructor(x: bigint) { 35 | this.limbs = bigintToLimbs(x); 36 | } 37 | 38 | get(i: number) { 39 | return this.limbs[i]; 40 | } 41 | } 42 | 43 | class MemoryField implements Field { 44 | x: Local; 45 | xi: Local; 46 | 47 | constructor(x: Local, xi: Local) { 48 | this.x = x; 49 | this.xi = xi; 50 | } 51 | 52 | get(i: number) { 53 | i32.load({ offset: 4 * i }, this.x); 54 | local.set(this.xi); 55 | return this.xi; 56 | } 57 | set(i: number, xi: Input) { 58 | i32.store({ offset: 4 * i }, this.x, xi); 59 | } 60 | } 61 | 62 | function isLower(x: Field, y: Field) { 63 | block({ out: [i32] }, (block) => { 64 | for (let i = n - 1; i >= 0; i--) { 65 | // set xi = x[i] and yi = y[i] 66 | let xi = x.get(i); 67 | let yi = y.get(i); 68 | 69 | // return true if (xi < yi) 70 | i32.lt_u(xi, yi); 71 | if_(null, () => { 72 | i32.const(1); 73 | br(block); 74 | }); 75 | 76 | // return false if (xi != yi) 77 | i32.ne(xi, yi); 78 | if_(null, () => { 79 | i32.const(0); 80 | br(block); 81 | }); 82 | } 83 | 84 | // fall-through case: return false if z = p 85 | i32.const(0); 86 | }); 87 | } 88 | 89 | const add = func( 90 | { 91 | in: [i32, i32, i32], 92 | locals: [i32, i32, i32], 93 | out: [], 94 | }, 95 | ([zPtr, xPtr, yPtr], [xi, yi, zi]) => { 96 | let x = new MemoryField(xPtr, xi); 97 | let y = new MemoryField(yPtr, yi); 98 | let z = new MemoryField(zPtr, zi); 99 | let P = new ConstantField(p); 100 | 101 | // z = x + y 102 | for (let i = 0; i < n; i++) { 103 | i32.add(x.get(i), y.get(i)); 104 | if (i > 0) i32.add(); 105 | local.set(zi); 106 | 107 | if (i < n - 1) i32.shr_u(zi, w); 108 | z.set(i, i32.and(zi, wordMax)); 109 | } 110 | 111 | // if (z < p) return; 112 | isLower(z, P); 113 | if_(null, () => return_()); 114 | 115 | // z -= p 116 | for (let i = 0; i < n; i++) { 117 | i32.sub(z.get(i), P.get(i)); 118 | if (i > 0) i32.add(); 119 | local.set(zi); 120 | 121 | if (i < n - 1) i32.shr_s(zi, w); 122 | z.set(i, i32.and(zi, wordMax)); 123 | } 124 | } 125 | ); 126 | 127 | // compile and use wasm code 128 | 129 | let module = Module({ 130 | exports: { add, memory: memory({ min: 1 }) }, 131 | }); 132 | let { 133 | instance: { exports: wasm }, 134 | } = await module.instantiate(); 135 | 136 | let offset = 0; 137 | 138 | let x = randomField(); 139 | let N = 1e7; 140 | 141 | // warm up 142 | for (let i = 0; i < 5; i++) { 143 | benchWasm(x, N); 144 | benchBigint(x, N); 145 | } 146 | 147 | for (let i = 0; i < 5; i++) { 148 | console.time("wasm"); 149 | let z0 = benchWasm(x, N); 150 | console.timeEnd("wasm"); 151 | 152 | console.time("bigint"); 153 | let z1 = benchBigint(x, N); 154 | console.timeEnd("bigint"); 155 | 156 | if (z0 !== z1) throw Error("wrong result"); 157 | } 158 | 159 | function addBigint(x: bigint, y: bigint) { 160 | let z = x + y; 161 | return z < p ? z : z - p; 162 | } 163 | 164 | function benchWasm(x0: bigint, N: number) { 165 | let z = fromBigint(0n); 166 | let x = fromBigint(x0); 167 | for (let i = 0; i < N; i++) { 168 | wasm.add(z, z, x); 169 | } 170 | return toBigint(z); 171 | } 172 | 173 | function benchBigint(x: bigint, N: number) { 174 | let z = 0n; 175 | for (let i = 0; i < N; i++) { 176 | z = addBigint(z, x); 177 | } 178 | return z; 179 | } 180 | 181 | // helpers 182 | 183 | function bigintToLimbs(x0: bigint, limbs = new Uint32Array(n)) { 184 | for (let i = 0; i < n; i++) { 185 | limbs[i] = Number(x0 & wordMaxn); 186 | x0 >>= wn; 187 | } 188 | return limbs; 189 | } 190 | function bigintFromLimbs(limbs: Uint32Array) { 191 | let x0 = 0n; 192 | let bitPosition = 0n; 193 | for (let i = 0; i < n; i++) { 194 | x0 += BigInt(limbs[i]) << bitPosition; 195 | bitPosition += wn; 196 | } 197 | return x0; 198 | } 199 | 200 | function fromBigint(x0: bigint) { 201 | let x = offset; 202 | offset += 4 * n; 203 | bigintToLimbs(x0, new Uint32Array(wasm.memory.buffer, x, n)); 204 | return x; 205 | } 206 | function toBigint(x: number) { 207 | return bigintFromLimbs(new Uint32Array(wasm.memory.buffer, x, n)); 208 | } 209 | 210 | function randomField() { 211 | while (true) { 212 | let arr = new Uint32Array(n); 213 | let limbs = crypto.getRandomValues(arr); 214 | let x = bigintFromLimbs(limbs); 215 | if (x < p) return x; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /examples/blog/add.ts: -------------------------------------------------------------------------------- 1 | import { 2 | i32, 3 | func, 4 | Local, 5 | if_, 6 | return_, 7 | Module, 8 | memory, 9 | local, 10 | block, 11 | br, 12 | Input, 13 | $, 14 | } from "wasmati"; 15 | 16 | let p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001n; 17 | 18 | let w = 29; // limb size 19 | let wordMax = (1 << w) - 1; 20 | let n = Math.ceil(p.toString(2).length / w); // number of limbs 21 | 22 | // bigints 23 | let wn = BigInt(w); 24 | let wordMaxn = BigInt(wordMax); 25 | 26 | let P = bigintToLimbs(p); 27 | 28 | /** 29 | * add two field elements stored in memory 30 | */ 31 | const add = func( 32 | { 33 | in: [i32, i32, i32], 34 | locals: [i32], 35 | out: [], 36 | }, 37 | ([z, x, y], [zi]) => { 38 | // z = x + y 39 | for (let i = 0; i < n; i++) { 40 | // zi = x[i] + y[i] + carry 41 | i32.add(loadLimb(x, i), loadLimb(y, i)); 42 | if (i > 0) i32.add(); // add carry 43 | local.set(zi); 44 | 45 | // perform carry on zi and store in z[i]; 46 | // carry bit is left on the stack for next i 47 | if (i < n - 1) i32.shr_s(zi, w); 48 | storeLimb(z, i, i32.and(zi, wordMax)); 49 | } 50 | 51 | // if (z < p) return; 52 | isLower(z, zi, P); 53 | if_(null, () => return_()); 54 | 55 | // z -= p 56 | for (let i = 0; i < n; i++) { 57 | // zi = z[i] - p[i] + carry 58 | i32.sub(loadLimb(z, i), P[i]); 59 | if (i > 0) i32.add(); // add carry 60 | local.set(zi); 61 | 62 | // perform carry on zi and store in z[i]; 63 | // carry "bit" (0 or -1) is left on the stack for next i 64 | if (i < n - 1) i32.shr_s(zi, w); 65 | storeLimb(z, i, i32.and(zi, wordMax)); 66 | } 67 | } 68 | ); 69 | 70 | function loadLimb(x: Local, i: number) { 71 | return i32.load({ offset: 4 * i }, x); 72 | } 73 | 74 | function storeLimb(x: Local, i: number, s: Input) { 75 | return i32.store({ offset: 4 * i }, x, s); 76 | } 77 | 78 | // compile and use wasm code 79 | 80 | let module = Module({ 81 | exports: { add, memory: memory({ min: 1 }) }, 82 | }); 83 | let { instance } = await module.instantiate(); 84 | let wasm = instance.exports; 85 | 86 | wasm.add satisfies (z: number, x: number, y: number) => void; 87 | 88 | let offset = 0; 89 | 90 | let x = randomField(); 91 | let N = 1e7; 92 | 93 | // warm up 94 | for (let i = 0; i < 5; i++) { 95 | benchWasm(x, N); 96 | benchBigint(x, N); 97 | } 98 | 99 | // benchmark 100 | console.time("wasm"); 101 | let z0 = benchWasm(x, N); 102 | console.timeEnd("wasm"); 103 | 104 | console.time("bigint"); 105 | let z1 = benchBigint(x, N); 106 | console.timeEnd("bigint"); 107 | 108 | // make sure that results are the same 109 | if (z0 !== z1) throw Error("wrong result"); 110 | 111 | function addBigint(x: bigint, y: bigint, p: bigint) { 112 | let z = x + y; 113 | return z < p ? z : z - p; 114 | } 115 | 116 | function benchWasm(x0: bigint, N: number) { 117 | let z = fromBigint(0n); 118 | let x = fromBigint(x0); 119 | for (let i = 0; i < N; i++) { 120 | wasm.add(z, z, x); 121 | } 122 | return toBigint(z); 123 | } 124 | 125 | function benchBigint(x: bigint, N: number) { 126 | let z = 0n; 127 | for (let i = 0; i < N; i++) { 128 | z = addBigint(z, x, p); 129 | } 130 | return z; 131 | } 132 | 133 | // helpers 134 | 135 | function bigintToLimbs(x: bigint, limbs = new Uint32Array(n)) { 136 | for (let i = 0; i < n; i++) { 137 | limbs[i] = Number(x & wordMaxn); 138 | x >>= wn; 139 | } 140 | return limbs; 141 | } 142 | function bigintFromLimbs(limbs: Uint32Array) { 143 | let x0 = 0n; 144 | let bitPosition = 0n; 145 | for (let i = 0; i < n; i++) { 146 | x0 += BigInt(limbs[i]) << bitPosition; 147 | bitPosition += wn; 148 | } 149 | return x0; 150 | } 151 | 152 | function fromBigint(x0: bigint) { 153 | let x = offset; 154 | offset += 4 * n; 155 | bigintToLimbs(x0, new Uint32Array(wasm.memory.buffer, x, n)); 156 | return x; 157 | } 158 | function toBigint(x: number) { 159 | return bigintFromLimbs(new Uint32Array(wasm.memory.buffer, x, n)); 160 | } 161 | 162 | function randomField() { 163 | while (true) { 164 | let arr = new Uint32Array(n); 165 | let limbs = crypto.getRandomValues(arr); 166 | let x = bigintFromLimbs(limbs); 167 | if (x < p) return x; 168 | } 169 | } 170 | 171 | /** 172 | * helper for checking that x < y, where x is stored in memory and y is 173 | * a constant stored as an array of 32-bit limbs 174 | */ 175 | function isLower(x: Local, xi: Local, y: Uint32Array) { 176 | block({ out: [i32] }, (block) => { 177 | for (let i = n - 1; i >= 0; i--) { 178 | // set xi = x[i] 179 | local.set(xi, loadLimb(x, i)); 180 | 181 | // return true if (xi < yi) 182 | i32.lt_s(xi, y[i]); 183 | if_(null, () => { 184 | i32.const(1); 185 | br(block); 186 | }); 187 | 188 | // return false if (xi != yi) 189 | i32.ne(xi, y[i]); 190 | if_(null, () => { 191 | i32.const(0); 192 | br(block); 193 | }); 194 | } 195 | 196 | // fall-through case: return false if z = p 197 | i32.const(0); 198 | }); 199 | } 200 | -------------------------------------------------------------------------------- /examples/blog/greater.ts: -------------------------------------------------------------------------------- 1 | import { i32, func, Local, if_, return_, Module, memory, local } from "wasmati"; 2 | 3 | const n = 9; // number of limbs 4 | 5 | const isGreater = func( 6 | { in: [i32, i32], locals: [i32, i32], out: [i32] }, 7 | ([x, y], [xi, yi]) => { 8 | for (let i = n - 1; i >= 0; i--) { 9 | // set xi = x[i] and yi = y[i] 10 | local.set(xi, loadLimb(x, i)); 11 | local.set(yi, loadLimb(y, i)); 12 | 13 | // return true if (xi > yi) 14 | i32.gt_u(xi, yi); 15 | if_(null, () => { 16 | i32.const(1); 17 | return_(); 18 | }); 19 | 20 | // return false if (xi != yi) 21 | i32.ne(xi, yi); 22 | if_(null, () => { 23 | i32.const(0); 24 | return_(); 25 | }); 26 | } 27 | 28 | // fall-through case: return false if x = y 29 | i32.const(0); 30 | } 31 | ); 32 | 33 | function loadLimb(x: Local, i: number) { 34 | return i32.load({ offset: 4 * i }, x); 35 | } 36 | 37 | // compile and use wasm code 38 | 39 | let module = Module({ exports: { isGreater, memory: memory({ min: 1 }) } }); 40 | let { 41 | instance: { exports: wasm }, 42 | } = await module.instantiate(); 43 | 44 | let offset = 0; 45 | let w = 32n; 46 | let wordMax = (1n << w) - 1n; 47 | 48 | function fromBigint(x0: bigint) { 49 | let x = offset; 50 | offset += 4 * n; 51 | let arr = new Uint32Array(wasm.memory.buffer, x, n); 52 | for (let i = 0; i < n; i++) { 53 | arr[i] = Number(x0 & wordMax); 54 | x0 >>= w; 55 | } 56 | return x; 57 | } 58 | 59 | let x = fromBigint(20n); 60 | let y = fromBigint(19n); 61 | 62 | let isXGreater = wasm.isGreater(x, y); 63 | 64 | console.log({ isXGreater }); 65 | -------------------------------------------------------------------------------- /examples/blog/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasmati-examples", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wasmati-examples", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "typescript": "^5.0.4", 13 | "wasmati": "^0.1.9" 14 | } 15 | }, 16 | "node_modules/ieee754": { 17 | "version": "1.2.1", 18 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 19 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 20 | "funding": [ 21 | { 22 | "type": "github", 23 | "url": "https://github.com/sponsors/feross" 24 | }, 25 | { 26 | "type": "patreon", 27 | "url": "https://www.patreon.com/feross" 28 | }, 29 | { 30 | "type": "consulting", 31 | "url": "https://feross.org/support" 32 | } 33 | ] 34 | }, 35 | "node_modules/typescript": { 36 | "version": "5.0.4", 37 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", 38 | "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", 39 | "bin": { 40 | "tsc": "bin/tsc", 41 | "tsserver": "bin/tsserver" 42 | }, 43 | "engines": { 44 | "node": ">=12.20" 45 | } 46 | }, 47 | "node_modules/wasmati": { 48 | "version": "0.1.9", 49 | "resolved": "https://registry.npmjs.org/wasmati/-/wasmati-0.1.9.tgz", 50 | "integrity": "sha512-G1lIWaT8PQyS+7ptGK4YCWAfHZcFRz7t7XLRmL/RRqymje9V4qxFIfGA2m9xZFMe8JAXRq3UBuhJMDQSeApKUg==", 51 | "dependencies": { 52 | "ieee754": "^1.2.1" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasmati-examples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "typescript": "^5.0.4", 15 | "wasmati": "^0.1.9" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/blog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "nodenext", 5 | "strict": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/deno.ts: -------------------------------------------------------------------------------- 1 | // Deno example - run with `deno run examples/deno.ts` 2 | import { i64, func, Module } from "npm:wasmati"; 3 | 4 | const myFunction = func({ in: [i64, i64], out: [i64] }, ([x, y]) => { 5 | i64.mul(x, y); 6 | }); 7 | 8 | let module = Module({ exports: { myFunction } }); 9 | let { instance } = await module.instantiate(); 10 | 11 | let result = instance.exports.myFunction(5n, 20n); 12 | console.log({ result }); 13 | // { result: 100n } 14 | -------------------------------------------------------------------------------- /examples/example.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Module, 3 | func, 4 | control, 5 | i32, 6 | i64, 7 | local, 8 | global, 9 | ref, 10 | drop, 11 | select, 12 | funcref, 13 | importFunc, 14 | importGlobal, 15 | memory, 16 | Const, 17 | f64, 18 | call, 19 | block, 20 | loop, 21 | br, 22 | br_if, 23 | unreachable, 24 | call_indirect, 25 | v128, 26 | i32x4, 27 | f64x2, 28 | table, 29 | $, 30 | importMemory, 31 | atomic, 32 | StackVar, 33 | } from "../build/index.js"; 34 | import assert from "node:assert"; 35 | import Wabt from "wabt"; 36 | import { writeFile } from "../src/util-node.js"; 37 | 38 | const wabt = await Wabt(); 39 | 40 | let log = (...args: any) => console.log("logging from wasm:", ...args); 41 | 42 | let consoleLog = importFunc({ in: [i32], out: [] }, log); 43 | let consoleLog64 = importFunc({ in: [i64], out: [] }, log); 44 | let consoleLogF64 = importFunc({ in: [f64], out: [] }, log); 45 | let consoleLogFunc = importFunc({ in: [funcref], out: [] }, log); 46 | 47 | let mem = importMemory( 48 | { min: 1, max: 1 << 16, shared: true }, 49 | undefined, 50 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 51 | ); 52 | 53 | let myFunc = func( 54 | { in: [i32, i32], locals: [i32, i32], out: [i32] }, 55 | ([x, y], [tmp, i], ctx) => { 56 | i64.trunc_sat_f64_s(1.125); 57 | call(consoleLog64); 58 | i32.add(x, 0); 59 | i32.add(y, $); 60 | block({ in: [i32], out: [i32] }, ($block) => { 61 | local.tee(tmp, $); 62 | call(consoleLog); 63 | loop({}, ($loop) => { 64 | call(consoleLog, [i]); 65 | local.tee(i, i32.add(i, 1)); 66 | i32.eq($, 5); 67 | control.if({}, () => { 68 | local.get(tmp); 69 | control.return(); 70 | // fine that this is missing input, because code path is unreachable 71 | call(consoleLog); 72 | }); 73 | br($loop); 74 | // unreachable 75 | local.get(i); 76 | // i64.const(10n); 77 | i32.ne(); 78 | br_if(0); 79 | }); 80 | local.get(tmp); 81 | local.get(tmp); 82 | drop(); 83 | }); 84 | } 85 | ); 86 | 87 | let importedGlobal = importGlobal(i64, 1000n); 88 | let myFuncGlobal = global(Const.refFunc(myFunc)); 89 | let f64Global = global(Const.f64(0), { mutable: true }); 90 | 91 | // this function is not part of the import graph of the module, so won't end up in the assembly 92 | let testUnreachable = func({ in: [i32, i32], out: [i32] }, ([x]) => { 93 | unreachable(); 94 | // global.get(importedGlobal); // uncommenting shows that we handle type errors after unreachable correctly 95 | i32.add($, x); 96 | }); 97 | 98 | let funcTable = table({ type: funcref, min: 4 }, [ 99 | Const.refFunc(consoleLogFunc), 100 | Const.refFunc(myFunc), 101 | Const.refFuncNull, 102 | Const.refFuncNull, 103 | ]); 104 | 105 | let exportedFunc = func( 106 | { 107 | in: [i32, i32], 108 | locals: [v128, i32, v128], 109 | out: [i32], 110 | }, 111 | ([x, doLog], [_, y, v]) => { 112 | // call(testUnreachable); 113 | ref.func(myFunc); // TODO this fails if there is no table but a global, seems to be a V8 bug 114 | call(consoleLogFunc); 115 | global.get(myFuncGlobal); 116 | i32.const(0); 117 | call_indirect(funcTable, { in: [funcref], out: [] }); 118 | global.set(f64Global, 1.001); 119 | f64.mul(1.01, f64Global); 120 | call(consoleLogF64); 121 | local.get(x); 122 | local.get(doLog); 123 | control.if(null, () => { 124 | call(consoleLog, [x]); 125 | }); 126 | i32.const(2 ** 31 - 1); 127 | i32.const(-(2 ** 31)); 128 | local.get(doLog); 129 | select(); 130 | call(consoleLog); 131 | // drop(); 132 | // local.get(x); 133 | local.set(y); 134 | let r1: StackVar = call(myFunc, [y, 5]); 135 | // unreachable(); 136 | 137 | i32.const(10); 138 | memory.grow(); 139 | drop(); 140 | 141 | // move int32 at location 4 to location 0 142 | i32.store({}, 0, i32.load({ offset: 4 }, 0)); 143 | 144 | // test i64 145 | call(consoleLog64, [64n]); 146 | 147 | // test vector instr 148 | v128.const("i64x2", [1n, 2n]); 149 | v128.const("i32x4", [3, 4, 5, 6]); 150 | local.set(v, i32x4.add()); 151 | let $0 = v128.const("f64x2", [0.1, 0.2]); 152 | let $1 = f64x2.splat(6.25); 153 | f64x2.mul($0, $1); 154 | f64x2.extract_lane(1); 155 | call(consoleLogF64); // should log 1.25 156 | 157 | // test table 158 | ref.null(funcref); 159 | i32.const(10); 160 | table.grow(funcTable); 161 | drop(); 162 | 163 | // test atomic 164 | i32.atomic.rmw.add({}, 0, 4); 165 | memory.atomic.notify({}, 0, 0); 166 | drop(); 167 | drop(); 168 | atomic.fence(); 169 | } 170 | ); 171 | 172 | const fma = func({ in: [f64, f64, f64], out: [f64] }, ([x, y, z]) => { 173 | f64x2.splat(x); 174 | f64x2.splat(y); 175 | f64x2.splat(z); 176 | f64x2.relaxed_madd(); 177 | f64x2.extract_lane(0); 178 | }); 179 | 180 | let startFunc = importFunc({ in: [], out: [] }, () => 181 | console.log("starting wasm") 182 | ); 183 | 184 | let module = Module({ 185 | exports: { exportedFunc, fma, importedGlobal, memory: mem }, 186 | start: startFunc, 187 | }); 188 | 189 | console.dir(module.module, { depth: Infinity }); 190 | 191 | // create byte code and check roundtrip 192 | let wasmByteCode = module.toBytes(); 193 | console.log(`wasm size: ${wasmByteCode.length} byte`); 194 | let recoveredModule = Module.fromBytes(wasmByteCode); 195 | assert.deepStrictEqual(recoveredModule.module, module.module); 196 | 197 | // write wat file for comparison 198 | let wabtModule = wabt.readWasm(wasmByteCode, wabtFeatures()); 199 | let wat = wabtModule.toText({}); 200 | await writeFile(import.meta.url.slice(7).replace(".ts", ".wat"), wat); 201 | 202 | // instantiate 203 | let wasmModule = await module.instantiate(); 204 | let { exports } = wasmModule.instance; 205 | console.log(exports); 206 | 207 | // check type inference 208 | exports.exportedFunc satisfies (x: number, y: number) => number; 209 | exports.importedGlobal satisfies WebAssembly.Global; 210 | exports.importedGlobal.value satisfies bigint; 211 | exports.memory satisfies WebAssembly.Memory; 212 | 213 | // run exported function 214 | let result = exports.exportedFunc(10, 0); 215 | assert(result === 15); 216 | assert(exports.importedGlobal.value === 1000n); 217 | console.log({ 218 | result, 219 | importedGlobal: exports.importedGlobal.value, 220 | memory: new Uint8Array(exports.memory.buffer, 0, 8), 221 | }); 222 | 223 | // wabt features 224 | 225 | function wabtFeatures() { 226 | return { 227 | /** Experimental exception handling. */ 228 | exceptions: true, 229 | /** Import/export mutable globals. */ 230 | mutable_globals: true, 231 | /** Saturating float-to-int operators. */ 232 | sat_float_to_int: true, 233 | /** Sign-extension operators. */ 234 | sign_extension: true, 235 | /** SIMD support. */ 236 | simd: true, 237 | /** Threading support. */ 238 | threads: true, 239 | /** Multi-value. */ 240 | multi_value: true, 241 | /** Tail-call support. */ 242 | tail_call: true, 243 | /** Bulk-memory operations. */ 244 | bulk_memory: true, 245 | /** Reference types (externref). */ 246 | reference_types: true, 247 | /** Relaxed SIMD */ 248 | relaxed_simd: true, 249 | }; 250 | } 251 | -------------------------------------------------------------------------------- /examples/example.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (type (;0;) (func (param i64))) 3 | (type (;1;) (func (param i32))) 4 | (type (;2;) (func (param funcref))) 5 | (type (;3;) (func (param f64))) 6 | (type (;4;) (func)) 7 | (type (;5;) (func (param i32 i32) (result i32))) 8 | (type (;6;) (func (param f64 f64 f64) (result f64))) 9 | (type (;7;) (func (param i32) (result i32))) 10 | (import "" "f0" (func (;0;) (type 0))) 11 | (import "" "f1" (func (;1;) (type 1))) 12 | (import "" "f2" (func (;2;) (type 2))) 13 | (import "" "f3" (func (;3;) (type 3))) 14 | (import "" "f4" (func (;4;) (type 4))) 15 | (import "" "g0" (global (;0;) i64)) 16 | (import "" "m0" (memory (;0;) 1 65536 shared)) 17 | (func (;5;) (type 5) (param i32 i32) (result i32) 18 | (local v128 v128 i32) 19 | ref.func 6 20 | call 2 21 | global.get 1 22 | i32.const 0 23 | call_indirect (type 2) 24 | f64.const 0x1.004189374bc6ap+0 (;=1.001;) 25 | global.set 2 26 | f64.const 0x1.028f5c28f5c29p+0 (;=1.01;) 27 | global.get 2 28 | f64.mul 29 | call 3 30 | local.get 0 31 | local.get 1 32 | if ;; label = @1 33 | local.get 0 34 | call 1 35 | end 36 | i32.const 2147483647 37 | i32.const -2147483648 38 | local.get 1 39 | select 40 | call 1 41 | local.set 4 42 | local.get 4 43 | i32.const 5 44 | call 6 45 | i32.const 10 46 | memory.grow 47 | drop 48 | i32.const 0 49 | i32.const 0 50 | i32.load offset=4 51 | i32.store 52 | i64.const 64 53 | call 0 54 | v128.const i32x4 0x00000001 0x00000000 0x00000002 0x00000000 55 | v128.const i32x4 0x00000003 0x00000004 0x00000005 0x00000006 56 | i32x4.add 57 | local.set 3 58 | v128.const i32x4 0x9999999a 0x3fb99999 0x9999999a 0x3fc99999 59 | f64.const 0x1.9p+2 (;=6.25;) 60 | f64x2.splat 61 | f64x2.mul 62 | f64x2.extract_lane 1 63 | call 3 64 | ref.null func 65 | i32.const 10 66 | table.grow 0 67 | drop 68 | i32.const 0 69 | i32.const 4 70 | i32.atomic.rmw.add 71 | i32.const 0 72 | i32.const 0 73 | memory.atomic.notify 74 | drop 75 | drop 76 | atomic.fence) 77 | (func (;6;) (type 5) (param i32 i32) (result i32) 78 | (local i32 i32) 79 | f64.const 0x1.2p+0 (;=1.125;) 80 | i64.trunc_sat_f64_s 81 | call 0 82 | local.get 1 83 | local.get 0 84 | i32.const 0 85 | i32.add 86 | i32.add 87 | block (param i32) (result i32) ;; label = @1 88 | local.tee 2 89 | call 1 90 | loop ;; label = @2 91 | local.get 3 92 | call 1 93 | local.get 3 94 | i32.const 1 95 | i32.add 96 | local.tee 3 97 | i32.const 5 98 | i32.eq 99 | if ;; label = @3 100 | local.get 2 101 | return 102 | call 1 103 | end 104 | br 0 (;@2;) 105 | local.get 3 106 | i32.ne 107 | br_if 0 (;@2;) 108 | end 109 | local.get 2 110 | local.get 2 111 | drop 112 | end) 113 | (func (;7;) (type 6) (param f64 f64 f64) (result f64) 114 | local.get 0 115 | f64x2.splat 116 | local.get 1 117 | f64x2.splat 118 | local.get 2 119 | f64x2.splat 120 | f64x2.relaxed_madd 121 | f64x2.extract_lane 0) 122 | (table (;0;) 4 funcref) 123 | (global (;1;) funcref (ref.func 6)) 124 | (global (;2;) (mut f64) (f64.const 0x0p+0 (;=0;))) 125 | (export "exportedFunc" (func 5)) 126 | (export "fma" (func 7)) 127 | (export "importedGlobal" (global 0)) 128 | (export "memory" (memory 0)) 129 | (start 4) 130 | (elem (;0;) (i32.const 0) funcref (ref.func 2) (ref.func 6) (ref.null func) (ref.null func)) 131 | (data (;0;) (i32.const 0) "\00\01\02\03\04\05\06\07\08\09\0a\0b")) 132 | -------------------------------------------------------------------------------- /examples/fma.ts: -------------------------------------------------------------------------------- 1 | // run with `node --loader=ts-node/esm examples/fma.ts` 2 | import assert from "assert"; 3 | import { f64, f64x2, func, Module } from "../src/index.js"; 4 | 5 | const fma = func({ in: [f64, f64, f64], out: [f64] }, ([x, y, z]) => { 6 | let r = f64x2.relaxed_madd(f64x2.splat(x), f64x2.splat(y), f64x2.splat(z)); 7 | f64x2.extract_lane(0, r); 8 | }); 9 | 10 | const fnma = func({ in: [f64, f64, f64], out: [f64] }, ([x, y, z]) => { 11 | let r = f64x2.relaxed_nmadd(f64x2.splat(x), f64x2.splat(y), f64x2.splat(z)); 12 | f64x2.extract_lane(0, r); 13 | }); 14 | 15 | let module = Module({ exports: { fma, fnma } }); 16 | let { instance } = await module.instantiate(); 17 | 18 | console.dir(module.module, { depth: Infinity }); 19 | 20 | let [x, y, z] = [0.5, 3, 1.25]; 21 | 22 | let r1 = instance.exports.fma(x, y, z); 23 | let r2 = instance.exports.fnma(x, y, z); 24 | 25 | assert.equal(r1, x * y + z); 26 | assert.equal(r2, -(x * y) + z); 27 | 28 | console.log({ r1, r2 }); 29 | // { r1: 2.75, r2: -0.25 } 30 | -------------------------------------------------------------------------------- /examples/simple.ts: -------------------------------------------------------------------------------- 1 | // run with `node --loader=ts-node/esm examples/simple.ts` 2 | import { i64, func, Module } from "../src/index.js"; 3 | 4 | const myFunction = func({ in: [i64, i64], out: [i64] }, ([x, y]) => { 5 | i64.mul(x, y); 6 | }); 7 | 8 | let module = Module({ exports: { myFunction } }); 9 | let { instance } = await module.instantiate(); 10 | 11 | let result = instance.exports.myFunction(5n, 20n); 12 | console.log({ result }); 13 | // { result: 100n } 14 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasmati", 3 | "version": "0.2.4", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wasmati", 9 | "version": "0.2.4", 10 | "license": "MIT", 11 | "dependencies": { 12 | "ieee754": "^1.2.1" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^18.15.11", 16 | "ts-node": "^10.9.1", 17 | "typescript": "^5.1.3", 18 | "wabt": "^1.0.36" 19 | } 20 | }, 21 | "node_modules/@cspotcode/source-map-support": { 22 | "version": "0.8.1", 23 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 24 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 25 | "dev": true, 26 | "dependencies": { 27 | "@jridgewell/trace-mapping": "0.3.9" 28 | }, 29 | "engines": { 30 | "node": ">=12" 31 | } 32 | }, 33 | "node_modules/@jridgewell/resolve-uri": { 34 | "version": "3.1.0", 35 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", 36 | "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", 37 | "dev": true, 38 | "engines": { 39 | "node": ">=6.0.0" 40 | } 41 | }, 42 | "node_modules/@jridgewell/sourcemap-codec": { 43 | "version": "1.4.14", 44 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", 45 | "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", 46 | "dev": true 47 | }, 48 | "node_modules/@jridgewell/trace-mapping": { 49 | "version": "0.3.9", 50 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 51 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 52 | "dev": true, 53 | "dependencies": { 54 | "@jridgewell/resolve-uri": "^3.0.3", 55 | "@jridgewell/sourcemap-codec": "^1.4.10" 56 | } 57 | }, 58 | "node_modules/@tsconfig/node10": { 59 | "version": "1.0.9", 60 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", 61 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", 62 | "dev": true 63 | }, 64 | "node_modules/@tsconfig/node12": { 65 | "version": "1.0.11", 66 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 67 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 68 | "dev": true 69 | }, 70 | "node_modules/@tsconfig/node14": { 71 | "version": "1.0.3", 72 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 73 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 74 | "dev": true 75 | }, 76 | "node_modules/@tsconfig/node16": { 77 | "version": "1.0.3", 78 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", 79 | "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", 80 | "dev": true 81 | }, 82 | "node_modules/@types/node": { 83 | "version": "18.15.11", 84 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", 85 | "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", 86 | "dev": true 87 | }, 88 | "node_modules/acorn": { 89 | "version": "8.8.2", 90 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", 91 | "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", 92 | "dev": true, 93 | "bin": { 94 | "acorn": "bin/acorn" 95 | }, 96 | "engines": { 97 | "node": ">=0.4.0" 98 | } 99 | }, 100 | "node_modules/acorn-walk": { 101 | "version": "8.2.0", 102 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", 103 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", 104 | "dev": true, 105 | "engines": { 106 | "node": ">=0.4.0" 107 | } 108 | }, 109 | "node_modules/arg": { 110 | "version": "4.1.3", 111 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 112 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 113 | "dev": true 114 | }, 115 | "node_modules/create-require": { 116 | "version": "1.1.1", 117 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 118 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 119 | "dev": true 120 | }, 121 | "node_modules/diff": { 122 | "version": "4.0.2", 123 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 124 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 125 | "dev": true, 126 | "engines": { 127 | "node": ">=0.3.1" 128 | } 129 | }, 130 | "node_modules/ieee754": { 131 | "version": "1.2.1", 132 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 133 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 134 | "funding": [ 135 | { 136 | "type": "github", 137 | "url": "https://github.com/sponsors/feross" 138 | }, 139 | { 140 | "type": "patreon", 141 | "url": "https://www.patreon.com/feross" 142 | }, 143 | { 144 | "type": "consulting", 145 | "url": "https://feross.org/support" 146 | } 147 | ] 148 | }, 149 | "node_modules/make-error": { 150 | "version": "1.3.6", 151 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 152 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 153 | "dev": true 154 | }, 155 | "node_modules/ts-node": { 156 | "version": "10.9.1", 157 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", 158 | "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", 159 | "dev": true, 160 | "dependencies": { 161 | "@cspotcode/source-map-support": "^0.8.0", 162 | "@tsconfig/node10": "^1.0.7", 163 | "@tsconfig/node12": "^1.0.7", 164 | "@tsconfig/node14": "^1.0.0", 165 | "@tsconfig/node16": "^1.0.2", 166 | "acorn": "^8.4.1", 167 | "acorn-walk": "^8.1.1", 168 | "arg": "^4.1.0", 169 | "create-require": "^1.1.0", 170 | "diff": "^4.0.1", 171 | "make-error": "^1.1.1", 172 | "v8-compile-cache-lib": "^3.0.1", 173 | "yn": "3.1.1" 174 | }, 175 | "bin": { 176 | "ts-node": "dist/bin.js", 177 | "ts-node-cwd": "dist/bin-cwd.js", 178 | "ts-node-esm": "dist/bin-esm.js", 179 | "ts-node-script": "dist/bin-script.js", 180 | "ts-node-transpile-only": "dist/bin-transpile.js", 181 | "ts-script": "dist/bin-script-deprecated.js" 182 | }, 183 | "peerDependencies": { 184 | "@swc/core": ">=1.2.50", 185 | "@swc/wasm": ">=1.2.50", 186 | "@types/node": "*", 187 | "typescript": ">=2.7" 188 | }, 189 | "peerDependenciesMeta": { 190 | "@swc/core": { 191 | "optional": true 192 | }, 193 | "@swc/wasm": { 194 | "optional": true 195 | } 196 | } 197 | }, 198 | "node_modules/typescript": { 199 | "version": "5.1.3", 200 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", 201 | "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", 202 | "dev": true, 203 | "bin": { 204 | "tsc": "bin/tsc", 205 | "tsserver": "bin/tsserver" 206 | }, 207 | "engines": { 208 | "node": ">=14.17" 209 | } 210 | }, 211 | "node_modules/v8-compile-cache-lib": { 212 | "version": "3.0.1", 213 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 214 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 215 | "dev": true 216 | }, 217 | "node_modules/wabt": { 218 | "version": "1.0.36", 219 | "resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.36.tgz", 220 | "integrity": "sha512-GAfEcFyvYRZ51xIZKeeCmIKytTz3ejCeEU9uevGNhEnqt9qXp3a8Q2O4ByZr6rKWcd8jV/Oj5cbDJFtmTYdchg==", 221 | "dev": true, 222 | "bin": { 223 | "wasm-decompile": "bin/wasm-decompile", 224 | "wasm-interp": "bin/wasm-interp", 225 | "wasm-objdump": "bin/wasm-objdump", 226 | "wasm-stats": "bin/wasm-stats", 227 | "wasm-strip": "bin/wasm-strip", 228 | "wasm-validate": "bin/wasm-validate", 229 | "wasm2c": "bin/wasm2c", 230 | "wasm2wat": "bin/wasm2wat", 231 | "wat2wasm": "bin/wat2wasm" 232 | } 233 | }, 234 | "node_modules/yn": { 235 | "version": "3.1.1", 236 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 237 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 238 | "dev": true, 239 | "engines": { 240 | "node": ">=6" 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasmati", 3 | "version": "0.2.4", 4 | "description": "Write low-level WebAssembly, from JavaScript", 5 | "type": "module", 6 | "main": "build/index.js", 7 | "types": "./build/index.d.ts", 8 | "exports": { 9 | "types": "./build/index.d.ts", 10 | "default": "./build/index.js" 11 | }, 12 | "scripts": { 13 | "test": "node --loader=ts-node/esm examples/example.ts", 14 | "build": "tsc", 15 | "clean": "rm -rf build", 16 | "prepublishOnly": "npm run clean && npm run build && npm test" 17 | }, 18 | "files": [ 19 | "src", 20 | "build" 21 | ], 22 | "keywords": [ 23 | "webassembly", 24 | "wasm" 25 | ], 26 | "author": "Gregor Mitscha-Baude ", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/zksecurity/wasmati" 31 | }, 32 | "dependencies": { 33 | "ieee754": "^1.2.1" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^18.15.11", 37 | "ts-node": "^10.9.1", 38 | "typescript": "^5.1.3", 39 | "wabt": "^1.0.36" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/binable.ts: -------------------------------------------------------------------------------- 1 | import { Tuple } from "./util.js"; 2 | 3 | export { 4 | Binable, 5 | tuple, 6 | record, 7 | array, 8 | iso, 9 | constant, 10 | withByteCode, 11 | withPreamble, 12 | withValidation, 13 | Byte, 14 | Bool, 15 | One, 16 | Zero, 17 | Undefined, 18 | or, 19 | and, 20 | orUndefined, 21 | orDefault, 22 | byteEnum, 23 | Zero as TODO, 24 | }; 25 | 26 | type Binable = { 27 | toBytes(value: T): number[]; 28 | readBytes(bytes: number[], offset: number): [value: T, offset: number]; 29 | fromBytes(bytes: number[] | Uint8Array): T; 30 | }; 31 | 32 | function Binable({ 33 | toBytes, 34 | readBytes, 35 | }: { 36 | toBytes(t: T): number[]; 37 | readBytes(bytes: number[], offset: number): [value: T, offset: number]; 38 | }): Binable { 39 | return { 40 | toBytes, 41 | readBytes, 42 | // spec: fromBytes throws if the input bytes are not all used 43 | fromBytes([...bytes]) { 44 | let [value, offset] = readBytes(bytes, 0); 45 | if (offset < bytes.length) 46 | throw Error("fromBytes: input bytes left over"); 47 | return value; 48 | }, 49 | }; 50 | } 51 | 52 | type Byte = number; 53 | const Byte = Binable({ 54 | toBytes(b) { 55 | return [b]; 56 | }, 57 | readBytes(bytes, offset) { 58 | let byte = bytes[offset]; 59 | return [byte, offset + 1]; 60 | }, 61 | }); 62 | 63 | type Bool = boolean; 64 | const Bool = Binable({ 65 | toBytes(b) { 66 | return [Number(b)]; 67 | }, 68 | readBytes(bytes, offset) { 69 | let byte = bytes[offset]; 70 | if (byte !== 0 && byte !== 1) { 71 | throw Error("not a valid boolean"); 72 | } 73 | return [!!byte, offset + 1]; 74 | }, 75 | }); 76 | 77 | function withByteCode(code: number, binable: Binable): Binable { 78 | return Binable({ 79 | toBytes(t) { 80 | return [code].concat(binable.toBytes(t)); 81 | }, 82 | readBytes(bytes, offset) { 83 | if (bytes[offset++] !== code) throw Error("invalid start byte"); 84 | return binable.readBytes(bytes, offset); 85 | }, 86 | }); 87 | } 88 | 89 | function withPreamble(preamble: number[], binable: Binable): Binable { 90 | let length = preamble.length; 91 | return Binable({ 92 | toBytes(t) { 93 | return preamble.concat(binable.toBytes(t)); 94 | }, 95 | readBytes(bytes, offset) { 96 | for (let i = 0; i < length; i++) { 97 | if (bytes[offset + i] !== preamble[i]) throw Error("invalid preamble"); 98 | } 99 | return binable.readBytes(bytes, offset + length); 100 | }, 101 | }); 102 | } 103 | 104 | function withValidation(binable: Binable, validate: (t: T) => void) { 105 | return Binable({ 106 | toBytes(t) { 107 | validate(t); 108 | return binable.toBytes(t); 109 | }, 110 | readBytes(bytes, offset) { 111 | let [t, end] = binable.readBytes(bytes, offset); 112 | validate(t); 113 | return [t, end]; 114 | }, 115 | }); 116 | } 117 | 118 | type Union> = T[number]; 119 | 120 | function record>(binables: { 121 | [i in keyof Types]-?: Binable; 122 | }): Binable { 123 | let keys = Object.keys(binables); 124 | let binablesTuple = keys.map((key) => binables[key]) as Tuple>; 125 | let tupleBinable = tuple>(binablesTuple); 126 | return Binable({ 127 | toBytes(t) { 128 | let array = keys.map((key) => t[key]) as Tuple; 129 | return tupleBinable.toBytes(array); 130 | }, 131 | readBytes(bytes, start) { 132 | let [tupleValue, end] = tupleBinable.readBytes(bytes, start); 133 | let value = Object.fromEntries( 134 | keys.map((key, i) => [key, tupleValue[i]]) 135 | ) as any; 136 | return [value, end]; 137 | }, 138 | }); 139 | } 140 | 141 | function tuple>(binables: { 142 | [i in keyof Types]: Binable; 143 | }): Binable { 144 | let n = (binables as any[]).length; 145 | return Binable({ 146 | toBytes(t) { 147 | let bytes: number[] = []; 148 | for (let i = 0; i < n; i++) { 149 | let subBytes = binables[i].toBytes(t[i]); 150 | bytes.push(...subBytes); 151 | } 152 | return bytes; 153 | }, 154 | readBytes(bytes, offset) { 155 | let values: Types[number] = []; 156 | for (let i = 0; i < n; i++) { 157 | let [value, newOffset] = binables[i].readBytes(bytes, offset); 158 | offset = newOffset; 159 | values.push(value); 160 | } 161 | return [values as Types, offset]; 162 | }, 163 | }); 164 | } 165 | 166 | function array(binable: Binable, size: number): Binable { 167 | return Binable({ 168 | toBytes(ts) { 169 | if (ts.length !== size) throw Error("array length mismatch"); 170 | let bytes: number[] = []; 171 | for (let i = 0; i < size; i++) { 172 | let subBytes = binable.toBytes(ts[i]); 173 | bytes.push(...subBytes); 174 | } 175 | return bytes; 176 | }, 177 | readBytes(bytes, offset) { 178 | let values: T[] = []; 179 | for (let i = 0; i < size; i++) { 180 | let [value, newOffset] = binable.readBytes(bytes, offset); 181 | offset = newOffset; 182 | values.push(value); 183 | } 184 | return [values, offset]; 185 | }, 186 | }); 187 | } 188 | 189 | function iso( 190 | binable: Binable, 191 | { to, from }: { to(s: S): T; from(t: T): S } 192 | ): Binable { 193 | return Binable({ 194 | toBytes(s: S) { 195 | return binable.toBytes(to(s)); 196 | }, 197 | readBytes(bytes, offset) { 198 | let [value, end] = binable.readBytes(bytes, offset); 199 | return [from(value), end]; 200 | }, 201 | }); 202 | } 203 | 204 | function constant(c: C) { 205 | return Binable({ 206 | toBytes() { 207 | return []; 208 | }, 209 | readBytes(_bytes, offset) { 210 | return [c, offset]; 211 | }, 212 | }); 213 | } 214 | 215 | type Zero = never; 216 | const Zero = Binable({ 217 | toBytes() { 218 | throw Error("can not write Zero"); 219 | }, 220 | readBytes() { 221 | throw Error("can not parse Zero"); 222 | }, 223 | }); 224 | type One = undefined; 225 | const One = constant(undefined); 226 | type Undefined = undefined; 227 | const Undefined = One; 228 | 229 | const and = tuple; 230 | 231 | function or>( 232 | binables: { 233 | [i in keyof Types]: Binable; 234 | }, 235 | distinguish: (t: Union) => 236 | | { 237 | [i in keyof Types]: Binable; 238 | }[number] 239 | | number 240 | | undefined 241 | ): Binable> { 242 | return Binable({ 243 | toBytes(value) { 244 | let result = distinguish(value); 245 | if (result === undefined) 246 | throw Error("or: input matches no allowed type"); 247 | let binable = typeof result === "number" ? binables[result] : result; 248 | return binable.toBytes(value); 249 | }, 250 | readBytes(bytes, offset) { 251 | let n = (binables as any[]).length; 252 | for (let i = 0; i < n; i++) { 253 | try { 254 | let [value, end] = binables[i].readBytes(bytes, offset); 255 | if (distinguish(value) === binables[i]) return [value, end]; 256 | } catch {} 257 | } 258 | throw Error("or: could not parse any of the possible types"); 259 | }, 260 | }); 261 | } 262 | 263 | function orUndefined(binable: Binable): Binable { 264 | return or([binable, One], (value) => (value === undefined ? One : binable)); 265 | } 266 | 267 | function orDefault( 268 | binable: Binable, 269 | defaultValue: T, 270 | isDefault: (t: T) => boolean 271 | ): Binable { 272 | return iso(orUndefined(binable), { 273 | to: (t: T) => (isDefault(t) ? undefined : t), 274 | from: (t: T | undefined) => t ?? defaultValue, 275 | }); 276 | } 277 | 278 | function byteEnum< 279 | Enum extends Record 280 | >(binables: { 281 | [b in keyof Enum & number]: { 282 | kind: Enum[b]["kind"]; 283 | value: Binable; 284 | }; 285 | }): Binable { 286 | let kindToByte = Object.fromEntries( 287 | Object.entries(binables).map(([byte, { kind }]) => [kind, Number(byte)]) 288 | ); 289 | return Binable({ 290 | toBytes({ kind, value }) { 291 | let byte = kindToByte[kind]; 292 | let binable = binables[byte].value; 293 | return [byte].concat(binable.toBytes(value)); 294 | }, 295 | readBytes(bytes, offset) { 296 | let byte = bytes[offset++]; 297 | let entry = binables[byte]; 298 | if (entry === undefined) 299 | throw Error(`byte ${byte} matches none of the possible types`); 300 | let { kind, value: binable } = entry; 301 | let [value, end] = binable.readBytes(bytes, offset); 302 | return [{ kind, value }, end]; 303 | }, 304 | }); 305 | } 306 | -------------------------------------------------------------------------------- /src/dependency.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * interfaces for declaring functions/globals/etc stand-alone (without reference to a module) 3 | * that keep track of their dependencies. Declaring them as the exports of a module 4 | * should enable to automatically include all dependencies in that module and determine 5 | * indices for them. 6 | */ 7 | 8 | import { 9 | FunctionType, 10 | GlobalType, 11 | MemoryType, 12 | RefType, 13 | TableType, 14 | ValueType, 15 | } from "./types.js"; 16 | import { Byte } from "./binable.js"; 17 | 18 | export { 19 | t, 20 | Export, 21 | anyDependency, 22 | Type, 23 | type, 24 | Func, 25 | HasRefTo, 26 | Global, 27 | Table, 28 | Memory, 29 | HasMemory, 30 | Data, 31 | Elem, 32 | ImportFunc, 33 | ImportGlobal, 34 | ImportTable, 35 | ImportMemory, 36 | AnyFunc, 37 | AnyGlobal, 38 | AnyMemory, 39 | AnyTable, 40 | AnyImport, 41 | Instruction, 42 | Const, 43 | }; 44 | export { hasRefTo, hasMemory, dependencyKinds, kindToExportKind }; 45 | 46 | type anyDependency = { kind: string; deps: anyDependency[] }; 47 | 48 | type Export = AnyFunc | AnyGlobal | AnyMemory | AnyTable; 49 | 50 | type t = 51 | | Type 52 | | Func 53 | | HasRefTo 54 | | Global 55 | | Table 56 | | Memory 57 | | HasMemory 58 | | Data 59 | | Elem 60 | | ImportFunc 61 | | ImportGlobal 62 | | ImportTable 63 | | ImportMemory; 64 | 65 | type Type = { kind: "type"; type: FunctionType; deps: [] }; 66 | function type(type: FunctionType): Type { 67 | return { kind: "type", type, deps: [] }; 68 | } 69 | 70 | type Func = { 71 | kind: "function"; 72 | type: FunctionType; 73 | locals: ValueType[]; 74 | body: Instruction[]; 75 | deps: t[]; 76 | }; 77 | type HasRefTo = { kind: "hasRefTo"; value: AnyFunc; deps: [] }; 78 | function hasRefTo(value: AnyFunc): HasRefTo { 79 | return { kind: "hasRefTo", value, deps: [] }; 80 | } 81 | 82 | type Global = { 83 | kind: "global"; 84 | type: GlobalType; 85 | init: Const.t; 86 | deps: (AnyGlobal | AnyFunc)[]; 87 | }; 88 | 89 | type Table = { 90 | kind: "table"; 91 | type: TableType; 92 | deps: Elem[]; 93 | }; 94 | type Memory = { 95 | kind: "memory"; 96 | type: MemoryType; 97 | deps: Data[]; 98 | }; 99 | type HasMemory = { kind: "hasMemory"; deps: [] }; 100 | const hasMemory: HasMemory = { kind: "hasMemory", deps: [] }; 101 | 102 | type Data = { 103 | kind: "data"; 104 | init: Byte[]; 105 | mode: "passive" | { memory: 0; offset: Const.i32 | Const.globalGet<"i32"> }; 106 | deps: (HasMemory | AnyGlobal | AnyMemory)[]; 107 | }; 108 | 109 | type Elem = { 110 | kind: "elem"; 111 | type: RefType; 112 | init: (Const.refFunc | Const.refNull)[]; 113 | mode: 114 | | "passive" 115 | | "declarative" 116 | | { 117 | table: AnyTable; 118 | offset: Const.i32 | Const.globalGet<"i32">; 119 | }; 120 | deps: (AnyTable | AnyFunc | AnyGlobal)[]; 121 | }; 122 | 123 | type ImportPath = { module?: string; string?: string }; 124 | type ImportFunc = ImportPath & { 125 | kind: "importFunction"; 126 | type: FunctionType; 127 | value: Function; 128 | deps: []; 129 | }; 130 | type ImportGlobal = ImportPath & { 131 | kind: "importGlobal"; 132 | type: GlobalType; 133 | value: WebAssembly.Global; 134 | deps: []; 135 | }; 136 | type ImportTable = ImportPath & { 137 | kind: "importTable"; 138 | type: TableType; 139 | value: WebAssembly.Table; 140 | deps: Elem[]; 141 | }; 142 | type ImportMemory = ImportPath & { 143 | kind: "importMemory"; 144 | type: MemoryType; 145 | value: WebAssembly.Memory; 146 | deps: Data[]; 147 | }; 148 | 149 | type AnyFunc = Func | ImportFunc; 150 | type AnyGlobal = Global | ImportGlobal; 151 | type AnyTable = Table | ImportTable; 152 | type AnyMemory = Memory | ImportMemory; 153 | type AnyImport = ImportFunc | ImportGlobal | ImportTable | ImportMemory; 154 | 155 | const dependencyKinds = [ 156 | "function", 157 | "type", 158 | "hasRefTo", 159 | "global", 160 | "table", 161 | "memory", 162 | "hasMemory", 163 | "data", 164 | "elem", 165 | "importFunction", 166 | "importGlobal", 167 | "importTable", 168 | "importMemory", 169 | ] as const satisfies readonly t["kind"][]; 170 | 171 | const kindToExportKind: Record< 172 | (AnyFunc | AnyGlobal | AnyTable | AnyMemory)["kind"], 173 | (Func | Global | Table | Memory)["kind"] 174 | > = { 175 | function: "function", 176 | importFunction: "function", 177 | global: "global", 178 | importGlobal: "global", 179 | memory: "memory", 180 | importMemory: "memory", 181 | table: "table", 182 | importTable: "table", 183 | }; 184 | 185 | // general instruction 186 | 187 | type Instruction = { 188 | string: string; 189 | type: FunctionType; 190 | deps: t[]; 191 | resolveArgs: any[]; 192 | }; 193 | 194 | // constant instructions 195 | 196 | type ConstInstruction = { 197 | string: string; 198 | type: { args: []; results: [T] }; 199 | deps: t[]; 200 | resolveArgs: any[]; 201 | }; 202 | 203 | namespace Const { 204 | export type i32 = ConstInstruction<"i32"> & { string: "i32.const" }; 205 | export type i64 = ConstInstruction<"i64"> & { string: "i64.const" }; 206 | export type f32 = ConstInstruction<"f32"> & { string: "f32.const" }; 207 | export type f64 = ConstInstruction<"f64"> & { string: "f64.const" }; 208 | export type refNull = ConstInstruction & { 209 | string: "ref.null"; 210 | }; 211 | export type refFunc = ConstInstruction<"funcref"> & { string: "ref.func" }; 212 | export type globalGet = ConstInstruction & { 213 | string: "global.get"; 214 | }; 215 | export type t_ = 216 | | i32 217 | | i64 218 | | f32 219 | | f64 220 | | refNull 221 | | refFunc 222 | | globalGet; 223 | export type t = ConstInstruction & { 224 | string: t_["string"]; 225 | }; 226 | } 227 | 228 | const Const = { 229 | i32(x: number | bigint): Const.i32 { 230 | return { 231 | string: "i32.const", 232 | type: { args: [], results: ["i32"] }, 233 | deps: [], 234 | resolveArgs: [Number(x)], 235 | }; 236 | }, 237 | i64(x: number | bigint): Const.i64 { 238 | return { 239 | string: "i64.const", 240 | type: { args: [], results: ["i64"] }, 241 | deps: [], 242 | resolveArgs: [BigInt(x)], 243 | }; 244 | }, 245 | f32(x: number): Const.f32 { 246 | return { 247 | string: "f32.const", 248 | type: { args: [], results: ["f32"] }, 249 | deps: [], 250 | resolveArgs: [x], 251 | }; 252 | }, 253 | f64(x: number): Const.f64 { 254 | return { 255 | string: "f64.const", 256 | type: { args: [], results: ["f64"] }, 257 | deps: [], 258 | resolveArgs: [x], 259 | }; 260 | }, 261 | refFuncNull: { 262 | string: "ref.null", 263 | type: { args: [], results: ["funcref"] }, 264 | deps: [], 265 | resolveArgs: ["funcref"], 266 | } as Const.refNull<"funcref">, 267 | refExternNull: { 268 | string: "ref.null", 269 | type: { args: [], results: ["externref"] }, 270 | deps: [], 271 | resolveArgs: ["externref"], 272 | } as Const.refNull<"externref">, 273 | refFunc(func: AnyFunc): Const.refFunc { 274 | return { 275 | string: "ref.func", 276 | type: { args: [], results: ["funcref"] }, 277 | deps: [func], 278 | resolveArgs: [], 279 | }; 280 | }, 281 | globalGet(global: Global): Const.globalGet { 282 | if (global.type.mutable) 283 | throw Error("global in a const expression can not be mutable"); 284 | return { 285 | string: "global.get", 286 | type: { args: [], results: [global.type.value] }, 287 | deps: [global], 288 | resolveArgs: [], 289 | }; 290 | }, 291 | }; 292 | -------------------------------------------------------------------------------- /src/export.ts: -------------------------------------------------------------------------------- 1 | import { Binable, byteEnum, record } from "./binable.js"; 2 | import { Name, U32 } from "./immediate.js"; 3 | import { 4 | FunctionType, 5 | Type, 6 | GlobalType, 7 | JSValue, 8 | MemoryType, 9 | TableType, 10 | TypeIndex, 11 | ValueType, 12 | valueTypeLiteral, 13 | valueTypeLiterals, 14 | } from "./types.js"; 15 | import { ToTypeTuple } from "./func.js"; 16 | import { Tuple } from "./util.js"; 17 | import * as Dependency from "./dependency.js"; 18 | import { ImportFunc } from "./func-types.js"; 19 | import { dataConstructor } from "./memory.js"; 20 | 21 | export { Export, Import, ExternType, importFunc, importGlobal, importMemory }; 22 | 23 | type ExternType = 24 | | { kind: "function"; value: FunctionType } 25 | | { kind: "table"; value: TableType } 26 | | { kind: "memory"; value: MemoryType } 27 | | { kind: "global"; value: GlobalType }; 28 | 29 | type ExportDescription = { 30 | kind: "function" | "table" | "memory" | "global"; 31 | value: U32; 32 | }; 33 | const ExportDescription: Binable = byteEnum<{ 34 | 0x00: { kind: "function"; value: U32 }; 35 | 0x01: { kind: "table"; value: U32 }; 36 | 0x02: { kind: "memory"; value: U32 }; 37 | 0x03: { kind: "global"; value: U32 }; 38 | }>({ 39 | 0x00: { kind: "function", value: U32 }, 40 | 0x01: { kind: "table", value: U32 }, 41 | 0x02: { kind: "memory", value: U32 }, 42 | 0x03: { kind: "global", value: U32 }, 43 | }); 44 | 45 | type Export = { name: string; description: ExportDescription }; 46 | const Export = record({ name: Name, description: ExportDescription }); 47 | 48 | type ImportDescription = 49 | | { kind: "function"; value: TypeIndex } 50 | | { kind: "table"; value: TableType } 51 | | { kind: "memory"; value: MemoryType } 52 | | { kind: "global"; value: GlobalType }; 53 | const ImportDescription: Binable = byteEnum<{ 54 | 0x00: { kind: "function"; value: TypeIndex }; 55 | 0x01: { kind: "table"; value: TableType }; 56 | 0x02: { kind: "memory"; value: MemoryType }; 57 | 0x03: { kind: "global"; value: GlobalType }; 58 | }>({ 59 | 0x00: { kind: "function", value: TypeIndex }, 60 | 0x01: { kind: "table", value: TableType }, 61 | 0x02: { kind: "memory", value: MemoryType }, 62 | 0x03: { kind: "global", value: GlobalType }, 63 | }); 64 | 65 | type Import = { 66 | module: string; 67 | name: string; 68 | description: ImportDescription; 69 | }; 70 | const Import = record({ 71 | module: Name, 72 | name: Name, 73 | description: ImportDescription, 74 | }); 75 | 76 | function importFunc< 77 | const Args extends Tuple, 78 | const Results extends Tuple 79 | >( 80 | { 81 | in: args_, 82 | out: results_, 83 | }: { 84 | in: ToTypeTuple; 85 | out: ToTypeTuple; 86 | }, 87 | run: Function 88 | ): ImportFunc { 89 | let args = valueTypeLiterals(args_); 90 | let results = valueTypeLiterals(results_); 91 | let type = { args, results }; 92 | return { kind: "importFunction", type, deps: [], value: run }; 93 | } 94 | 95 | function importGlobal( 96 | type: Type, 97 | value: JSValue, 98 | { mutable = false } = {} 99 | ): Dependency.ImportGlobal { 100 | let globalType = { value: valueTypeLiteral(type), mutable }; 101 | let valueType: WebAssembly.ValueType = 102 | type.kind === "funcref" ? "anyfunc" : type.kind; 103 | let value_ = new WebAssembly.Global({ value: valueType, mutable }, value); 104 | return { kind: "importGlobal", type: globalType, deps: [], value: value_ }; 105 | } 106 | 107 | function importMemory( 108 | { 109 | min, 110 | max, 111 | shared = false, 112 | }: { 113 | min: number; 114 | max?: number; 115 | shared?: boolean; 116 | }, 117 | memory?: WebAssembly.Memory, 118 | ...content: (number[] | Uint8Array)[] 119 | ) { 120 | let type = { limits: { min, max, shared } }; 121 | let value = 122 | memory ?? new WebAssembly.Memory({ initial: min, maximum: max, shared }); 123 | let memory_: Dependency.ImportMemory = { 124 | kind: "importMemory", 125 | type, 126 | deps: [], 127 | value, 128 | }; 129 | let offset = 0; 130 | for (let init of content) { 131 | dataConstructor( 132 | { memory: memory_, offset: Dependency.Const.i32(offset) }, 133 | init 134 | ); 135 | offset += init.length; 136 | } 137 | return memory_; 138 | } 139 | -------------------------------------------------------------------------------- /src/func-types.ts: -------------------------------------------------------------------------------- 1 | import * as Dependency from "./dependency.js"; 2 | import { ValueType } from "./types.js"; 3 | 4 | export { Func, ImportFunc, AnyFunc }; 5 | 6 | type Func< 7 | Args extends readonly ValueType[], 8 | Results extends readonly ValueType[] 9 | > = { 10 | kind: "function"; 11 | locals: ValueType[]; 12 | body: Dependency.Instruction[]; 13 | deps: Dependency.t[]; 14 | type: { args: Args; results: Results }; 15 | }; 16 | 17 | type ImportFunc< 18 | Args extends readonly ValueType[], 19 | Results extends readonly ValueType[] 20 | > = { 21 | module?: string; 22 | string?: string; 23 | kind: "importFunction"; 24 | type: { args: Args; results: Results }; 25 | value: Function; 26 | deps: []; 27 | }; 28 | 29 | type AnyFunc< 30 | Args extends readonly ValueType[], 31 | Results extends readonly ValueType[] 32 | > = Func | ImportFunc; 33 | -------------------------------------------------------------------------------- /src/func.ts: -------------------------------------------------------------------------------- 1 | import { Binable, iso, record, tuple } from "./binable.js"; 2 | import * as Dependency from "./dependency.js"; 3 | import { U32, vec, withByteLength } from "./immediate.js"; 4 | import { ResolvedInstruction } from "./instruction/base.js"; 5 | import { Expression } from "./instruction/binable.js"; 6 | import { 7 | LocalContext, 8 | StackVar, 9 | formatStack, 10 | popStack, 11 | withContext, 12 | } from "./local-context.js"; 13 | import { 14 | FunctionIndex, 15 | FunctionType, 16 | JSValue, 17 | Local, 18 | Type, 19 | TypeIndex, 20 | ValueType, 21 | valueTypeLiterals, 22 | } from "./types.js"; 23 | import { Tuple } from "./util.js"; 24 | import { Func } from "./func-types.js"; 25 | 26 | // external 27 | export { func, Local }; 28 | // internal 29 | export { FinalizedFunc, Code, JSFunction, ToTypeTuple }; 30 | 31 | function func< 32 | const Args extends Tuple, 33 | const Locals extends Tuple, 34 | const Results extends Tuple 35 | >( 36 | ctx: LocalContext, 37 | signature: { 38 | in: ToTypeTuple; 39 | locals?: ToTypeTuple; 40 | out: ToTypeTuple; 41 | }, 42 | run: (args: ToLocal, locals: ToLocal, ctx: LocalContext) => void 43 | ): Func { 44 | let { 45 | in: args, 46 | locals = [] as ToTypeTuple, 47 | out: results, 48 | } = signature; 49 | ctx.stack = []; 50 | let argsArray = valueTypeLiterals(args); 51 | let localsArray = valueTypeLiterals(locals); 52 | let resultsArray = valueTypeLiterals(results); 53 | let type: { args: Args; results: Results } & FunctionType = { 54 | args: argsArray as any, 55 | results: resultsArray as any, 56 | }; 57 | let nArgs = argsArray.length; 58 | let argsInput = argsArray.map( 59 | (type, index): Local => ({ 60 | kind: "local", 61 | type, 62 | index, 63 | }) 64 | ) as ToLocal; 65 | let { sortedLocals, localIndices } = sortLocals(localsArray, nArgs); 66 | let localsInput = localIndices.map( 67 | (index, j): Local => ({ 68 | kind: "local", 69 | type: localsArray[j], 70 | index, 71 | }) 72 | ) as ToLocal; 73 | let stack: StackVar[] = []; 74 | let { body, deps } = withContext( 75 | ctx, 76 | { 77 | locals: [...argsArray, ...sortedLocals], 78 | body: [], 79 | deps: [], 80 | stack, 81 | return: resultsArray, 82 | frames: [ 83 | { 84 | label: "top", 85 | opcode: "function", 86 | stack, 87 | startTypes: argsArray, 88 | endTypes: resultsArray, 89 | unreachable: false, 90 | }, 91 | ], 92 | }, 93 | () => { 94 | run(argsInput, localsInput, ctx); 95 | popStack(ctx, resultsArray); 96 | // TODO nice error 97 | if (ctx.stack.length !== 0) 98 | throw Error( 99 | `expected stack to be empty, got ${formatStack(ctx.stack)}` 100 | ); 101 | } 102 | ); 103 | let func = { 104 | kind: "function", 105 | type, 106 | body, 107 | deps, 108 | locals: sortedLocals, 109 | } satisfies Dependency.Func; 110 | return func; 111 | } 112 | 113 | // type inference of function signature 114 | 115 | // example: 116 | // type Test = JSFunction>; 117 | // ^ (arg_0: bigint, arg_1: number) => number 118 | 119 | type ObjectValues = UnionToTuple; 120 | 121 | type JSValues = { 122 | [i in keyof T]: JSValue; 123 | }; 124 | type ReturnValues = T extends [] 125 | ? void 126 | : T extends [ValueType] 127 | ? JSValue 128 | : JSValues; 129 | 130 | type JSFunctionType_ = ( 131 | ...arg: JSValues 132 | ) => ReturnValues; 133 | 134 | type JSFunction = JSFunctionType_< 135 | T["type"]["args"], 136 | T["type"]["results"] 137 | >; 138 | 139 | type ToLocal> = { 140 | [K in keyof T]: Local; 141 | }; 142 | type ToTypeRecord> = { 143 | [K in keyof T]: { kind: T[K] }; 144 | }; 145 | type ToTypeTuple = { 146 | [K in keyof T]: Type; 147 | }; 148 | 149 | // bad hack :/ 150 | 151 | type UnionToIntersection = ( 152 | U extends any ? (arg: U) => any : never 153 | ) extends (arg: infer I) => void 154 | ? I 155 | : never; 156 | 157 | type UnionToTuple = UnionToIntersection< 158 | T extends any ? (t: T) => T : never 159 | > extends (_: any) => infer W 160 | ? [...UnionToTuple>, W] 161 | : []; 162 | 163 | type FinalizedFunc = { 164 | funcIdx: FunctionIndex; 165 | typeIdx: TypeIndex; 166 | type: FunctionType; 167 | locals: ValueType[]; 168 | body: ResolvedInstruction[]; 169 | }; 170 | 171 | // helper 172 | 173 | function sortLocals(locals: ValueType[], offset: number) { 174 | let typeIndex: Record = {}; 175 | let nextIndex = 0; 176 | let count: number[] = []; 177 | let offsetWithin: number[] = []; 178 | for (let local of locals) { 179 | if (typeIndex[local] === undefined) { 180 | typeIndex[local] = nextIndex; 181 | nextIndex++; 182 | } 183 | let i = typeIndex[local]; 184 | count[i] ??= 0; 185 | offsetWithin.push(count[i]); 186 | count[i]++; 187 | } 188 | let typeOffset: number[] = Array(count.length).fill(0); 189 | for (let i = 1; i < count.length; i++) { 190 | typeOffset[i] = count[i - 1] + typeOffset[i - 1]; 191 | } 192 | let localIndices: number[] = []; 193 | for (let j = 0; j < locals.length; j++) { 194 | localIndices[j] = 195 | offset + typeOffset[typeIndex[locals[j]]] + offsetWithin[j]; 196 | } 197 | let sortedLocals: ValueType[] = Object.entries(typeIndex).flatMap( 198 | ([type, i]) => Array(count[i]).fill(type as ValueType) 199 | ); 200 | return { sortedLocals, localIndices }; 201 | } 202 | 203 | // binable 204 | 205 | const CompressedLocals = vec(tuple([U32, ValueType])); 206 | const Locals = iso<[number, ValueType][], ValueType[]>(CompressedLocals, { 207 | to(locals) { 208 | let count: Record = {}; 209 | for (let local of locals) { 210 | count[local] ??= 0; 211 | count[local]++; 212 | } 213 | return Object.entries(count).map(([kind, count]) => [ 214 | count, 215 | kind as ValueType, 216 | ]); 217 | }, 218 | from(compressed) { 219 | let locals: ValueType[] = []; 220 | for (let [count, local] of compressed) { 221 | locals.push(...Array(count).fill(local)); 222 | } 223 | return locals; 224 | }, 225 | }); 226 | 227 | type Code = { locals: ValueType[]; body: Expression }; 228 | const Code = withByteLength( 229 | record({ locals: Locals, body: Expression }) 230 | ) satisfies Binable; 231 | -------------------------------------------------------------------------------- /src/immediate.ts: -------------------------------------------------------------------------------- 1 | import { write, read } from "ieee754"; 2 | import { Binable } from "./binable.js"; 3 | 4 | export { vec, withByteLength, Name, U8, U32, I32, I64, S33, F32, F64 }; 5 | 6 | type U8 = number; 7 | type U32 = number; 8 | type I32 = number; 9 | type I64 = bigint; 10 | type F32 = number; 11 | type F64 = number; 12 | 13 | function vec(Element: Binable) { 14 | return Binable({ 15 | toBytes(vec) { 16 | let length = U32.toBytes(vec.length); 17 | let elements = vec.map((t) => Element.toBytes(t)); 18 | return length.concat(elements.flat()); 19 | }, 20 | readBytes(bytes, start) { 21 | let [length, offset] = U32.readBytes(bytes, start); 22 | let elements: T[] = []; 23 | for (let i = 0; i < length; i++) { 24 | let element: T; 25 | [element, offset] = Element.readBytes(bytes, offset); 26 | elements.push(element); 27 | } 28 | return [elements, offset]; 29 | }, 30 | }); 31 | } 32 | 33 | const Name = Binable({ 34 | toBytes(string: string) { 35 | return [...U32.toBytes(string.length), ...new TextEncoder().encode(string)]; 36 | }, 37 | readBytes(bytes, start) { 38 | let [length, offset] = U32.readBytes(bytes, start); 39 | let end = offset + length; 40 | let stringBytes = Uint8Array.from(bytes.slice(offset, end)); 41 | let string = new TextDecoder().decode(stringBytes); 42 | return [string, end]; 43 | }, 44 | }); 45 | 46 | function withByteLength(binable: Binable): Binable { 47 | return Binable({ 48 | toBytes(t) { 49 | let bytes = binable.toBytes(t); 50 | return U32.toBytes(bytes.length).concat(bytes); 51 | }, 52 | readBytes(bytes, offset) { 53 | let [length, start] = U32.readBytes(bytes, offset); 54 | let [value, end] = binable.readBytes(bytes, start); 55 | if (end !== start + length) throw Error("invalid length encoding"); 56 | return [value, end]; 57 | }, 58 | }); 59 | } 60 | 61 | const U8 = Binable({ 62 | toBytes(x: U8) { 63 | return toULEB128(x); 64 | }, 65 | readBytes(bytes, offset): [U8, number] { 66 | let [x, end] = fromULEB128(bytes, offset); 67 | return [Number(x), end]; 68 | }, 69 | }); 70 | 71 | const U32 = Binable({ 72 | toBytes(x: U32) { 73 | return toULEB128(x); 74 | }, 75 | readBytes(bytes, offset): [U32, number] { 76 | let [x, end] = fromULEB128(bytes, offset); 77 | return [Number(x), end]; 78 | }, 79 | }); 80 | 81 | const I32 = Binable({ 82 | toBytes(x: I32) { 83 | return toSLEB128(x); 84 | }, 85 | readBytes(bytes, offset): [I32, number] { 86 | let [x, end] = fromSLEB128(bytes, offset); 87 | return [Number(x), end]; 88 | }, 89 | }); 90 | 91 | const I64 = Binable({ 92 | toBytes(x: I64) { 93 | return toSLEB128(x); 94 | }, 95 | readBytes(bytes, offset): [I64, number] { 96 | return fromSLEB128(bytes, offset); 97 | }, 98 | }); 99 | 100 | const S33 = Binable({ 101 | toBytes(x: U32) { 102 | return toSLEB128(x); 103 | }, 104 | readBytes(bytes, offset): [U32, number] { 105 | let [x, end] = fromSLEB128(bytes, offset); 106 | return [Number(x), end]; 107 | }, 108 | }); 109 | 110 | // https://en.wikipedia.org/wiki/LEB128 111 | 112 | function toULEB128(x0: bigint | number) { 113 | let x = BigInt(x0); 114 | let bytes: number[] = []; 115 | while (true) { 116 | let byte = Number(x & 0b0111_1111n); // low 7 bits 117 | x >>= 7n; 118 | if (x !== 0n) byte |= 0b1000_0000; 119 | bytes.push(byte); 120 | if (x === 0n) break; 121 | } 122 | return bytes; 123 | } 124 | function fromULEB128(bytes: number[], offset: number) { 125 | let x = 0n; 126 | let shift = 0n; 127 | while (true) { 128 | let byte = bytes[offset++]; 129 | x |= BigInt(byte & 0b0111_1111) << shift; 130 | if ((byte & 0b1000_0000) === 0) break; 131 | shift += 7n; 132 | } 133 | return [x, offset] as [bigint, number]; 134 | } 135 | 136 | function toSLEB128(x0: bigint | number): number[] { 137 | let x = BigInt(x0); 138 | let bytes: number[] = []; 139 | while (true) { 140 | let byte = Number(x & 0b0111_1111n); 141 | x >>= 7n; 142 | if ( 143 | (x === 0n && (byte & 0b0100_0000) === 0) || 144 | (x === -1n && (byte & 0b0100_0000) !== 0) 145 | ) { 146 | bytes.push(byte); 147 | return bytes; 148 | } 149 | bytes.push(byte | 0b1000_0000); 150 | } 151 | } 152 | 153 | function fromSLEB128(bytes: number[], offset: number) { 154 | let x = 0n; 155 | let shift = 0n; 156 | let byte: number; 157 | while (true) { 158 | byte = bytes[offset++]; 159 | x |= BigInt(byte & 0b0111_1111) << shift; 160 | shift += 7n; 161 | if ((byte & 0b1000_0000) === 0) break; 162 | } 163 | // on wikipedia, they say to check for (shift < bitSize) here (https://en.wikipedia.org/wiki/LEB128) 164 | // but for bigints, this gives wrong results for numbers close to 2**(bitSize). 165 | // the bit arithmetic of negative bigints seems to work as if they are infinite two's complements 166 | // e.g. -2n = ...111110, 1n = ...000001 and so we get -2n | 1n == ...111111 == -1n 167 | // so you never have to consider 'bit size' 168 | if (byte & 0b0100_0000) { 169 | x |= -1n << shift; // if negative, we OR with ...11110..0 to make the result a negative bigint 170 | } 171 | return [x, offset] as [bigint, number]; 172 | } 173 | 174 | // float 175 | 176 | const f32Mantissa = 23; 177 | const f64Mantissa = 52; 178 | 179 | const F32 = Binable({ 180 | toBytes(t) { 181 | let bytes = new Uint8Array(4); 182 | write(bytes, t, 0, true, f32Mantissa, 4); 183 | return [...bytes]; 184 | }, 185 | readBytes(bytes, offset) { 186 | let size = 4; 187 | let f32Bytes = Uint8Array.from(bytes.slice(offset, offset + size)); 188 | let value = read(f32Bytes, 0, true, f32Mantissa, size); 189 | return [value, offset + size]; 190 | }, 191 | }); 192 | 193 | const F64 = Binable({ 194 | toBytes(t) { 195 | let bytes = new Uint8Array(8); 196 | write(bytes, t, 0, true, f64Mantissa, 8); 197 | return [...bytes]; 198 | }, 199 | readBytes(bytes, offset) { 200 | let size = 8; 201 | let f32Bytes = Uint8Array.from(bytes.slice(offset, offset + size)); 202 | let value = read(f32Bytes, 0, true, f64Mantissa, size); 203 | return [value, offset + size]; 204 | }, 205 | }); 206 | -------------------------------------------------------------------------------- /src/immediate.unit-test.ts: -------------------------------------------------------------------------------- 1 | import { I32, I64, U32 } from "./immediate.js"; 2 | import { equal } from "node:assert/strict"; 3 | 4 | function roundtrip(x: number) { 5 | try { 6 | equal(I32.fromBytes(I32.toBytes(x)), x); 7 | } catch { 8 | console.log( 9 | "failing roundtrip", 10 | x, 11 | I32.fromBytes(I32.toBytes(x)), 12 | Math.log2(Math.abs(x)) 13 | ); 14 | console.log(I32.toBytes(x)); 15 | equal(I32.fromBytes(I32.toBytes(x)), x); 16 | } 17 | } 18 | function roundtripU(x: number) { 19 | try { 20 | equal(U32.fromBytes(U32.toBytes(x)), x); 21 | } catch { 22 | console.log( 23 | "failing roundtrip", 24 | x, 25 | U32.fromBytes(U32.toBytes(x)), 26 | Math.log2(Math.abs(x)) 27 | ); 28 | console.log(U32.toBytes(x)); 29 | equal(U32.fromBytes(U32.toBytes(x)), x); 30 | } 31 | } 32 | 33 | roundtrip(0); 34 | roundtrip(1); 35 | roundtrip(10); 36 | roundtrip(9999); 37 | roundtrip(2187698); 38 | roundtrip(2 ** 30 + 2 ** 16); 39 | roundtrip(2 ** 31 - 1); 40 | roundtrip(2 ** 27); 41 | roundtripU(2 ** 27); 42 | roundtrip(2 ** 6); 43 | roundtripU(2 ** 6); 44 | 45 | roundtrip(63); 46 | roundtrip(64); 47 | roundtrip(127); 48 | roundtrip(-1); 49 | roundtrip(-2); 50 | roundtrip(-10); 51 | roundtrip(-999); 52 | roundtrip(-(2 ** 21)); 53 | roundtrip(-(2 ** 28)); 54 | roundtrip(-(2 ** 29)); 55 | roundtrip(-(2 ** 31)); 56 | roundtrip(-(2 ** 40) + 1); 57 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Module } from "./module.js"; 2 | import { 3 | globalConstructor, 4 | refOps, 5 | bindLocalOps, 6 | bindGlobalOps, 7 | } from "./instruction/variable.js"; 8 | import { f32Ops, f64Ops, i32Ops, i64Ops } from "./instruction/numeric.js"; 9 | import { memoryOps, dataOps, tableOps, elemOps } from "./instruction/memory.js"; 10 | import { 11 | bindControlOps, 12 | control as controlOps, 13 | parametric, 14 | } from "./instruction/control.js"; 15 | import { 16 | emptyContext, 17 | LocalContext, 18 | Label, 19 | StackVar, 20 | Unknown, 21 | } from "./local-context.js"; 22 | import { Tuple } from "./util.js"; 23 | import { 24 | f32t, 25 | f64t, 26 | i32t, 27 | i64t, 28 | v128t, 29 | funcref, 30 | externref, 31 | ValueType, 32 | ValueTypeObject, 33 | RefType, 34 | RefTypeObject, 35 | Type, 36 | JSValue, 37 | } from "./types.js"; 38 | import { Func, ImportFunc, AnyFunc } from "./func-types.js"; 39 | import { 40 | JSFunction, 41 | Local, 42 | func as originalFunc, 43 | ToTypeTuple, 44 | } from "./func.js"; 45 | import { Instruction, FunctionTypeInput } from "./instruction/base.js"; 46 | import { 47 | f32x4Ops, 48 | f64x2Ops, 49 | i16x8Ops, 50 | i32x4Ops, 51 | i64x2Ops, 52 | i8x16Ops, 53 | v128Ops, 54 | wrapConst, 55 | } from "./instruction/vector.js"; 56 | import { 57 | dataConstructor, 58 | elemConstructor, 59 | memoryConstructor, 60 | tableConstructor, 61 | } from "./memory.js"; 62 | import * as Dependency from "./dependency.js"; 63 | import { 64 | Global, 65 | ImportGlobal, 66 | AnyGlobal, 67 | ImportMemory, 68 | AnyMemory, 69 | } from "./dependency.js"; 70 | import { Const } from "./dependency.js"; 71 | import { importFunc, importGlobal, importMemory } from "./export.js"; 72 | import { TupleN } from "./util.js"; 73 | import { ModuleExport } from "./module.js"; 74 | import { Input } from "./instruction/stack-args.js"; 75 | import { 76 | atomicOps, 77 | i32AtomicOps, 78 | i32AtomicRmw16Ops, 79 | i32AtomicRmw8Ops, 80 | i32AtomicRmwOps, 81 | i64AtomicOps, 82 | i64AtomicRmw16Ops, 83 | i64AtomicRmw32Ops, 84 | i64AtomicRmw8Ops, 85 | i64AtomicRmwOps, 86 | memoryAtomicOps, 87 | } from "./instruction/atomic.js"; 88 | 89 | // instruction API 90 | export { 91 | i32, 92 | i64, 93 | f32, 94 | f64, 95 | local, 96 | global, 97 | ref, 98 | control, 99 | drop, 100 | select, 101 | memory, 102 | data, 103 | table, 104 | elem, 105 | v128, 106 | i8x16, 107 | i16x8, 108 | i32x4, 109 | i64x2, 110 | f32x4, 111 | f64x2, 112 | atomic, 113 | }; 114 | export { 115 | nop, 116 | unreachable, 117 | block, 118 | loop, 119 | if_, 120 | br, 121 | br_if, 122 | br_table, 123 | return_, 124 | call, 125 | call_indirect, 126 | }; 127 | 128 | // other public API 129 | export { defaultCtx }; 130 | export { func, Func, importFunc, ImportFunc, AnyFunc }; 131 | export { importMemory, ImportMemory, AnyMemory }; 132 | export { Global, importGlobal, ImportGlobal, AnyGlobal }; 133 | export { 134 | funcref, 135 | externref, 136 | Local, 137 | $, 138 | StackVar, 139 | Input, 140 | Type, 141 | ValueType, 142 | ValueTypeObject, 143 | RefType, 144 | RefTypeObject, 145 | }; 146 | export { Const, Dependency }; 147 | export type { 148 | ToTypeTuple, 149 | FunctionTypeInput, 150 | Label, 151 | TupleN, 152 | Instruction, 153 | ModuleExport, 154 | JSFunction, 155 | JSValue, 156 | }; 157 | 158 | type i32 = "i32"; 159 | type i64 = "i64"; 160 | type f32 = "f32"; 161 | type f64 = "f64"; 162 | type v128 = "v128"; 163 | 164 | const defaultCtx = emptyContext(); 165 | 166 | const { 167 | func, 168 | i32, 169 | i64, 170 | f32, 171 | f64, 172 | local, 173 | global, 174 | ref, 175 | control, 176 | drop, 177 | select, 178 | memory, 179 | data, 180 | table, 181 | elem, 182 | v128, 183 | i8x16, 184 | i16x8, 185 | i32x4, 186 | i64x2, 187 | f32x4, 188 | f64x2, 189 | atomic, 190 | } = createInstructions(defaultCtx); 191 | 192 | let { 193 | nop, 194 | unreachable, 195 | block, 196 | loop, 197 | if: if_, 198 | br, 199 | br_if, 200 | br_table, 201 | return: return_, 202 | call, 203 | call_indirect, 204 | } = control; 205 | 206 | const $: StackVar = StackVar(Unknown); 207 | 208 | function createInstructions(ctx: LocalContext) { 209 | const func = removeContext(ctx, originalFunc); 210 | 211 | const atomic = removeContexts(ctx, atomicOps); 212 | const memoryAtomic = removeContexts(ctx, memoryAtomicOps); 213 | 214 | const i32AtomicBase = removeContexts(ctx, i32AtomicOps); 215 | const i32AtomicRmw = removeContexts(ctx, i32AtomicRmwOps); 216 | const i32AtomicRmw8 = removeContexts(ctx, i32AtomicRmw8Ops); 217 | const i32AtomicRmw16 = removeContexts(ctx, i32AtomicRmw16Ops); 218 | const i32Atomic = Object.assign(i32AtomicBase, { 219 | rmw: i32AtomicRmw, 220 | rmw8: i32AtomicRmw8, 221 | rmw16: i32AtomicRmw16, 222 | }); 223 | 224 | const i64AtomicBase = removeContexts(ctx, i64AtomicOps); 225 | const i64AtomicRmw = removeContexts(ctx, i64AtomicRmwOps); 226 | const i64AtomicRmw8 = removeContexts(ctx, i64AtomicRmw8Ops); 227 | const i64AtomicRmw16 = removeContexts(ctx, i64AtomicRmw16Ops); 228 | const i64AtomicRmw32 = removeContexts(ctx, i64AtomicRmw32Ops); 229 | const i64Atomic = Object.assign(i64AtomicBase, { 230 | rmw: i64AtomicRmw, 231 | rmw8: i64AtomicRmw8, 232 | rmw16: i64AtomicRmw16, 233 | rmw32: i64AtomicRmw32, 234 | }); 235 | 236 | const i32 = Object.assign(i32t, removeContexts(ctx, i32Ops), { 237 | atomic: i32Atomic, 238 | }); 239 | const i64 = Object.assign(i64t, removeContexts(ctx, i64Ops), { 240 | atomic: i64Atomic, 241 | }); 242 | const f32 = Object.assign(f32t, removeContexts(ctx, f32Ops)); 243 | const f64 = Object.assign(f64t, removeContexts(ctx, f64Ops)); 244 | 245 | const local = bindLocalOps(ctx); 246 | const global = Object.assign(globalConstructor, bindGlobalOps(ctx)); 247 | const ref = removeContexts(ctx, refOps); 248 | 249 | const control1 = removeContexts(ctx, controlOps); 250 | const control2 = bindControlOps(ctx); 251 | const control = Object.assign(control1, control2); 252 | 253 | const { drop, select_poly, select_t } = removeContexts(ctx, parametric); 254 | 255 | const memory = Object.assign( 256 | memoryConstructor, 257 | removeContexts(ctx, memoryOps), 258 | { atomic: memoryAtomic } 259 | ); 260 | const data = Object.assign(dataConstructor, removeContexts(ctx, dataOps)); 261 | const table = Object.assign(tableConstructor, removeContexts(ctx, tableOps)); 262 | const elem = Object.assign(elemConstructor, removeContexts(ctx, elemOps)); 263 | 264 | const v128_ = removeContexts(ctx, v128Ops); 265 | const v128 = Object.assign(v128t, { 266 | ...v128_, 267 | const: wrapConst(v128_.const), 268 | }); 269 | 270 | const i8x16 = removeContexts(ctx, i8x16Ops); 271 | const i16x8 = removeContexts(ctx, i16x8Ops); 272 | const i32x4 = removeContexts(ctx, i32x4Ops); 273 | const i64x2 = removeContexts(ctx, i64x2Ops); 274 | const f32x4 = removeContexts(ctx, f32x4Ops); 275 | const f64x2 = removeContexts(ctx, f64x2Ops); 276 | 277 | // wrappers for instructions that take optional arguments 278 | function select(t?: ValueTypeObject) { 279 | return t === undefined ? select_poly() : select_t(t); 280 | } 281 | 282 | return { 283 | func, 284 | i32, 285 | i64, 286 | f32, 287 | f64, 288 | local, 289 | global, 290 | ref, 291 | control, 292 | drop, 293 | select, 294 | memory, 295 | data, 296 | table, 297 | elem, 298 | v128, 299 | i8x16, 300 | i16x8, 301 | i32x4, 302 | i64x2, 303 | f32x4, 304 | f64x2, 305 | atomic, 306 | }; 307 | } 308 | 309 | function removeContexts< 310 | T extends { 311 | [K in any]: (ctx: LocalContext, ...args: any) => any; 312 | } 313 | >( 314 | ctx: LocalContext, 315 | instructions: T 316 | ): { 317 | [K in keyof T]: RemoveContext; 318 | } { 319 | let result: { 320 | [K in keyof T]: RemoveContext; 321 | } = {} as any; 322 | for (let k in instructions) { 323 | result[k] = ((...args: any) => instructions[k](ctx, ...args)) as any; 324 | } 325 | return result; 326 | } 327 | 328 | type RemoveContext any> = 329 | F extends (ctx: LocalContext, ...args: infer CreateArgs) => infer Return 330 | ? (...args: CreateArgs) => Return 331 | : never; 332 | 333 | function removeContext, Return extends any>( 334 | ctx: LocalContext, 335 | op: (ctx: LocalContext, ...args: Args) => Return 336 | ): (...args: Args) => Return { 337 | return (...args: Args) => op(ctx, ...args); 338 | } 339 | -------------------------------------------------------------------------------- /src/instruction/atomic.ts: -------------------------------------------------------------------------------- 1 | import { Byte } from "../binable.js"; 2 | import { i32t, i64t } from "../types.js"; 3 | import { baseInstruction } from "./base.js"; 4 | import { memoryInstruction as mi } from "./memory.js"; 5 | 6 | export { 7 | memoryAtomicOps, 8 | atomicOps, 9 | i32AtomicOps, 10 | i32AtomicRmwOps, 11 | i32AtomicRmw8Ops, 12 | i32AtomicRmw16Ops, 13 | i64AtomicOps, 14 | i64AtomicRmwOps, 15 | i64AtomicRmw8Ops, 16 | i64AtomicRmw16Ops, 17 | i64AtomicRmw32Ops, 18 | }; 19 | 20 | // memory.atomic.X 21 | const memoryAtomicOps = { 22 | notify: mi("memory.atomic.notify", 32, [i32t, i32t], [i32t]), 23 | wait32: mi("memory.atomic.wait32", 32, [i32t, i32t, i32t], [i32t]), 24 | wait64: mi("memory.atomic.wait64", 64, [i32t, i64t, i64t], [i32t]), 25 | }; 26 | 27 | // atomic.X 28 | const atomicOps = { 29 | fence: baseInstruction("atomic.fence", Byte, { 30 | create() { 31 | return { in: [], out: [], deps: [], resolveArgs: [0] }; 32 | }, 33 | }), 34 | }; 35 | 36 | // i32.atomic.X 37 | const i32AtomicOps = { 38 | load: mi("i32.atomic.load", 32, [i32t], [i32t]), 39 | load8_u: mi("i32.atomic.load8_u", 8, [i32t], [i32t]), 40 | load16_u: mi("i32.atomic.load16_u", 16, [i32t], [i32t]), 41 | store: mi("i32.atomic.store", 32, [i32t, i32t], []), 42 | store8: mi("i32.atomic.store8", 8, [i32t, i32t], []), 43 | store16: mi("i32.atomic.store16", 16, [i32t, i32t], []), 44 | }; 45 | 46 | // i32.atomic.rmw.X 47 | const i32AtomicRmwOps = { 48 | add: mi("i32.atomic.rmw.add", 32, [i32t, i32t], [i32t]), 49 | sub: mi("i32.atomic.rmw.sub", 32, [i32t, i32t], [i32t]), 50 | and: mi("i32.atomic.rmw.and", 32, [i32t, i32t], [i32t]), 51 | or: mi("i32.atomic.rmw.or", 32, [i32t, i32t], [i32t]), 52 | xor: mi("i32.atomic.rmw.xor", 32, [i32t, i32t], [i32t]), 53 | xchg: mi("i32.atomic.rmw.xchg", 32, [i32t, i32t], [i32t]), 54 | cmpxchg: mi("i32.atomic.rmw.cmpxchg", 32, [i32t, i32t, i32t], [i32t]), 55 | }; 56 | 57 | // i32.atomic.rmw8.X 58 | const i32AtomicRmw8Ops = { 59 | add_u: mi("i32.atomic.rmw8.add_u", 8, [i32t, i32t], [i32t]), 60 | sub_u: mi("i32.atomic.rmw8.sub_u", 8, [i32t, i32t], [i32t]), 61 | and_u: mi("i32.atomic.rmw8.and_u", 8, [i32t, i32t], [i32t]), 62 | or_u: mi("i32.atomic.rmw8.or_u", 8, [i32t, i32t], [i32t]), 63 | xor_u: mi("i32.atomic.rmw8.xor_u", 8, [i32t, i32t], [i32t]), 64 | xchg_u: mi("i32.atomic.rmw8.xchg_u", 8, [i32t, i32t], [i32t]), 65 | cmpxchg_u: mi("i32.atomic.rmw8.cmpxchg_u", 8, [i32t, i32t, i32t], [i32t]), 66 | }; 67 | 68 | // i32.atomic.rmw16.X 69 | const i32AtomicRmw16Ops = { 70 | add_u: mi("i32.atomic.rmw16.add_u", 16, [i32t, i32t], [i32t]), 71 | sub_u: mi("i32.atomic.rmw16.sub_u", 16, [i32t, i32t], [i32t]), 72 | and_u: mi("i32.atomic.rmw16.and_u", 16, [i32t, i32t], [i32t]), 73 | or_u: mi("i32.atomic.rmw16.or_u", 16, [i32t, i32t], [i32t]), 74 | xor_u: mi("i32.atomic.rmw16.xor_u", 16, [i32t, i32t], [i32t]), 75 | xchg_u: mi("i32.atomic.rmw16.xchg_u", 16, [i32t, i32t], [i32t]), 76 | cmpxchg_u: mi("i32.atomic.rmw16.cmpxchg_u", 16, [i32t, i32t, i32t], [i32t]), 77 | }; 78 | 79 | // i64.atomic.X 80 | const i64AtomicOps = { 81 | load: mi("i64.atomic.load", 64, [i32t], [i64t]), 82 | load8_u: mi("i64.atomic.load8_u", 8, [i32t], [i64t]), 83 | load16_u: mi("i64.atomic.load16_u", 16, [i32t], [i64t]), 84 | load32_u: mi("i64.atomic.load32_u", 32, [i32t], [i64t]), 85 | store: mi("i64.atomic.store", 64, [i32t, i64t], []), 86 | store8: mi("i64.atomic.store8", 8, [i32t, i64t], []), 87 | store16: mi("i64.atomic.store16", 16, [i32t, i64t], []), 88 | store32: mi("i64.atomic.store32", 32, [i32t, i64t], []), 89 | }; 90 | 91 | // i64.atomic.rmw.X 92 | const i64AtomicRmwOps = { 93 | add: mi("i64.atomic.rmw.add", 64, [i32t, i64t], [i64t]), 94 | sub: mi("i64.atomic.rmw.sub", 64, [i32t, i64t], [i64t]), 95 | and: mi("i64.atomic.rmw.and", 64, [i32t, i64t], [i64t]), 96 | or: mi("i64.atomic.rmw.or", 64, [i32t, i64t], [i64t]), 97 | xor: mi("i64.atomic.rmw.xor", 64, [i32t, i64t], [i64t]), 98 | xchg: mi("i64.atomic.rmw.xchg", 64, [i32t, i64t], [i64t]), 99 | cmpxchg: mi("i64.atomic.rmw.cmpxchg", 64, [i32t, i64t, i64t], [i64t]), 100 | }; 101 | 102 | // i64.atomic.rmw8.X 103 | const i64AtomicRmw8Ops = { 104 | add_u: mi("i64.atomic.rmw8.add_u", 8, [i32t, i64t], [i64t]), 105 | sub_u: mi("i64.atomic.rmw8.sub_u", 8, [i32t, i64t], [i64t]), 106 | and_u: mi("i64.atomic.rmw8.and_u", 8, [i32t, i64t], [i64t]), 107 | or_u: mi("i64.atomic.rmw8.or_u", 8, [i32t, i64t], [i64t]), 108 | xor_u: mi("i64.atomic.rmw8.xor_u", 8, [i32t, i64t], [i64t]), 109 | xchg_u: mi("i64.atomic.rmw8.xchg_u", 8, [i32t, i64t], [i64t]), 110 | cmpxchg_u: mi("i64.atomic.rmw8.cmpxchg_u", 8, [i32t, i64t, i64t], [i64t]), 111 | }; 112 | 113 | // i64.atomic.rmw16.X 114 | const i64AtomicRmw16Ops = { 115 | add_u: mi("i64.atomic.rmw16.add_u", 16, [i32t, i64t], [i64t]), 116 | sub_u: mi("i64.atomic.rmw16.sub_u", 16, [i32t, i64t], [i64t]), 117 | and_u: mi("i64.atomic.rmw16.and_u", 16, [i32t, i64t], [i64t]), 118 | or_u: mi("i64.atomic.rmw16.or_u", 16, [i32t, i64t], [i64t]), 119 | xor_u: mi("i64.atomic.rmw16.xor_u", 16, [i32t, i64t], [i64t]), 120 | xchg_u: mi("i64.atomic.rmw16.xchg_u", 16, [i32t, i64t], [i64t]), 121 | cmpxchg_u: mi("i64.atomic.rmw16.cmpxchg_u", 16, [i32t, i64t, i64t], [i64t]), 122 | }; 123 | 124 | // i64.atomic.rmw32.X 125 | const i64AtomicRmw32Ops = { 126 | add_u: mi("i64.atomic.rmw32.add_u", 32, [i32t, i64t], [i64t]), 127 | sub_u: mi("i64.atomic.rmw32.sub_u", 32, [i32t, i64t], [i64t]), 128 | and_u: mi("i64.atomic.rmw32.and_u", 32, [i32t, i64t], [i64t]), 129 | or_u: mi("i64.atomic.rmw32.or_u", 32, [i32t, i64t], [i64t]), 130 | xor_u: mi("i64.atomic.rmw32.xor_u", 32, [i32t, i64t], [i64t]), 131 | xchg_u: mi("i64.atomic.rmw32.xchg_u", 32, [i32t, i64t], [i64t]), 132 | cmpxchg_u: mi("i64.atomic.rmw32.cmpxchg_u", 32, [i32t, i64t, i64t], [i64t]), 133 | }; 134 | -------------------------------------------------------------------------------- /src/instruction/base.ts: -------------------------------------------------------------------------------- 1 | import { Binable, Undefined } from "../binable.js"; 2 | import * as Dependency from "../dependency.js"; 3 | import { 4 | formatStack, 5 | LocalContext, 6 | popStack, 7 | pushInstruction, 8 | RandomLabel, 9 | StackVar, 10 | stackVars, 11 | withContext, 12 | } from "../local-context.js"; 13 | import { 14 | FunctionType, 15 | ValueType, 16 | valueTypeLiterals, 17 | ValueTypeObject, 18 | ValueTypeObjects, 19 | } from "../types.js"; 20 | import { Tuple } from "../util.js"; 21 | import { InstructionName, nameToOpcode } from "./opcodes.js"; 22 | 23 | export { 24 | baseInstructionWithImmediate, 25 | baseInstruction, 26 | BaseInstruction, 27 | ResolvedInstruction, 28 | resolveInstruction, 29 | resolveExpression, 30 | createExpressionWithType, 31 | FunctionTypeInput, 32 | lookupInstruction, 33 | lookupOpcode, 34 | lookupSubcode, 35 | typeFromInput, 36 | Instruction, 37 | isInstruction, 38 | Instruction_, 39 | }; 40 | 41 | const nameToInstruction: Record = {}; 42 | const opcodeToInstruction: Record< 43 | number, 44 | BaseInstruction | Record 45 | > = {}; 46 | 47 | type BaseInstruction = { 48 | string: string; 49 | opcode: number | [number, number]; 50 | immediate: Binable | undefined; 51 | resolve: (deps: number[], ...args: any) => any; 52 | }; 53 | type ResolvedInstruction = { name: string; immediate: any }; 54 | 55 | /** 56 | * Most general function to create instructions 57 | */ 58 | function baseInstruction< 59 | Immediate, 60 | CreateArgs extends Tuple, 61 | ResolveArgs extends Tuple, 62 | Args extends Tuple | ValueType[], 63 | Results extends Tuple | ValueType[] 64 | >( 65 | string: InstructionName, 66 | immediate: Binable | undefined = undefined, 67 | { 68 | create, 69 | resolve, 70 | }: { 71 | create( 72 | ctx: LocalContext, 73 | ...args: CreateArgs 74 | ): { 75 | in: Args; 76 | out: Results; 77 | deps?: Dependency.t[]; 78 | resolveArgs?: ResolveArgs; 79 | }; 80 | resolve?(deps: number[], ...args: ResolveArgs): Immediate; 81 | } 82 | ): (( 83 | ctx: LocalContext, 84 | ...createArgs: CreateArgs 85 | ) => Instruction_) & { 86 | create(ctx: LocalContext, ...createArgs: CreateArgs): Dependency.Instruction; 87 | } { 88 | resolve ??= noResolve; 89 | let opcode = nameToOpcode[string]; 90 | let instruction = { string, opcode, immediate, resolve }; 91 | nameToInstruction[string] = instruction; 92 | if (typeof opcode === "number") { 93 | opcodeToInstruction[opcode] = instruction; 94 | } else { 95 | opcodeToInstruction[opcode[0]] ??= {} as Record; 96 | (opcodeToInstruction[opcode[0]] as Record)[ 97 | opcode[1] 98 | ] = instruction; 99 | } 100 | 101 | function wrapCreate( 102 | ctx: LocalContext, 103 | ...createArgs: CreateArgs 104 | ): Dependency.Instruction { 105 | let { 106 | in: args, 107 | out: results, 108 | deps = [], 109 | resolveArgs = createArgs, 110 | } = create(ctx, ...createArgs); 111 | return { string, deps, type: { args, results }, resolveArgs }; 112 | } 113 | 114 | /** 115 | * "Calling" an instruction does two different things: 116 | * - creates a `Dependency.Instruction` (that represents the instruction before resolving dependencies) 117 | * - how to this is defined by the input `create()` function 118 | * - applies the instruction to the current stack/ctx, and validates stack types 119 | * 120 | * If you only want creation, use `instruction.create()` instead of `instruction()` 121 | * (this is currently not exposed to the public API) 122 | */ 123 | return Object.assign( 124 | function instruction(ctx: LocalContext, ...createArgs: CreateArgs) { 125 | let instr = wrapCreate(ctx, ...createArgs); 126 | pushInstruction(ctx, instr); 127 | let results = instr.type.results; 128 | return ( 129 | results.length === 0 130 | ? undefined 131 | : results.length === 1 132 | ? StackVar(results[0]) 133 | : results.map(StackVar) 134 | ) as Instruction_; 135 | }, 136 | { create: wrapCreate } 137 | ); 138 | } 139 | 140 | function isInstruction( 141 | value: BaseInstruction | Record 142 | ): value is BaseInstruction { 143 | return "opcode" in value; 144 | } 145 | 146 | type Instruction = { 147 | [i in keyof Results]: StackVar; 148 | } & { in?: Args }; 149 | 150 | type Instruction_ = Results extends [] 151 | ? void 152 | : Results extends [ValueType] 153 | ? StackVar 154 | : Instruction; 155 | 156 | /** 157 | * Instruction of constant type without dependencies, 158 | * but with an immediate argument. 159 | * 160 | * Allows passing a validation callback for the immediate value. 161 | */ 162 | function baseInstructionWithImmediate< 163 | Args extends Tuple, 164 | Results extends Tuple, 165 | Immediate extends any 166 | >( 167 | name: InstructionName, 168 | immediate: Binable | undefined, 169 | args: ValueTypeObjects, 170 | results: ValueTypeObjects, 171 | validateImmediate?: (immediate: Immediate) => void 172 | ) { 173 | immediate = immediate === Undefined ? undefined : immediate; 174 | type CreateArgs = Immediate extends undefined ? [] : [immediate: Immediate]; 175 | let instr = { 176 | in: valueTypeLiterals(args), 177 | out: valueTypeLiterals(results), 178 | }; 179 | 180 | return baseInstruction( 181 | name, 182 | immediate, 183 | { 184 | create: 185 | // validate immediate if we have a validation callback 186 | validateImmediate && immediate !== undefined 187 | ? (_ctx, ...args) => { 188 | validateImmediate(args[0] as Immediate); 189 | return instr; 190 | } 191 | : () => instr, 192 | } 193 | ); 194 | } 195 | 196 | function resolveInstruction( 197 | { string: name, deps, resolveArgs }: Dependency.Instruction, 198 | depToIndex: Map 199 | ): ResolvedInstruction { 200 | let instr = lookupInstruction(name); 201 | let depIndices: number[] = []; 202 | for (let dep of deps) { 203 | let index = depToIndex.get(dep); 204 | if (index === undefined) { 205 | if (dep.kind === "hasRefTo") index = 0; 206 | else if (dep.kind === "hasMemory") index = 0; 207 | else throw Error("bug: no index for dependency"); 208 | } 209 | depIndices.push(index); 210 | } 211 | let immediate = instr.resolve(depIndices, ...resolveArgs); 212 | return { name, immediate }; 213 | } 214 | 215 | const noResolve = (_: number[], ...args: any) => args[0]; 216 | 217 | type FunctionTypeInput = { 218 | in?: ValueTypeObject[]; 219 | out?: ValueTypeObject[]; 220 | } | null; 221 | 222 | function typeFromInput(type: FunctionTypeInput): FunctionType { 223 | return { 224 | args: valueTypeLiterals(type?.in ?? []), 225 | results: valueTypeLiterals(type?.out ?? []), 226 | }; 227 | } 228 | 229 | function createExpressionWithType( 230 | name: LocalContext["frames"][number]["opcode"], 231 | ctx: LocalContext, 232 | type: FunctionTypeInput, 233 | run: (label: RandomLabel) => void 234 | ): { 235 | body: Dependency.Instruction[]; 236 | type: FunctionType; 237 | deps: Dependency.t[]; 238 | } { 239 | let args = valueTypeLiterals(type?.in ?? []); 240 | let results = valueTypeLiterals(type?.out ?? []); 241 | let stack = stackVars(args); 242 | let label = String(Math.random()) as RandomLabel; 243 | let subCtx = withContext( 244 | ctx, 245 | { 246 | body: [], 247 | stack, 248 | frames: [ 249 | { 250 | label, 251 | opcode: name, 252 | startTypes: args, 253 | endTypes: results, 254 | unreachable: false, 255 | stack, 256 | }, 257 | ...ctx.frames, 258 | ], 259 | }, 260 | () => run(label) 261 | ); 262 | popStack(subCtx, results); 263 | if (stack.length !== 0) 264 | throw Error( 265 | `expected stack to be empty at the end of block, got ${formatStack( 266 | stack 267 | )}` 268 | ); 269 | let { body } = subCtx; 270 | return { body, type: { args, results }, deps: body.flatMap((i) => i.deps) }; 271 | } 272 | 273 | function resolveExpression(deps: number[], body: Dependency.Instruction[]) { 274 | let instructions: ResolvedInstruction[] = []; 275 | let offset = 0; 276 | for (let instr of body) { 277 | let n = instr.deps.length; 278 | let myDeps = deps.slice(offset, offset + n); 279 | let instrObject = lookupInstruction(instr.string); 280 | let immediate = instrObject.resolve(myDeps, ...instr.resolveArgs); 281 | instructions.push({ name: instr.string, immediate }); 282 | offset += n; 283 | } 284 | return instructions; 285 | } 286 | 287 | function lookupInstruction(name: string) { 288 | let instr = nameToInstruction[name]; 289 | if (instr === undefined) throw Error(`invalid instruction name "${name}"`); 290 | return instr; 291 | } 292 | function lookupOpcode(opcode: number) { 293 | let instr = opcodeToInstruction[opcode]; 294 | if (instr === undefined) throw Error(`invalid opcode "${opcode}"`); 295 | return instr; 296 | } 297 | 298 | function lookupSubcode( 299 | opcode: number, 300 | subcode: number, 301 | codes: Record 302 | ) { 303 | let instr = codes[subcode]; 304 | if (instr === undefined) 305 | throw Error(`invalid opcode (${opcode}, ${subcode})`); 306 | return instr; 307 | } 308 | -------------------------------------------------------------------------------- /src/instruction/binable.ts: -------------------------------------------------------------------------------- 1 | import { Binable, constant, or, record, withByteCode } from "../binable.js"; 2 | import { S33, U32 } from "../immediate.js"; 3 | import { ValueType } from "../types.js"; 4 | import { 5 | BaseInstruction, 6 | isInstruction, 7 | lookupInstruction, 8 | lookupOpcode, 9 | lookupSubcode, 10 | ResolvedInstruction, 11 | } from "./base.js"; 12 | 13 | export { Expression, ConstExpression, Block, IfBlock }; 14 | 15 | const Instruction = Binable({ 16 | toBytes({ name, immediate }) { 17 | let instr = lookupInstruction(name); 18 | let imm: number[] = []; 19 | if (instr.immediate !== undefined) { 20 | imm = instr.immediate.toBytes(immediate); 21 | } 22 | if (typeof instr.opcode === "number") return [instr.opcode, ...imm]; 23 | return [instr.opcode[0], ...U32.toBytes(instr.opcode[1]), ...imm]; 24 | }, 25 | readBytes(bytes, offset) { 26 | let opcode = bytes[offset++]; 27 | let instr_ = lookupOpcode(opcode); 28 | let instr: BaseInstruction; 29 | if (isInstruction(instr_)) instr = instr_; 30 | else { 31 | let subcode: number; 32 | [subcode, offset] = U32.readBytes(bytes, offset); 33 | instr = lookupSubcode(opcode, subcode, instr_); 34 | } 35 | if (instr.immediate === undefined) 36 | return [{ name: instr.string, immediate: undefined }, offset]; 37 | let [immediate, end] = instr.immediate.readBytes(bytes, offset); 38 | return [{ name: instr.string, immediate }, end]; 39 | }, 40 | }); 41 | 42 | const END = 0x0b; 43 | type Expression = ResolvedInstruction[]; 44 | const Expression = Binable({ 45 | toBytes(t) { 46 | let instructions = t.map(Instruction.toBytes).flat(); 47 | instructions.push(END); 48 | return instructions; 49 | }, 50 | readBytes(bytes, offset) { 51 | let instructions: ResolvedInstruction[] = []; 52 | while (bytes[offset] !== END) { 53 | let instr: ResolvedInstruction; 54 | [instr, offset] = Instruction.readBytes(bytes, offset); 55 | instructions.push(instr); 56 | } 57 | return [instructions, offset + 1]; 58 | }, 59 | }); 60 | 61 | const ELSE = 0x05; 62 | type IfExpression = { 63 | if: ResolvedInstruction[]; 64 | else?: ResolvedInstruction[]; 65 | }; 66 | const IfExpression = Binable({ 67 | toBytes(t) { 68 | let instructions = t.if.map(Instruction.toBytes).flat(); 69 | if (t.else !== undefined) { 70 | instructions.push(ELSE, ...t.else.map(Instruction.toBytes).flat()); 71 | } 72 | instructions.push(END); 73 | return instructions; 74 | }, 75 | readBytes(bytes, offset) { 76 | let t: IfExpression = { if: [], else: undefined }; 77 | let instructions = t.if; 78 | while (true) { 79 | if (bytes[offset] === ELSE) { 80 | instructions = t.else = []; 81 | offset++; 82 | continue; 83 | } else if (bytes[offset] === END) { 84 | offset++; 85 | break; 86 | } 87 | let instr: ResolvedInstruction; 88 | [instr, offset] = Instruction.readBytes(bytes, offset); 89 | instructions.push(instr); 90 | } 91 | return [t, offset]; 92 | }, 93 | }); 94 | 95 | type ConstExpression = Expression; 96 | const ConstExpression = Expression; 97 | 98 | const Empty = withByteCode(0x40, constant("empty")); 99 | 100 | type BlockType = "empty" | ValueType | U32; 101 | const BlockType = or([Empty, S33, ValueType], (t) => 102 | t === "empty" ? Empty : typeof t === "number" ? S33 : ValueType 103 | ); 104 | 105 | const Block = record({ blockType: BlockType, instructions: Expression }); 106 | const IfBlock = record({ blockType: BlockType, instructions: IfExpression }); 107 | -------------------------------------------------------------------------------- /src/instruction/const.ts: -------------------------------------------------------------------------------- 1 | import { F32, F64, I32, I64 } from "../immediate.js"; 2 | import { baseInstructionWithImmediate } from "./base.js"; 3 | import { i32t, i64t, f32t, f64t } from "../types.js"; 4 | 5 | export { i32Const, i64Const, f32Const, f64Const }; 6 | 7 | const i32Const = baseInstructionWithImmediate( 8 | "i32.const", 9 | I32, 10 | [], 11 | [i32t], 12 | checkInt32 13 | ); 14 | const i64Const = baseInstructionWithImmediate( 15 | "i64.const", 16 | I64, 17 | [], 18 | [i64t], 19 | checkInt64 20 | ); 21 | const f32Const = baseInstructionWithImmediate("f32.const", F32, [], [f32t]); 22 | const f64Const = baseInstructionWithImmediate("f64.const", F64, [], [f64t]); 23 | 24 | // integer validation 25 | 26 | function checkInt32(value: number) { 27 | if (!Number.isInteger(value)) 28 | throw Error(`i32.const: value must be an integer, got ${value}`); 29 | if (value < -0x8000_0000 || value >= 0x1_0000_0000) 30 | throw Error( 31 | `i32.const: value lies outside the int32 and uint32 ranges, got ${value}` 32 | ); 33 | } 34 | 35 | function checkInt64(value: bigint) { 36 | if (value < -0x8000_0000_0000_0000n || value >= 0x1_0000_0000_0000_0000n) 37 | throw Error( 38 | `i64.const: value lies outside the int64 and uint64 ranges, got ${value}` 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/instruction/control.ts: -------------------------------------------------------------------------------- 1 | import { record, tuple, Undefined } from "../binable.js"; 2 | import * as Dependency from "../dependency.js"; 3 | import { AnyFunc } from "../func-types.js"; 4 | import { U32, vec } from "../immediate.js"; 5 | import { 6 | getFrameFromLabel, 7 | Label, 8 | labelTypes, 9 | popStack, 10 | popUnknown, 11 | pushStack, 12 | RandomLabel, 13 | setUnreachable, 14 | isNumberType, 15 | isVectorType, 16 | isSameType, 17 | LocalContext, 18 | } from "../local-context.js"; 19 | import { ValueType, valueTypeLiteral, ValueTypeObject } from "../types.js"; 20 | import { 21 | baseInstruction, 22 | createExpressionWithType, 23 | FunctionTypeInput, 24 | resolveExpression, 25 | baseInstructionWithImmediate, 26 | typeFromInput, 27 | Instruction_, 28 | } from "./base.js"; 29 | import { Block, IfBlock } from "./binable.js"; 30 | import { Input, Inputs, processStackArgs } from "./stack-args.js"; 31 | 32 | export { control, bindControlOps, parametric }; 33 | 34 | // control instructions 35 | 36 | const nop = baseInstructionWithImmediate("nop", Undefined, [], []); 37 | 38 | const unreachable = baseInstruction("unreachable", Undefined, { 39 | create(ctx) { 40 | setUnreachable(ctx); 41 | return { in: [], out: [] }; 42 | }, 43 | resolve: () => undefined, 44 | }); 45 | 46 | const block = baseInstruction("block", Block, { 47 | create(ctx, t: FunctionTypeInput, run: (label: RandomLabel) => void) { 48 | let { type, body, deps } = createExpressionWithType("block", ctx, t, run); 49 | return { 50 | in: type.args, 51 | out: type.results, 52 | deps: [Dependency.type(type), ...deps], 53 | resolveArgs: [body], 54 | }; 55 | }, 56 | resolve([blockType, ...deps], body: Dependency.Instruction[]) { 57 | let instructions = resolveExpression(deps, body); 58 | return { blockType, instructions }; 59 | }, 60 | }); 61 | 62 | const loop = baseInstruction("loop", Block, { 63 | create(ctx, t: FunctionTypeInput, run: (label: RandomLabel) => void) { 64 | let { type, body, deps } = createExpressionWithType("loop", ctx, t, run); 65 | return { 66 | in: type.args, 67 | out: type.results, 68 | deps: [Dependency.type(type), ...deps], 69 | resolveArgs: [body], 70 | }; 71 | }, 72 | resolve([blockType, ...deps], body: Dependency.Instruction[]) { 73 | let instructions = resolveExpression(deps, body); 74 | return { blockType, instructions }; 75 | }, 76 | }); 77 | 78 | const if_ = baseInstruction("if", IfBlock, { 79 | create( 80 | ctx, 81 | t: FunctionTypeInput, 82 | runIf: (label: RandomLabel) => void, 83 | runElse?: (label: RandomLabel) => void 84 | ) { 85 | popStack(ctx, ["i32"]); 86 | let { type, body, deps } = createExpressionWithType("if", ctx, t, runIf); 87 | let ifArgs = [...type.args, "i32"] as [...ValueType[], "i32"]; 88 | if (runElse === undefined) { 89 | pushStack(ctx, ["i32"]); 90 | return { 91 | in: ifArgs, 92 | out: type.results, 93 | deps: [Dependency.type(type), ...deps], 94 | resolveArgs: [body, undefined], 95 | }; 96 | } 97 | let elseExpr = createExpressionWithType("else", ctx, t, runElse); 98 | pushStack(ctx, ["i32"]); 99 | return { 100 | in: ifArgs, 101 | out: type.results, 102 | deps: [Dependency.type(type), ...deps, ...elseExpr.deps], 103 | resolveArgs: [body, elseExpr.body], 104 | }; 105 | }, 106 | resolve( 107 | [blockType, ...deps], 108 | ifBody: Dependency.Instruction[], 109 | elseBody?: Dependency.Instruction[] 110 | ) { 111 | let ifDepsLength = ifBody.reduce((acc, i) => acc + i.deps.length, 0); 112 | let if_ = resolveExpression(deps.slice(0, ifDepsLength), ifBody); 113 | let else_ = 114 | elseBody && resolveExpression(deps.slice(ifDepsLength), elseBody); 115 | return { blockType, instructions: { if: if_, else: else_ } }; 116 | }, 117 | }); 118 | 119 | const br = baseInstruction("br", U32, { 120 | create(ctx, label: Label | number) { 121 | let [i, frame] = getFrameFromLabel(ctx, label); 122 | let types = labelTypes(frame); 123 | popStack(ctx, types); 124 | setUnreachable(ctx); 125 | return { in: [], out: [], resolveArgs: [i] }; 126 | }, 127 | }); 128 | 129 | const br_if = baseInstruction("br_if", U32, { 130 | create(ctx, label: Label | number) { 131 | let [i, frame] = getFrameFromLabel(ctx, label); 132 | let types = labelTypes(frame); 133 | return { in: [...types, "i32"], out: types, resolveArgs: [i] }; 134 | }, 135 | }); 136 | 137 | const LabelTable = record({ indices: vec(U32), defaultIndex: U32 }); 138 | const br_table = baseInstruction("br_table", LabelTable, { 139 | create(ctx, labels: (Label | number)[], defaultLabel: Label | number) { 140 | popStack(ctx, ["i32"]); 141 | let [defaultIndex, defaultFrame] = getFrameFromLabel(ctx, defaultLabel); 142 | let types = labelTypes(defaultFrame); 143 | let arity = types.length; 144 | let indices: number[] = []; 145 | for (let label of labels) { 146 | let [j, frame] = getFrameFromLabel(ctx, label); 147 | indices.push(j); 148 | let types = labelTypes(frame); 149 | if (types.length !== arity) 150 | throw Error("inconsistent length of block label types in br_table"); 151 | pushStack(ctx, popStack(ctx, types)); 152 | } 153 | popStack(ctx, types); 154 | setUnreachable(ctx); 155 | pushStack(ctx, ["i32"]); 156 | return { in: ["i32"], out: [], resolveArgs: [{ indices, defaultIndex }] }; 157 | }, 158 | }); 159 | 160 | const return_ = baseInstruction("return", Undefined, { 161 | create(ctx) { 162 | let type = ctx.return; 163 | // TODO: do we need this for const expressions? 164 | if (type === null) throw Error("bug: called return outside a function"); 165 | popStack(ctx, type); 166 | setUnreachable(ctx); 167 | return { in: [], out: [] }; 168 | }, 169 | resolve: () => undefined, 170 | }); 171 | 172 | const call = baseInstruction("call", U32, { 173 | create(_, func: Dependency.AnyFunc) { 174 | return { in: func.type.args, out: func.type.results, deps: [func] }; 175 | }, 176 | resolve: ([funcIndex]) => funcIndex, 177 | }); 178 | 179 | const call_indirect = baseInstruction("call_indirect", tuple([U32, U32]), { 180 | create(_, table: Dependency.AnyTable, type: FunctionTypeInput) { 181 | let t = typeFromInput(type); 182 | return { 183 | in: [...t.args, "i32"], 184 | out: t.results, 185 | deps: [Dependency.type(t), table], 186 | }; 187 | }, 188 | resolve: ([typeIdx, tableIdx]) => [typeIdx, tableIdx], 189 | }); 190 | 191 | function bindControlOps(ctx: LocalContext) { 192 | return { 193 | call: >( 194 | func: F, 195 | args?: Inputs 196 | ): Instruction_ => { 197 | if (args !== undefined) { 198 | processStackArgs( 199 | ctx, 200 | "call", 201 | func.type.args, 202 | args as Input[] 203 | ); 204 | } 205 | return call(ctx, func) as any; 206 | }, 207 | }; 208 | } 209 | 210 | const control = { 211 | nop, 212 | unreachable, 213 | block, 214 | loop, 215 | if: if_, 216 | br, 217 | br_if, 218 | br_table, 219 | return: return_, 220 | // call, 221 | call_indirect, 222 | }; 223 | 224 | // parametric instructions 225 | 226 | const drop = baseInstruction("drop", Undefined, { 227 | create(ctx: LocalContext) { 228 | popUnknown(ctx); 229 | // TODO represent "unknown" in possible input types and remove this hack 230 | return { in: [] as any as [ValueType], out: [] }; 231 | }, 232 | resolve: () => undefined, 233 | }); 234 | 235 | const select_poly = baseInstruction("select", Undefined, { 236 | create(ctx: LocalContext) { 237 | popStack(ctx, ["i32"]); 238 | let t1 = popUnknown(ctx); 239 | let t2 = popUnknown(ctx); 240 | if ( 241 | !( 242 | (isNumberType(t1) && isNumberType(t2)) || 243 | (isVectorType(t1) && isVectorType(t2)) 244 | ) 245 | ) { 246 | throw Error( 247 | `select: polymorphic select can only be applied to number or vector types, got ${t1} and ${t2}.` 248 | ); 249 | } 250 | if (!isSameType(t1, t2)) { 251 | throw Error(`select: types must be equal, got ${t1} and ${t2}.`); 252 | } 253 | let t: ValueType; 254 | if (t1 !== "unknown") t = t1; 255 | else if (t2 !== "unknown") t = t2; 256 | else 257 | throw Error( 258 | "polymorphic select with two unknown types is not implemented." 259 | ); 260 | // TODO represent "unknown" in possible input types and remove this hack 261 | return { in: [] as any as ["i32", ValueType], out: [t] }; 262 | }, 263 | resolve: () => undefined, 264 | }); 265 | const select_t = baseInstruction("select_t", vec(ValueType), { 266 | create(_: LocalContext, t: ValueTypeObject) { 267 | let t_ = valueTypeLiteral(t); 268 | return { in: ["i32", t_, t_], out: [t_], resolveArgs: [[t_]] }; 269 | }, 270 | }); 271 | 272 | const parametric = { drop, select_t, select_poly }; 273 | -------------------------------------------------------------------------------- /src/instruction/memory.ts: -------------------------------------------------------------------------------- 1 | import { Instruction_, baseInstruction } from "./base.js"; 2 | import * as Dependency from "../dependency.js"; 3 | import { LocalContext } from "../local-context.js"; 4 | import { U32, U8 } from "../immediate.js"; 5 | import { record, tuple } from "../binable.js"; 6 | import { 7 | DataIndex, 8 | ElemIndex, 9 | MemoryIndex, 10 | TableIndex, 11 | ValueType, 12 | valueTypeLiterals, 13 | ValueTypeObjects, 14 | } from "../types.js"; 15 | import { Tuple } from "../util.js"; 16 | import { InstructionName } from "./opcodes.js"; 17 | import { Input, processStackArgs } from "./stack-args.js"; 18 | 19 | export { 20 | memoryOps, 21 | dataOps, 22 | tableOps, 23 | elemOps, 24 | memoryInstruction, 25 | memoryLaneInstruction, 26 | }; 27 | 28 | const memoryOps = { 29 | size: baseInstruction("memory.size", MemoryIndex, { 30 | create(_: LocalContext) { 31 | return { 32 | in: [], 33 | out: ["i32"], 34 | deps: [Dependency.hasMemory], 35 | resolveArgs: [0], 36 | }; 37 | }, 38 | }), 39 | grow: baseInstruction("memory.grow", MemoryIndex, { 40 | create(_: LocalContext) { 41 | return { 42 | in: ["i32"], 43 | out: ["i32"], 44 | deps: [Dependency.hasMemory], 45 | resolveArgs: [0], 46 | }; 47 | }, 48 | }), 49 | init: baseInstruction("memory.init", tuple([DataIndex, MemoryIndex]), { 50 | create(_: LocalContext, data: Dependency.Data) { 51 | return { 52 | in: ["i32", "i32", "i32"], 53 | out: [], 54 | deps: [data, Dependency.hasMemory], 55 | }; 56 | }, 57 | resolve([dataIdx]: number[]): [number, number] { 58 | return [dataIdx, 0]; 59 | }, 60 | }), 61 | copy: baseInstruction("memory.copy", tuple([MemoryIndex, MemoryIndex]), { 62 | create(_: LocalContext) { 63 | return { 64 | in: ["i32", "i32", "i32"], 65 | out: [], 66 | deps: [Dependency.hasMemory], 67 | resolveArgs: [[0, 0]], 68 | }; 69 | }, 70 | }), 71 | fill: baseInstruction("memory.fill", MemoryIndex, { 72 | create(_: LocalContext) { 73 | return { 74 | in: ["i32", "i32", "i32"], 75 | out: [], 76 | deps: [Dependency.hasMemory], 77 | resolveArgs: [0], 78 | }; 79 | }, 80 | }), 81 | }; 82 | 83 | const dataOps = { 84 | drop: baseInstruction("data.drop", DataIndex, { 85 | create(_: LocalContext, data: Dependency.Data) { 86 | return { 87 | in: [], 88 | out: [], 89 | deps: [data], 90 | }; 91 | }, 92 | resolve: ([dataIdx]) => dataIdx, 93 | }), 94 | }; 95 | 96 | const tableOps = { 97 | get: baseInstruction("table.get", TableIndex, { 98 | create(_: LocalContext, table: Dependency.Table) { 99 | return { 100 | in: ["i32"], 101 | out: [table.type.type], 102 | deps: [table], 103 | }; 104 | }, 105 | resolve: ([tableIdx]) => tableIdx, 106 | }), 107 | set: baseInstruction("table.set", TableIndex, { 108 | create(_: LocalContext, table: Dependency.Table) { 109 | return { 110 | in: ["i32", table.type.type], 111 | out: [], 112 | deps: [table], 113 | }; 114 | }, 115 | resolve: ([tableIdx]) => tableIdx, 116 | }), 117 | init: baseInstruction("table.init", tuple([ElemIndex, TableIndex]), { 118 | create(_: LocalContext, table: Dependency.Table, elem: Dependency.Elem) { 119 | return { 120 | in: ["i32", "i32", "i32"], 121 | out: [], 122 | deps: [elem, table], 123 | }; 124 | }, 125 | resolve: ([elemIdx, tableIdx]) => [elemIdx, tableIdx], 126 | }), 127 | copy: baseInstruction("table.init", tuple([TableIndex, TableIndex]), { 128 | create( 129 | _: LocalContext, 130 | table1: Dependency.Table, 131 | table2: Dependency.Table 132 | ) { 133 | return { 134 | in: ["i32", "i32", "i32"], 135 | out: [], 136 | deps: [table1, table2], 137 | }; 138 | }, 139 | resolve: ([tableIdx1, tableIdx2]) => [tableIdx1, tableIdx2], 140 | }), 141 | grow: baseInstruction("table.grow", TableIndex, { 142 | create(_: LocalContext, table: Dependency.Table) { 143 | return { 144 | in: [table.type.type, "i32"], 145 | out: ["i32"], 146 | deps: [table], 147 | }; 148 | }, 149 | resolve: ([tableIdx]) => tableIdx, 150 | }), 151 | size: baseInstruction("table.size", TableIndex, { 152 | create(_: LocalContext, table: Dependency.Table) { 153 | return { 154 | in: [], 155 | out: ["i32"], 156 | deps: [table], 157 | }; 158 | }, 159 | resolve: ([tableIdx]) => tableIdx, 160 | }), 161 | fill: baseInstruction("table.fill", TableIndex, { 162 | create(_: LocalContext, table: Dependency.Table) { 163 | return { 164 | in: ["i32", table.type.type, "i32"], 165 | out: [], 166 | deps: [table], 167 | }; 168 | }, 169 | resolve: ([tableIdx]) => tableIdx, 170 | }), 171 | }; 172 | 173 | const elemOps = { 174 | drop: baseInstruction("elem.drop", ElemIndex, { 175 | create(_: LocalContext, elem: Dependency.Elem) { 176 | return { 177 | in: [], 178 | out: [], 179 | deps: [elem], 180 | }; 181 | }, 182 | resolve: ([elemIdx]) => elemIdx, 183 | }), 184 | }; 185 | 186 | type MemArg = { align: U32; offset: U32 }; 187 | const MemArg = record({ align: U32, offset: U32 }); 188 | 189 | function memoryInstruction< 190 | const Args extends Tuple, 191 | const Results extends Tuple 192 | >( 193 | name: InstructionName, 194 | bits: number, 195 | args: ValueTypeObjects, 196 | results: ValueTypeObjects 197 | ): ((...args: [] | Args) => any) extends (...args: infer P) => any 198 | ? ( 199 | ctx: LocalContext, 200 | memArg: { offset?: number; align?: number }, 201 | ...args: { 202 | [i in keyof P]: Input; 203 | } 204 | ) => Instruction_ 205 | : never { 206 | let expectedArgs = valueTypeLiterals(args); 207 | let createInstr = baseInstruction< 208 | MemArg, 209 | [memArg: { offset?: number; align?: number }], 210 | [memArg: MemArg], 211 | Args, 212 | Results 213 | >(name, MemArg, { 214 | create(_, memArg) { 215 | return { 216 | in: expectedArgs, 217 | out: valueTypeLiterals(results), 218 | resolveArgs: [memArgFromInput(name, bits, memArg)], 219 | deps: [Dependency.hasMemory], 220 | }; 221 | }, 222 | }); 223 | return function createInstr_( 224 | ctx: LocalContext, 225 | memArgs: { offset?: number; align?: number }, 226 | ...actualArgs: Input[] 227 | ): Instruction_ { 228 | processStackArgs(ctx, name, expectedArgs, actualArgs); 229 | return createInstr(ctx, memArgs); 230 | }; 231 | } 232 | 233 | type MemArgAndLane = { memArg: MemArg; lane: U8 }; 234 | const MemArgAndLane = record({ memArg: MemArg, lane: U8 }); 235 | 236 | function memoryLaneInstruction< 237 | Args extends Tuple, 238 | Results extends Tuple 239 | >( 240 | name: InstructionName, 241 | bits: number, 242 | args: ValueTypeObjects, 243 | results: ValueTypeObjects 244 | ): ((...args: [] | Args) => any) extends (...args: infer P) => any 245 | ? ( 246 | ctx: LocalContext, 247 | memArg: { offset?: number; align?: number }, 248 | lane: number, 249 | ...args: { 250 | [i in keyof P]: Input; 251 | } 252 | ) => Instruction_ 253 | : never { 254 | let expectedArgs = valueTypeLiterals(args); 255 | let createInstr = baseInstruction< 256 | MemArgAndLane, 257 | [memArg: { offset?: number; align?: number }, lane: number], 258 | [memArgAndLane: MemArgAndLane], 259 | Args, 260 | Results 261 | >(name, MemArgAndLane, { 262 | create: (_, memArg, lane) => ({ 263 | in: expectedArgs, 264 | out: valueTypeLiterals(results), 265 | resolveArgs: [{ memArg: memArgFromInput(name, bits, memArg), lane }], 266 | deps: [Dependency.hasMemory], 267 | }), 268 | }); 269 | return function createInstr_( 270 | ctx: LocalContext, 271 | memArgs: { offset?: number; align?: number }, 272 | lane: number, 273 | ...actualArgs: Input[] 274 | ): Instruction_ { 275 | processStackArgs(ctx, name, expectedArgs, actualArgs); 276 | return createInstr(ctx, memArgs, lane); 277 | }; 278 | } 279 | 280 | function memArgFromInput( 281 | name: string, 282 | bits: number, 283 | { offset = 0, align = bits / 8 }: { offset?: number; align?: number } 284 | ) { 285 | let alignExponent = Math.log2(align); 286 | if (!Number.isInteger(alignExponent)) { 287 | throw Error(`${name}: \`align\` must be power of 2, got ${align}`); 288 | } 289 | return { offset, align: alignExponent }; 290 | } 291 | -------------------------------------------------------------------------------- /src/instruction/numeric.ts: -------------------------------------------------------------------------------- 1 | import { i32t, i64t, f32t, f64t } from "../types.js"; 2 | import { memoryInstruction } from "./memory.js"; 3 | import { instruction } from "./stack-args.js"; 4 | import { f32Const, f64Const, i32Const, i64Const } from "./const.js"; 5 | 6 | export { i32Ops, i64Ops, f32Ops, f64Ops }; 7 | 8 | const i32Ops = { 9 | // memory 10 | load: memoryInstruction("i32.load", 32, [i32t], [i32t]), 11 | load16_s: memoryInstruction("i32.load16_s", 16, [i32t], [i32t]), 12 | load16_u: memoryInstruction("i32.load16_u", 16, [i32t], [i32t]), 13 | load8_s: memoryInstruction("i32.load8_s", 8, [i32t], [i32t]), 14 | load8_u: memoryInstruction("i32.load8_u", 8, [i32t], [i32t]), 15 | store: memoryInstruction("i32.store", 32, [i32t, i32t], []), 16 | store16: memoryInstruction("i32.store16", 16, [i32t, i32t], []), 17 | store8: memoryInstruction("i32.store8", 8, [i32t, i32t], []), 18 | 19 | // const 20 | const: i32Const, 21 | 22 | // comparison 23 | eqz: instruction("i32.eqz", [i32t], [i32t]), 24 | eq: instruction("i32.eq", [i32t, i32t], [i32t]), 25 | ne: instruction("i32.ne", [i32t, i32t], [i32t]), 26 | lt_s: instruction("i32.lt_s", [i32t, i32t], [i32t]), 27 | lt_u: instruction("i32.lt_u", [i32t, i32t], [i32t]), 28 | gt_s: instruction("i32.gt_u", [i32t, i32t], [i32t]), 29 | gt_u: instruction("i32.gt_u", [i32t, i32t], [i32t]), 30 | le_s: instruction("i32.le_s", [i32t, i32t], [i32t]), 31 | le_u: instruction("i32.le_u", [i32t, i32t], [i32t]), 32 | ge_s: instruction("i32.ge_s", [i32t, i32t], [i32t]), 33 | ge_u: instruction("i32.ge_u", [i32t, i32t], [i32t]), 34 | 35 | // unary 36 | clz: instruction("i32.clz", [i32t], [i32t]), 37 | ctz: instruction("i32.ctz", [i32t], [i32t]), 38 | popcnt: instruction("i32.popcnt", [i32t], [i32t]), 39 | 40 | // binary 41 | add: instruction("i32.add", [i32t, i32t], [i32t]), 42 | sub: instruction("i32.sub", [i32t, i32t], [i32t]), 43 | mul: instruction("i32.mul", [i32t, i32t], [i32t]), 44 | div_s: instruction("i32.div_s", [i32t, i32t], [i32t]), 45 | div_u: instruction("i32.div_u", [i32t, i32t], [i32t]), 46 | rem_s: instruction("i32.rem_s", [i32t, i32t], [i32t]), 47 | rem_u: instruction("i32.rem_u", [i32t, i32t], [i32t]), 48 | and: instruction("i32.and", [i32t, i32t], [i32t]), 49 | or: instruction("i32.or", [i32t, i32t], [i32t]), 50 | xor: instruction("i32.xor", [i32t, i32t], [i32t]), 51 | shl: instruction("i32.shl", [i32t, i32t], [i32t]), 52 | shr_s: instruction("i32.shr_s", [i32t, i32t], [i32t]), 53 | shr_u: instruction("i32.shr_u", [i32t, i32t], [i32t]), 54 | rotl: instruction("i32.rotl", [i32t, i32t], [i32t]), 55 | rotr: instruction("i32.rotr", [i32t, i32t], [i32t]), 56 | 57 | // conversion 58 | wrap_i64: instruction("i32.wrap_i64", [i64t], [i32t]), 59 | trunc_f32_s: instruction("i32.trunc_f32_s", [f32t], [i32t]), 60 | trunc_f32_u: instruction("i32.trunc_f32_u", [f32t], [i32t]), 61 | trunc_f64_s: instruction("i32.trunc_f64_s", [f64t], [i32t]), 62 | trunc_f64_u: instruction("i32.trunc_f64_u", [f64t], [i32t]), 63 | reinterpret_f32: instruction("i32.reinterpret_f32", [f32t], [i32t]), 64 | extend8_s: instruction("i32.extend8_s", [i32t], [i32t]), 65 | extend16_s: instruction("i32.extend16_s", [i32t], [i32t]), 66 | 67 | // non-trapping conversion 68 | trunc_sat_f32_s: instruction("i32.trunc_sat_f32_s", [f32t], [i32t]), 69 | trunc_sat_f32_u: instruction("i32.trunc_sat_f32_u", [f32t], [i32t]), 70 | trunc_sat_f64_s: instruction("i32.trunc_sat_f64_s", [f64t], [i32t]), 71 | trunc_sat_f64_u: instruction("i32.trunc_sat_f64_u", [f64t], [i32t]), 72 | }; 73 | 74 | const i64Ops = { 75 | // memory 76 | load: memoryInstruction("i64.load", 64, [i32t], [i64t]), 77 | load32_s: memoryInstruction("i64.load32_s", 32, [i32t], [i64t]), 78 | load32_u: memoryInstruction("i64.load32_u", 32, [i32t], [i64t]), 79 | load16_s: memoryInstruction("i64.load16_s", 16, [i32t], [i64t]), 80 | load16_u: memoryInstruction("i64.load16_u", 16, [i32t], [i64t]), 81 | load8_s: memoryInstruction("i64.load8_s", 8, [i32t], [i64t]), 82 | load8_u: memoryInstruction("i64.load8_u", 8, [i32t], [i64t]), 83 | store: memoryInstruction("i64.store", 64, [i32t, i64t], []), 84 | store32: memoryInstruction("i64.store32", 32, [i32t, i64t], []), 85 | store16: memoryInstruction("i64.store16", 16, [i32t, i64t], []), 86 | store8: memoryInstruction("i64.store8", 8, [i32t, i64t], []), 87 | 88 | // const 89 | const: i64Const, 90 | 91 | // comparison 92 | eqz: instruction("i64.eqz", [i64t], [i32t]), 93 | eq: instruction("i64.eq", [i64t, i64t], [i32t]), 94 | ne: instruction("i64.ne", [i64t, i64t], [i32t]), 95 | lt_s: instruction("i64.lt_s", [i64t, i64t], [i32t]), 96 | lt_u: instruction("i64.lt_u", [i64t, i64t], [i32t]), 97 | gt_s: instruction("i64.gt_u", [i64t, i64t], [i32t]), 98 | gt_u: instruction("i64.gt_u", [i64t, i64t], [i32t]), 99 | le_s: instruction("i64.le_s", [i64t, i64t], [i32t]), 100 | le_u: instruction("i64.le_u", [i64t, i64t], [i32t]), 101 | ge_s: instruction("i64.ge_s", [i64t, i64t], [i32t]), 102 | ge_u: instruction("i64.ge_u", [i64t, i64t], [i32t]), 103 | 104 | // unary 105 | clz: instruction("i64.clz", [i64t], [i64t]), 106 | ctz: instruction("i64.ctz", [i64t], [i64t]), 107 | popcnt: instruction("i64.popcnt", [i64t], [i64t]), 108 | 109 | // binary 110 | add: instruction("i64.add", [i64t, i64t], [i64t]), 111 | sub: instruction("i64.sub", [i64t, i64t], [i64t]), 112 | mul: instruction("i64.mul", [i64t, i64t], [i64t]), 113 | div_s: instruction("i64.div_s", [i64t, i64t], [i64t]), 114 | div_u: instruction("i64.div_u", [i64t, i64t], [i64t]), 115 | rem_s: instruction("i64.rem_s", [i64t, i64t], [i64t]), 116 | rem_u: instruction("i64.rem_u", [i64t, i64t], [i64t]), 117 | and: instruction("i64.and", [i64t, i64t], [i64t]), 118 | or: instruction("i64.or", [i64t, i64t], [i64t]), 119 | xor: instruction("i64.xor", [i64t, i64t], [i64t]), 120 | shl: instruction("i64.shl", [i64t, i64t], [i64t]), 121 | shr_s: instruction("i64.shr_s", [i64t, i64t], [i64t]), 122 | shr_u: instruction("i64.shr_u", [i64t, i64t], [i64t]), 123 | rotl: instruction("i64.rotl", [i64t, i64t], [i64t]), 124 | rotr: instruction("i64.rotr", [i64t, i64t], [i64t]), 125 | 126 | // conversion 127 | extend_i32_s: instruction("i64.extend_i32_s", [i32t], [i64t]), 128 | extend_i32_u: instruction("i64.extend_i32_u", [i32t], [i64t]), 129 | trunc_f32_s: instruction("i64.trunc_f32_s", [f32t], [i64t]), 130 | trunc_f32_u: instruction("i64.trunc_f32_u", [f32t], [i64t]), 131 | trunc_f64_s: instruction("i64.trunc_f64_s", [f64t], [i64t]), 132 | trunc_f64_u: instruction("i64.trunc_f64_u", [f64t], [i64t]), 133 | reinterpret_f64: instruction("i64.reinterpret_f64", [f64t], [i64t]), 134 | extend8_s: instruction("i64.extend8_s", [i64t], [i64t]), 135 | extend16_s: instruction("i64.extend16_s", [i64t], [i64t]), 136 | extend32_s: instruction("i64.extend32_s", [i64t], [i64t]), 137 | 138 | // non-trapping conversion 139 | trunc_sat_f32_s: instruction("i64.trunc_sat_f32_s", [f32t], [i64t]), 140 | trunc_sat_f32_u: instruction("i64.trunc_sat_f32_u", [f32t], [i64t]), 141 | trunc_sat_f64_s: instruction("i64.trunc_sat_f64_s", [f64t], [i64t]), 142 | trunc_sat_f64_u: instruction("i64.trunc_sat_f64_u", [f64t], [i64t]), 143 | }; 144 | 145 | const f32Ops = { 146 | // memory 147 | load: memoryInstruction("f32.load", 32, [i32t], [f32t]), 148 | store: memoryInstruction("f32.store", 32, [i32t, f32t], []), 149 | 150 | // const 151 | const: f32Const, 152 | 153 | // comparison 154 | eq: instruction("f32.eq", [f32t, f32t], [i32t]), 155 | ne: instruction("f32.ne", [f32t, f32t], [i32t]), 156 | lt: instruction("f32.lt", [f32t, f32t], [i32t]), 157 | gt: instruction("f32.gt", [f32t, f32t], [i32t]), 158 | le: instruction("f32.le", [f32t, f32t], [i32t]), 159 | ge: instruction("f32.ge", [f32t, f32t], [i32t]), 160 | 161 | // unary 162 | abs: instruction("f32.abs", [f32t], [f32t]), 163 | neg: instruction("f32.neg", [f32t], [f32t]), 164 | ceil: instruction("f32.ceil", [f32t], [f32t]), 165 | floor: instruction("f32.floor", [f32t], [f32t]), 166 | trunc: instruction("f32.trunc", [f32t], [f32t]), 167 | nearest: instruction("f32.nearest", [f32t], [f32t]), 168 | sqrt: instruction("f32.sqrt", [f32t], [f32t]), 169 | 170 | // binary 171 | add: instruction("f32.add", [f32t, f32t], [f32t]), 172 | sub: instruction("f32.sub", [f32t, f32t], [f32t]), 173 | mul: instruction("f32.mul", [f32t, f32t], [f32t]), 174 | div: instruction("f32.div", [f32t, f32t], [f32t]), 175 | min: instruction("f32.min", [f32t, f32t], [f32t]), 176 | max: instruction("f32.max", [f32t, f32t], [f32t]), 177 | copysign: instruction("f32.copysign", [f32t, f32t], [f32t]), 178 | 179 | // conversion 180 | convert_i32_s: instruction("f32.convert_i32_s", [i32t], [f32t]), 181 | convert_i32_u: instruction("f32.convert_i32_u", [i32t], [f32t]), 182 | convert_i64_s: instruction("f32.convert_i64_s", [i64t], [f32t]), 183 | convert_i64_u: instruction("f32.convert_i64_u", [i64t], [f32t]), 184 | demote_f64: instruction("f32.demote_f64", [f64t], [f32t]), 185 | reinterpret_i32: instruction("f32.reinterpret_i32", [i32t], [f32t]), 186 | }; 187 | 188 | const f64Ops = { 189 | // memory 190 | load: memoryInstruction("f64.load", 64, [i32t], [f64t]), 191 | store: memoryInstruction("f64.store", 64, [i32t, f64t], []), 192 | 193 | // const 194 | const: f64Const, 195 | 196 | // comparison 197 | eq: instruction("f64.eq", [f64t, f64t], [i32t]), 198 | ne: instruction("f64.ne", [f64t, f64t], [i32t]), 199 | lt: instruction("f64.lt", [f64t, f64t], [i32t]), 200 | gt: instruction("f64.gt", [f64t, f64t], [i32t]), 201 | le: instruction("f64.le", [f64t, f64t], [i32t]), 202 | ge: instruction("f64.ge", [f64t, f64t], [i32t]), 203 | 204 | // unary 205 | abs: instruction("f64.abs", [f64t], [f64t]), 206 | neg: instruction("f64.neg", [f64t], [f64t]), 207 | ceil: instruction("f64.ceil", [f64t], [f64t]), 208 | floor: instruction("f64.floor", [f64t], [f64t]), 209 | trunc: instruction("f64.trunc", [f64t], [f64t]), 210 | nearest: instruction("f64.nearest", [f64t], [f64t]), 211 | sqrt: instruction("f64.sqrt", [f64t], [f64t]), 212 | 213 | // binary 214 | add: instruction("f64.add", [f64t, f64t], [f64t]), 215 | sub: instruction("f64.sub", [f64t, f64t], [f64t]), 216 | mul: instruction("f64.mul", [f64t, f64t], [f64t]), 217 | div: instruction("f64.div", [f64t, f64t], [f64t]), 218 | min: instruction("f64.min", [f64t, f64t], [f64t]), 219 | max: instruction("f64.max", [f64t, f64t], [f64t]), 220 | copysign: instruction("f64.copysign", [f64t, f64t], [f64t]), 221 | 222 | // conversion 223 | convert_i32_s: instruction("f64.convert_i32_s", [i32t], [f64t]), 224 | convert_i32_u: instruction("f64.convert_i32_u", [i32t], [f64t]), 225 | convert_i64_s: instruction("f64.convert_i64_s", [i64t], [f64t]), 226 | convert_i64_u: instruction("f64.convert_i64_u", [i64t], [f64t]), 227 | promote_f32: instruction("f64.promote_f32", [f32t], [f64t]), 228 | reinterpret_i64: instruction("f64.reinterpret_i64", [i64t], [f64t]), 229 | }; 230 | -------------------------------------------------------------------------------- /src/instruction/opcodes.ts: -------------------------------------------------------------------------------- 1 | export { opcodes, nameToOpcode, InstructionName }; 2 | 3 | type Opcodes = typeof opcodes; 4 | type NestedValue = { 5 | [k in keyof T]: T[k] extends string ? T[k] : NestedValue; 6 | }[keyof T]; 7 | type InstructionName = NestedValue; 8 | 9 | const opcodes = { 10 | // control 11 | 0x00: "unreachable", 12 | 0x01: "nop", 13 | 0x02: "block", 14 | 0x03: "loop", 15 | 0x04: "if", 16 | // 0x05: "else", // not an instruction 17 | // 0x0b: "end", // not an instruction 18 | 0x0c: "br", 19 | 0x0d: "br_if", 20 | 0x0e: "br_table", 21 | 0x0f: "return", 22 | 0x10: "call", 23 | 0x11: "call_indirect", 24 | 25 | // parametric 26 | 0x1a: "drop", 27 | 0x1b: "select", 28 | 0x1c: "select_t", 29 | 30 | // variable 31 | 0x20: "local.get", 32 | 0x21: "local.set", 33 | 0x22: "local.tee", 34 | 0x23: "global.get", 35 | 0x24: "global.set", 36 | 0x25: "table.get", 37 | 0x26: "table.set", 38 | 39 | // memory 40 | 0x28: "i32.load", 41 | 0x29: "i64.load", 42 | 0x2a: "f32.load", 43 | 0x2b: "f64.load", 44 | 0x2c: "i32.load8_s", 45 | 0x2d: "i32.load8_u", 46 | 0x2e: "i32.load16_s", 47 | 0x2f: "i32.load16_u", 48 | 0x30: "i64.load8_s", 49 | 0x31: "i64.load8_u", 50 | 0x32: "i64.load16_s", 51 | 0x33: "i64.load16_u", 52 | 0x34: "i64.load32_s", 53 | 0x35: "i64.load32_u", 54 | 0x36: "i32.store", 55 | 0x37: "i64.store", 56 | 0x38: "f32.store", 57 | 0x39: "f64.store", 58 | 0x3a: "i32.store8", 59 | 0x3b: "i32.store16", 60 | 0x3c: "i64.store8", 61 | 0x3d: "i64.store16", 62 | 0x3e: "i64.store32", 63 | 0x3f: "memory.size", 64 | 0x40: "memory.grow", 65 | 66 | // numeric 67 | 0x41: "i32.const", 68 | 0x42: "i64.const", 69 | 0x43: "f32.const", 70 | 0x44: "f64.const", 71 | 72 | 0x45: "i32.eqz", 73 | 0x46: "i32.eq", 74 | 0x47: "i32.ne", 75 | 0x48: "i32.lt_s", 76 | 0x49: "i32.lt_u", 77 | 0x4a: "i32.gt_s", 78 | 0x4b: "i32.gt_u", 79 | 0x4c: "i32.le_s", 80 | 0x4d: "i32.le_u", 81 | 0x4e: "i32.ge_s", 82 | 0x4f: "i32.ge_u", 83 | 84 | 0x50: "i64.eqz", 85 | 0x51: "i64.eq", 86 | 0x52: "i64.ne", 87 | 0x53: "i64.lt_s", 88 | 0x54: "i64.lt_u", 89 | 0x55: "i64.gt_s", 90 | 0x56: "i64.gt_u", 91 | 0x57: "i64.le_s", 92 | 0x58: "i64.le_u", 93 | 0x59: "i64.ge_s", 94 | 0x5a: "i64.ge_u", 95 | 96 | 0x5b: "f32.eq", 97 | 0x5c: "f32.ne", 98 | 0x5d: "f32.lt", 99 | 0x5e: "f32.gt", 100 | 0x5f: "f32.le", 101 | 0x60: "f32.ge", 102 | 103 | 0x61: "f64.eq", 104 | 0x62: "f64.ne", 105 | 0x63: "f64.lt", 106 | 0x64: "f64.gt", 107 | 0x65: "f64.le", 108 | 0x66: "f64.ge", 109 | 110 | 0x67: "i32.clz", 111 | 0x68: "i32.ctz", 112 | 0x69: "i32.popcnt", 113 | 114 | 0x6a: "i32.add", 115 | 0x6b: "i32.sub", 116 | 0x6c: "i32.mul", 117 | 0x6d: "i32.div_s", 118 | 0x6e: "i32.div_u", 119 | 0x6f: "i32.rem_s", 120 | 0x70: "i32.rem_u", 121 | 0x71: "i32.and", 122 | 0x72: "i32.or", 123 | 0x73: "i32.xor", 124 | 0x74: "i32.shl", 125 | 0x75: "i32.shr_s", 126 | 0x76: "i32.shr_u", 127 | 0x77: "i32.rotl", 128 | 0x78: "i32.rotr", 129 | 130 | 0x79: "i64.clz", 131 | 0x7a: "i64.ctz", 132 | 0x7b: "i64.popcnt", 133 | 134 | 0x7c: "i64.add", 135 | 0x7d: "i64.sub", 136 | 0x7e: "i64.mul", 137 | 0x7f: "i64.div_s", 138 | 0x80: "i64.div_u", 139 | 0x81: "i64.rem_s", 140 | 0x82: "i64.rem_u", 141 | 0x83: "i64.and", 142 | 0x84: "i64.or", 143 | 0x85: "i64.xor", 144 | 0x86: "i64.shl", 145 | 0x87: "i64.shr_s", 146 | 0x88: "i64.shr_u", 147 | 0x89: "i64.rotl", 148 | 0x8a: "i64.rotr", 149 | 150 | 0x8b: "f32.abs", 151 | 0x8c: "f32.neg", 152 | 0x8d: "f32.ceil", 153 | 0x8e: "f32.floor", 154 | 0x8f: "f32.trunc", 155 | 0x90: "f32.nearest", 156 | 0x91: "f32.sqrt", 157 | 158 | 0x92: "f32.add", 159 | 0x93: "f32.sub", 160 | 0x94: "f32.mul", 161 | 0x95: "f32.div", 162 | 0x96: "f32.min", 163 | 0x97: "f32.max", 164 | 0x98: "f32.copysign", 165 | 166 | 0x99: "f64.abs", 167 | 0x9a: "f64.neg", 168 | 0x9b: "f64.ceil", 169 | 0x9c: "f64.floor", 170 | 0x9d: "f64.trunc", 171 | 0x9e: "f64.nearest", 172 | 0x9f: "f64.sqrt", 173 | 174 | 0xa0: "f64.add", 175 | 0xa1: "f64.sub", 176 | 0xa2: "f64.mul", 177 | 0xa3: "f64.div", 178 | 0xa4: "f64.min", 179 | 0xa5: "f64.max", 180 | 0xa6: "f64.copysign", 181 | 182 | 0xa7: "i32.wrap_i64", 183 | 0xa8: "i32.trunc_f32_s", 184 | 0xa9: "i32.trunc_f32_u", 185 | 0xaa: "i32.trunc_f64_s", 186 | 0xab: "i32.trunc_f64_u", 187 | 188 | 0xac: "i64.extend_i32_s", 189 | 0xad: "i64.extend_i32_u", 190 | 0xae: "i64.trunc_f32_s", 191 | 0xaf: "i64.trunc_f32_u", 192 | 0xb0: "i64.trunc_f64_s", 193 | 0xb1: "i64.trunc_f64_u", 194 | 195 | 0xb2: "f32.convert_i32_s", 196 | 0xb3: "f32.convert_i32_u", 197 | 0xb4: "f32.convert_i64_s", 198 | 0xb5: "f32.convert_i64_u", 199 | 0xb6: "f32.demote_f64", 200 | 201 | 0xb7: "f64.convert_i32_s", 202 | 0xb8: "f64.convert_i32_u", 203 | 0xb9: "f64.convert_i64_s", 204 | 0xba: "f64.convert_i64_u", 205 | 0xbb: "f64.promote_f32", 206 | 207 | 0xbc: "i32.reinterpret_f32", 208 | 0xbd: "i64.reinterpret_f64", 209 | 0xbe: "f32.reinterpret_i32", 210 | 0xbf: "f64.reinterpret_i64", 211 | 212 | 0xc0: "i32.extend8_s", 213 | 0xc1: "i32.extend16_s", 214 | 0xc2: "i64.extend8_s", 215 | 0xc3: "i64.extend16_s", 216 | 0xc4: "i64.extend32_s", 217 | 218 | // reference 219 | 0xd0: "ref.null", 220 | 0xd1: "ref.is_null", 221 | 0xd2: "ref.func", 222 | 223 | 0xfc: { 224 | // saturating float to int 225 | 0: "i32.trunc_sat_f32_s", 226 | 1: "i32.trunc_sat_f32_u", 227 | 2: "i32.trunc_sat_f64_s", 228 | 3: "i32.trunc_sat_f64_u", 229 | 4: "i64.trunc_sat_f32_s", 230 | 5: "i64.trunc_sat_f32_u", 231 | 6: "i64.trunc_sat_f64_s", 232 | 7: "i64.trunc_sat_f64_u", 233 | 234 | // bulk memory 235 | 8: "memory.init", 236 | 9: "data.drop", 237 | 10: "memory.copy", 238 | 11: "memory.fill", 239 | 12: "table.init", 240 | 13: "elem.drop", 241 | 14: "table.copy", 242 | 15: "table.grow", 243 | 16: "table.size", 244 | 17: "table.fill", 245 | }, 246 | 247 | // simd 248 | 0xfd: { 249 | 0: "v128.load", 250 | 1: "v128.load8x8_s", 251 | 2: "v128.load8x8_u", 252 | 3: "v128.load16x4_s", 253 | 4: "v128.load16x4_u", 254 | 5: "v128.load32x2_s", 255 | 6: "v128.load32x2_u", 256 | 7: "v128.load8_splat", 257 | 8: "v128.load16_splat", 258 | 9: "v128.load32_splat", 259 | 10: "v128.load64_splat", 260 | 11: "v128.store", 261 | 12: "v128.const", 262 | 263 | 13: "i8x16.shuffle", 264 | 14: "i8x16.swizzle", 265 | 266 | 15: "i8x16.splat", 267 | 16: "i16x8.splat", 268 | 17: "i32x4.splat", 269 | 18: "i64x2.splat", 270 | 19: "f32x4.splat", 271 | 20: "f64x2.splat", 272 | 273 | 21: "i8x16.extract_lane_s", 274 | 22: "i8x16.extract_lane_u", 275 | 23: "i8x16.replace_lane", 276 | 24: "i16x8.extract_lane_s", 277 | 25: "i16x8.extract_lane_u", 278 | 26: "i16x8.replace_lane", 279 | 27: "i32x4.extract_lane", 280 | 28: "i32x4.replace_lane", 281 | 29: "i64x2.extract_lane", 282 | 30: "i64x2.replace_lane", 283 | 31: "f32x4.extract_lane", 284 | 32: "f32x4.replace_lane", 285 | 33: "f64x2.extract_lane", 286 | 34: "f64x2.replace_lane", 287 | 288 | 35: "i8x16.eq", 289 | 36: "i8x16.ne", 290 | 37: "i8x16.lt_s", 291 | 38: "i8x16.lt_u", 292 | 39: "i8x16.gt_s", 293 | 40: "i8x16.gt_u", 294 | 41: "i8x16.le_s", 295 | 42: "i8x16.le_u", 296 | 43: "i8x16.ge_s", 297 | 44: "i8x16.ge_u", 298 | 299 | 45: "i16x8.eq", 300 | 46: "i16x8.ne", 301 | 47: "i16x8.lt_s", 302 | 48: "i16x8.lt_u", 303 | 49: "i16x8.gt_s", 304 | 50: "i16x8.gt_u", 305 | 51: "i16x8.le_s", 306 | 52: "i16x8.le_u", 307 | 53: "i16x8.ge_s", 308 | 54: "i16x8.ge_u", 309 | 310 | 55: "i32x4.eq", 311 | 56: "i32x4.ne", 312 | 57: "i32x4.lt_s", 313 | 58: "i32x4.lt_u", 314 | 59: "i32x4.gt_s", 315 | 60: "i32x4.gt_u", 316 | 61: "i32x4.le_s", 317 | 62: "i32x4.le_u", 318 | 63: "i32x4.ge_s", 319 | 64: "i32x4.ge_u", 320 | 321 | 65: "f32x4.eq", 322 | 66: "f32x4.ne", 323 | 67: "f32x4.lt", 324 | 68: "f32x4.gt", 325 | 69: "f32x4.le", 326 | 70: "f32x4.ge", 327 | 328 | 71: "f64x2.eq", 329 | 72: "f64x2.ne", 330 | 73: "f64x2.lt", 331 | 74: "f64x2.gt", 332 | 75: "f64x2.le", 333 | 76: "f64x2.ge", 334 | 335 | 77: "v128.not", 336 | 78: "v128.and", 337 | 79: "v128.andnot", 338 | 80: "v128.or", 339 | 81: "v128.xor", 340 | 82: "v128.bitselect", 341 | 83: "v128.any_true", 342 | 343 | 84: "v128.load8_lane", 344 | 85: "v128.load16_lane", 345 | 86: "v128.load32_lane", 346 | 87: "v128.load64_lane", 347 | 88: "v128.store8_lane", 348 | 89: "v128.store16_lane", 349 | 90: "v128.store32_lane", 350 | 91: "v128.store64_lane", 351 | 92: "v128.load32_zero", 352 | 93: "v128.load64_zero", 353 | 354 | 94: "f32x4.demote_f64x2_zero", 355 | 95: "f64x2.promote_low_f32x4", 356 | 357 | 96: "i8x16.abs", 358 | 97: "i8x16.neg", 359 | 98: "i8x16.popcnt", 360 | 99: "i8x16.all_true", 361 | 100: "i8x16.bitmask", 362 | 101: "i8x16.narrow_i16x8_s", 363 | 102: "i8x16.narrow_i16x8_u", 364 | 365 | 103: "f32x4.ceil", 366 | 104: "f32x4.floor", 367 | 105: "f32x4.trunc", 368 | 106: "f32x4.nearest", 369 | 370 | 107: "i8x16.shl", 371 | 108: "i8x16.shr_s", 372 | 109: "i8x16.shr_u", 373 | 110: "i8x16.add", 374 | 111: "i8x16.add_sat_s", 375 | 112: "i8x16.add_sat_u", 376 | 113: "i8x16.sub", 377 | 114: "i8x16.sub_sat_s", 378 | 115: "i8x16.sub_sat_u", 379 | 380 | 116: "f64x2.ceil", 381 | 117: "f64x2.floor", 382 | 383 | 118: "i8x16.min_s", 384 | 119: "i8x16.min_u", 385 | 120: "i8x16.max_s", 386 | 121: "i8x16.max_u", 387 | 388 | 122: "f64x2.trunc", 389 | 390 | 123: "i8x16.avgr_u", 391 | 392 | 124: "i16x8.extadd_pairwise_i8x16_s", 393 | 125: "i16x8.extadd_pairwise_i8x16_u", 394 | 126: "i32x4.extadd_pairwise_i16x8_s", 395 | 127: "i32x4.extadd_pairwise_i16x8_u", 396 | 397 | 128: "i16x8.abs", 398 | 129: "i16x8.neg", 399 | 130: "i16x8.q15mulr_sat_s", 400 | 131: "i16x8.all_true", 401 | 132: "i16x8.bitmask", 402 | 133: "i16x8.narrow_i32x4_s", 403 | 134: "i16x8.narrow_i32x4_u", 404 | 135: "i16x8.extend_low_i8x16_s", 405 | 136: "i16x8.extend_high_i8x16_s", 406 | 137: "i16x8.extend_low_i8x16_u", 407 | 138: "i16x8.extend_high_i8x16_u", 408 | 139: "i16x8.shl", 409 | 140: "i16x8.shr_s", 410 | 141: "i16x8.shr_u", 411 | 142: "i16x8.add", 412 | 143: "i16x8.add_sat_s", 413 | 144: "i16x8.add_sat_u", 414 | 145: "i16x8.sub", 415 | 146: "i16x8.sub_sat_s", 416 | 147: "i16x8.sub_sat_u", 417 | 418 | 148: "f64x2.nearest", 419 | 420 | 149: "i16x8.mul", 421 | 150: "i16x8.min_s", 422 | 151: "i16x8.min_u", 423 | 152: "i16x8.max_s", 424 | 153: "i16x8.max_u", 425 | 426 | 155: "i16x8.avgr_u", 427 | 156: "i16x8.extmul_low_i8x16_s", 428 | 157: "i16x8.extmul_high_i8x16_s", 429 | 158: "i16x8.extmul_low_i8x16_u", 430 | 159: "i16x8.extmul_high_i8x16_u", 431 | 432 | 160: "i32x4.abs", 433 | 161: "i32x4.neg", 434 | 435 | 163: "i32x4.all_true", 436 | 164: "i32x4.bitmask", 437 | 438 | 167: "i32x4.extend_low_i16x8_s", 439 | 168: "i32x4.extend_high_i16x8_s", 440 | 169: "i32x4.extend_low_i16x8_u", 441 | 170: "i32x4.extend_high_i16x8_u", 442 | 171: "i32x4.shl", 443 | 172: "i32x4.shr_s", 444 | 173: "i32x4.shr_u", 445 | 174: "i32x4.add", 446 | 447 | 177: "i32x4.sub", 448 | 449 | 181: "i32x4.mul", 450 | 182: "i32x4.min_s", 451 | 183: "i32x4.min_u", 452 | 184: "i32x4.max_s", 453 | 185: "i32x4.max_u", 454 | 186: "i32x4.dot_i16x8_s", 455 | 188: "i32x4.extmul_low_i16x8_s", 456 | 189: "i32x4.extmul_high_i16x8_s", 457 | 190: "i32x4.extmul_low_i16x8_u", 458 | 191: "i32x4.extmul_high_i16x8_u", 459 | 460 | 192: "i64x2.abs", 461 | 193: "i64x2.neg", 462 | 463 | 195: "i64x2.all_true", 464 | 196: "i64x2.bitmask", 465 | 466 | 199: "i64x2.extend_low_i32x4_s", 467 | 200: "i64x2.extend_high_i32x4_s", 468 | 201: "i64x2.extend_low_i32x4_u", 469 | 202: "i64x2.extend_high_i32x4_u", 470 | 203: "i64x2.shl", 471 | 204: "i64x2.shr_s", 472 | 205: "i64x2.shr_u", 473 | 206: "i64x2.add", 474 | 475 | 209: "i64x2.sub", 476 | 477 | 213: "i64x2.mul", 478 | 214: "i64x2.eq", 479 | 215: "i64x2.ne", 480 | 216: "i64x2.lt_s", 481 | 217: "i64x2.gt_s", 482 | 218: "i64x2.le_s", 483 | 219: "i64x2.ge_s", 484 | 220: "i64x2.extmul_low_i32x4_s", 485 | 221: "i64x2.extmul_high_i32x4_s", 486 | 222: "i64x2.extmul_low_i32x4_u", 487 | 223: "i64x2.extmul_high_i32x4_u", 488 | 489 | 224: "f32x4.abs", 490 | 225: "f32x4.neg", 491 | 492 | 227: "f32x4.sqrt", 493 | 228: "f32x4.add", 494 | 229: "f32x4.sub", 495 | 230: "f32x4.mul", 496 | 231: "f32x4.div", 497 | 232: "f32x4.min", 498 | 233: "f32x4.max", 499 | 234: "f32x4.pmin", 500 | 235: "f32x4.pmax", 501 | 502 | 236: "f64x2.abs", 503 | 237: "f64x2.neg", 504 | 505 | 239: "f64x2.sqrt", 506 | 240: "f64x2.add", 507 | 241: "f64x2.sub", 508 | 242: "f64x2.mul", 509 | 243: "f64x2.div", 510 | 244: "f64x2.min", 511 | 245: "f64x2.max", 512 | 246: "f64x2.pmin", 513 | 247: "f64x2.pmax", 514 | 515 | 248: "i32x4.trunc_sat_f32x4_s", 516 | 249: "i32x4.trunc_sat_f32x4_u", 517 | 250: "f32x4.convert_i32x4_s", 518 | 251: "f32x4.convert_i32x4_u", 519 | 252: "i32x4.trunc_sat_f64x2_s_zero", 520 | 253: "i32x4.trunc_sat_f64x2_u_zero", 521 | 254: "f64x2.convert_low_i32x4_s", 522 | 255: "f64x2.convert_low_i32x4_u", 523 | 524 | // relaxed SIMD 525 | // https://github.com/WebAssembly/relaxed-simd/blob/main/proposals/relaxed-simd/Overview.md 526 | 256: "i8x16.relaxed_swizzle", 527 | 257: "i32x4.relaxed_trunc_f32x4_s", 528 | 258: "i32x4.relaxed_trunc_f32x4_u", 529 | 259: "i32x4.relaxed_trunc_f64x2_s_zero", 530 | 260: "i32x4.relaxed_trunc_f64x2_u_zero", 531 | 261: "f32x4.relaxed_madd", 532 | 262: "f32x4.relaxed_nmadd", 533 | 263: "f64x2.relaxed_madd", 534 | 264: "f64x2.relaxed_nmadd", 535 | 265: "i8x16.relaxed_laneselect", 536 | 266: "i16x8.relaxed_laneselect", 537 | 267: "i32x4.relaxed_laneselect", 538 | 268: "i64x2.relaxed_laneselect", 539 | 269: "f32x4.relaxed_min", 540 | 270: "f32x4.relaxed_max", 541 | 271: "f64x2.relaxed_min", 542 | 272: "f64x2.relaxed_max", 543 | 273: "i16x8.relaxed_q15mulr_s", 544 | 274: "i16x8.relaxed_dot_i8x16_i7x16_s", 545 | 275: "i32x4.relaxed_dot_i8x16_i7x16_add_s", 546 | }, 547 | 548 | // threads 549 | 0xfe: { 550 | 0: "memory.atomic.notify", 551 | 1: "memory.atomic.wait32", 552 | 2: "memory.atomic.wait64", 553 | 3: "atomic.fence", 554 | 555 | 16: "i32.atomic.load", 556 | 17: "i64.atomic.load", 557 | 18: "i32.atomic.load8_u", 558 | 19: "i32.atomic.load16_u", 559 | 20: "i64.atomic.load8_u", 560 | 21: "i64.atomic.load16_u", 561 | 22: "i64.atomic.load32_u", 562 | 23: "i32.atomic.store", 563 | 24: "i64.atomic.store", 564 | 25: "i32.atomic.store8", 565 | 26: "i32.atomic.store16", 566 | 27: "i64.atomic.store8", 567 | 28: "i64.atomic.store16", 568 | 29: "i64.atomic.store32", 569 | 570 | 30: "i32.atomic.rmw.add", 571 | 31: "i64.atomic.rmw.add", 572 | 32: "i32.atomic.rmw8.add_u", 573 | 33: "i32.atomic.rmw16.add_u", 574 | 34: "i64.atomic.rmw8.add_u", 575 | 35: "i64.atomic.rmw16.add_u", 576 | 36: "i64.atomic.rmw32.add_u", 577 | 578 | 37: "i32.atomic.rmw.sub", 579 | 38: "i64.atomic.rmw.sub", 580 | 39: "i32.atomic.rmw8.sub_u", 581 | 40: "i32.atomic.rmw16.sub_u", 582 | 41: "i64.atomic.rmw8.sub_u", 583 | 42: "i64.atomic.rmw16.sub_u", 584 | 43: "i64.atomic.rmw32.sub_u", 585 | 586 | 44: "i32.atomic.rmw.and", 587 | 45: "i64.atomic.rmw.and", 588 | 46: "i32.atomic.rmw8.and_u", 589 | 47: "i32.atomic.rmw16.and_u", 590 | 48: "i64.atomic.rmw8.and_u", 591 | 49: "i64.atomic.rmw16.and_u", 592 | 50: "i64.atomic.rmw32.and_u", 593 | 594 | 51: "i32.atomic.rmw.or", 595 | 52: "i64.atomic.rmw.or", 596 | 53: "i32.atomic.rmw8.or_u", 597 | 54: "i32.atomic.rmw16.or_u", 598 | 55: "i64.atomic.rmw8.or_u", 599 | 56: "i64.atomic.rmw16.or_u", 600 | 57: "i64.atomic.rmw32.or_u", 601 | 602 | 58: "i32.atomic.rmw.xor", 603 | 59: "i64.atomic.rmw.xor", 604 | 60: "i32.atomic.rmw8.xor_u", 605 | 61: "i32.atomic.rmw16.xor_u", 606 | 62: "i64.atomic.rmw8.xor_u", 607 | 63: "i64.atomic.rmw16.xor_u", 608 | 64: "i64.atomic.rmw32.xor_u", 609 | 610 | 65: "i32.atomic.rmw.xchg", 611 | 66: "i64.atomic.rmw.xchg", 612 | 67: "i32.atomic.rmw8.xchg_u", 613 | 68: "i32.atomic.rmw16.xchg_u", 614 | 69: "i64.atomic.rmw8.xchg_u", 615 | 70: "i64.atomic.rmw16.xchg_u", 616 | 71: "i64.atomic.rmw32.xchg_u", 617 | 618 | 72: "i32.atomic.rmw.cmpxchg", 619 | 73: "i64.atomic.rmw.cmpxchg", 620 | 74: "i32.atomic.rmw8.cmpxchg_u", 621 | 75: "i32.atomic.rmw16.cmpxchg_u", 622 | 76: "i64.atomic.rmw8.cmpxchg_u", 623 | 77: "i64.atomic.rmw16.cmpxchg_u", 624 | 78: "i64.atomic.rmw32.cmpxchg_u", 625 | }, 626 | } as const satisfies Record>; 627 | 628 | // inverted map 629 | const nameToOpcode: Record = {}; 630 | for (let code in opcodes) { 631 | let value = opcodes[Number(code) as keyof Opcodes]; 632 | if (typeof value === "string") nameToOpcode[value] = Number(code); 633 | else { 634 | for (let code_ in value) { 635 | let value_ = value[Number(code_) as keyof typeof value]; 636 | nameToOpcode[value_] = [Number(code), Number(code_)]; 637 | } 638 | } 639 | } 640 | -------------------------------------------------------------------------------- /src/instruction/stack-args.ts: -------------------------------------------------------------------------------- 1 | import { Binable, Undefined } from "../binable.js"; 2 | import { AnyGlobal } from "../dependency.js"; 3 | import { Dependency } from "../index.js"; 4 | import { formatStack, pushStack } from "../local-context.js"; 5 | import { popStack } from "../local-context.js"; 6 | import { emptyContext } from "../local-context.js"; 7 | import { LocalContext, StackVar, Unknown } from "../local-context.js"; 8 | import { 9 | Local, 10 | ValueType, 11 | valueTypeLiterals, 12 | ValueTypeObjects, 13 | } from "../types.js"; 14 | import { Tuple } from "../util.js"; 15 | import { Instruction_, baseInstruction } from "./base.js"; 16 | import { f32Const, f64Const, i32Const, i64Const } from "./const.js"; 17 | import { InstructionName } from "./opcodes.js"; 18 | import { globalGet, localGet } from "./variable-get.js"; 19 | 20 | export { 21 | instruction, 22 | instructionWithArg, 23 | Input, 24 | Inputs, 25 | processStackArgs, 26 | insertInstruction, 27 | }; 28 | 29 | type JSNumberValue = T extends "i32" 30 | ? number 31 | : T extends "i64" 32 | ? bigint 33 | : T extends "f32" 34 | ? number 35 | : T extends "f64" 36 | ? number 37 | : never; 38 | 39 | type Input = 40 | | StackVar 41 | | (T extends ValueType ? Local | AnyGlobal | JSNumberValue : never); 42 | 43 | function isLocal(x: Input): x is Local { 44 | return typeof x === "object" && x !== null && x.kind === "local"; 45 | } 46 | function isGlobal(x: Input): x is AnyGlobal { 47 | return ( 48 | typeof x === "object" && 49 | x !== null && 50 | (x.kind === "global" || x.kind === "importGlobal") 51 | ); 52 | } 53 | function isStackVar(x: Input): x is StackVar { 54 | return typeof x === "object" && x !== null && x.kind === "stack-var"; 55 | } 56 | 57 | type Inputs

= { 58 | [i in keyof P]: Input; 59 | }; 60 | 61 | type InputsAsParameters = (( 62 | ...args: [] | Args 63 | ) => any) extends (...args: infer P) => any 64 | ? ( 65 | ...args: { 66 | [i in keyof P]: Input; 67 | } 68 | ) => any 69 | : never; 70 | 71 | /** 72 | * instruction that is completely fixed 73 | */ 74 | function instruction< 75 | Args extends Tuple, 76 | Results extends Tuple 77 | >( 78 | name: InstructionName, 79 | args: ValueTypeObjects, 80 | results: ValueTypeObjects 81 | ): ((...args: [] | Args) => any) extends (...args: infer P) => any 82 | ? ( 83 | ctx: LocalContext, 84 | ...args: { 85 | [i in keyof P]: Input; 86 | } 87 | ) => Instruction_ 88 | : never { 89 | let instr = { 90 | in: valueTypeLiterals(args), 91 | out: valueTypeLiterals(results), 92 | }; 93 | let createInstr = baseInstruction( 94 | name, 95 | Undefined, 96 | { create: () => instr } 97 | ); 98 | return function createInstr_( 99 | ctx: LocalContext, 100 | ...actualArgs: Input[] 101 | ): Instruction_ { 102 | processStackArgs(ctx, name, instr.in, actualArgs); 103 | return createInstr(ctx); 104 | }; 105 | } 106 | 107 | /** 108 | * instruction of constant type without dependencies, 109 | * but with an immediate argument 110 | */ 111 | function instructionWithArg< 112 | Args extends Tuple, 113 | Results extends Tuple, 114 | Immediate extends any 115 | >( 116 | name: InstructionName, 117 | immediate: Binable, 118 | args: ValueTypeObjects, 119 | results: ValueTypeObjects 120 | ): ((...args: [] | Args) => any) extends (...args: infer P) => any 121 | ? ( 122 | ctx: LocalContext, 123 | immediate: Immediate, 124 | ...args: { 125 | [i in keyof P]: Input; 126 | } 127 | ) => Instruction_ 128 | : never { 129 | let instr = { 130 | in: valueTypeLiterals(args), 131 | out: valueTypeLiterals(results), 132 | }; 133 | let createInstr = baseInstruction< 134 | Immediate, 135 | [immediate: Immediate], 136 | [immediate: Immediate], 137 | Args, 138 | Results 139 | >(name, immediate, { create: () => instr }); 140 | return function createInstr_( 141 | ctx: LocalContext, 142 | immediate: Immediate, 143 | ...actualArgs: Input[] 144 | ): Instruction_ { 145 | processStackArgs(ctx, name, instr.in, actualArgs); 146 | return createInstr(ctx, immediate); 147 | }; 148 | } 149 | 150 | function processStackArgs( 151 | ctx: LocalContext, 152 | string: string, 153 | expectedArgs: ValueType[], 154 | actualArgs: Input[] 155 | ) { 156 | if (actualArgs.length === 0) return; 157 | let n = expectedArgs.length; 158 | if (actualArgs.length !== n) { 159 | throw Error( 160 | `${string}: Expected 0 or ${n} arguments, got ${actualArgs.length}.` 161 | ); 162 | } 163 | 164 | let mustReorder = false; 165 | let hadNewInstr = false; 166 | for (let x of actualArgs) { 167 | if (!isStackVar(x)) hadNewInstr = true; 168 | else if (hadNewInstr) mustReorder = true; 169 | } 170 | 171 | for (let i = 0; i < n; i++) { 172 | // if reordering, process inputs from last to first 173 | let x = mustReorder ? actualArgs[n - 1 - i] : actualArgs[i]; 174 | let type = mustReorder ? expectedArgs[n - 1 - i] : expectedArgs[i]; 175 | if (isLocal(x)) { 176 | if (x.type !== type) 177 | throw Error( 178 | `${string}: Expected type ${type}, got local of type ${x.type}.` 179 | ); 180 | if (mustReorder) insertInstruction(ctx, i, localGet.create(ctx, x)); 181 | else localGet(ctx, x); 182 | } else if (isGlobal(x)) { 183 | if (x.type.value !== type) 184 | throw Error( 185 | `${string}: Expected type ${type}, got global of type ${x.type.value}.` 186 | ); 187 | if (mustReorder) insertInstruction(ctx, i, globalGet.create(ctx, x)); 188 | else globalGet(ctx, x); 189 | } else if (isStackVar(x)) { 190 | if (x.type !== type && x.type !== Unknown) 191 | throw Error( 192 | `${string}: Expected argument of type ${type}, got ${x.type}.` 193 | ); 194 | } else { 195 | // could be const 196 | let unsupported = `${string}: Unsupported input for type ${type}, got ${x}.`; 197 | switch (type) { 198 | case "i32": 199 | if (typeof x !== "number") throw Error(unsupported); 200 | if (mustReorder) insertInstruction(ctx, i, i32Const.create(ctx, x)); 201 | else i32Const(ctx, x); 202 | break; 203 | case "i64": 204 | if (typeof x !== "bigint") throw Error(unsupported); 205 | if (mustReorder) insertInstruction(ctx, i, i64Const.create(ctx, x)); 206 | else i64Const(ctx, x); 207 | break; 208 | case "f32": 209 | if (typeof x !== "number") throw Error(unsupported); 210 | if (mustReorder) insertInstruction(ctx, i, f32Const.create(ctx, x)); 211 | else f32Const(ctx, x); 212 | break; 213 | case "f64": 214 | if (typeof x !== "number") throw Error(unsupported); 215 | if (mustReorder) insertInstruction(ctx, i, f64Const.create(ctx, x)); 216 | else f64Const(ctx, x); 217 | break; 218 | case "v128": 219 | case "funcref": 220 | case "externref": 221 | default: 222 | throw Error(unsupported); 223 | } 224 | } 225 | } 226 | } 227 | 228 | /** 229 | * travel back in time and insert instruction so that its result occupies 230 | * position i in the stack, where i is counted from the top 231 | * (so i=0 means apply the instruction as usual, i=1 means your output should be put below the current top variable, etc) 232 | */ 233 | function insertInstruction( 234 | ctx: LocalContext, 235 | i: number, 236 | instr: Dependency.Instruction 237 | ) { 238 | if (ctx.frames[0].unreachable) 239 | throw Error("Can't insert instruction from unreachable code"); 240 | if (ctx.stack.length < i) 241 | throw Error( 242 | `insertInstruction: trying to insert instruction at position ${i} > stack length ${ctx.stack.length}` 243 | ); 244 | let stack = [...ctx.stack]; 245 | let pseudoCtx: LocalContext = { 246 | ...emptyContext(), 247 | stack, 248 | frames: [{ ...dummyFrame, stack }], 249 | }; 250 | let toReapply: Dependency.Instruction[] = []; 251 | if (stack.length === i) { 252 | for (let instruction of [...ctx.body].reverse()) { 253 | if (stack.length === 0) break; 254 | unapply(pseudoCtx, instruction); 255 | toReapply.unshift(instruction); 256 | } 257 | if (stack.length !== 0) 258 | throw Error( 259 | `Cannot insert constant instruction into stack ${formatStack( 260 | stack 261 | )} at position ${i}` 262 | ); 263 | } else { 264 | let variable = stack[stack.length - i - 1]; 265 | for (let instruction of [...ctx.body].reverse()) { 266 | if (stack[stack.length - 1].id === variable.id) break; 267 | unapply(pseudoCtx, instruction); 268 | toReapply.unshift(instruction); 269 | if (!stack.find((v) => v.id === variable.id)) 270 | throw Error( 271 | `Cannot insert constant instruction into stack ${formatStack( 272 | stack 273 | )} at position ${i}` 274 | ); 275 | } 276 | } 277 | // we successfully unapplied instructions up to a state where `instr` can be inserted 278 | let nInstructions = toReapply.length; 279 | apply(pseudoCtx, instr); 280 | toReapply.forEach((i) => apply(pseudoCtx, i)); 281 | // now `stack` matches what we want, so swap out the current stack with it and insert instruction into body 282 | ctx.stack.splice(0, ctx.stack.length, ...stack); 283 | ctx.body.splice(ctx.body.length - nInstructions, 0, instr); 284 | } 285 | 286 | const dummyFrame = { 287 | label: "0.1" as const, 288 | opcode: "block" as const, 289 | unreachable: false, 290 | endTypes: [], 291 | startTypes: [], 292 | }; 293 | 294 | function apply(ctx: LocalContext, instruction: Dependency.Instruction) { 295 | // console.log( 296 | // `applying ${printFunctionType(instruction.type)} to stack ${formatStack( 297 | // ctx.stack 298 | // )}` 299 | // ); 300 | popStack(ctx, instruction.type.args); 301 | pushStack(ctx, instruction.type.results); 302 | } 303 | function unapply(ctx: LocalContext, instruction: Dependency.Instruction) { 304 | // console.log( 305 | // `unapplying ${printFunctionType(instruction.type)} to stack ${formatStack( 306 | // ctx.stack 307 | // )}` 308 | // ); 309 | popStack(ctx, instruction.type.results); 310 | pushStack(ctx, instruction.type.args); 311 | } 312 | -------------------------------------------------------------------------------- /src/instruction/variable-get.ts: -------------------------------------------------------------------------------- 1 | import * as Dependency from "../dependency.js"; 2 | import { U32 } from "../immediate.js"; 3 | import { baseInstruction } from "./base.js"; 4 | import { Local } from "../types.js"; 5 | 6 | export { localGet, globalGet }; 7 | 8 | const localGet = baseInstruction("local.get", U32, { 9 | create({ locals }, x: Local) { 10 | let local = locals[x.index]; 11 | if (local === undefined) 12 | throw Error(`local with index ${x.index} not available`); 13 | return { in: [], out: [local] }; 14 | }, 15 | resolve: (_, x: Local) => x.index, 16 | }); 17 | 18 | const globalGet = baseInstruction("global.get", U32, { 19 | create(_, global: Dependency.AnyGlobal) { 20 | return { 21 | in: [], 22 | out: [global.type.value], 23 | deps: [global], 24 | }; 25 | }, 26 | resolve: ([globalIdx]) => globalIdx, 27 | }); 28 | -------------------------------------------------------------------------------- /src/instruction/variable.ts: -------------------------------------------------------------------------------- 1 | import { Undefined } from "../binable.js"; 2 | import { Const } from "../dependency.js"; 3 | import * as Dependency from "../dependency.js"; 4 | import { U32 } from "../immediate.js"; 5 | import { baseInstruction } from "./base.js"; 6 | import { 7 | Local, 8 | RefType, 9 | RefTypeObject, 10 | ValueType, 11 | valueTypeLiteral, 12 | } from "../types.js"; 13 | import { LocalContext, StackVar } from "../local-context.js"; 14 | import { globalGet, localGet } from "./variable-get.js"; 15 | import { Input, processStackArgs } from "./stack-args.js"; 16 | 17 | export { 18 | localOps, 19 | bindLocalOps, 20 | globalOps, 21 | bindGlobalOps, 22 | globalConstructor, 23 | refOps, 24 | }; 25 | 26 | const localOps = { 27 | get: localGet, 28 | set: baseInstruction("local.set", U32, { 29 | create({ locals }, x: Local) { 30 | let local = locals[x.index]; 31 | if (local === undefined) 32 | throw Error(`local with index ${x.index} not available`); 33 | return { in: [local], out: [] }; 34 | }, 35 | resolve: (_, x: Local) => x.index, 36 | }), 37 | tee: baseInstruction("local.tee", U32, { 38 | create({ locals }, x: Local) { 39 | let type = locals[x.index]; 40 | if (type === undefined) 41 | throw Error(`local with index ${x.index} not available`); 42 | return { in: [type], out: [type] }; 43 | }, 44 | resolve: (_, x: Local) => x.index, 45 | }), 46 | }; 47 | 48 | function bindLocalOps(ctx: LocalContext) { 49 | return { 50 | get: function (x: Local) { 51 | return localOps.get(ctx, x) as StackVar; 52 | }, 53 | set: function (x: L, value?: Input) { 54 | processStackArgs( 55 | ctx, 56 | "local.set", 57 | [x.type], 58 | value === undefined ? [] : [value] 59 | ); 60 | return localOps.set(ctx, x); 61 | }, 62 | tee: function (x: L, value?: Input) { 63 | processStackArgs( 64 | ctx, 65 | "local.tee", 66 | [x.type], 67 | value === undefined ? [] : [value] 68 | ); 69 | return localOps.tee(ctx, x) as StackVar; 70 | }, 71 | }; 72 | } 73 | 74 | const globalOps = { 75 | get: globalGet, 76 | set: baseInstruction("global.set", U32, { 77 | create(_, global: Dependency.AnyGlobal) { 78 | if (!global.type.mutable) { 79 | throw Error("global.set used on immutable global"); 80 | } 81 | return { 82 | in: [global.type.value], 83 | out: [], 84 | deps: [global], 85 | }; 86 | }, 87 | resolve: ([globalIdx]) => globalIdx, 88 | }), 89 | }; 90 | 91 | function bindGlobalOps(ctx: LocalContext) { 92 | return { 93 | get: function (x: Dependency.AnyGlobal) { 94 | return globalOps.get(ctx, x) as StackVar; 95 | }, 96 | set: function ( 97 | x: G, 98 | value?: Input 99 | ) { 100 | processStackArgs( 101 | ctx, 102 | "global.set", 103 | [x.type.value], 104 | value === undefined ? [] : [value] 105 | ); 106 | return globalOps.set(ctx, x); 107 | }, 108 | }; 109 | } 110 | 111 | function globalConstructor( 112 | init: Const.t, 113 | { mutable = false } = {} 114 | ): Dependency.Global { 115 | let deps = init.deps as Dependency.Global["deps"]; 116 | let type = init.type.results[0]; 117 | return { kind: "global", type: { value: type, mutable }, init, deps }; 118 | } 119 | 120 | const refOps = { 121 | null: baseInstruction("ref.null", RefType, { 122 | create(_, type: RefTypeObject) { 123 | return { 124 | in: [], 125 | out: [valueTypeLiteral(type)], 126 | resolveArgs: [valueTypeLiteral(type)], 127 | }; 128 | }, 129 | }), 130 | is_null: baseInstruction("ref.is_null", Undefined, { 131 | create({ stack }: LocalContext) { 132 | return { in: [stack[stack.length - 1].type], out: ["i32"] }; 133 | }, 134 | resolve: () => undefined, 135 | }), 136 | func: baseInstruction("ref.func", U32, { 137 | create(_, func: Dependency.AnyFunc) { 138 | return { 139 | in: [], 140 | out: ["funcref"], 141 | deps: [func, Dependency.hasRefTo(func)], 142 | }; 143 | }, 144 | resolve: ([funcIdx]) => funcIdx, 145 | }), 146 | }; 147 | -------------------------------------------------------------------------------- /src/local-context.ts: -------------------------------------------------------------------------------- 1 | import * as Dependency from "./dependency.js"; 2 | import { InstructionName } from "./instruction/opcodes.js"; 3 | import { ValueType } from "./types.js"; 4 | 5 | export { 6 | LocalContext, 7 | StackVar, 8 | StackVars, 9 | stackVars, 10 | Unknown, 11 | Label, 12 | RandomLabel, 13 | popStack, 14 | popUnknown, 15 | pushStack, 16 | setUnreachable, 17 | labelTypes, 18 | getFrameFromLabel, 19 | pushInstruction, 20 | emptyContext, 21 | withContext, 22 | isNumberType, 23 | isVectorType, 24 | isSameType, 25 | formatStack, 26 | }; 27 | 28 | type Unknown = "unknown"; 29 | const Unknown = "unknown"; 30 | type RandomLabel = `0.${string}`; 31 | type Label = "top" | RandomLabel; 32 | 33 | type StackVar = { 34 | id: number; 35 | kind: "stack-var"; 36 | type: T; 37 | }; 38 | 39 | type ControlFrame = { 40 | label: Label; // unique id 41 | opcode: InstructionName | "function" | "else"; 42 | startTypes: ValueType[]; 43 | endTypes: ValueType[]; 44 | unreachable: boolean; 45 | stack: StackVar[]; 46 | }; 47 | 48 | type LocalContext = { 49 | locals: ValueType[]; 50 | deps: Dependency.t[]; 51 | body: Dependency.Instruction[]; 52 | stack: StackVar[]; // === frames[0].stack 53 | frames: ControlFrame[]; 54 | return: ValueType[] | null; 55 | }; 56 | 57 | function emptyContext(): LocalContext { 58 | return { 59 | locals: [], 60 | body: [], 61 | deps: [], 62 | return: [], 63 | stack: [], 64 | frames: [], 65 | }; 66 | } 67 | 68 | // this should be replaced with simpler withFunc / withBlock for the two use cases 69 | function withContext( 70 | ctx: LocalContext, 71 | override: Partial, 72 | run: (ctx: LocalContext) => void 73 | ): LocalContext { 74 | let oldCtx = { ...ctx }; 75 | Object.assign(ctx, override); 76 | if (ctx.frames.length === 0) 77 | throw Error("invariant violation: frames must not be empty"); 78 | if (ctx.stack !== ctx.frames[0].stack) 79 | throw Error( 80 | "invariant violation: stack does not equal the stack on the current top frame" 81 | ); 82 | let resultCtx: LocalContext; 83 | try { 84 | run(ctx); 85 | resultCtx = { ...ctx }; 86 | } finally { 87 | Object.assign(ctx, oldCtx); 88 | } 89 | return resultCtx; 90 | } 91 | 92 | function pushInstruction(ctx: LocalContext, instr: Dependency.Instruction) { 93 | let { body, deps } = ctx; 94 | popStack(ctx, instr.type.args); 95 | pushStack(ctx, instr.type.results); 96 | body.push(instr); 97 | for (let dep of instr.deps) { 98 | if (!deps.includes(dep)) { 99 | deps.push(dep); 100 | } 101 | } 102 | } 103 | 104 | function popStack( 105 | { stack, frames }: LocalContext, 106 | values: ValueType[] 107 | ): ValueType[] { 108 | // TODO nicer errors, which display entire stack vs entire instruction signature 109 | let n = values.length; 110 | for (let i = n - 1; i >= 0; i--) { 111 | let stackValue = stack.pop(); 112 | let value = values[i]; 113 | if ( 114 | (stackValue === undefined && !frames[0].unreachable) || 115 | (stackValue !== undefined && value !== stackValue.type) 116 | ) { 117 | throw Error( 118 | `expected ${value} on the stack, got ${stackValue?.type ?? "nothing"}` 119 | ); 120 | } 121 | } 122 | return values; 123 | } 124 | 125 | function popUnknown({ stack, frames }: LocalContext): ValueType | Unknown { 126 | let stackValue = stack.pop(); 127 | if (stackValue === undefined && frames[0].unreachable) { 128 | return Unknown; 129 | } 130 | if (stackValue === undefined) { 131 | throw Error(`expected value on the stack, got nothing`); 132 | } 133 | return stackValue.type; 134 | } 135 | 136 | function pushStack( 137 | { stack }: LocalContext, 138 | values: ValueType[] 139 | ): StackVar[] { 140 | let stackVars = values.map(StackVar); 141 | stack.push(...stackVars); 142 | return stackVars; 143 | } 144 | 145 | function setUnreachable(ctx: LocalContext) { 146 | ctx.stack.splice(0, ctx.stack.length); 147 | ctx.frames[0].unreachable = true; 148 | } 149 | 150 | function labelTypes(frame: ControlFrame) { 151 | return frame.opcode === "loop" ? frame.startTypes : frame.endTypes; 152 | } 153 | 154 | function getFrameFromLabel( 155 | ctx: LocalContext, 156 | label: Label | number 157 | ): [number, ControlFrame] { 158 | if (typeof label === "number") { 159 | let frame = ctx.frames[label]; 160 | if (frame === undefined) throw Error(`no block found for label ${label}`); 161 | return [label, frame]; 162 | } else { 163 | let i = ctx.frames.findIndex((f) => f.label === label); 164 | if (i === -1) throw Error(`no block found for label ${label}`); 165 | return [i, ctx.frames[i]]; 166 | } 167 | } 168 | 169 | function StackVar(type: T): StackVar { 170 | return { kind: "stack-var", id: id(), type }; 171 | } 172 | 173 | type StackVars = { 174 | [k in keyof Results]: StackVar; 175 | }; 176 | 177 | function stackVars(types: ValueType[]) { 178 | return types.map(StackVar); 179 | } 180 | 181 | let i = 0; 182 | function id() { 183 | return i++; 184 | } 185 | 186 | // helpers 187 | 188 | function isNumberType(type: ValueType | Unknown) { 189 | return ( 190 | type === "i32" || 191 | type === "i64" || 192 | type === "f32" || 193 | type === "f64" || 194 | type === Unknown 195 | ); 196 | } 197 | 198 | function isVectorType(type: ValueType | Unknown) { 199 | return type === "v128" || type === Unknown; 200 | } 201 | 202 | function isSameType(t1: ValueType | Unknown, t2: ValueType | Unknown) { 203 | return t1 === t2 || t1 === Unknown || t2 === Unknown; 204 | } 205 | 206 | function formatStack(stack: StackVar[]): string { 207 | return `[${stack.map((v) => v.type).join(",")}]`; 208 | } 209 | -------------------------------------------------------------------------------- /src/memory-binable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Binable, 3 | Byte, 4 | constant, 5 | iso, 6 | or, 7 | record, 8 | tuple, 9 | withValidation, 10 | } from "./binable.js"; 11 | import { U32, vec } from "./immediate.js"; 12 | import { ConstExpression, Expression } from "./instruction/binable.js"; 13 | import { FunctionIndex, GlobalType, RefType, TableIndex } from "./types.js"; 14 | 15 | export { Global, Data, Elem }; 16 | 17 | type Global = { type: GlobalType; init: ConstExpression }; 18 | const Global = record({ type: GlobalType, init: ConstExpression }); 19 | 20 | type Data = { 21 | init: Byte[]; 22 | mode: "passive" | { memory: U32; offset: ConstExpression }; 23 | }; 24 | 25 | const Offset0 = record({ memory: constant(0), offset: ConstExpression }); 26 | const Offset = record({ memory: U32, offset: ConstExpression }); 27 | 28 | type ActiveData = { 29 | init: Byte[]; 30 | mode: { memory: 0; offset: ConstExpression }; 31 | }; 32 | const ActiveData = withU32(0, record({ mode: Offset0, init: vec(Byte) })); 33 | 34 | type PassiveData = { init: Byte[]; mode: "passive" }; 35 | const PassiveData = withU32( 36 | 1, 37 | record({ 38 | mode: constant("passive" as const), 39 | init: vec(Byte), 40 | }) 41 | ); 42 | 43 | type ActiveDataMultiMemory = { 44 | init: Byte[]; 45 | mode: { memory: U32; offset: ConstExpression }; 46 | }; 47 | const ActiveDataMultiMemory = withU32( 48 | 2, 49 | record({ mode: Offset, init: vec(Byte) }) 50 | ); 51 | 52 | const Data: Binable = or( 53 | [ActiveData, PassiveData, ActiveDataMultiMemory], 54 | (t: Data) => 55 | t.mode === "passive" 56 | ? PassiveData 57 | : t.mode.memory === 0 58 | ? ActiveData 59 | : ActiveDataMultiMemory 60 | ); 61 | 62 | type Elem = { 63 | type: RefType; 64 | init: Expression[]; 65 | mode: "passive" | "declarative" | { table: U32; offset: ConstExpression }; 66 | }; 67 | 68 | type FunctionIndices = FunctionIndex[]; 69 | const FunctionIndices = vec(FunctionIndex); 70 | type Expressions = Expression[]; 71 | const Expressions = vec(Expression); 72 | 73 | function fromFuncIdx(funcIdx: FunctionIndices): Expressions { 74 | return funcIdx.map((i) => [{ name: "ref.func", immediate: i }]); 75 | } 76 | function toFuncIdx(expr: Expressions) { 77 | return expr.map((e) => e[0].immediate as FunctionIndex); 78 | } 79 | function isFuncIdx(expr: Expressions) { 80 | return expr.every((e) => e.length === 1 && e[0].name === "ref.func"); 81 | } 82 | 83 | const Elem = Binable({ 84 | toBytes({ type, init, mode }) { 85 | // write code 86 | let isPassive = Number(typeof mode === "string"); 87 | let isExplicit = Number(!(type === "funcref" && isFuncIdx(init))); 88 | let isBit1 = Number( 89 | typeof mode !== "string" 90 | ? mode.table !== 0 && !isExplicit 91 | : mode === "declarative" 92 | ); 93 | let bytes = U32.toBytes( 94 | (isPassive << 0) | (isBit1 << 1) | (isExplicit << 2) 95 | ); 96 | // in active mode, write table and offset 97 | if (typeof mode !== "string") { 98 | let table = isBit1 ? TableIndex.toBytes(mode.table) : []; 99 | let offset = Expression.toBytes(mode.offset); 100 | bytes.push(...table, ...offset); 101 | } 102 | // write type 103 | let typeBytes = 104 | isPassive | isBit1 ? (isExplicit ? RefType.toBytes(type) : [0x00]) : []; 105 | bytes.push(...typeBytes); 106 | // write init 107 | let initBytes = isExplicit 108 | ? Expressions.toBytes(init) 109 | : FunctionIndices.toBytes(toFuncIdx(init)); 110 | bytes.push(...initBytes); 111 | return bytes; 112 | }, 113 | readBytes(bytes, offset) { 114 | let code: number; 115 | [code, offset] = U32.readBytes(bytes, offset); 116 | let [isPassive, isBit1, isExplicit] = [code & 1, code & 2, code & 4]; 117 | // parse mode / table / offset 118 | let mode: Elem["mode"]; 119 | if (isPassive) mode = isBit1 ? "declarative" : "passive"; 120 | else { 121 | let table: TableIndex = 0; 122 | let tableOffset: ConstExpression; 123 | if (isBit1) [table, offset] = TableIndex.readBytes(bytes, offset); 124 | [tableOffset, offset] = ConstExpression.readBytes(bytes, offset); 125 | mode = { table, offset: tableOffset }; 126 | } 127 | // parse type 128 | let type: RefType = "funcref"; 129 | if (isPassive | isBit1) { 130 | if (isExplicit) [type, offset] = RefType.readBytes(bytes, offset); 131 | else if (bytes[offset++] !== 0x00) throw Error("Elem: invalid elemkind"); 132 | } 133 | // parse init 134 | let init: Expressions; 135 | if (isExplicit) [init, offset] = Expressions.readBytes(bytes, offset); 136 | else { 137 | let idx: FunctionIndices; 138 | [idx, offset] = FunctionIndices.readBytes(bytes, offset); 139 | init = fromFuncIdx(idx); 140 | } 141 | return [{ mode, type, init }, offset]; 142 | }, 143 | }); 144 | 145 | function withU32(code: number, binable: Binable): Binable { 146 | return second( 147 | code, 148 | withValidation(tuple([U32, binable]), ([code_]) => { 149 | if (code !== code_) 150 | throw Error(`invalid u32 code, expected ${code}, got ${code_}`); 151 | }) 152 | ); 153 | } 154 | 155 | function second(first: F, tuple: Binable<[F, S]>): Binable { 156 | return iso<[F, S], S>(tuple, { 157 | to: (second) => [first, second], 158 | from: ([, second]) => second, 159 | }); 160 | } 161 | -------------------------------------------------------------------------------- /src/memory.ts: -------------------------------------------------------------------------------- 1 | import { Const } from "./dependency.js"; 2 | import * as Dependency from "./dependency.js"; 3 | import { RefType, RefTypeObject, valueTypeLiteral } from "./types.js"; 4 | 5 | export { 6 | memoryConstructor, 7 | dataConstructor, 8 | tableConstructor, 9 | elemConstructor, 10 | }; 11 | 12 | function memoryConstructor( 13 | { 14 | min, 15 | max, 16 | shared = false, 17 | }: { 18 | min: number; 19 | max?: number; 20 | shared?: boolean; 21 | }, 22 | ...content: (number[] | Uint8Array)[] 23 | ): Dependency.Memory { 24 | let memory: Dependency.Memory = { 25 | kind: "memory", 26 | type: { limits: { min, max, shared } }, 27 | deps: [], 28 | }; 29 | let offset = 0; 30 | for (let init of content) { 31 | dataConstructor({ memory, offset: Const.i32(offset) }, init); 32 | offset += init.length; 33 | } 34 | return memory; 35 | } 36 | 37 | function dataConstructor( 38 | mode: 39 | | { 40 | memory?: Dependency.AnyMemory; 41 | offset: Const.i32 | Const.globalGet<"i32">; 42 | } 43 | | "passive", 44 | [...init]: number[] | Uint8Array 45 | ): Dependency.Data { 46 | if (mode === "passive") { 47 | return { kind: "data", init, mode, deps: [] }; 48 | } 49 | let { memory, offset } = mode; 50 | let deps = [...offset.deps] as Dependency.AnyGlobal[]; 51 | let result: Dependency.Data = { 52 | kind: "data", 53 | init, 54 | mode: { memory: 0, offset }, 55 | deps, 56 | }; 57 | if (memory !== undefined) { 58 | result.deps.push(memory); 59 | memory.deps.push(result); 60 | } else { 61 | result.deps.push(Dependency.hasMemory); 62 | } 63 | return result; 64 | } 65 | 66 | function tableConstructor( 67 | { 68 | type, 69 | min, 70 | max, 71 | }: { 72 | type: RefTypeObject; 73 | min: number; 74 | max?: number; 75 | }, 76 | content?: (Const.refFunc | Const.refNull)[] 77 | ): Dependency.Table { 78 | let table = { 79 | kind: "table" as const, 80 | type: { type: valueTypeLiteral(type), limits: { min, max, shared: false } }, 81 | deps: [], 82 | }; 83 | if (content !== undefined) { 84 | elemConstructor({ type, mode: { table, offset: Const.i32(0) } }, content); 85 | } 86 | return table; 87 | } 88 | 89 | function elemConstructor( 90 | { 91 | type, 92 | mode, 93 | }: { 94 | type: RefTypeObject; 95 | mode: 96 | | "passive" 97 | | "declarative" 98 | | { 99 | table: Dependency.AnyTable; 100 | offset: Const.i32 | Const.globalGet<"i32">; 101 | }; 102 | }, 103 | init: (Const.refFunc | Const.refNull)[] 104 | ): Dependency.Elem { 105 | let deps = init.flatMap((i) => i.deps as Dependency.Elem["deps"]); 106 | let result = { 107 | kind: "elem" as const, 108 | type: valueTypeLiteral(type), 109 | init, 110 | mode, 111 | deps, 112 | }; 113 | if (typeof mode === "object") { 114 | mode.table.deps.push(result); 115 | deps.push(mode.table); 116 | deps.push(...(mode.offset.deps as Dependency.Elem["deps"])); 117 | } 118 | return result; 119 | } 120 | -------------------------------------------------------------------------------- /src/module-binable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Binable, 3 | Byte, 4 | iso, 5 | orDefault, 6 | orUndefined, 7 | record, 8 | tuple, 9 | withByteCode, 10 | withPreamble, 11 | withValidation, 12 | } from "./binable.js"; 13 | import { U32, vec, withByteLength } from "./immediate.js"; 14 | import { 15 | FunctionIndex, 16 | FunctionType, 17 | GlobalType, 18 | MemoryType, 19 | TableType, 20 | ValueTypeObject, 21 | } from "./types.js"; 22 | import { Export, Import } from "./export.js"; 23 | import { Data, Elem, Global } from "./memory-binable.js"; 24 | import { Code, FinalizedFunc } from "./func.js"; 25 | 26 | export { Module }; 27 | 28 | type Module = { 29 | types: FunctionType[]; 30 | funcs: FinalizedFunc[]; 31 | tables: TableType[]; 32 | memory?: MemoryType; 33 | globals: Global[]; 34 | elems: Elem[]; 35 | datas: Data[]; 36 | start?: FunctionIndex; 37 | imports: Import[]; 38 | exports: Export[]; 39 | }; 40 | 41 | function section(code: number, b: Binable) { 42 | return withByteCode(code, withByteLength(b)); 43 | } 44 | // 0: CustomSection 45 | 46 | // 1: TypeSection 47 | type TypeSection = FunctionType[]; 48 | let TypeSection = section(1, vec(FunctionType)); 49 | 50 | // 2: ImportSection 51 | type ImportSection = Import[]; 52 | let ImportSection = section(2, vec(Import)); 53 | 54 | // 3: FuncSection 55 | type FuncSection = U32[]; 56 | let FuncSection = section(3, vec(U32)); 57 | 58 | // 4: TableSection 59 | type TableSection = TableType[]; 60 | let TableSection = section(4, vec(TableType)); 61 | 62 | // 5: MemorySection 63 | type MemorySection = MemoryType[]; 64 | let MemorySection = section(5, vec(MemoryType)); 65 | 66 | // 6: GlobalSection 67 | type GlobalSection = Global[]; 68 | let GlobalSection = section(6, vec(Global)); 69 | 70 | // 7: ExportSection 71 | type ExportSection = Export[]; 72 | let ExportSection = section(7, vec(Export)); 73 | 74 | // 8: StartSection 75 | type StartSection = U32; 76 | let StartSection = section(8, U32); 77 | 78 | // 9: ElementSection 79 | type ElemSection = Elem[]; 80 | let ElemSection = section(9, vec(Elem)); 81 | 82 | // 10: CodeSection 83 | type CodeSection = Code[]; 84 | let CodeSection = section(10, vec(Code)); 85 | 86 | // 11: DataSection 87 | type DataSection = Data[]; 88 | let DataSection = section(11, vec(Data)); 89 | 90 | // 12: DataCountSection 91 | type DataCountSection = U32; 92 | let DataCountSection = section(12, U32); 93 | 94 | const Version = iso(tuple([Byte, Byte, Byte, Byte]), { 95 | to(n: number) { 96 | return [n, 0x00, 0x00, 0x00]; 97 | }, 98 | from([n0, n1, n2, n3]) { 99 | if (n1 || n2 || n3) throw Error("invalid version"); 100 | return n0; 101 | }, 102 | }); 103 | 104 | const isEmpty = (arr: unknown[]) => arr.length === 0; 105 | 106 | type ParsedModule = { 107 | version: number; 108 | typeSection: TypeSection; 109 | importSection: ImportSection; 110 | funcSection: FuncSection; 111 | tableSection: TableSection; 112 | memorySection: MemorySection; 113 | globalSection: GlobalSection; 114 | exportSection: ExportSection; 115 | startSection?: StartSection; 116 | elemSection: ElemSection; 117 | dataCountSection?: DataCountSection; 118 | codeSection: CodeSection; 119 | dataSection: DataSection; 120 | }; 121 | 122 | let ParsedModule = withPreamble( 123 | [0x00, 0x61, 0x73, 0x6d], 124 | record({ 125 | version: Version, 126 | typeSection: orDefault(TypeSection, [], isEmpty), 127 | importSection: orDefault(ImportSection, [], isEmpty), 128 | funcSection: orDefault(FuncSection, [], isEmpty), 129 | tableSection: orDefault(TableSection, [], isEmpty), 130 | memorySection: orDefault(MemorySection, [], isEmpty), 131 | globalSection: orDefault(GlobalSection, [], isEmpty), 132 | exportSection: orDefault(ExportSection, [], isEmpty), 133 | startSection: orUndefined(StartSection), 134 | elemSection: orDefault(ElemSection, [], isEmpty), 135 | dataCountSection: orUndefined(DataCountSection), 136 | codeSection: orDefault(CodeSection, [], isEmpty), 137 | dataSection: orDefault(DataSection, [], isEmpty), 138 | }) 139 | ); 140 | 141 | ParsedModule = withValidation( 142 | ParsedModule, 143 | ({ 144 | version, 145 | funcSection, 146 | codeSection, 147 | memorySection, 148 | dataSection, 149 | dataCountSection, 150 | }) => { 151 | if (version !== 1) throw Error("unsupported version"); 152 | if (funcSection.length !== codeSection.length) { 153 | throw Error("length of function and code sections do not match."); 154 | } 155 | if (memorySection.length > 1) { 156 | throw Error("multiple memories are not allowed"); 157 | } 158 | if (dataSection.length !== (dataCountSection ?? 0)) 159 | throw Error("data section length does not match data count section"); 160 | } 161 | ); 162 | 163 | const Module = iso(ParsedModule, { 164 | to({ 165 | types, 166 | imports, 167 | funcs, 168 | tables, 169 | memory, 170 | globals, 171 | exports, 172 | start, 173 | datas, 174 | elems, 175 | }) { 176 | let funcSection = funcs.map((f) => f.typeIdx); 177 | let memorySection = memory ? [memory] : []; 178 | let codeSection = funcs.map(({ locals, body }) => ({ locals, body })); 179 | let exportSection: Export[] = exports; 180 | return { 181 | version: 1, 182 | typeSection: types, 183 | importSection: imports, 184 | funcSection, 185 | tableSection: tables, 186 | memorySection, 187 | globalSection: globals, 188 | exportSection, 189 | startSection: start, 190 | codeSection, 191 | dataSection: datas, 192 | dataCountSection: datas.length, 193 | elemSection: elems, 194 | }; 195 | }, 196 | from({ 197 | typeSection, 198 | importSection, 199 | funcSection, 200 | tableSection, 201 | memorySection, 202 | globalSection, 203 | exportSection, 204 | startSection, 205 | codeSection, 206 | dataSection, 207 | elemSection, 208 | }): Module { 209 | let importedFunctionsLength = importSection.filter( 210 | (i) => i.description.kind === "function" 211 | ).length; 212 | let funcs = funcSection.map((typeIdx, funcIdx) => { 213 | let type = typeSection[typeIdx]; 214 | let { locals, body } = codeSection[funcIdx]; 215 | return { 216 | funcIdx: importedFunctionsLength + funcIdx, 217 | typeIdx, 218 | type, 219 | locals, 220 | body, 221 | }; 222 | }); 223 | let [memory] = memorySection; 224 | let exports: Export[] = exportSection; 225 | return { 226 | types: typeSection, 227 | imports: importSection, 228 | funcs, 229 | tables: tableSection, 230 | memory, 231 | globals: globalSection, 232 | exports, 233 | start: startSection, 234 | datas: dataSection, 235 | elems: elemSection, 236 | }; 237 | }, 238 | }); 239 | 240 | // validation context according to spec.. may remain unused 241 | type ValidationContext = { 242 | types: FunctionType[]; 243 | funcs: FunctionType[]; 244 | tables: TableType[]; 245 | memories: MemoryType[]; 246 | globals: GlobalType[]; 247 | elems: Elem[]; 248 | datas: Data[]; 249 | locals: ValueTypeObject[]; 250 | labels: ValueTypeObject[][]; 251 | return?: ValueTypeObject[]; 252 | refs: FunctionIndex[]; 253 | }; 254 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import * as Dependency from "./dependency.js"; 2 | import { Export, Import } from "./export.js"; 3 | import { FinalizedFunc, JSFunction } from "./func.js"; 4 | import { resolveInstruction } from "./instruction/base.js"; 5 | import { Module as BinableModule } from "./module-binable.js"; 6 | import { Data, Elem, Global } from "./memory-binable.js"; 7 | import { 8 | FunctionType, 9 | functionTypeEquals, 10 | JSValue, 11 | Limits, 12 | MemoryType, 13 | TableType, 14 | } from "./types.js"; 15 | import { memoryConstructor } from "./memory.js"; 16 | 17 | export { Module, ModuleExport }; 18 | 19 | type Module = ReturnType; 20 | 21 | function ModuleConstructor>({ 22 | exports: inputExports, 23 | memory: inputMemory, 24 | start: inputStart, 25 | }: { 26 | exports: Exports; 27 | memory?: Limits | Dependency.AnyMemory; 28 | start?: Dependency.AnyFunc; 29 | }) { 30 | // collect all dependencies (by kind) 31 | let dependencies = new Set(); 32 | for (let name in inputExports) { 33 | pushDependency(dependencies, inputExports[name]); 34 | } 35 | if (inputMemory !== undefined) { 36 | let memory = 37 | "kind" in inputMemory ? inputMemory : memoryConstructor(inputMemory); 38 | pushDependency(dependencies, memory); 39 | } 40 | if (inputStart !== undefined) { 41 | pushDependency(dependencies, inputStart); 42 | } 43 | let dependencyByKind: { 44 | [K in Dependency.t["kind"]]: (Dependency.t & { kind: K })[]; 45 | } = Object.fromEntries( 46 | Dependency.dependencyKinds.map((key) => [key, []]) 47 | ) as any; 48 | for (let dep of dependencies) { 49 | (dependencyByKind[dep.kind] as Dependency.t[]).push(dep); 50 | } 51 | let depToIndex = new Map(); 52 | 53 | // process imports, along with types of imported functions 54 | let imports: Import[] = []; 55 | let importMap: WebAssembly.Imports = {}; 56 | let types: FunctionType[] = []; 57 | 58 | dependencyByKind.importFunction.forEach((func, funcIdx) => { 59 | let typeIdx = pushType(types, func.type); 60 | let description = { kind: "function" as const, value: typeIdx }; 61 | depToIndex.set(func, funcIdx); 62 | let imp = addImport(func, description, funcIdx, importMap); 63 | imports.push(imp); 64 | }); 65 | dependencyByKind.importGlobal.forEach((global, globalIdx) => { 66 | depToIndex.set(global, globalIdx); 67 | let description = { kind: "global" as const, value: global.type }; 68 | let imp = addImport(global, description, globalIdx, importMap); 69 | imports.push(imp); 70 | }); 71 | dependencyByKind.importTable.forEach((table, tableIdx) => { 72 | depToIndex.set(table, tableIdx); 73 | let description = { kind: "table" as const, value: table.type }; 74 | let imp = addImport(table, description, tableIdx, importMap); 75 | imports.push(imp); 76 | }); 77 | dependencyByKind.importMemory.forEach((memory, memoryIdx) => { 78 | depToIndex.set(memory, memoryIdx); 79 | let description = { kind: "memory" as const, value: memory.type }; 80 | let imp = addImport(memory, description, memoryIdx, importMap); 81 | imports.push(imp); 82 | }); 83 | 84 | // index funcs + their types 85 | let funcs0: (Dependency.Func & { typeIdx: number; funcIdx: number })[] = []; 86 | let nImportFuncs = dependencyByKind.importFunction.length; 87 | for (let func of dependencyByKind.function) { 88 | let typeIdx = pushType(types, func.type); 89 | let funcIdx = nImportFuncs + funcs0.length; 90 | funcs0.push({ ...func, typeIdx, funcIdx }); 91 | depToIndex.set(func, funcIdx); 92 | } 93 | 94 | // index other types 95 | for (let type of dependencyByKind.type) { 96 | let typeIdx = pushType(types, type.type); 97 | depToIndex.set(type, typeIdx); 98 | } 99 | // index globals 100 | let nImportGlobals = dependencyByKind.importGlobal.length; 101 | dependencyByKind.global.forEach((global, globalIdx) => 102 | depToIndex.set(global, globalIdx + nImportGlobals) 103 | ); 104 | // index tables 105 | let nImportTables = dependencyByKind.importTable.length; 106 | dependencyByKind.table.forEach((table, tableIdx) => 107 | depToIndex.set(table, tableIdx + nImportTables) 108 | ); 109 | // index elems 110 | dependencyByKind.elem.forEach((elem, elemIdx) => 111 | depToIndex.set(elem, elemIdx) 112 | ); 113 | // index memories 114 | let nImportMemories = dependencyByKind.importMemory.length; 115 | dependencyByKind.memory.forEach((memory, memoryIdx) => 116 | depToIndex.set(memory, memoryIdx + nImportMemories) 117 | ); 118 | // index datas 119 | dependencyByKind.data.forEach((data, dataIdx) => 120 | depToIndex.set(data, dataIdx) 121 | ); 122 | 123 | // finalize functions 124 | let funcs: FinalizedFunc[] = funcs0.map(({ typeIdx, funcIdx, ...func }) => { 125 | let body = func.body.map((instr) => resolveInstruction(instr, depToIndex)); 126 | return { 127 | funcIdx: funcIdx, 128 | typeIdx: typeIdx, 129 | type: func.type, 130 | locals: func.locals, 131 | body, 132 | }; 133 | }); 134 | // finalize globals 135 | let globals: Global[] = dependencyByKind.global.map(({ type, init }) => { 136 | let init_ = [resolveInstruction(init, depToIndex)]; 137 | return { type, init: init_ }; 138 | }); 139 | // finalize tables 140 | let tables: TableType[] = dependencyByKind.table.map(({ type }) => type); 141 | // finalize elems 142 | let elems: Elem[] = dependencyByKind.elem.map(({ type, init, mode }) => { 143 | let init_ = init.map((i) => [resolveInstruction(i, depToIndex)]); 144 | let mode_: Elem["mode"] = 145 | typeof mode === "object" 146 | ? { 147 | table: depToIndex.get(mode.table)!, 148 | offset: [resolveInstruction(mode.offset, depToIndex)], 149 | } 150 | : mode; 151 | return { type, init: init_, mode: mode_ }; 152 | }); 153 | // finalize memory 154 | let memory = checkMemory(dependencyByKind); 155 | // finalize datas 156 | let datas: Data[] = dependencyByKind.data.map(({ init, mode }) => { 157 | let mode_: Data["mode"] = 158 | mode !== "passive" 159 | ? { 160 | memory: mode.memory, 161 | offset: [resolveInstruction(mode.offset, depToIndex)], 162 | } 163 | : mode; 164 | return { init, mode: mode_ }; 165 | }); 166 | 167 | // start 168 | let start = inputStart === undefined ? undefined : depToIndex.get(inputStart); 169 | 170 | // exports 171 | let exports: Export[] = []; 172 | for (let name in inputExports) { 173 | let exp = inputExports[name]; 174 | let kind = Dependency.kindToExportKind[exp.kind]; 175 | let value = depToIndex.get(exp)!; 176 | exports.push({ name, description: { kind, value } }); 177 | } 178 | let binableModule: BinableModule = { 179 | types, 180 | funcs, 181 | imports, 182 | exports, 183 | datas, 184 | elems, 185 | tables, 186 | globals, 187 | memory, 188 | start, 189 | }; 190 | return createModule(binableModule, importMap); 191 | } 192 | 193 | function createModule>( 194 | binableModule: BinableModule, 195 | importMap: WebAssembly.Imports 196 | ) { 197 | let module = { 198 | module: binableModule, 199 | importMap, 200 | async instantiate() { 201 | let wasmByteCode = BinableModule.toBytes(binableModule); 202 | return (await WebAssembly.instantiate( 203 | Uint8Array.from(wasmByteCode), 204 | importMap 205 | )) as { 206 | instance: WebAssembly.Instance & { 207 | exports: { 208 | [K in keyof Exports]: ModuleExport; 209 | }; 210 | }; 211 | module: WebAssembly.Module; 212 | }; 213 | }, 214 | toBytes() { 215 | let bytes = BinableModule.toBytes(module.module); 216 | return Uint8Array.from(bytes); 217 | }, 218 | }; 219 | return module; 220 | } 221 | 222 | type ModuleExport = 223 | Export extends Dependency.AnyFunc 224 | ? JSFunction 225 | : Export extends Dependency.AnyGlobal 226 | ? { 227 | value: JSValue; 228 | valueOf(): JSValue; 229 | } 230 | : Export extends Dependency.AnyMemory 231 | ? WebAssembly.Memory 232 | : Export extends Dependency.AnyTable 233 | ? WebAssembly.Table 234 | : unknown; 235 | 236 | const Module = Object.assign(ModuleConstructor, { 237 | fromBytes>( 238 | bytes: Uint8Array, 239 | importMap: WebAssembly.Imports = {} 240 | ) { 241 | let binableModule = BinableModule.fromBytes(bytes); 242 | return createModule(binableModule, importMap); 243 | }, 244 | }); 245 | 246 | function pushDependency( 247 | existing: Set, 248 | dep: Dependency.anyDependency 249 | ) { 250 | if (existing.has(dep)) return; 251 | existing.add(dep); 252 | for (let dep_ of dep.deps) { 253 | pushDependency(existing, dep_); 254 | } 255 | } 256 | 257 | function pushType(types: FunctionType[], type: FunctionType) { 258 | let typeIndex = types.findIndex((t) => functionTypeEquals(t, type)); 259 | if (typeIndex === -1) { 260 | typeIndex = types.length; 261 | types.push(type); 262 | } 263 | return typeIndex; 264 | } 265 | 266 | function addImport( 267 | { kind, module = "", string, value }: Dependency.AnyImport, 268 | description: Import["description"], 269 | i: number, 270 | importMap: WebAssembly.Imports 271 | ): Import { 272 | let prefix = { 273 | importFunction: "f", 274 | importGlobal: "g", 275 | importMemory: "m", 276 | importTable: "t", 277 | }[kind]; 278 | string ??= `${prefix}${i}`; 279 | let import_ = { module, name: string, description }; 280 | let importModule = (importMap[module] ??= {}); 281 | if (string in importModule && importModule[string] !== value) { 282 | throw Error( 283 | `Overwriting import "${module}" > "${string}" with different value. Use the same value twice instead.` 284 | ); 285 | } 286 | importModule[string] = value; 287 | return import_; 288 | } 289 | 290 | function checkMemory(dependencyByKind: { 291 | importMemory: Dependency.ImportMemory[]; 292 | memory: Dependency.Memory[]; 293 | hasMemory: Dependency.HasMemory[]; 294 | }): MemoryType | undefined { 295 | let nMemoriesTotal = 296 | dependencyByKind.importMemory.length + dependencyByKind.memory.length; 297 | if (nMemoriesTotal === 0) { 298 | if (dependencyByKind.hasMemory.length > 0) { 299 | throw Error(`Module(): The module depends on the existence of a memory, but no memory was found. You can add a memory like this: 300 | 301 | let module = Module({ 302 | //... 303 | memory: Memory({ min: 1 }) 304 | }); 305 | `); 306 | } 307 | } 308 | return dependencyByKind.memory[0]?.type; 309 | } 310 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Binable, Bool, Byte, record, withByteCode } from "./binable.js"; 2 | import { U32, vec } from "./immediate.js"; 3 | import { Tuple } from "./util.js"; 4 | 5 | export { i32t, i64t, f32t, f64t, v128t, funcref, externref }; 6 | export { 7 | TypeIndex, 8 | FunctionIndex, 9 | MemoryIndex, 10 | TableIndex, 11 | ElemIndex, 12 | DataIndex, 13 | }; 14 | export { 15 | ValueTypeObject, 16 | RefTypeObject, 17 | FunctionType, 18 | MemoryType, 19 | GlobalType, 20 | TableType, 21 | ValueType, 22 | RefType, 23 | Type, 24 | Local, 25 | ResultType, 26 | invertRecord, 27 | valueType, 28 | ValueTypeObjects, 29 | valueTypeLiteral, 30 | valueTypeLiterals, 31 | ValueTypeLiterals, 32 | functionTypeEquals, 33 | printFunctionType, 34 | JSValue, 35 | Limits, 36 | valueTypeSet, 37 | }; 38 | 39 | type RefType = "funcref" | "externref"; 40 | type ValueType = "i32" | "i64" | "f32" | "f64" | "v128" | RefType; 41 | 42 | type Type = { kind: L }; 43 | type Local = { kind: "local"; type: L; index: number }; 44 | 45 | function valueTypeLiteral({ kind }: { kind: L }): L { 46 | return kind; 47 | } 48 | type ValueTypeObjects> = { 49 | [i in keyof T]: Type; 50 | }; 51 | function valueType(kind: L): Type { 52 | return { kind }; 53 | } 54 | type ValueTypeLiterals> = { 55 | [i in keyof T]: T[i] extends { kind: infer L } ? L : never; 56 | }; 57 | function valueTypeLiterals(types: { 58 | [i in keyof L]: Type; 59 | }): L & ValueType[] { 60 | return types.map((t) => t.kind) as L; 61 | } 62 | 63 | const valueTypeCodes: Record = { 64 | i32: 0x7f, 65 | i64: 0x7e, 66 | f32: 0x7d, 67 | f64: 0x7c, 68 | v128: 0x7b, 69 | funcref: 0x70, 70 | externref: 0x6f, 71 | }; 72 | const i32t = valueType("i32"); 73 | const i64t = valueType("i64"); 74 | const f32t = valueType("f32"); 75 | const f64t = valueType("f64"); 76 | const v128t = valueType("v128"); 77 | const funcref = valueType("funcref"); 78 | const externref = valueType("externref"); 79 | 80 | const codeToValueType = invertRecord(valueTypeCodes); 81 | 82 | const valueTypeSet = new Set(Object.keys(valueTypeCodes) as ValueType[]); 83 | 84 | type ValueTypeObject = { kind: ValueType }; 85 | const ValueType = Binable({ 86 | toBytes(type) { 87 | let code = valueTypeCodes[type]; 88 | if (code === undefined) throw Error(`Invalid value type ${type}`); 89 | return [code]; 90 | }, 91 | readBytes(bytes, offset) { 92 | let code = bytes[offset++]; 93 | let type = codeToValueType.get(code); 94 | if (type === undefined) 95 | throw Error(`Invalid value type code ${code.toString(16)}.`); 96 | return [type, offset]; 97 | }, 98 | }); 99 | 100 | type RefTypeObject = { kind: RefType }; 101 | const RefType = Binable({ 102 | toBytes(t) { 103 | return ValueType.toBytes(t); 104 | }, 105 | readBytes(bytes, offset) { 106 | let [type, end] = ValueType.readBytes(bytes, offset); 107 | if (type !== "funcref" && type !== "externref") 108 | throw Error("invalid reftype"); 109 | return [type, end]; 110 | }, 111 | }); 112 | 113 | type GlobalType = { value: T; mutable: boolean }; 114 | const GlobalType = record({ value: ValueType, mutable: Bool }); 115 | 116 | type Limits = { min: number; max?: number; shared: boolean }; 117 | const Limits = Binable({ 118 | toBytes({ min, max, shared }) { 119 | if (max === undefined) return [0x00, ...U32.toBytes(min)]; 120 | let startByte = shared ? 0x03 : 0x01; 121 | return [startByte, ...U32.toBytes(min), ...U32.toBytes(max)]; 122 | }, 123 | readBytes(bytes, offset) { 124 | let type: number, min: number, max: number | undefined; 125 | [type, offset] = Byte.readBytes(bytes, offset); 126 | [min, offset] = U32.readBytes(bytes, offset); 127 | let shared = type === 0x03; 128 | if (type === 0x01 || type === 0x03) { 129 | [max, offset] = U32.readBytes(bytes, offset); 130 | } else if (type !== 0x00) { 131 | throw Error("invalid limit type"); 132 | } 133 | return [{ min, max, shared }, offset]; 134 | }, 135 | }); 136 | 137 | type MemoryType = { limits: Limits }; 138 | const MemoryType = record({ limits: Limits }); 139 | 140 | type TableType = { type: RefType; limits: Limits }; 141 | const TableType = record({ type: RefType, limits: Limits }); 142 | 143 | const ResultType = vec(ValueType); 144 | 145 | type FunctionType = { args: ValueType[]; results: ValueType[] }; 146 | const FunctionType = withByteCode( 147 | 0x60, 148 | record({ args: ResultType, results: ResultType }) 149 | ); 150 | 151 | type TypeIndex = U32; 152 | const TypeIndex = U32; 153 | type FunctionIndex = U32; 154 | const FunctionIndex = U32; 155 | type TableIndex = U32; 156 | const TableIndex = U32; 157 | type MemoryIndex = U32; 158 | const MemoryIndex = U32; 159 | type ElemIndex = U32; 160 | const ElemIndex = U32; 161 | type DataIndex = U32; 162 | const DataIndex = U32; 163 | 164 | function invertRecord(record: Record): Map { 165 | let map = new Map(); 166 | for (let key in record) { 167 | map.set(record[key], key); 168 | } 169 | return map; 170 | } 171 | 172 | function functionTypeEquals( 173 | { args: fArgs, results: fResults }: FunctionType, 174 | { args: gArgs, results: gResults }: FunctionType 175 | ) { 176 | let nArgs = fArgs.length; 177 | let nResults = fResults.length; 178 | if (gArgs.length !== nArgs || gResults.length !== nResults) return false; 179 | for (let i = 0; i < nArgs; i++) { 180 | if (fArgs[i] !== gArgs[i]) return false; 181 | } 182 | for (let i = 0; i < nResults; i++) { 183 | if (fResults[i] !== gResults[i]) return false; 184 | } 185 | return true; 186 | } 187 | 188 | function printFunctionType({ args, results }: FunctionType) { 189 | return `[${args}] -> [${results}]`; 190 | } 191 | 192 | // infer JS values 193 | 194 | type JSValue = T extends "i32" 195 | ? number 196 | : T extends "f32" 197 | ? number 198 | : T extends "f64" 199 | ? number 200 | : T extends "i64" 201 | ? bigint 202 | : T extends "v128" 203 | ? never 204 | : T extends "funcref" 205 | ? Function | null 206 | : T extends "externref" 207 | ? unknown 208 | : never; 209 | -------------------------------------------------------------------------------- /src/util-node.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | 3 | export { writeFile }; 4 | 5 | async function writeFile(fileName: string, content: string | Uint8Array) { 6 | if (typeof content === "string") { 7 | await fs.writeFile(fileName, content, "utf8"); 8 | } else { 9 | await fs.writeFile(fileName, content); 10 | } 11 | console.log(`wrote ${(content.length / 1e3).toFixed(1)}kB to ${fileName}`); 12 | } 13 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export { Tuple, TupleN }; 2 | 3 | type Tuple = [] | [T, ...T[]]; 4 | 5 | type TupleN = N extends N 6 | ? number extends N 7 | ? T[] 8 | : _TupleOf 9 | : never; 10 | type _TupleOf = R["length"] extends N 11 | ? R 12 | : _TupleOf; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*.ts"], 3 | "compilerOptions": { 4 | /* Visit https://aka.ms/tsconfig to read more about this file */ 5 | 6 | /* Projects */ 7 | "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, 8 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 9 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 10 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 11 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 12 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 13 | 14 | /* Language and Environment */ 15 | "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 16 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | 28 | /* Modules */ 29 | "module": "nodenext" /* Specify what module code is generated. */, 30 | // "rootDir": "./", /* Specify the root folder within your source files. */ 31 | "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */, 32 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 33 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 36 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files. */ 40 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 41 | 42 | /* JavaScript Support */ 43 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 44 | // "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 46 | 47 | /* Emit */ 48 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 49 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 50 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 51 | "sourceMap": true /* Create source map files for emitted JavaScript files. */, 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 53 | "outDir": "./build" /* Specify an output folder for all emitted files. */, 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 63 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 64 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 65 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 66 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 67 | "noEmitOnError": true /* Disable emitting files if any type checking errors are reported. */, 68 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 69 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 70 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 78 | 79 | /* Type Checking */ 80 | "strict": true /* Enable all strict type-checking options. */, 81 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 82 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 87 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 103 | } 104 | } 105 | --------------------------------------------------------------------------------