├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── chapter01.js ├── chapter01 ├── 01-voidlang.js └── 02-noplang.js ├── chapter02.js ├── chapter02 ├── 01-noplang.js ├── 02-numbers.js ├── 03-examples.js ├── 04-jsValue.js └── 05-toWasm.js ├── chapter03.js ├── chapter03 ├── 01-exprStub.js ├── 02-waferAdd.js ├── 03-waferAddSub.js ├── 04-toWasm.js └── 05-toWasmMulDiv.js ├── chapter04.js ├── chapter04 ├── 01-identifiers.js ├── 02-primaryexpr.js ├── 03-compileLocals.js ├── 04-buildSymbolTable.js ├── 05-toWasm.js ├── 06-toWasm-readVars.js ├── 07-toWasm-writeVars.js └── 08-compileAndEval.js ├── chapter05.js ├── chapter05 ├── 01-buildModule-two.js ├── 02-buildModule-many.js ├── 03-buildModule-call.js ├── 04-functionDecl.js ├── 05-toWasm.js ├── 06-buildSymbolTable.js ├── 07-test-e2e.js └── 08-functionCalls.js ├── chapter06.js ├── chapter06 ├── 01-ifExpr.js ├── 02-test-ifExpr.js ├── 03-test-comparison.js ├── 04-test-while.js └── 05-test-all.js ├── chapter07.js ├── chapter07 ├── 01-externFunctionDecl.js ├── 02-buildModule-imports.js └── 03-test-e2e.js ├── chapter08.js ├── chapter08 ├── draw.wafer ├── index.html └── waferc.js ├── chapter09.js ├── chapter09 ├── 01-buildModule-memory.js ├── 02-test-load-and-store.js └── 03-test-e2e.js ├── chapter10.js ├── chapter10 ├── 01-i32-arrays.js ├── 02-bounds-checking.js ├── 03-buildModule-data.js └── 04-strings.js ├── chapter11.js ├── chapter11 └── 01-hello-world.js ├── chapterd1.js ├── chapterd3.js ├── chapterd4.js ├── chapterx.js ├── package-lock.json └── package.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['eslint:recommended', 'google', 'prettier'], 7 | overrides: [ 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | sourceType: 'module', 12 | }, 13 | rules: { 14 | 'camelcase': 0, 15 | 'require-jsdoc': 0, 16 | 'spaced-comment': 0 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib*.js 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "experimentalTernaries": true, 3 | "quoteProps": "consistent", 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "bracketSpacing": false 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Patrick Dubroy & Mariano Guerra 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧱 WebAssembly from the Ground Up 2 | 3 | _(From the book [WebAssembly from the Ground Up](https://wasmgroundup.com) — learn Wasm by building a simple compiler in JavaScript.)_ 4 | 5 | Welcome! This repo contains the code from every checkpoint in the book. 6 | 7 | Here are some useful commands: 8 | 9 | - `npm test` to run all tests for all checkpoints. 10 | - `node ` to run the tests in a specific checkpoint/file. 11 | E.g. `node chapter02/01-noplang.js` 12 | `node chapter01.js` 13 | -------------------------------------------------------------------------------- /chapter01.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import {basename} from 'node:path'; 3 | import process from 'node:process'; 4 | import {default as nodeTest} from 'node:test'; 5 | import {fileURLToPath} from 'node:url'; 6 | 7 | function makeTestFn(url) { 8 | const filename = fileURLToPath(url); 9 | // Return a function with the same interface as Node's `test` function. 10 | return (name, ...args) => { 11 | // Only register the test if the current module is on the command line. 12 | // All other tests are ignored. 13 | if (process.argv[1] === filename) { 14 | // Add the chapter name to the test description. 15 | const chapterName = basename(filename, '.js'); 16 | nodeTest(`[${chapterName}] ${name}`, ...args); 17 | } 18 | }; 19 | } 20 | 21 | const test = makeTestFn(import.meta.url); 22 | 23 | test('setup', () => { 24 | assert(true); 25 | }); 26 | 27 | function compileVoidLang(code) { 28 | if (code !== '') { 29 | throw new Error(`Expected empty code, got: "${code}"`); 30 | } 31 | const bytes = [magic(), version()].flat(Infinity); 32 | return Uint8Array.from(bytes); 33 | } 34 | 35 | test('compileVoidLang result compiles to a WebAssembly object', async () => { 36 | const {instance, module} = await WebAssembly.instantiate(compileVoidLang('')); 37 | 38 | assert.strictEqual(instance instanceof WebAssembly.Instance, true); 39 | assert.strictEqual(module instanceof WebAssembly.Module, true); 40 | }); 41 | 42 | function stringToBytes(s) { 43 | const bytes = new TextEncoder().encode(s); 44 | return Array.from(bytes); 45 | } 46 | 47 | function magic() { 48 | // [0x00, 0x61, 0x73, 0x6d] 49 | return stringToBytes('\0asm'); 50 | } 51 | 52 | function version() { 53 | return [0x01, 0x00, 0x00, 0x00]; 54 | } 55 | 56 | // for simplicity we include the complete implementation of u32 and i32 here 57 | // this allows the next chapters to use all the functionality from this chapter 58 | // without having to redefine or patch the complete definitions 59 | 60 | const CONTINUATION_BIT = 0b10000000; 61 | const SEVEN_BIT_MASK_BIG_INT = 0b01111111n; 62 | 63 | function leb128(v) { 64 | let val = BigInt(v); 65 | let more = true; 66 | const r = []; 67 | 68 | while (more) { 69 | const b = Number(val & SEVEN_BIT_MASK_BIG_INT); 70 | val = val >> 7n; 71 | more = val !== 0n; 72 | if (more) { 73 | r.push(b | CONTINUATION_BIT); 74 | } else { 75 | r.push(b); 76 | } 77 | } 78 | 79 | return r; 80 | } 81 | 82 | const MIN_U32 = 0; 83 | const MAX_U32 = 2 ** 32 - 1; 84 | 85 | function u32(v) { 86 | if (v < MIN_U32 || v > MAX_U32) { 87 | throw Error(`Value out of range for u32: ${v}`); 88 | } 89 | 90 | return leb128(v); 91 | } 92 | 93 | function sleb128(v) { 94 | let val = BigInt(v); 95 | let more = true; 96 | const r = []; 97 | 98 | while (more) { 99 | const b = Number(val & SEVEN_BIT_MASK_BIG_INT); 100 | const signBitSet = !!(b & 0x40); 101 | 102 | val = val >> 7n; 103 | 104 | if ((val === 0n && !signBitSet) || (val === -1n && signBitSet)) { 105 | more = false; 106 | r.push(b); 107 | } else { 108 | r.push(b | CONTINUATION_BIT); 109 | } 110 | } 111 | 112 | return r; 113 | } 114 | 115 | const MIN_I32 = -(2 ** 32 / 2); 116 | const MAX_I32 = 2 ** 32 / 2 - 1; 117 | const I32_NEG_OFFSET = 2 ** 32; 118 | 119 | function i32(v) { 120 | if (v < MIN_I32 || v > MAX_U32) { 121 | throw Error(`Value out of range for i32: ${v}`); 122 | } 123 | 124 | if (v > MAX_I32) { 125 | return sleb128(v - I32_NEG_OFFSET); 126 | } 127 | 128 | return sleb128(v); 129 | } 130 | 131 | function section(id, contents) { 132 | const sizeInBytes = contents.flat(Infinity).length; 133 | return [id, u32(sizeInBytes), contents]; 134 | } 135 | 136 | function vec(elements) { 137 | return [u32(elements.length), elements]; 138 | } 139 | 140 | const SECTION_ID_TYPE = 1; 141 | 142 | function functype(paramTypes, resultTypes) { 143 | return [0x60, vec(paramTypes), vec(resultTypes)]; 144 | } 145 | 146 | function typesec(functypes) { 147 | return section(SECTION_ID_TYPE, vec(functypes)); 148 | } 149 | 150 | const SECTION_ID_FUNCTION = 3; 151 | 152 | const typeidx = (x) => u32(x); 153 | 154 | function funcsec(typeidxs) { 155 | return section(SECTION_ID_FUNCTION, vec(typeidxs)); 156 | } 157 | 158 | const SECTION_ID_CODE = 10; 159 | 160 | function code(func) { 161 | const sizeInBytes = func.flat(Infinity).length; 162 | return [u32(sizeInBytes), func]; 163 | } 164 | 165 | function func(locals, body) { 166 | return [vec(locals), body]; 167 | } 168 | 169 | function codesec(codes) { 170 | return section(SECTION_ID_CODE, vec(codes)); 171 | } 172 | 173 | const instr = { 174 | end: 0x0b, 175 | }; 176 | 177 | function compileNopLang(source) { 178 | if (source !== '') { 179 | throw new Error(`Expected empty code, got: "${source}"`); 180 | } 181 | 182 | const mod = module([ 183 | typesec([functype([], [])]), 184 | funcsec([typeidx(0)]), 185 | exportsec([export_('main', exportdesc.func(0))]), 186 | codesec([code(func([], [instr.end]))]), 187 | ]); 188 | return Uint8Array.from(mod.flat(Infinity)); 189 | } 190 | 191 | test('compileNopLang compiles to a wasm module', async () => { 192 | const {instance, module} = await WebAssembly.instantiate(compileNopLang('')); 193 | 194 | assert.strictEqual(instance instanceof WebAssembly.Instance, true); 195 | assert.strictEqual(module instanceof WebAssembly.Module, true); 196 | }); 197 | 198 | const SECTION_ID_EXPORT = 7; 199 | 200 | function name(s) { 201 | return vec(stringToBytes(s)); 202 | } 203 | 204 | function export_(nm, exportdesc) { 205 | return [name(nm), exportdesc]; 206 | } 207 | 208 | function exportsec(exports) { 209 | return section(SECTION_ID_EXPORT, vec(exports)); 210 | } 211 | 212 | const funcidx = (x) => u32(x); 213 | 214 | const exportdesc = { 215 | func(idx) { 216 | return [0x00, funcidx(idx)]; 217 | }, 218 | }; 219 | 220 | function module(sections) { 221 | return [magic(), version(), sections]; 222 | } 223 | 224 | test('compileNopLang result compiles to a wasm module', async () => { 225 | const {instance} = await WebAssembly.instantiate(compileNopLang('')); 226 | 227 | assert.strictEqual(instance.exports.main(), undefined); 228 | assert.throws(() => compileNopLang('42')); 229 | }); 230 | 231 | export { 232 | code, 233 | codesec, 234 | export_, 235 | exportdesc, 236 | exportsec, 237 | func, 238 | funcidx, 239 | funcsec, 240 | functype, 241 | i32, 242 | instr, 243 | magic, 244 | makeTestFn, 245 | module, 246 | name, 247 | section, 248 | SECTION_ID_CODE, 249 | SECTION_ID_EXPORT, 250 | SECTION_ID_FUNCTION, 251 | SECTION_ID_TYPE, 252 | stringToBytes, 253 | typeidx, 254 | typesec, 255 | u32, 256 | vec, 257 | version, 258 | }; 259 | -------------------------------------------------------------------------------- /chapter01/01-voidlang.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import {basename} from 'node:path'; 3 | import process from 'node:process'; 4 | import {default as nodeTest} from 'node:test'; 5 | import {fileURLToPath} from 'node:url'; 6 | 7 | function makeTestFn(url) { 8 | const filename = fileURLToPath(url); 9 | // Return a function with the same interface as Node's `test` function. 10 | return (name, ...args) => { 11 | // Only register the test if the current module is on the command line. 12 | // All other tests are ignored. 13 | if (process.argv[1] === filename) { 14 | // Add the chapter name to the test description. 15 | const chapterName = basename(filename, '.js'); 16 | nodeTest(`[${chapterName}] ${name}`, ...args); 17 | } 18 | }; 19 | } 20 | 21 | const test = makeTestFn(import.meta.url); 22 | 23 | test('setup', () => { 24 | assert(true); 25 | }); 26 | 27 | function compileVoidLang(code) { 28 | if (code !== '') { 29 | throw new Error(`Expected empty code, got: "${code}"`); 30 | } 31 | return Uint8Array.from([0, 97, 115, 109, 1, 0, 0, 0]); 32 | } 33 | 34 | test('compileVoidLang result compiles to a WebAssembly object', async () => { 35 | const {instance, module} = await WebAssembly.instantiate(compileVoidLang('')); 36 | 37 | assert.strictEqual(instance instanceof WebAssembly.Instance, true); 38 | assert.strictEqual(module instanceof WebAssembly.Module, true); 39 | }); 40 | -------------------------------------------------------------------------------- /chapter01/02-noplang.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import {basename} from 'node:path'; 3 | import process from 'node:process'; 4 | import {default as nodeTest} from 'node:test'; 5 | import {fileURLToPath} from 'node:url'; 6 | 7 | function makeTestFn(url) { 8 | const filename = fileURLToPath(url); 9 | // Return a function with the same interface as Node's `test` function. 10 | return (name, ...args) => { 11 | // Only register the test if the current module is on the command line. 12 | // All other tests are ignored. 13 | if (process.argv[1] === filename) { 14 | // Add the chapter name to the test description. 15 | const chapterName = basename(filename, '.js'); 16 | nodeTest(`[${chapterName}] ${name}`, ...args); 17 | } 18 | }; 19 | } 20 | 21 | const test = makeTestFn(import.meta.url); 22 | 23 | test('setup', () => { 24 | assert(true); 25 | }); 26 | 27 | function compileVoidLang(code) { 28 | if (code !== '') { 29 | throw new Error(`Expected empty code, got: "${code}"`); 30 | } 31 | const bytes = [magic(), version()].flat(Infinity); 32 | return Uint8Array.from(bytes); 33 | } 34 | 35 | test('compileVoidLang result compiles to a WebAssembly object', async () => { 36 | const {instance, module} = await WebAssembly.instantiate(compileVoidLang('')); 37 | 38 | assert.strictEqual(instance instanceof WebAssembly.Instance, true); 39 | assert.strictEqual(module instanceof WebAssembly.Module, true); 40 | }); 41 | 42 | function stringToBytes(s) { 43 | const bytes = new TextEncoder().encode(s); 44 | return Array.from(bytes); 45 | } 46 | 47 | function magic() { 48 | // [0x00, 0x61, 0x73, 0x6d] 49 | return stringToBytes('\0asm'); 50 | } 51 | 52 | function version() { 53 | return [0x01, 0x00, 0x00, 0x00]; 54 | } 55 | 56 | // for simplicity we include the complete implementation of u32 and i32 here 57 | // this allows the next chapters to use all the functionality from this chapter 58 | // without having to redefine or patch the complete definitions 59 | 60 | const CONTINUATION_BIT = 0b10000000; 61 | const SEVEN_BIT_MASK_BIG_INT = 0b01111111n; 62 | 63 | function leb128(v) { 64 | let val = BigInt(v); 65 | let more = true; 66 | const r = []; 67 | 68 | while (more) { 69 | const b = Number(val & SEVEN_BIT_MASK_BIG_INT); 70 | val = val >> 7n; 71 | more = val !== 0n; 72 | if (more) { 73 | r.push(b | CONTINUATION_BIT); 74 | } else { 75 | r.push(b); 76 | } 77 | } 78 | 79 | return r; 80 | } 81 | 82 | const MIN_U32 = 0; 83 | const MAX_U32 = 2 ** 32 - 1; 84 | 85 | function u32(v) { 86 | if (v < MIN_U32 || v > MAX_U32) { 87 | throw Error(`Value out of range for u32: ${v}`); 88 | } 89 | 90 | return leb128(v); 91 | } 92 | 93 | function sleb128(v) { 94 | let val = BigInt(v); 95 | let more = true; 96 | const r = []; 97 | 98 | while (more) { 99 | const b = Number(val & SEVEN_BIT_MASK_BIG_INT); 100 | const signBitSet = !!(b & 0x40); 101 | 102 | val = val >> 7n; 103 | 104 | if ((val === 0n && !signBitSet) || (val === -1n && signBitSet)) { 105 | more = false; 106 | r.push(b); 107 | } else { 108 | r.push(b | CONTINUATION_BIT); 109 | } 110 | } 111 | 112 | return r; 113 | } 114 | 115 | const MIN_I32 = -(2 ** 32 / 2); 116 | const MAX_I32 = 2 ** 32 / 2 - 1; 117 | const I32_NEG_OFFSET = 2 ** 32; 118 | 119 | function i32(v) { 120 | if (v < MIN_I32 || v > MAX_U32) { 121 | throw Error(`Value out of range for i32: ${v}`); 122 | } 123 | 124 | if (v > MAX_I32) { 125 | return sleb128(v - I32_NEG_OFFSET); 126 | } 127 | 128 | return sleb128(v); 129 | } 130 | 131 | function section(id, contents) { 132 | const sizeInBytes = contents.flat(Infinity).length; 133 | return [id, u32(sizeInBytes), contents]; 134 | } 135 | 136 | function vec(elements) { 137 | return [u32(elements.length), elements]; 138 | } 139 | 140 | const SECTION_ID_TYPE = 1; 141 | 142 | function functype(paramTypes, resultTypes) { 143 | return [0x60, vec(paramTypes), vec(resultTypes)]; 144 | } 145 | 146 | function typesec(functypes) { 147 | return section(SECTION_ID_TYPE, vec(functypes)); 148 | } 149 | 150 | const SECTION_ID_FUNCTION = 3; 151 | 152 | const typeidx = (x) => u32(x); 153 | 154 | function funcsec(typeidxs) { 155 | return section(SECTION_ID_FUNCTION, vec(typeidxs)); 156 | } 157 | 158 | const SECTION_ID_CODE = 10; 159 | 160 | function code(func) { 161 | const sizeInBytes = func.flat(Infinity).length; 162 | return [u32(sizeInBytes), func]; 163 | } 164 | 165 | function func(locals, body) { 166 | return [vec(locals), body]; 167 | } 168 | 169 | function codesec(codes) { 170 | return section(SECTION_ID_CODE, vec(codes)); 171 | } 172 | 173 | const instr = { 174 | end: 0x0b, 175 | }; 176 | 177 | function compileNopLang(source) { 178 | if (source !== '') { 179 | throw new Error(`Expected empty code, got: "${source}"`); 180 | } 181 | 182 | const mod = module([ 183 | typesec([functype([], [])]), 184 | funcsec([typeidx(0)]), 185 | exportsec([export_('main', exportdesc.func(0))]), 186 | codesec([code(func([], [instr.end]))]), 187 | ]); 188 | return Uint8Array.from(mod.flat(Infinity)); 189 | } 190 | 191 | test('compileNopLang compiles to a wasm module', async () => { 192 | const {instance, module} = await WebAssembly.instantiate(compileNopLang('')); 193 | 194 | assert.strictEqual(instance instanceof WebAssembly.Instance, true); 195 | assert.strictEqual(module instanceof WebAssembly.Module, true); 196 | }); 197 | 198 | const SECTION_ID_EXPORT = 7; 199 | 200 | function name(s) { 201 | return vec(stringToBytes(s)); 202 | } 203 | 204 | function export_(nm, exportdesc) { 205 | return [name(nm), exportdesc]; 206 | } 207 | 208 | function exportsec(exports) { 209 | return section(SECTION_ID_EXPORT, vec(exports)); 210 | } 211 | 212 | const funcidx = (x) => u32(x); 213 | 214 | const exportdesc = { 215 | func(idx) { 216 | return [0x00, funcidx(idx)]; 217 | }, 218 | }; 219 | 220 | function module(sections) { 221 | return [magic(), version(), sections]; 222 | } 223 | -------------------------------------------------------------------------------- /chapter02.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | import {extractExamples} from 'ohm-js/extras'; 4 | 5 | import { 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcsec, 13 | functype, 14 | i32, 15 | instr, 16 | makeTestFn, 17 | module, 18 | typeidx, 19 | typesec, 20 | } from './chapter01.js'; 21 | 22 | const test = makeTestFn(import.meta.url); 23 | 24 | const grammarDef = ` 25 | Wafer { 26 | Main = number 27 | number = digit+ 28 | 29 | // Examples: 30 | //+ "42", "1" 31 | //- "abc" 32 | } 33 | `; 34 | 35 | function testExtractedExamples(grammarSource) { 36 | const grammar = ohm.grammar(grammarSource); 37 | for (const ex of extractExamples(grammarSource)) { 38 | const result = grammar.match(ex.example, ex.rule); 39 | assert.strictEqual(result.succeeded(), ex.shouldMatch, JSON.stringify(ex)); 40 | } 41 | } 42 | 43 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 44 | 45 | const wafer = ohm.grammar(grammarDef); 46 | 47 | const semantics = wafer.createSemantics(); 48 | semantics.addOperation('jsValue', { 49 | Main(num) { 50 | // To evaluate a program, we need to evaluate the number. 51 | return num.jsValue(); 52 | }, 53 | number(digits) { 54 | // Evaluate the number with JavaScript's built in `parseInt` function. 55 | return parseInt(this.sourceString, 10); 56 | }, 57 | }); 58 | 59 | test('jsValue', () => { 60 | const getJsValue = (input) => semantics(wafer.match(input)).jsValue(); 61 | assert.equal(getJsValue('42'), 42); 62 | assert.equal(getJsValue('0'), 0); 63 | assert.equal(getJsValue('99'), 99); 64 | }); 65 | 66 | const valtype = { 67 | i32: 0x7f, 68 | i64: 0x7e, 69 | f32: 0x7d, 70 | f64: 0x7c, 71 | }; 72 | 73 | function compile(source) { 74 | const matchResult = wafer.match(source); 75 | if (!matchResult.succeeded()) { 76 | throw new Error(matchResult.message); 77 | } 78 | 79 | const mod = module([ 80 | typesec([functype([], [valtype.i32])]), 81 | funcsec([typeidx(0)]), 82 | exportsec([export_('main', exportdesc.func(0))]), 83 | codesec([code(func([], semantics(matchResult).toWasm()))]), 84 | ]); 85 | return Uint8Array.from(mod.flat(Infinity)); 86 | } 87 | 88 | instr.i32 = {const: 0x41}; 89 | instr.i64 = {const: 0x42}; 90 | instr.f32 = {const: 0x43}; 91 | instr.f64 = {const: 0x44}; 92 | 93 | semantics.addOperation('toWasm', { 94 | Main(num) { 95 | return [num.toWasm(), instr.end]; 96 | }, 97 | number(digits) { 98 | const value = this.jsValue(); 99 | return [instr.i32.const, ...i32(value)]; 100 | }, 101 | }); 102 | 103 | function loadMod(bytes) { 104 | const mod = new WebAssembly.Module(bytes); 105 | return new WebAssembly.Instance(mod).exports; 106 | } 107 | 108 | test('toWasm', async () => { 109 | assert.equal(loadMod(compile('42')).main(), 42); 110 | assert.equal(loadMod(compile('0')).main(), 0); 111 | assert.equal(loadMod(compile('31')).main(), 31); 112 | }); 113 | 114 | export * from './chapter01.js'; 115 | export {loadMod, testExtractedExamples, valtype}; 116 | -------------------------------------------------------------------------------- /chapter02/01-noplang.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | import {extractExamples} from 'ohm-js/extras'; 4 | 5 | import { 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcsec, 13 | functype, 14 | i32, 15 | instr, 16 | makeTestFn, 17 | module, 18 | typeidx, 19 | typesec, 20 | } from '../chapter01.js'; 21 | 22 | const test = makeTestFn(import.meta.url); 23 | 24 | const grammarDef = ` 25 | NopLang { 26 | Main = "" 27 | } 28 | `; 29 | 30 | const grammar = ohm.grammar(grammarDef); 31 | 32 | const matchResult = grammar.match(''); 33 | 34 | test('NopLang', () => { 35 | assert.strictEqual(matchResult.succeeded(), true); 36 | assert.strictEqual(grammar.match('3').succeeded(), false); 37 | }); 38 | -------------------------------------------------------------------------------- /chapter02/02-numbers.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | import {extractExamples} from 'ohm-js/extras'; 4 | 5 | import { 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcsec, 13 | functype, 14 | i32, 15 | instr, 16 | makeTestFn, 17 | module, 18 | typeidx, 19 | typesec, 20 | } from '../chapter01.js'; 21 | 22 | const test = makeTestFn(import.meta.url); 23 | 24 | const grammarDef = ` 25 | Wafer { 26 | Main = number 27 | number = digit+ 28 | } 29 | `; 30 | 31 | const wafer = ohm.grammar(grammarDef); 32 | 33 | test('Wafer', () => { 34 | assert.ok(wafer.match('42').succeeded()); 35 | assert.ok(wafer.match('1').succeeded()); 36 | assert.ok(wafer.match('abc').failed()); 37 | }); 38 | -------------------------------------------------------------------------------- /chapter02/03-examples.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | import {extractExamples} from 'ohm-js/extras'; 4 | 5 | import { 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcsec, 13 | functype, 14 | i32, 15 | instr, 16 | makeTestFn, 17 | module, 18 | typeidx, 19 | typesec, 20 | } from '../chapter01.js'; 21 | 22 | const test = makeTestFn(import.meta.url); 23 | 24 | const grammarDef = ` 25 | Wafer { 26 | Main = number 27 | number = digit+ 28 | 29 | // Examples: 30 | //+ "42", "1" 31 | //- "abc" 32 | } 33 | `; 34 | 35 | function testExtractedExamples(grammarSource) { 36 | const grammar = ohm.grammar(grammarSource); 37 | for (const ex of extractExamples(grammarSource)) { 38 | const result = grammar.match(ex.example, ex.rule); 39 | assert.strictEqual(result.succeeded(), ex.shouldMatch, JSON.stringify(ex)); 40 | } 41 | } 42 | 43 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 44 | -------------------------------------------------------------------------------- /chapter02/04-jsValue.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | import {extractExamples} from 'ohm-js/extras'; 4 | 5 | import { 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcsec, 13 | functype, 14 | i32, 15 | instr, 16 | makeTestFn, 17 | module, 18 | typeidx, 19 | typesec, 20 | } from '../chapter01.js'; 21 | 22 | const test = makeTestFn(import.meta.url); 23 | 24 | const grammarDef = ` 25 | Wafer { 26 | Main = number 27 | number = digit+ 28 | 29 | // Examples: 30 | //+ "42", "1" 31 | //- "abc" 32 | } 33 | `; 34 | 35 | function testExtractedExamples(grammarSource) { 36 | const grammar = ohm.grammar(grammarSource); 37 | for (const ex of extractExamples(grammarSource)) { 38 | const result = grammar.match(ex.example, ex.rule); 39 | assert.strictEqual(result.succeeded(), ex.shouldMatch, JSON.stringify(ex)); 40 | } 41 | } 42 | 43 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 44 | 45 | const wafer = ohm.grammar(grammarDef); 46 | 47 | const semantics = wafer.createSemantics(); 48 | semantics.addOperation('jsValue', { 49 | Main(num) { 50 | // To evaluate a program, we need to evaluate the number. 51 | return num.jsValue(); 52 | }, 53 | number(digits) { 54 | // Evaluate the number with JavaScript's built in `parseInt` function. 55 | return parseInt(this.sourceString, 10); 56 | }, 57 | }); 58 | 59 | test('jsValue', () => { 60 | const getJsValue = (input) => semantics(wafer.match(input)).jsValue(); 61 | assert.equal(getJsValue('42'), 42); 62 | assert.equal(getJsValue('0'), 0); 63 | assert.equal(getJsValue('99'), 99); 64 | }); 65 | -------------------------------------------------------------------------------- /chapter02/05-toWasm.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | import {extractExamples} from 'ohm-js/extras'; 4 | 5 | import { 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcsec, 13 | functype, 14 | i32, 15 | instr, 16 | makeTestFn, 17 | module, 18 | typeidx, 19 | typesec, 20 | } from '../chapter01.js'; 21 | 22 | const test = makeTestFn(import.meta.url); 23 | 24 | const grammarDef = ` 25 | Wafer { 26 | Main = number 27 | number = digit+ 28 | 29 | // Examples: 30 | //+ "42", "1" 31 | //- "abc" 32 | } 33 | `; 34 | 35 | function testExtractedExamples(grammarSource) { 36 | const grammar = ohm.grammar(grammarSource); 37 | for (const ex of extractExamples(grammarSource)) { 38 | const result = grammar.match(ex.example, ex.rule); 39 | assert.strictEqual(result.succeeded(), ex.shouldMatch, JSON.stringify(ex)); 40 | } 41 | } 42 | 43 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 44 | 45 | const wafer = ohm.grammar(grammarDef); 46 | 47 | const semantics = wafer.createSemantics(); 48 | semantics.addOperation('jsValue', { 49 | Main(num) { 50 | // To evaluate a program, we need to evaluate the number. 51 | return num.jsValue(); 52 | }, 53 | number(digits) { 54 | // Evaluate the number with JavaScript's built in `parseInt` function. 55 | return parseInt(this.sourceString, 10); 56 | }, 57 | }); 58 | 59 | test('jsValue', () => { 60 | const getJsValue = (input) => semantics(wafer.match(input)).jsValue(); 61 | assert.equal(getJsValue('42'), 42); 62 | assert.equal(getJsValue('0'), 0); 63 | assert.equal(getJsValue('99'), 99); 64 | }); 65 | 66 | const valtype = { 67 | i32: 0x7f, 68 | i64: 0x7e, 69 | f32: 0x7d, 70 | f64: 0x7c, 71 | }; 72 | 73 | function compile(source) { 74 | const matchResult = wafer.match(source); 75 | if (!matchResult.succeeded()) { 76 | throw new Error(matchResult.message); 77 | } 78 | 79 | const mod = module([ 80 | typesec([functype([], [valtype.i32])]), 81 | funcsec([typeidx(0)]), 82 | exportsec([export_('main', exportdesc.func(0))]), 83 | codesec([code(func([], semantics(matchResult).toWasm()))]), 84 | ]); 85 | return Uint8Array.from(mod.flat(Infinity)); 86 | } 87 | 88 | instr.i32 = {const: 0x41}; 89 | instr.i64 = {const: 0x42}; 90 | instr.f32 = {const: 0x43}; 91 | instr.f64 = {const: 0x44}; 92 | 93 | semantics.addOperation('toWasm', { 94 | Main(num) { 95 | return [num.toWasm(), instr.end]; 96 | }, 97 | number(digits) { 98 | const value = this.jsValue(); 99 | return [instr.i32.const, ...i32(value)]; 100 | }, 101 | }); 102 | 103 | function loadMod(bytes) { 104 | const mod = new WebAssembly.Module(bytes); 105 | return new WebAssembly.Instance(mod).exports; 106 | } 107 | 108 | test('toWasm', async () => { 109 | assert.equal(loadMod(compile('42')).main(), 42); 110 | assert.equal(loadMod(compile('0')).main(), 0); 111 | assert.equal(loadMod(compile('31')).main(), 31); 112 | }); 113 | -------------------------------------------------------------------------------- /chapter03.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import {i32, instr, makeTestFn, testExtractedExamples} from './chapter02.js'; 5 | 6 | const test = makeTestFn(import.meta.url); 7 | 8 | const grammarDef = ` 9 | Wafer { 10 | Main = Expr 11 | Expr = PrimaryExpr (op PrimaryExpr)* 12 | 13 | PrimaryExpr = "(" Expr ")" -- paren 14 | | number 15 | 16 | op = "+" | "-" | "*" | "/" 17 | number = digit+ 18 | 19 | // Examples: 20 | //+ "42", "1", "66 + 99", "1 + 2 - 3", "1 + (2 * 3)", "(((1) / 2))" 21 | //- "abc" 22 | } 23 | `; 24 | 25 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 26 | 27 | const wafer = ohm.grammar(grammarDef); 28 | const semantics = wafer.createSemantics(); 29 | 30 | semantics.addOperation('jsValue', { 31 | Main(num) { 32 | // To evaluate main, we need to evaluate the number. 33 | return num.jsValue(); 34 | }, 35 | number(digits) { 36 | // Evaluate the number with JavaScript's built in `parseInt` function. 37 | return parseInt(this.sourceString, 10); 38 | }, 39 | }); 40 | 41 | semantics.addOperation('toWasm', { 42 | Main(expr) { 43 | return [expr.toWasm(), instr.end]; 44 | }, 45 | Expr(num, iterOps, iterOperands) { 46 | const result = [num.toWasm()]; 47 | for (let i = 0; i < iterOps.numChildren; i++) { 48 | const op = iterOps.child(i); 49 | const operand = iterOperands.child(i); 50 | result.push(operand.toWasm(), op.toWasm()); 51 | } 52 | return result; 53 | }, 54 | PrimaryExpr_paren(_lparen, expr, _rparen) { 55 | return expr.toWasm(); 56 | }, 57 | op(char) { 58 | const op = char.sourceString; 59 | const instructionByOp = { 60 | '+': instr.i32.add, 61 | '-': instr.i32.sub, 62 | '*': instr.i32.mul, 63 | '/': instr.i32.div_s, 64 | }; 65 | if (!Object.hasOwn(instructionByOp, op)) { 66 | throw new Error(`Unhandled operator '${op}'`); 67 | } 68 | return instructionByOp[op]; 69 | }, 70 | number(digits) { 71 | const num = this.jsValue(); 72 | return [instr.i32.const, ...i32(num)]; 73 | }, 74 | }); 75 | 76 | instr.i32.add = 0x6a; 77 | instr.i32.sub = 0x6b; 78 | instr.i32.mul = 0x6c; 79 | instr.i32.div_s = 0x6d; 80 | 81 | function toWasmFlat(input) { 82 | const matchResult = wafer.match(input); 83 | const bytes = semantics(matchResult).toWasm(); 84 | return bytes.flat(Infinity); 85 | } 86 | 87 | test('toWasm bytecodes', () => { 88 | assert.deepEqual(toWasmFlat('1'), [instr.i32.const, 1, instr.end]); 89 | assert.deepEqual( 90 | toWasmFlat('1 + 2'), 91 | [ 92 | [instr.i32.const, 1], 93 | [instr.i32.const, 2], 94 | instr.i32.add, 95 | instr.end, 96 | ].flat(), 97 | ); 98 | assert.deepEqual( 99 | toWasmFlat('7 - 3 + 11'), 100 | [ 101 | [instr.i32.const, 7], 102 | [instr.i32.const, 3], 103 | instr.i32.sub, 104 | [instr.i32.const, 11], 105 | instr.i32.add, 106 | instr.end, 107 | ].flat(), 108 | ); 109 | assert.deepEqual( 110 | toWasmFlat('6 / (2 * 1)'), 111 | [ 112 | [instr.i32.const, 6], 113 | [instr.i32.const, 2], 114 | [instr.i32.const, 1], 115 | instr.i32.mul, 116 | instr.i32.div_s, 117 | instr.end, 118 | ].flat(), 119 | ); 120 | }); 121 | 122 | export * from './chapter02.js'; 123 | -------------------------------------------------------------------------------- /chapter03/01-exprStub.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import {i32, instr, makeTestFn, testExtractedExamples} from '../chapter02.js'; 5 | 6 | const test = makeTestFn(import.meta.url); 7 | 8 | const grammarDef = ` 9 | Wafer { 10 | Main = Expr 11 | Expr = number 12 | number = digit+ 13 | 14 | // Examples: 15 | //+ "42", "1" 16 | //- "abc" 17 | } 18 | `; 19 | 20 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 21 | 22 | const wafer = ohm.grammar(grammarDef); 23 | -------------------------------------------------------------------------------- /chapter03/02-waferAdd.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import {i32, instr, makeTestFn, testExtractedExamples} from '../chapter02.js'; 5 | 6 | const test = makeTestFn(import.meta.url); 7 | 8 | const grammarDef = ` 9 | Wafer { 10 | Main = Expr 11 | Expr = number ("+" number)* 12 | number = digit+ 13 | 14 | // Examples: 15 | //+ "42", "1" 16 | //- "abc" 17 | } 18 | `; 19 | 20 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 21 | -------------------------------------------------------------------------------- /chapter03/03-waferAddSub.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import {i32, instr, makeTestFn, testExtractedExamples} from '../chapter02.js'; 5 | 6 | const test = makeTestFn(import.meta.url); 7 | 8 | const grammarDef = ` 9 | Wafer { 10 | Main = Expr 11 | Expr = number (op number)* 12 | 13 | op = "+" | "-" 14 | number = digit+ 15 | 16 | // Examples: 17 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 18 | //- "abc" 19 | } 20 | `; 21 | 22 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 23 | -------------------------------------------------------------------------------- /chapter03/04-toWasm.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import {i32, instr, makeTestFn, testExtractedExamples} from '../chapter02.js'; 5 | 6 | const test = makeTestFn(import.meta.url); 7 | 8 | const grammarDef = ` 9 | Wafer { 10 | Main = Expr 11 | Expr = number (op number)* 12 | 13 | op = "+" | "-" 14 | number = digit+ 15 | 16 | // Examples: 17 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 18 | //- "abc" 19 | } 20 | `; 21 | 22 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 23 | 24 | const wafer = ohm.grammar(grammarDef); 25 | const semantics = wafer.createSemantics(); 26 | 27 | semantics.addOperation('jsValue', { 28 | Main(num) { 29 | // To evaluate main, we need to evaluate the number. 30 | return num.jsValue(); 31 | }, 32 | number(digits) { 33 | // Evaluate the number with JavaScript's built in `parseInt` function. 34 | return parseInt(this.sourceString, 10); 35 | }, 36 | }); 37 | 38 | semantics.addOperation('toWasm', { 39 | Main(expr) { 40 | return [expr.toWasm(), instr.end]; 41 | }, 42 | Expr(num, iterOps, iterOperands) { 43 | const result = [num.toWasm()]; 44 | for (let i = 0; i < iterOps.numChildren; i++) { 45 | const op = iterOps.child(i); 46 | const operand = iterOperands.child(i); 47 | result.push(operand.toWasm(), op.toWasm()); 48 | } 49 | return result; 50 | }, 51 | op(char) { 52 | return [char.sourceString === '+' ? instr.i32.add : instr.i32.sub]; 53 | }, 54 | number(digits) { 55 | const num = this.jsValue(); 56 | return [instr.i32.const, ...i32(num)]; 57 | }, 58 | }); 59 | 60 | instr.i32.add = 0x6a; 61 | instr.i32.sub = 0x6b; 62 | 63 | function toWasmFlat(input) { 64 | const matchResult = wafer.match(input); 65 | const bytes = semantics(matchResult).toWasm(); 66 | return bytes.flat(Infinity); 67 | } 68 | 69 | test('toWasm bytecodes', () => { 70 | assert.deepEqual(toWasmFlat('1'), [instr.i32.const, 1, instr.end]); 71 | assert.deepEqual( 72 | toWasmFlat('1 + 2'), 73 | [ 74 | [instr.i32.const, 1], 75 | [instr.i32.const, 2], 76 | instr.i32.add, 77 | instr.end, 78 | ].flat(), 79 | ); 80 | assert.deepEqual( 81 | toWasmFlat('7 - 3 + 11'), 82 | [ 83 | [instr.i32.const, 7], 84 | [instr.i32.const, 3], 85 | instr.i32.sub, 86 | [instr.i32.const, 11], 87 | instr.i32.add, 88 | instr.end, 89 | ].flat(), 90 | ); 91 | }); 92 | -------------------------------------------------------------------------------- /chapter03/05-toWasmMulDiv.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import {i32, instr, makeTestFn, testExtractedExamples} from '../chapter02.js'; 5 | 6 | const test = makeTestFn(import.meta.url); 7 | 8 | const grammarDef = ` 9 | Wafer { 10 | Main = Expr 11 | Expr = PrimaryExpr (op PrimaryExpr)* 12 | 13 | PrimaryExpr = "(" Expr ")" -- paren 14 | | number 15 | 16 | op = "+" | "-" | "*" | "/" 17 | number = digit+ 18 | 19 | // Examples: 20 | //+ "42", "1", "66 + 99", "1 + 2 - 3", "1 + (2 * 3)", "(((1) / 2))" 21 | //- "abc" 22 | } 23 | `; 24 | 25 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 26 | 27 | const wafer = ohm.grammar(grammarDef); 28 | const semantics = wafer.createSemantics(); 29 | 30 | semantics.addOperation('jsValue', { 31 | Main(num) { 32 | // To evaluate main, we need to evaluate the number. 33 | return num.jsValue(); 34 | }, 35 | number(digits) { 36 | // Evaluate the number with JavaScript's built in `parseInt` function. 37 | return parseInt(this.sourceString, 10); 38 | }, 39 | }); 40 | 41 | semantics.addOperation('toWasm', { 42 | Main(expr) { 43 | return [expr.toWasm(), instr.end]; 44 | }, 45 | Expr(num, iterOps, iterOperands) { 46 | const result = [num.toWasm()]; 47 | for (let i = 0; i < iterOps.numChildren; i++) { 48 | const op = iterOps.child(i); 49 | const operand = iterOperands.child(i); 50 | result.push(operand.toWasm(), op.toWasm()); 51 | } 52 | return result; 53 | }, 54 | PrimaryExpr_paren(_lparen, expr, _rparen) { 55 | return expr.toWasm(); 56 | }, 57 | op(char) { 58 | const op = char.sourceString; 59 | const instructionByOp = { 60 | '+': instr.i32.add, 61 | '-': instr.i32.sub, 62 | '*': instr.i32.mul, 63 | '/': instr.i32.div_s, 64 | }; 65 | if (!Object.hasOwn(instructionByOp, op)) { 66 | throw new Error(`Unhandled operator '${op}'`); 67 | } 68 | return instructionByOp[op]; 69 | }, 70 | number(digits) { 71 | const num = this.jsValue(); 72 | return [instr.i32.const, ...i32(num)]; 73 | }, 74 | }); 75 | 76 | instr.i32.add = 0x6a; 77 | instr.i32.sub = 0x6b; 78 | instr.i32.mul = 0x6c; 79 | instr.i32.div_s = 0x6d; 80 | 81 | function toWasmFlat(input) { 82 | const matchResult = wafer.match(input); 83 | const bytes = semantics(matchResult).toWasm(); 84 | return bytes.flat(Infinity); 85 | } 86 | 87 | test('toWasm bytecodes', () => { 88 | assert.deepEqual(toWasmFlat('1'), [instr.i32.const, 1, instr.end]); 89 | assert.deepEqual( 90 | toWasmFlat('1 + 2'), 91 | [ 92 | [instr.i32.const, 1], 93 | [instr.i32.const, 2], 94 | instr.i32.add, 95 | instr.end, 96 | ].flat(), 97 | ); 98 | assert.deepEqual( 99 | toWasmFlat('7 - 3 + 11'), 100 | [ 101 | [instr.i32.const, 7], 102 | [instr.i32.const, 3], 103 | instr.i32.sub, 104 | [instr.i32.const, 11], 105 | instr.i32.add, 106 | instr.end, 107 | ].flat(), 108 | ); 109 | assert.deepEqual( 110 | toWasmFlat('6 / (2 * 1)'), 111 | [ 112 | [instr.i32.const, 6], 113 | [instr.i32.const, 2], 114 | [instr.i32.const, 1], 115 | instr.i32.mul, 116 | instr.i32.div_s, 117 | instr.end, 118 | ].flat(), 119 | ); 120 | }); 121 | -------------------------------------------------------------------------------- /chapter04.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from './chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Statement* Expr 30 | 31 | Statement = LetStatement 32 | | ExprStatement 33 | 34 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 35 | //- "let y;" 36 | LetStatement = "let" identifier "=" Expr ";" 37 | 38 | ExprStatement = Expr ";" 39 | 40 | Expr = AssignmentExpr -- assignment 41 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 42 | 43 | //+ "x := 3", "y := 2 + 1" 44 | AssignmentExpr = identifier ":=" Expr 45 | 46 | PrimaryExpr = "(" Expr ")" -- paren 47 | | number 48 | | identifier -- var 49 | 50 | op = "+" | "-" | "*" | "/" 51 | number = digit+ 52 | 53 | //+ "x", "élan", "_", "_99" 54 | //- "1", "$nope" 55 | identifier = identStart identPart* 56 | identStart = letter | "_" 57 | identPart = identStart | digit 58 | 59 | // Examples: 60 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 61 | //+ "let x = 3; 42" 62 | //- "3abc" 63 | //- "let x = 3;" 64 | } 65 | `; 66 | 67 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 68 | 69 | instr.local = {}; 70 | instr.local.get = 0x20; 71 | instr.local.set = 0x21; 72 | instr.local.tee = 0x22; 73 | 74 | function locals(n, type) { 75 | return [u32(n), type]; 76 | } 77 | 78 | const localidx = (x) => u32(x); 79 | 80 | function compileLocals() { 81 | const mod = module([ 82 | typesec([functype([valtype.i32], [valtype.i32])]), 83 | funcsec([typeidx(0)]), 84 | exportsec([export_('f1', exportdesc.func(0))]), 85 | codesec([ 86 | code( 87 | func( 88 | [locals(1, valtype.i32)], 89 | [ 90 | [instr.i32.const, i32(42)], 91 | [instr.local.set, localidx(1)], 92 | [instr.local.get, localidx(0)], 93 | [instr.local.get, localidx(1)], 94 | instr.i32.add, 95 | instr.end, 96 | ], 97 | ), 98 | ), 99 | ]), 100 | ]); 101 | 102 | return Uint8Array.from(mod.flat(Infinity)); 103 | } 104 | 105 | test('compileLocals', () => { 106 | assert.strictEqual(loadMod(compileLocals()).f1(10), 52); 107 | }); 108 | 109 | function buildSymbolTable(grammar, matchResult) { 110 | const tempSemantics = grammar.createSemantics(); 111 | const symbols = new Map(); 112 | symbols.set('main', new Map()); 113 | tempSemantics.addOperation('buildSymbolTable', { 114 | _default(...children) { 115 | return children.forEach((c) => c.buildSymbolTable()); 116 | }, 117 | LetStatement(_let, id, _eq, _expr, _) { 118 | const name = id.sourceString; 119 | const idx = symbols.get('main').size; 120 | const info = {name, idx, what: 'local'}; 121 | symbols.get('main').set(name, info); 122 | }, 123 | }); 124 | tempSemantics(matchResult).buildSymbolTable(); 125 | return symbols; 126 | } 127 | 128 | function resolveSymbol(identNode, locals) { 129 | const identName = identNode.sourceString; 130 | if (locals.has(identName)) { 131 | return locals.get(identName); 132 | } 133 | throw new Error(`Error: undeclared identifier '${identName}'`); 134 | } 135 | 136 | const wafer = ohm.grammar(grammarDef); 137 | 138 | test('symbol table', () => { 139 | const getVarNames = (str) => { 140 | const symbols = buildSymbolTable(wafer, wafer.match(str)); 141 | return Array.from(symbols.get('main').keys()); 142 | }; 143 | 144 | assert.deepEqual(getVarNames('42'), []); 145 | assert.deepEqual(getVarNames('let x = 0; 42'), ['x']); 146 | assert.deepEqual(getVarNames('let x = 0; let y = 1; 42'), ['x', 'y']); 147 | 148 | const symbols = buildSymbolTable( 149 | wafer, 150 | wafer.match('let x = 0; let y = 1; 42'), 151 | ); 152 | const localVars = symbols.get('main'); 153 | assert.strictEqual(resolveSymbol({sourceString: 'x'}, localVars).idx, 0); 154 | assert.strictEqual(resolveSymbol({sourceString: 'y'}, localVars).idx, 1); 155 | assert.throws(() => resolveSymbol({sourceString: 'z'}, localVars)); 156 | }); 157 | 158 | function defineToWasm(semantics, localVars) { 159 | semantics.addOperation('toWasm', { 160 | Main(statementIter, expr) { 161 | return [ 162 | statementIter.children.map((c) => c.toWasm()), 163 | expr.toWasm(), 164 | instr.end, 165 | ]; 166 | }, 167 | LetStatement(_let, ident, _eq, expr, _) { 168 | const info = resolveSymbol(ident, localVars); 169 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 170 | }, 171 | ExprStatement(expr, _) { 172 | return [expr.toWasm(), instr.drop]; 173 | }, 174 | Expr_arithmetic(num, iterOps, iterOperands) { 175 | const result = [num.toWasm()]; 176 | for (let i = 0; i < iterOps.numChildren; i++) { 177 | const op = iterOps.child(i); 178 | const operand = iterOperands.child(i); 179 | result.push(operand.toWasm(), op.toWasm()); 180 | } 181 | return result; 182 | }, 183 | AssignmentExpr(ident, _, expr) { 184 | const info = resolveSymbol(ident, localVars); 185 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 186 | }, 187 | PrimaryExpr_paren(_lparen, expr, _rparen) { 188 | return expr.toWasm(); 189 | }, 190 | PrimaryExpr_var(ident) { 191 | const info = resolveSymbol(ident, localVars); 192 | return [instr.local.get, localidx(info.idx)]; 193 | }, 194 | op(char) { 195 | const op = char.sourceString; 196 | const instructionByOp = { 197 | '+': instr.i32.add, 198 | '-': instr.i32.sub, 199 | '*': instr.i32.mul, 200 | '/': instr.i32.div_s, 201 | }; 202 | if (!Object.hasOwn(instructionByOp, op)) { 203 | throw new Error(`Unhandled operator '${op}'`); 204 | } 205 | return instructionByOp[op]; 206 | }, 207 | number(_digits) { 208 | const num = parseInt(this.sourceString, 10); 209 | return [instr.i32.const, ...i32(num)]; 210 | }, 211 | }); 212 | } 213 | 214 | function toWasmFlat(input) { 215 | const matchResult = wafer.match(input); 216 | const symbols = buildSymbolTable(wafer, matchResult); 217 | const semantics = wafer.createSemantics(); 218 | defineToWasm(semantics, symbols.get('main')); 219 | return semantics(matchResult).toWasm().flat(Infinity); 220 | } 221 | 222 | test('toWasm bytecodes - locals & assignment', () => { 223 | assert.deepEqual(toWasmFlat('42'), [instr.i32.const, 42, instr.end]); 224 | assert.deepEqual( 225 | toWasmFlat('let x = 10; 42'), 226 | [ 227 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 228 | [instr.i32.const, 42], 229 | instr.end, 230 | ].flat(), 231 | ); 232 | assert.deepEqual( 233 | toWasmFlat('let x = 10; x'), 234 | [ 235 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 236 | [instr.local.get, 0], 237 | instr.end, 238 | ].flat(), 239 | ); 240 | assert.deepEqual( 241 | toWasmFlat('let x = 10; x := 9; x'), 242 | [ 243 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 244 | [instr.i32.const, 9, instr.local.tee, 0, instr.drop], // x := 9; 245 | [instr.local.get, 0], // x 246 | instr.end, 247 | ].flat(), 248 | ); 249 | }); 250 | 251 | instr.drop = 0x1a; 252 | 253 | function compile(source) { 254 | const matchResult = wafer.match(source); 255 | if (!matchResult.succeeded()) { 256 | throw new Error(matchResult.message); 257 | } 258 | 259 | const semantics = wafer.createSemantics(); 260 | const symbols = buildSymbolTable(wafer, matchResult); 261 | const localVars = symbols.get('main'); 262 | defineToWasm(semantics, localVars); 263 | 264 | const mainFn = func( 265 | [locals(localVars.size, valtype.i32)], 266 | semantics(matchResult).toWasm(), 267 | ); 268 | const mod = module([ 269 | typesec([functype([], [valtype.i32])]), 270 | funcsec([typeidx(0)]), 271 | exportsec([export_('main', exportdesc.func(0))]), 272 | codesec([code(mainFn)]), 273 | ]); 274 | return Uint8Array.from(mod.flat(Infinity)); 275 | } 276 | 277 | test('module with locals & assignment', () => { 278 | assert.deepEqual(loadMod(compile('let x = 42; x')).main(), 42); 279 | assert.deepEqual( 280 | loadMod(compile('let a = 13; let b = 15; a := 10; a + b')).main(), 281 | 25, 282 | ); 283 | }); 284 | 285 | export * from './chapter03.js'; 286 | export {buildSymbolTable, resolveSymbol, locals, localidx}; 287 | -------------------------------------------------------------------------------- /chapter04/01-identifiers.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from '../chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Expr 30 | Expr = PrimaryExpr (op PrimaryExpr)* 31 | 32 | PrimaryExpr = "(" Expr ")" -- paren 33 | | number 34 | 35 | op = "+" | "-" | "*" | "/" 36 | number = digit+ 37 | 38 | //+ "x", "élan", "_", "_99" 39 | //- "1", "$nope" 40 | identifier = identStart identPart* 41 | identStart = letter | "_" 42 | identPart = letter | "_" | digit 43 | 44 | // Examples: 45 | //+ "42", "1" 46 | //- "abc" 47 | } 48 | `; 49 | 50 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 51 | -------------------------------------------------------------------------------- /chapter04/02-primaryexpr.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from '../chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Statement* Expr 30 | 31 | Statement = LetStatement 32 | 33 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 34 | //- "let y;" 35 | LetStatement = "let" identifier "=" Expr ";" 36 | 37 | Expr = PrimaryExpr (op PrimaryExpr)* 38 | 39 | PrimaryExpr = "(" Expr ")" -- paren 40 | | number 41 | | identifier 42 | 43 | op = "+" | "-" | "*" | "/" 44 | number = digit+ 45 | 46 | //+ "x", "élan", "_", "_99" 47 | //- "1", "$nope" 48 | identifier = identStart identPart* 49 | identStart = letter | "_" 50 | identPart = identStart | digit 51 | 52 | // Examples: 53 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 54 | //+ "let x = 3; 42" 55 | //- "3abc" 56 | //- "let x = 3;" 57 | } 58 | `; 59 | 60 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 61 | -------------------------------------------------------------------------------- /chapter04/03-compileLocals.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from '../chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Statement* Expr 30 | 31 | Statement = LetStatement 32 | 33 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 34 | //- "let y;" 35 | LetStatement = "let" identifier "=" Expr ";" 36 | 37 | Expr = PrimaryExpr (op PrimaryExpr)* 38 | 39 | PrimaryExpr = "(" Expr ")" -- paren 40 | | number 41 | | identifier -- var 42 | 43 | op = "+" | "-" | "*" | "/" 44 | number = digit+ 45 | 46 | //+ "x", "élan", "_", "_99" 47 | //- "1", "$nope" 48 | identifier = identStart identPart* 49 | identStart = letter | "_" 50 | identPart = identStart | digit 51 | 52 | // Examples: 53 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 54 | //+ "let x = 3; 42" 55 | //- "3abc" 56 | //- "let x = 3;" 57 | } 58 | `; 59 | 60 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 61 | 62 | instr.local = {}; 63 | instr.local.get = 0x20; 64 | instr.local.set = 0x21; 65 | instr.local.tee = 0x22; 66 | 67 | function locals(n, type) { 68 | return [u32(n), type]; 69 | } 70 | 71 | const localidx = (x) => u32(x); 72 | 73 | function compileLocals() { 74 | const mod = module([ 75 | typesec([functype([valtype.i32], [valtype.i32])]), 76 | funcsec([typeidx(0)]), 77 | exportsec([export_('f1', exportdesc.func(0))]), 78 | codesec([ 79 | code( 80 | func( 81 | [locals(1, valtype.i32)], 82 | [ 83 | [instr.i32.const, i32(42)], 84 | [instr.local.set, localidx(1)], 85 | [instr.local.get, localidx(0)], 86 | [instr.local.get, localidx(1)], 87 | instr.i32.add, 88 | instr.end, 89 | ], 90 | ), 91 | ), 92 | ]), 93 | ]); 94 | 95 | return Uint8Array.from(mod.flat(Infinity)); 96 | } 97 | 98 | test('compileLocals', () => { 99 | assert.strictEqual(loadMod(compileLocals()).f1(10), 52); 100 | }); 101 | -------------------------------------------------------------------------------- /chapter04/04-buildSymbolTable.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from '../chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Statement* Expr 30 | 31 | Statement = LetStatement 32 | 33 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 34 | //- "let y;" 35 | LetStatement = "let" identifier "=" Expr ";" 36 | 37 | Expr = PrimaryExpr (op PrimaryExpr)* 38 | 39 | PrimaryExpr = "(" Expr ")" -- paren 40 | | number 41 | | identifier -- var 42 | 43 | op = "+" | "-" | "*" | "/" 44 | number = digit+ 45 | 46 | //+ "x", "élan", "_", "_99" 47 | //- "1", "$nope" 48 | identifier = identStart identPart* 49 | identStart = letter | "_" 50 | identPart = identStart | digit 51 | 52 | // Examples: 53 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 54 | //+ "let x = 3; 42" 55 | //- "3abc" 56 | //- "let x = 3;" 57 | } 58 | `; 59 | 60 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 61 | 62 | instr.local = {}; 63 | instr.local.get = 0x20; 64 | instr.local.set = 0x21; 65 | instr.local.tee = 0x22; 66 | 67 | function locals(n, type) { 68 | return [u32(n), type]; 69 | } 70 | 71 | const localidx = (x) => u32(x); 72 | 73 | function compileLocals() { 74 | const mod = module([ 75 | typesec([functype([valtype.i32], [valtype.i32])]), 76 | funcsec([typeidx(0)]), 77 | exportsec([export_('f1', exportdesc.func(0))]), 78 | codesec([ 79 | code( 80 | func( 81 | [locals(1, valtype.i32)], 82 | [ 83 | [instr.i32.const, i32(42)], 84 | [instr.local.set, localidx(1)], 85 | [instr.local.get, localidx(0)], 86 | [instr.local.get, localidx(1)], 87 | instr.i32.add, 88 | instr.end, 89 | ], 90 | ), 91 | ), 92 | ]), 93 | ]); 94 | 95 | return Uint8Array.from(mod.flat(Infinity)); 96 | } 97 | 98 | test('compileLocals', () => { 99 | assert.strictEqual(loadMod(compileLocals()).f1(10), 52); 100 | }); 101 | 102 | function buildSymbolTable(grammar, matchResult) { 103 | const tempSemantics = grammar.createSemantics(); 104 | const symbols = new Map(); 105 | symbols.set('main', new Map()); 106 | tempSemantics.addOperation('buildSymbolTable', { 107 | _default(...children) { 108 | return children.forEach((c) => c.buildSymbolTable()); 109 | }, 110 | LetStatement(_let, id, _eq, _expr, _) { 111 | const name = id.sourceString; 112 | const idx = symbols.get('main').size; 113 | const info = {name, idx, what: 'local'}; 114 | symbols.get('main').set(name, info); 115 | }, 116 | }); 117 | tempSemantics(matchResult).buildSymbolTable(); 118 | return symbols; 119 | } 120 | 121 | function resolveSymbol(identNode, locals) { 122 | const identName = identNode.sourceString; 123 | if (locals.has(identName)) { 124 | return locals.get(identName); 125 | } 126 | throw new Error(`Error: undeclared identifier '${identName}'`); 127 | } 128 | 129 | const wafer = ohm.grammar(grammarDef); 130 | 131 | test('symbol table', () => { 132 | const getVarNames = (str) => { 133 | const symbols = buildSymbolTable(wafer, wafer.match(str)); 134 | return Array.from(symbols.get('main').keys()); 135 | }; 136 | 137 | assert.deepEqual(getVarNames('42'), []); 138 | assert.deepEqual(getVarNames('let x = 0; 42'), ['x']); 139 | assert.deepEqual(getVarNames('let x = 0; let y = 1; 42'), ['x', 'y']); 140 | 141 | const symbols = buildSymbolTable( 142 | wafer, 143 | wafer.match('let x = 0; let y = 1; 42'), 144 | ); 145 | const localVars = symbols.get('main'); 146 | assert.strictEqual(resolveSymbol({sourceString: 'x'}, localVars).idx, 0); 147 | assert.strictEqual(resolveSymbol({sourceString: 'y'}, localVars).idx, 1); 148 | assert.throws(() => resolveSymbol({sourceString: 'z'}, localVars)); 149 | }); 150 | -------------------------------------------------------------------------------- /chapter04/05-toWasm.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from '../chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Statement* Expr 30 | 31 | Statement = LetStatement 32 | 33 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 34 | //- "let y;" 35 | LetStatement = "let" identifier "=" Expr ";" 36 | 37 | Expr = PrimaryExpr (op PrimaryExpr)* 38 | 39 | PrimaryExpr = "(" Expr ")" -- paren 40 | | number 41 | | identifier -- var 42 | 43 | op = "+" | "-" | "*" | "/" 44 | number = digit+ 45 | 46 | //+ "x", "élan", "_", "_99" 47 | //- "1", "$nope" 48 | identifier = identStart identPart* 49 | identStart = letter | "_" 50 | identPart = identStart | digit 51 | 52 | // Examples: 53 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 54 | //+ "let x = 3; 42" 55 | //- "3abc" 56 | //- "let x = 3;" 57 | } 58 | `; 59 | 60 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 61 | 62 | instr.local = {}; 63 | instr.local.get = 0x20; 64 | instr.local.set = 0x21; 65 | instr.local.tee = 0x22; 66 | 67 | function locals(n, type) { 68 | return [u32(n), type]; 69 | } 70 | 71 | const localidx = (x) => u32(x); 72 | 73 | function compileLocals() { 74 | const mod = module([ 75 | typesec([functype([valtype.i32], [valtype.i32])]), 76 | funcsec([typeidx(0)]), 77 | exportsec([export_('f1', exportdesc.func(0))]), 78 | codesec([ 79 | code( 80 | func( 81 | [locals(1, valtype.i32)], 82 | [ 83 | [instr.i32.const, i32(42)], 84 | [instr.local.set, localidx(1)], 85 | [instr.local.get, localidx(0)], 86 | [instr.local.get, localidx(1)], 87 | instr.i32.add, 88 | instr.end, 89 | ], 90 | ), 91 | ), 92 | ]), 93 | ]); 94 | 95 | return Uint8Array.from(mod.flat(Infinity)); 96 | } 97 | 98 | test('compileLocals', () => { 99 | assert.strictEqual(loadMod(compileLocals()).f1(10), 52); 100 | }); 101 | 102 | function buildSymbolTable(grammar, matchResult) { 103 | const tempSemantics = grammar.createSemantics(); 104 | const symbols = new Map(); 105 | symbols.set('main', new Map()); 106 | tempSemantics.addOperation('buildSymbolTable', { 107 | _default(...children) { 108 | return children.forEach((c) => c.buildSymbolTable()); 109 | }, 110 | LetStatement(_let, id, _eq, _expr, _) { 111 | const name = id.sourceString; 112 | const idx = symbols.get('main').size; 113 | const info = {name, idx, what: 'local'}; 114 | symbols.get('main').set(name, info); 115 | }, 116 | }); 117 | tempSemantics(matchResult).buildSymbolTable(); 118 | return symbols; 119 | } 120 | 121 | function resolveSymbol(identNode, locals) { 122 | const identName = identNode.sourceString; 123 | if (locals.has(identName)) { 124 | return locals.get(identName); 125 | } 126 | throw new Error(`Error: undeclared identifier '${identName}'`); 127 | } 128 | 129 | const wafer = ohm.grammar(grammarDef); 130 | 131 | test('symbol table', () => { 132 | const getVarNames = (str) => { 133 | const symbols = buildSymbolTable(wafer, wafer.match(str)); 134 | return Array.from(symbols.get('main').keys()); 135 | }; 136 | 137 | assert.deepEqual(getVarNames('42'), []); 138 | assert.deepEqual(getVarNames('let x = 0; 42'), ['x']); 139 | assert.deepEqual(getVarNames('let x = 0; let y = 1; 42'), ['x', 'y']); 140 | 141 | const symbols = buildSymbolTable( 142 | wafer, 143 | wafer.match('let x = 0; let y = 1; 42'), 144 | ); 145 | const localVars = symbols.get('main'); 146 | assert.strictEqual(resolveSymbol({sourceString: 'x'}, localVars).idx, 0); 147 | assert.strictEqual(resolveSymbol({sourceString: 'y'}, localVars).idx, 1); 148 | assert.throws(() => resolveSymbol({sourceString: 'z'}, localVars)); 149 | }); 150 | 151 | function defineToWasm(semantics, localVars) { 152 | semantics.addOperation('toWasm', { 153 | Main(statementIter, expr) { 154 | return [ 155 | statementIter.children.map((c) => c.toWasm()), 156 | expr.toWasm(), 157 | instr.end, 158 | ]; 159 | }, 160 | LetStatement(_let, ident, _eq, expr, _) { 161 | const info = resolveSymbol(ident, localVars); 162 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 163 | }, 164 | Expr(num, iterOps, iterOperands) { 165 | const result = [num.toWasm()]; 166 | for (let i = 0; i < iterOps.numChildren; i++) { 167 | const op = iterOps.child(i); 168 | const operand = iterOperands.child(i); 169 | result.push(operand.toWasm(), op.toWasm()); 170 | } 171 | return result; 172 | }, 173 | PrimaryExpr_paren(_lparen, expr, _rparen) { 174 | return expr.toWasm(); 175 | }, 176 | op(char) { 177 | const op = char.sourceString; 178 | const instructionByOp = { 179 | '+': instr.i32.add, 180 | '-': instr.i32.sub, 181 | '*': instr.i32.mul, 182 | '/': instr.i32.div_s, 183 | }; 184 | if (!Object.hasOwn(instructionByOp, op)) { 185 | throw new Error(`Unhandled operator '${op}'`); 186 | } 187 | return instructionByOp[op]; 188 | }, 189 | number(_digits) { 190 | const num = parseInt(this.sourceString, 10); 191 | return [instr.i32.const, ...i32(num)]; 192 | }, 193 | }); 194 | } 195 | 196 | function toWasmFlat(input) { 197 | const matchResult = wafer.match(input); 198 | const symbols = buildSymbolTable(wafer, matchResult); 199 | const semantics = wafer.createSemantics(); 200 | defineToWasm(semantics, symbols.get('main')); 201 | return semantics(matchResult).toWasm().flat(Infinity); 202 | } 203 | 204 | test('toWasm bytecodes - locals & assignment', () => { 205 | assert.deepEqual(toWasmFlat('42'), [instr.i32.const, 42, instr.end]); 206 | assert.deepEqual( 207 | toWasmFlat('let x = 10; 42'), 208 | [ 209 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 210 | [instr.i32.const, 42], 211 | instr.end, 212 | ].flat(), 213 | ); 214 | }); 215 | -------------------------------------------------------------------------------- /chapter04/06-toWasm-readVars.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from '../chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Statement* Expr 30 | 31 | Statement = LetStatement 32 | 33 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 34 | //- "let y;" 35 | LetStatement = "let" identifier "=" Expr ";" 36 | 37 | Expr = PrimaryExpr (op PrimaryExpr)* 38 | 39 | PrimaryExpr = "(" Expr ")" -- paren 40 | | number 41 | | identifier -- var 42 | 43 | op = "+" | "-" | "*" | "/" 44 | number = digit+ 45 | 46 | //+ "x", "élan", "_", "_99" 47 | //- "1", "$nope" 48 | identifier = identStart identPart* 49 | identStart = letter | "_" 50 | identPart = identStart | digit 51 | 52 | // Examples: 53 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 54 | //+ "let x = 3; 42" 55 | //- "3abc" 56 | //- "let x = 3;" 57 | } 58 | `; 59 | 60 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 61 | 62 | instr.local = {}; 63 | instr.local.get = 0x20; 64 | instr.local.set = 0x21; 65 | instr.local.tee = 0x22; 66 | 67 | function locals(n, type) { 68 | return [u32(n), type]; 69 | } 70 | 71 | const localidx = (x) => u32(x); 72 | 73 | function compileLocals() { 74 | const mod = module([ 75 | typesec([functype([valtype.i32], [valtype.i32])]), 76 | funcsec([typeidx(0)]), 77 | exportsec([export_('f1', exportdesc.func(0))]), 78 | codesec([ 79 | code( 80 | func( 81 | [locals(1, valtype.i32)], 82 | [ 83 | [instr.i32.const, i32(42)], 84 | [instr.local.set, localidx(1)], 85 | [instr.local.get, localidx(0)], 86 | [instr.local.get, localidx(1)], 87 | instr.i32.add, 88 | instr.end, 89 | ], 90 | ), 91 | ), 92 | ]), 93 | ]); 94 | 95 | return Uint8Array.from(mod.flat(Infinity)); 96 | } 97 | 98 | test('compileLocals', () => { 99 | assert.strictEqual(loadMod(compileLocals()).f1(10), 52); 100 | }); 101 | 102 | function buildSymbolTable(grammar, matchResult) { 103 | const tempSemantics = grammar.createSemantics(); 104 | const symbols = new Map(); 105 | symbols.set('main', new Map()); 106 | tempSemantics.addOperation('buildSymbolTable', { 107 | _default(...children) { 108 | return children.forEach((c) => c.buildSymbolTable()); 109 | }, 110 | LetStatement(_let, id, _eq, _expr, _) { 111 | const name = id.sourceString; 112 | const idx = symbols.get('main').size; 113 | const info = {name, idx, what: 'local'}; 114 | symbols.get('main').set(name, info); 115 | }, 116 | }); 117 | tempSemantics(matchResult).buildSymbolTable(); 118 | return symbols; 119 | } 120 | 121 | function resolveSymbol(identNode, locals) { 122 | const identName = identNode.sourceString; 123 | if (locals.has(identName)) { 124 | return locals.get(identName); 125 | } 126 | throw new Error(`Error: undeclared identifier '${identName}'`); 127 | } 128 | 129 | const wafer = ohm.grammar(grammarDef); 130 | 131 | test('symbol table', () => { 132 | const getVarNames = (str) => { 133 | const symbols = buildSymbolTable(wafer, wafer.match(str)); 134 | return Array.from(symbols.get('main').keys()); 135 | }; 136 | 137 | assert.deepEqual(getVarNames('42'), []); 138 | assert.deepEqual(getVarNames('let x = 0; 42'), ['x']); 139 | assert.deepEqual(getVarNames('let x = 0; let y = 1; 42'), ['x', 'y']); 140 | 141 | const symbols = buildSymbolTable( 142 | wafer, 143 | wafer.match('let x = 0; let y = 1; 42'), 144 | ); 145 | const localVars = symbols.get('main'); 146 | assert.strictEqual(resolveSymbol({sourceString: 'x'}, localVars).idx, 0); 147 | assert.strictEqual(resolveSymbol({sourceString: 'y'}, localVars).idx, 1); 148 | assert.throws(() => resolveSymbol({sourceString: 'z'}, localVars)); 149 | }); 150 | 151 | function defineToWasm(semantics, localVars) { 152 | semantics.addOperation('toWasm', { 153 | Main(statementIter, expr) { 154 | return [ 155 | statementIter.children.map((c) => c.toWasm()), 156 | expr.toWasm(), 157 | instr.end, 158 | ]; 159 | }, 160 | LetStatement(_let, ident, _eq, expr, _) { 161 | const info = resolveSymbol(ident, localVars); 162 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 163 | }, 164 | Expr(num, iterOps, iterOperands) { 165 | const result = [num.toWasm()]; 166 | for (let i = 0; i < iterOps.numChildren; i++) { 167 | const op = iterOps.child(i); 168 | const operand = iterOperands.child(i); 169 | result.push(operand.toWasm(), op.toWasm()); 170 | } 171 | return result; 172 | }, 173 | PrimaryExpr_paren(_lparen, expr, _rparen) { 174 | return expr.toWasm(); 175 | }, 176 | PrimaryExpr_var(ident) { 177 | const info = resolveSymbol(ident, localVars); 178 | return [instr.local.get, localidx(info.idx)]; 179 | }, 180 | op(char) { 181 | const op = char.sourceString; 182 | const instructionByOp = { 183 | '+': instr.i32.add, 184 | '-': instr.i32.sub, 185 | '*': instr.i32.mul, 186 | '/': instr.i32.div_s, 187 | }; 188 | if (!Object.hasOwn(instructionByOp, op)) { 189 | throw new Error(`Unhandled operator '${op}'`); 190 | } 191 | return instructionByOp[op]; 192 | }, 193 | number(_digits) { 194 | const num = parseInt(this.sourceString, 10); 195 | return [instr.i32.const, ...i32(num)]; 196 | }, 197 | }); 198 | } 199 | 200 | function toWasmFlat(input) { 201 | const matchResult = wafer.match(input); 202 | const symbols = buildSymbolTable(wafer, matchResult); 203 | const semantics = wafer.createSemantics(); 204 | defineToWasm(semantics, symbols.get('main')); 205 | return semantics(matchResult).toWasm().flat(Infinity); 206 | } 207 | 208 | test('toWasm bytecodes - locals & assignment', () => { 209 | assert.deepEqual(toWasmFlat('42'), [instr.i32.const, 42, instr.end]); 210 | assert.deepEqual( 211 | toWasmFlat('let x = 10; 42'), 212 | [ 213 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 214 | [instr.i32.const, 42], 215 | instr.end, 216 | ].flat(), 217 | ); 218 | assert.deepEqual( 219 | toWasmFlat('let x = 10; x'), 220 | [ 221 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 222 | [instr.local.get, 0], // x 223 | instr.end, 224 | ].flat(), 225 | ); 226 | }); 227 | -------------------------------------------------------------------------------- /chapter04/07-toWasm-writeVars.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from '../chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Statement* Expr 30 | 31 | Statement = LetStatement 32 | | ExprStatement 33 | 34 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 35 | //- "let y;" 36 | LetStatement = "let" identifier "=" Expr ";" 37 | 38 | ExprStatement = Expr ";" 39 | 40 | Expr = AssignmentExpr -- assignment 41 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 42 | 43 | //+ "x := 3", "y := 2 + 1" 44 | AssignmentExpr = identifier ":=" Expr 45 | 46 | PrimaryExpr = "(" Expr ")" -- paren 47 | | number 48 | | identifier -- var 49 | 50 | op = "+" | "-" | "*" | "/" 51 | number = digit+ 52 | 53 | //+ "x", "élan", "_", "_99" 54 | //- "1", "$nope" 55 | identifier = identStart identPart* 56 | identStart = letter | "_" 57 | identPart = identStart | digit 58 | 59 | // Examples: 60 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 61 | //+ "let x = 3; 42" 62 | //- "3abc" 63 | //- "let x = 3;" 64 | } 65 | `; 66 | 67 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 68 | 69 | instr.local = {}; 70 | instr.local.get = 0x20; 71 | instr.local.set = 0x21; 72 | instr.local.tee = 0x22; 73 | 74 | function locals(n, type) { 75 | return [u32(n), type]; 76 | } 77 | 78 | const localidx = (x) => u32(x); 79 | 80 | function compileLocals() { 81 | const mod = module([ 82 | typesec([functype([valtype.i32], [valtype.i32])]), 83 | funcsec([typeidx(0)]), 84 | exportsec([export_('f1', exportdesc.func(0))]), 85 | codesec([ 86 | code( 87 | func( 88 | [locals(1, valtype.i32)], 89 | [ 90 | [instr.i32.const, i32(42)], 91 | [instr.local.set, localidx(1)], 92 | [instr.local.get, localidx(0)], 93 | [instr.local.get, localidx(1)], 94 | instr.i32.add, 95 | instr.end, 96 | ], 97 | ), 98 | ), 99 | ]), 100 | ]); 101 | 102 | return Uint8Array.from(mod.flat(Infinity)); 103 | } 104 | 105 | test('compileLocals', () => { 106 | assert.strictEqual(loadMod(compileLocals()).f1(10), 52); 107 | }); 108 | 109 | function buildSymbolTable(grammar, matchResult) { 110 | const tempSemantics = grammar.createSemantics(); 111 | const symbols = new Map(); 112 | symbols.set('main', new Map()); 113 | tempSemantics.addOperation('buildSymbolTable', { 114 | _default(...children) { 115 | return children.forEach((c) => c.buildSymbolTable()); 116 | }, 117 | LetStatement(_let, id, _eq, _expr, _) { 118 | const name = id.sourceString; 119 | const idx = symbols.get('main').size; 120 | const info = {name, idx, what: 'local'}; 121 | symbols.get('main').set(name, info); 122 | }, 123 | }); 124 | tempSemantics(matchResult).buildSymbolTable(); 125 | return symbols; 126 | } 127 | 128 | function resolveSymbol(identNode, locals) { 129 | const identName = identNode.sourceString; 130 | if (locals.has(identName)) { 131 | return locals.get(identName); 132 | } 133 | throw new Error(`Error: undeclared identifier '${identName}'`); 134 | } 135 | 136 | const wafer = ohm.grammar(grammarDef); 137 | 138 | test('symbol table', () => { 139 | const getVarNames = (str) => { 140 | const symbols = buildSymbolTable(wafer, wafer.match(str)); 141 | return Array.from(symbols.get('main').keys()); 142 | }; 143 | 144 | assert.deepEqual(getVarNames('42'), []); 145 | assert.deepEqual(getVarNames('let x = 0; 42'), ['x']); 146 | assert.deepEqual(getVarNames('let x = 0; let y = 1; 42'), ['x', 'y']); 147 | 148 | const symbols = buildSymbolTable( 149 | wafer, 150 | wafer.match('let x = 0; let y = 1; 42'), 151 | ); 152 | const localVars = symbols.get('main'); 153 | assert.strictEqual(resolveSymbol({sourceString: 'x'}, localVars).idx, 0); 154 | assert.strictEqual(resolveSymbol({sourceString: 'y'}, localVars).idx, 1); 155 | assert.throws(() => resolveSymbol({sourceString: 'z'}, localVars)); 156 | }); 157 | 158 | function defineToWasm(semantics, localVars) { 159 | semantics.addOperation('toWasm', { 160 | Main(statementIter, expr) { 161 | return [ 162 | statementIter.children.map((c) => c.toWasm()), 163 | expr.toWasm(), 164 | instr.end, 165 | ]; 166 | }, 167 | LetStatement(_let, ident, _eq, expr, _) { 168 | const info = resolveSymbol(ident, localVars); 169 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 170 | }, 171 | ExprStatement(expr, _) { 172 | return [expr.toWasm(), instr.drop]; 173 | }, 174 | Expr_arithmetic(num, iterOps, iterOperands) { 175 | const result = [num.toWasm()]; 176 | for (let i = 0; i < iterOps.numChildren; i++) { 177 | const op = iterOps.child(i); 178 | const operand = iterOperands.child(i); 179 | result.push(operand.toWasm(), op.toWasm()); 180 | } 181 | return result; 182 | }, 183 | AssignmentExpr(ident, _, expr) { 184 | const info = resolveSymbol(ident, localVars); 185 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 186 | }, 187 | PrimaryExpr_paren(_lparen, expr, _rparen) { 188 | return expr.toWasm(); 189 | }, 190 | PrimaryExpr_var(ident) { 191 | const info = resolveSymbol(ident, localVars); 192 | return [instr.local.get, localidx(info.idx)]; 193 | }, 194 | op(char) { 195 | const op = char.sourceString; 196 | const instructionByOp = { 197 | '+': instr.i32.add, 198 | '-': instr.i32.sub, 199 | '*': instr.i32.mul, 200 | '/': instr.i32.div_s, 201 | }; 202 | if (!Object.hasOwn(instructionByOp, op)) { 203 | throw new Error(`Unhandled operator '${op}'`); 204 | } 205 | return instructionByOp[op]; 206 | }, 207 | number(_digits) { 208 | const num = parseInt(this.sourceString, 10); 209 | return [instr.i32.const, ...i32(num)]; 210 | }, 211 | }); 212 | } 213 | 214 | function toWasmFlat(input) { 215 | const matchResult = wafer.match(input); 216 | const symbols = buildSymbolTable(wafer, matchResult); 217 | const semantics = wafer.createSemantics(); 218 | defineToWasm(semantics, symbols.get('main')); 219 | return semantics(matchResult).toWasm().flat(Infinity); 220 | } 221 | 222 | test('toWasm bytecodes - locals & assignment', () => { 223 | assert.deepEqual(toWasmFlat('42'), [instr.i32.const, 42, instr.end]); 224 | assert.deepEqual( 225 | toWasmFlat('let x = 10; 42'), 226 | [ 227 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 228 | [instr.i32.const, 42], 229 | instr.end, 230 | ].flat(), 231 | ); 232 | assert.deepEqual( 233 | toWasmFlat('let x = 10; x'), 234 | [ 235 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 236 | [instr.local.get, 0], 237 | instr.end, 238 | ].flat(), 239 | ); 240 | assert.deepEqual( 241 | toWasmFlat('let x = 10; x := 9; x'), 242 | [ 243 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 244 | [instr.i32.const, 9, instr.local.tee, 0, instr.drop], // x := 9; 245 | [instr.local.get, 0], // x 246 | instr.end, 247 | ].flat(), 248 | ); 249 | }); 250 | 251 | instr.drop = 0x1a; 252 | -------------------------------------------------------------------------------- /chapter04/08-compileAndEval.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | export_, 8 | exportdesc, 9 | exportsec, 10 | func, 11 | funcsec, 12 | functype, 13 | i32, 14 | instr, 15 | loadMod, 16 | makeTestFn, 17 | module, 18 | testExtractedExamples, 19 | typeidx, 20 | typesec, 21 | u32, 22 | valtype, 23 | } from '../chapter03.js'; 24 | 25 | const test = makeTestFn(import.meta.url); 26 | 27 | const grammarDef = ` 28 | Wafer { 29 | Main = Statement* Expr 30 | 31 | Statement = LetStatement 32 | | ExprStatement 33 | 34 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 35 | //- "let y;" 36 | LetStatement = "let" identifier "=" Expr ";" 37 | 38 | ExprStatement = Expr ";" 39 | 40 | Expr = AssignmentExpr -- assignment 41 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 42 | 43 | //+ "x := 3", "y := 2 + 1" 44 | AssignmentExpr = identifier ":=" Expr 45 | 46 | PrimaryExpr = "(" Expr ")" -- paren 47 | | number 48 | | identifier -- var 49 | 50 | op = "+" | "-" | "*" | "/" 51 | number = digit+ 52 | 53 | //+ "x", "élan", "_", "_99" 54 | //- "1", "$nope" 55 | identifier = identStart identPart* 56 | identStart = letter | "_" 57 | identPart = identStart | digit 58 | 59 | // Examples: 60 | //+ "42", "1", "66 + 99", "1 + 2 - 3" 61 | //+ "let x = 3; 42" 62 | //- "3abc" 63 | //- "let x = 3;" 64 | } 65 | `; 66 | 67 | test('Extracted examples', () => testExtractedExamples(grammarDef)); 68 | 69 | instr.local = {}; 70 | instr.local.get = 0x20; 71 | instr.local.set = 0x21; 72 | instr.local.tee = 0x22; 73 | 74 | function locals(n, type) { 75 | return [u32(n), type]; 76 | } 77 | 78 | const localidx = (x) => u32(x); 79 | 80 | function compileLocals() { 81 | const mod = module([ 82 | typesec([functype([valtype.i32], [valtype.i32])]), 83 | funcsec([typeidx(0)]), 84 | exportsec([export_('f1', exportdesc.func(0))]), 85 | codesec([ 86 | code( 87 | func( 88 | [locals(1, valtype.i32)], 89 | [ 90 | [instr.i32.const, i32(42)], 91 | [instr.local.set, localidx(1)], 92 | [instr.local.get, localidx(0)], 93 | [instr.local.get, localidx(1)], 94 | instr.i32.add, 95 | instr.end, 96 | ], 97 | ), 98 | ), 99 | ]), 100 | ]); 101 | 102 | return Uint8Array.from(mod.flat(Infinity)); 103 | } 104 | 105 | test('compileLocals', () => { 106 | assert.strictEqual(loadMod(compileLocals()).f1(10), 52); 107 | }); 108 | 109 | function buildSymbolTable(grammar, matchResult) { 110 | const tempSemantics = grammar.createSemantics(); 111 | const symbols = new Map(); 112 | symbols.set('main', new Map()); 113 | tempSemantics.addOperation('buildSymbolTable', { 114 | _default(...children) { 115 | return children.forEach((c) => c.buildSymbolTable()); 116 | }, 117 | LetStatement(_let, id, _eq, _expr, _) { 118 | const name = id.sourceString; 119 | const idx = symbols.get('main').size; 120 | const info = {name, idx, what: 'local'}; 121 | symbols.get('main').set(name, info); 122 | }, 123 | }); 124 | tempSemantics(matchResult).buildSymbolTable(); 125 | return symbols; 126 | } 127 | 128 | function resolveSymbol(identNode, locals) { 129 | const identName = identNode.sourceString; 130 | if (locals.has(identName)) { 131 | return locals.get(identName); 132 | } 133 | throw new Error(`Error: undeclared identifier '${identName}'`); 134 | } 135 | 136 | const wafer = ohm.grammar(grammarDef); 137 | 138 | test('symbol table', () => { 139 | const getVarNames = (str) => { 140 | const symbols = buildSymbolTable(wafer, wafer.match(str)); 141 | return Array.from(symbols.get('main').keys()); 142 | }; 143 | 144 | assert.deepEqual(getVarNames('42'), []); 145 | assert.deepEqual(getVarNames('let x = 0; 42'), ['x']); 146 | assert.deepEqual(getVarNames('let x = 0; let y = 1; 42'), ['x', 'y']); 147 | 148 | const symbols = buildSymbolTable( 149 | wafer, 150 | wafer.match('let x = 0; let y = 1; 42'), 151 | ); 152 | const localVars = symbols.get('main'); 153 | assert.strictEqual(resolveSymbol({sourceString: 'x'}, localVars).idx, 0); 154 | assert.strictEqual(resolveSymbol({sourceString: 'y'}, localVars).idx, 1); 155 | assert.throws(() => resolveSymbol({sourceString: 'z'}, localVars)); 156 | }); 157 | 158 | function defineToWasm(semantics, localVars) { 159 | semantics.addOperation('toWasm', { 160 | Main(statementIter, expr) { 161 | return [ 162 | statementIter.children.map((c) => c.toWasm()), 163 | expr.toWasm(), 164 | instr.end, 165 | ]; 166 | }, 167 | LetStatement(_let, ident, _eq, expr, _) { 168 | const info = resolveSymbol(ident, localVars); 169 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 170 | }, 171 | ExprStatement(expr, _) { 172 | return [expr.toWasm(), instr.drop]; 173 | }, 174 | Expr_arithmetic(num, iterOps, iterOperands) { 175 | const result = [num.toWasm()]; 176 | for (let i = 0; i < iterOps.numChildren; i++) { 177 | const op = iterOps.child(i); 178 | const operand = iterOperands.child(i); 179 | result.push(operand.toWasm(), op.toWasm()); 180 | } 181 | return result; 182 | }, 183 | AssignmentExpr(ident, _, expr) { 184 | const info = resolveSymbol(ident, localVars); 185 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 186 | }, 187 | PrimaryExpr_paren(_lparen, expr, _rparen) { 188 | return expr.toWasm(); 189 | }, 190 | PrimaryExpr_var(ident) { 191 | const info = resolveSymbol(ident, localVars); 192 | return [instr.local.get, localidx(info.idx)]; 193 | }, 194 | op(char) { 195 | const op = char.sourceString; 196 | const instructionByOp = { 197 | '+': instr.i32.add, 198 | '-': instr.i32.sub, 199 | '*': instr.i32.mul, 200 | '/': instr.i32.div_s, 201 | }; 202 | if (!Object.hasOwn(instructionByOp, op)) { 203 | throw new Error(`Unhandled operator '${op}'`); 204 | } 205 | return instructionByOp[op]; 206 | }, 207 | number(_digits) { 208 | const num = parseInt(this.sourceString, 10); 209 | return [instr.i32.const, ...i32(num)]; 210 | }, 211 | }); 212 | } 213 | 214 | function toWasmFlat(input) { 215 | const matchResult = wafer.match(input); 216 | const symbols = buildSymbolTable(wafer, matchResult); 217 | const semantics = wafer.createSemantics(); 218 | defineToWasm(semantics, symbols.get('main')); 219 | return semantics(matchResult).toWasm().flat(Infinity); 220 | } 221 | 222 | test('toWasm bytecodes - locals & assignment', () => { 223 | assert.deepEqual(toWasmFlat('42'), [instr.i32.const, 42, instr.end]); 224 | assert.deepEqual( 225 | toWasmFlat('let x = 10; 42'), 226 | [ 227 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 228 | [instr.i32.const, 42], 229 | instr.end, 230 | ].flat(), 231 | ); 232 | assert.deepEqual( 233 | toWasmFlat('let x = 10; x'), 234 | [ 235 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 236 | [instr.local.get, 0], 237 | instr.end, 238 | ].flat(), 239 | ); 240 | assert.deepEqual( 241 | toWasmFlat('let x = 10; x := 9; x'), 242 | [ 243 | [instr.i32.const, 10, instr.local.set, 0], // let x = 10; 244 | [instr.i32.const, 9, instr.local.tee, 0, instr.drop], // x := 9; 245 | [instr.local.get, 0], // x 246 | instr.end, 247 | ].flat(), 248 | ); 249 | }); 250 | 251 | instr.drop = 0x1a; 252 | 253 | function compile(source) { 254 | const matchResult = wafer.match(source); 255 | if (!matchResult.succeeded()) { 256 | throw new Error(matchResult.message); 257 | } 258 | 259 | const semantics = wafer.createSemantics(); 260 | const symbols = buildSymbolTable(wafer, matchResult); 261 | const localVars = symbols.get('main'); 262 | defineToWasm(semantics, localVars); 263 | 264 | const mainFn = func( 265 | [locals(localVars.size, valtype.i32)], 266 | semantics(matchResult).toWasm(), 267 | ); 268 | const mod = module([ 269 | typesec([functype([], [valtype.i32])]), 270 | funcsec([typeidx(0)]), 271 | exportsec([export_('main', exportdesc.func(0))]), 272 | codesec([code(mainFn)]), 273 | ]); 274 | return Uint8Array.from(mod.flat(Infinity)); 275 | } 276 | 277 | test('module with locals & assignment', () => { 278 | assert.deepEqual(loadMod(compile('let x = 42; x')).main(), 42); 279 | assert.deepEqual( 280 | loadMod(compile('let a = 13; let b = 15; a := 10; a + b')).main(), 281 | 25, 282 | ); 283 | }); 284 | -------------------------------------------------------------------------------- /chapter05/01-buildModule-two.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildSymbolTable, 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcidx, 13 | funcsec, 14 | functype, 15 | i32, 16 | instr, 17 | loadMod, 18 | localidx, 19 | locals, 20 | makeTestFn, 21 | module, 22 | resolveSymbol, 23 | testExtractedExamples, 24 | typeidx, 25 | typesec, 26 | valtype, 27 | } from '../chapter04.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | function buildModule() { 32 | const functionDecls = [ 33 | { 34 | name: 'main', 35 | locals: [locals(1, valtype.i32)], 36 | body: [instr.i32.const, i32(42), instr.end], 37 | }, 38 | { 39 | name: 'backup', 40 | locals: [], 41 | body: [instr.i32.const, i32(43), instr.end], 42 | }, 43 | ]; 44 | const funcs = functionDecls.map((f) => typeidx(0)); 45 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 46 | const exports = functionDecls.map((f, i) => 47 | export_(f.name, exportdesc.func(i)), 48 | ); 49 | 50 | const mod = module([ 51 | typesec([functype([], [valtype.i32])]), 52 | funcsec(funcs), 53 | exportsec(exports), 54 | codesec(codes), 55 | ]); 56 | return Uint8Array.from(mod.flat(Infinity)); 57 | } 58 | 59 | test('buildModule', () => { 60 | const exports = loadMod(buildModule()); 61 | assert.strictEqual(exports.main(), 42); 62 | assert.strictEqual(exports.backup(), 43); 63 | }); 64 | -------------------------------------------------------------------------------- /chapter05/02-buildModule-many.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildSymbolTable, 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcidx, 13 | funcsec, 14 | functype, 15 | i32, 16 | instr, 17 | loadMod, 18 | localidx, 19 | locals, 20 | makeTestFn, 21 | module, 22 | resolveSymbol, 23 | testExtractedExamples, 24 | typeidx, 25 | typesec, 26 | valtype, 27 | } from '../chapter04.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | function buildModule(functionDecls) { 32 | const types = functionDecls.map((f) => 33 | functype(f.paramTypes, [f.resultType]), 34 | ); 35 | const funcs = functionDecls.map((f, i) => typeidx(i)); 36 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 37 | const exports = functionDecls.map((f, i) => 38 | export_(f.name, exportdesc.func(i)), 39 | ); 40 | 41 | const mod = module([ 42 | typesec(types), 43 | funcsec(funcs), 44 | exportsec(exports), 45 | codesec(codes), 46 | ]); 47 | return Uint8Array.from(mod.flat(Infinity)); 48 | } 49 | 50 | test('buildModule', () => { 51 | const functionDecls = [ 52 | { 53 | name: 'main', 54 | paramTypes: [], 55 | resultType: valtype.i32, 56 | locals: [locals(1, valtype.i32)], 57 | body: [instr.i32.const, i32(42), instr.end], 58 | }, 59 | { 60 | name: 'backup', 61 | paramTypes: [], 62 | resultType: valtype.i32, 63 | locals: [], 64 | body: [instr.i32.const, i32(43), instr.end], 65 | }, 66 | ]; 67 | const exports = loadMod(buildModule(functionDecls)); 68 | assert.strictEqual(exports.main(), 42); 69 | assert.strictEqual(exports.backup(), 43); 70 | }); 71 | -------------------------------------------------------------------------------- /chapter05/03-buildModule-call.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildSymbolTable, 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcidx, 13 | funcsec, 14 | functype, 15 | i32, 16 | instr, 17 | loadMod, 18 | localidx, 19 | locals, 20 | makeTestFn, 21 | module, 22 | resolveSymbol, 23 | testExtractedExamples, 24 | typeidx, 25 | typesec, 26 | valtype, 27 | } from '../chapter04.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | function buildModule(functionDecls) { 32 | const types = functionDecls.map((f) => 33 | functype(f.paramTypes, [f.resultType]), 34 | ); 35 | const funcs = functionDecls.map((f, i) => typeidx(i)); 36 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 37 | const exports = functionDecls.map((f, i) => 38 | export_(f.name, exportdesc.func(i)), 39 | ); 40 | 41 | const mod = module([ 42 | typesec(types), 43 | funcsec(funcs), 44 | exportsec(exports), 45 | codesec(codes), 46 | ]); 47 | return Uint8Array.from(mod.flat(Infinity)); 48 | } 49 | 50 | test('buildModule', () => { 51 | const functionDecls = [ 52 | { 53 | name: 'main', 54 | paramTypes: [], 55 | resultType: valtype.i32, 56 | locals: [locals(1, valtype.i32)], 57 | body: [instr.i32.const, i32(42), instr.call, funcidx(1), instr.end], 58 | }, 59 | { 60 | name: 'backup', 61 | paramTypes: [valtype.i32], 62 | resultType: valtype.i32, 63 | locals: [], 64 | body: [instr.i32.const, i32(43), instr.end], 65 | }, 66 | ]; 67 | const exports = loadMod(buildModule(functionDecls)); 68 | assert.strictEqual(exports.main(), 43); 69 | assert.strictEqual(exports.backup(), 43); 70 | }); 71 | 72 | instr.call = 0x10; 73 | -------------------------------------------------------------------------------- /chapter05/04-functionDecl.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildSymbolTable, 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcidx, 13 | funcsec, 14 | functype, 15 | i32, 16 | instr, 17 | loadMod, 18 | localidx, 19 | locals, 20 | makeTestFn, 21 | module, 22 | resolveSymbol, 23 | testExtractedExamples, 24 | typeidx, 25 | typesec, 26 | valtype, 27 | } from '../chapter04.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | function buildModule(functionDecls) { 32 | const types = functionDecls.map((f) => 33 | functype(f.paramTypes, [f.resultType]), 34 | ); 35 | const funcs = functionDecls.map((f, i) => typeidx(i)); 36 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 37 | const exports = functionDecls.map((f, i) => 38 | export_(f.name, exportdesc.func(i)), 39 | ); 40 | 41 | const mod = module([ 42 | typesec(types), 43 | funcsec(funcs), 44 | exportsec(exports), 45 | codesec(codes), 46 | ]); 47 | return Uint8Array.from(mod.flat(Infinity)); 48 | } 49 | 50 | test('buildModule', () => { 51 | const functionDecls = [ 52 | { 53 | name: 'main', 54 | paramTypes: [], 55 | resultType: valtype.i32, 56 | locals: [locals(1, valtype.i32)], 57 | body: [instr.i32.const, i32(42), instr.call, funcidx(1), instr.end], 58 | }, 59 | { 60 | name: 'backup', 61 | paramTypes: [valtype.i32], 62 | resultType: valtype.i32, 63 | locals: [], 64 | body: [instr.i32.const, i32(43), instr.end], 65 | }, 66 | ]; 67 | const exports = loadMod(buildModule(functionDecls)); 68 | assert.strictEqual(exports.main(), 43); 69 | assert.strictEqual(exports.backup(), 43); 70 | }); 71 | 72 | instr.call = 0x10; 73 | 74 | const grammarDef = ` 75 | Wafer { 76 | Module = FunctionDecl* 77 | 78 | Statement = LetStatement 79 | | ExprStatement 80 | 81 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 82 | //- "let y;" 83 | LetStatement = "let" identifier "=" Expr ";" 84 | 85 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 86 | //- "func x", "func x();" 87 | FunctionDecl = "func" identifier "(" Params? ")" BlockExpr 88 | 89 | Params = identifier ("," identifier)* 90 | 91 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 92 | //+ "{ let x = 3; 42 }" 93 | //- "{ 3abc }" 94 | BlockExpr = "{" Statement* Expr "}" 95 | 96 | ExprStatement = Expr ";" 97 | 98 | Expr = AssignmentExpr -- assignment 99 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 100 | 101 | //+ "x := 3", "y := 2 + 1" 102 | AssignmentExpr = identifier ":=" Expr 103 | 104 | PrimaryExpr = "(" Expr ")" -- paren 105 | | number 106 | | identifier -- var 107 | 108 | op = "+" | "-" | "*" | "/" 109 | number = digit+ 110 | 111 | //+ "x", "élan", "_", "_99" 112 | //- "1", "$nope" 113 | identifier = identStart identPart* 114 | identStart = letter | "_" 115 | identPart = identStart | digit 116 | 117 | // Examples: 118 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 119 | //- "42", "let x", "func x {}" 120 | } 121 | `; 122 | 123 | test('extracted examples', () => testExtractedExamples(grammarDef)); 124 | 125 | const wafer = ohm.grammar(grammarDef); 126 | -------------------------------------------------------------------------------- /chapter05/05-toWasm.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildSymbolTable, 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcidx, 13 | funcsec, 14 | functype, 15 | i32, 16 | instr, 17 | loadMod, 18 | localidx, 19 | locals, 20 | makeTestFn, 21 | module, 22 | resolveSymbol, 23 | testExtractedExamples, 24 | typeidx, 25 | typesec, 26 | valtype, 27 | } from '../chapter04.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | function buildModule(functionDecls) { 32 | const types = functionDecls.map((f) => 33 | functype(f.paramTypes, [f.resultType]), 34 | ); 35 | const funcs = functionDecls.map((f, i) => typeidx(i)); 36 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 37 | const exports = functionDecls.map((f, i) => 38 | export_(f.name, exportdesc.func(i)), 39 | ); 40 | 41 | const mod = module([ 42 | typesec(types), 43 | funcsec(funcs), 44 | exportsec(exports), 45 | codesec(codes), 46 | ]); 47 | return Uint8Array.from(mod.flat(Infinity)); 48 | } 49 | 50 | test('buildModule', () => { 51 | const functionDecls = [ 52 | { 53 | name: 'main', 54 | paramTypes: [], 55 | resultType: valtype.i32, 56 | locals: [locals(1, valtype.i32)], 57 | body: [instr.i32.const, i32(42), instr.call, funcidx(1), instr.end], 58 | }, 59 | { 60 | name: 'backup', 61 | paramTypes: [valtype.i32], 62 | resultType: valtype.i32, 63 | locals: [], 64 | body: [instr.i32.const, i32(43), instr.end], 65 | }, 66 | ]; 67 | const exports = loadMod(buildModule(functionDecls)); 68 | assert.strictEqual(exports.main(), 43); 69 | assert.strictEqual(exports.backup(), 43); 70 | }); 71 | 72 | instr.call = 0x10; 73 | 74 | const grammarDef = ` 75 | Wafer { 76 | Module = FunctionDecl* 77 | 78 | Statement = LetStatement 79 | | ExprStatement 80 | 81 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 82 | //- "let y;" 83 | LetStatement = "let" identifier "=" Expr ";" 84 | 85 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 86 | //- "func x", "func x();" 87 | FunctionDecl = "func" identifier "(" Params? ")" BlockExpr 88 | 89 | Params = identifier ("," identifier)* 90 | 91 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 92 | //+ "{ let x = 3; 42 }" 93 | //- "{ 3abc }" 94 | BlockExpr = "{" Statement* Expr "}" 95 | 96 | ExprStatement = Expr ";" 97 | 98 | Expr = AssignmentExpr -- assignment 99 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 100 | 101 | //+ "x := 3", "y := 2 + 1" 102 | AssignmentExpr = identifier ":=" Expr 103 | 104 | PrimaryExpr = "(" Expr ")" -- paren 105 | | number 106 | | identifier -- var 107 | 108 | op = "+" | "-" | "*" | "/" 109 | number = digit+ 110 | 111 | //+ "x", "élan", "_", "_99" 112 | //- "1", "$nope" 113 | identifier = identStart identPart* 114 | identStart = letter | "_" 115 | identPart = identStart | digit 116 | 117 | // Examples: 118 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 119 | //- "42", "let x", "func x {}" 120 | } 121 | `; 122 | 123 | test('extracted examples', () => testExtractedExamples(grammarDef)); 124 | 125 | const wafer = ohm.grammar(grammarDef); 126 | 127 | function defineToWasm(semantics, symbols) { 128 | const scopes = [symbols]; 129 | semantics.addOperation('toWasm', { 130 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 131 | scopes.push(symbols.get(ident.sourceString)); 132 | const result = [blockExpr.toWasm(), instr.end]; 133 | scopes.pop(); 134 | return result; 135 | }, 136 | BlockExpr(_lbrace, iterStatement, expr, _rbrace) { 137 | return [...iterStatement.children, expr].map((c) => c.toWasm()); 138 | }, 139 | LetStatement(_let, ident, _eq, expr, _) { 140 | const info = resolveSymbol(ident, scopes.at(-1)); 141 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 142 | }, 143 | ExprStatement(expr, _) { 144 | return [expr.toWasm(), instr.drop]; 145 | }, 146 | Expr_arithmetic(num, iterOps, iterOperands) { 147 | const result = [num.toWasm()]; 148 | for (let i = 0; i < iterOps.numChildren; i++) { 149 | const op = iterOps.child(i); 150 | const operand = iterOperands.child(i); 151 | result.push(operand.toWasm(), op.toWasm()); 152 | } 153 | return result; 154 | }, 155 | AssignmentExpr(ident, _, expr) { 156 | const info = resolveSymbol(ident, scopes.at(-1)); 157 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 158 | }, 159 | PrimaryExpr_paren(_lparen, expr, _rparen) { 160 | return expr.toWasm(); 161 | }, 162 | PrimaryExpr_var(ident) { 163 | const info = resolveSymbol(ident, scopes.at(-1)); 164 | return [instr.local.get, localidx(info.idx)]; 165 | }, 166 | op(char) { 167 | const op = char.sourceString; 168 | const instructionByOp = { 169 | '+': instr.i32.add, 170 | '-': instr.i32.sub, 171 | '*': instr.i32.mul, 172 | '/': instr.i32.div_s, 173 | }; 174 | if (!Object.hasOwn(instructionByOp, op)) { 175 | throw new Error(`Unhandled operator '${op}'`); 176 | } 177 | return instructionByOp[op]; 178 | }, 179 | number(_digits) { 180 | const num = parseInt(this.sourceString, 10); 181 | return [instr.i32.const, ...i32(num)]; 182 | }, 183 | }); 184 | } 185 | 186 | test('toWasm bytecodes - locals & assignment', () => { 187 | assert.deepEqual( 188 | toWasmFlat('func main() { 42 }'), 189 | [[instr.i32.const, 42], instr.end].flat(), 190 | ); 191 | assert.deepEqual( 192 | toWasmFlat('func main() { let x = 0; 42 }'), 193 | [ 194 | [instr.i32.const, 0], 195 | [instr.local.set, 0], 196 | [instr.i32.const, 42], 197 | instr.end, 198 | ].flat(), 199 | ); 200 | assert.deepEqual( 201 | toWasmFlat('func main() { let x = 0; x }'), 202 | [ 203 | [instr.i32.const, 0], 204 | [instr.local.set, 0], 205 | [instr.local.get, 0], 206 | instr.end, 207 | ].flat(), 208 | ); 209 | }); 210 | 211 | function toWasmFlat(input) { 212 | const matchResult = wafer.match(input, 'FunctionDecl'); 213 | const symbols = buildSymbolTable(wafer, matchResult); 214 | const semantics = wafer.createSemantics(); 215 | defineToWasm(semantics, symbols); 216 | return semantics(matchResult).toWasm().flat(Infinity); 217 | } 218 | -------------------------------------------------------------------------------- /chapter05/06-buildSymbolTable.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | // buildSymbolTable, 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcidx, 13 | funcsec, 14 | functype, 15 | i32, 16 | instr, 17 | loadMod, 18 | localidx, 19 | locals, 20 | makeTestFn, 21 | module, 22 | resolveSymbol, 23 | testExtractedExamples, 24 | typeidx, 25 | typesec, 26 | valtype, 27 | } from '../chapter04.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | function buildModule(functionDecls) { 32 | const types = functionDecls.map((f) => 33 | functype(f.paramTypes, [f.resultType]), 34 | ); 35 | const funcs = functionDecls.map((f, i) => typeidx(i)); 36 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 37 | const exports = functionDecls.map((f, i) => 38 | export_(f.name, exportdesc.func(i)), 39 | ); 40 | 41 | const mod = module([ 42 | typesec(types), 43 | funcsec(funcs), 44 | exportsec(exports), 45 | codesec(codes), 46 | ]); 47 | return Uint8Array.from(mod.flat(Infinity)); 48 | } 49 | 50 | test('buildModule', () => { 51 | const functionDecls = [ 52 | { 53 | name: 'main', 54 | paramTypes: [], 55 | resultType: valtype.i32, 56 | locals: [locals(1, valtype.i32)], 57 | body: [instr.i32.const, i32(42), instr.call, funcidx(1), instr.end], 58 | }, 59 | { 60 | name: 'backup', 61 | paramTypes: [valtype.i32], 62 | resultType: valtype.i32, 63 | locals: [], 64 | body: [instr.i32.const, i32(43), instr.end], 65 | }, 66 | ]; 67 | const exports = loadMod(buildModule(functionDecls)); 68 | assert.strictEqual(exports.main(), 43); 69 | assert.strictEqual(exports.backup(), 43); 70 | }); 71 | 72 | instr.call = 0x10; 73 | 74 | const grammarDef = ` 75 | Wafer { 76 | Module = FunctionDecl* 77 | 78 | Statement = LetStatement 79 | | ExprStatement 80 | 81 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 82 | //- "let y;" 83 | LetStatement = "let" identifier "=" Expr ";" 84 | 85 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 86 | //- "func x", "func x();" 87 | FunctionDecl = "func" identifier "(" Params? ")" BlockExpr 88 | 89 | Params = identifier ("," identifier)* 90 | 91 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 92 | //+ "{ let x = 3; 42 }" 93 | //- "{ 3abc }" 94 | BlockExpr = "{" Statement* Expr "}" 95 | 96 | ExprStatement = Expr ";" 97 | 98 | Expr = AssignmentExpr -- assignment 99 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 100 | 101 | //+ "x := 3", "y := 2 + 1" 102 | AssignmentExpr = identifier ":=" Expr 103 | 104 | PrimaryExpr = "(" Expr ")" -- paren 105 | | number 106 | | identifier -- var 107 | 108 | op = "+" | "-" | "*" | "/" 109 | number = digit+ 110 | 111 | //+ "x", "élan", "_", "_99" 112 | //- "1", "$nope" 113 | identifier = identStart identPart* 114 | identStart = letter | "_" 115 | identPart = identStart | digit 116 | 117 | // Examples: 118 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 119 | //- "42", "let x", "func x {}" 120 | } 121 | `; 122 | 123 | test('extracted examples', () => testExtractedExamples(grammarDef)); 124 | 125 | const wafer = ohm.grammar(grammarDef); 126 | 127 | function defineToWasm(semantics, symbols) { 128 | const scopes = [symbols]; 129 | semantics.addOperation('toWasm', { 130 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 131 | scopes.push(symbols.get(ident.sourceString)); 132 | const result = [blockExpr.toWasm(), instr.end]; 133 | scopes.pop(); 134 | return result; 135 | }, 136 | BlockExpr(_lbrace, iterStatement, expr, _rbrace) { 137 | return [...iterStatement.children, expr].map((c) => c.toWasm()); 138 | }, 139 | LetStatement(_let, ident, _eq, expr, _) { 140 | const info = resolveSymbol(ident, scopes.at(-1)); 141 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 142 | }, 143 | ExprStatement(expr, _) { 144 | return [expr.toWasm(), instr.drop]; 145 | }, 146 | Expr_arithmetic(num, iterOps, iterOperands) { 147 | const result = [num.toWasm()]; 148 | for (let i = 0; i < iterOps.numChildren; i++) { 149 | const op = iterOps.child(i); 150 | const operand = iterOperands.child(i); 151 | result.push(operand.toWasm(), op.toWasm()); 152 | } 153 | return result; 154 | }, 155 | AssignmentExpr(ident, _, expr) { 156 | const info = resolveSymbol(ident, scopes.at(-1)); 157 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 158 | }, 159 | PrimaryExpr_paren(_lparen, expr, _rparen) { 160 | return expr.toWasm(); 161 | }, 162 | PrimaryExpr_var(ident) { 163 | const info = resolveSymbol(ident, scopes.at(-1)); 164 | return [instr.local.get, localidx(info.idx)]; 165 | }, 166 | op(char) { 167 | const op = char.sourceString; 168 | const instructionByOp = { 169 | '+': instr.i32.add, 170 | '-': instr.i32.sub, 171 | '*': instr.i32.mul, 172 | '/': instr.i32.div_s, 173 | }; 174 | if (!Object.hasOwn(instructionByOp, op)) { 175 | throw new Error(`Unhandled operator '${op}'`); 176 | } 177 | return instructionByOp[op]; 178 | }, 179 | number(_digits) { 180 | const num = parseInt(this.sourceString, 10); 181 | return [instr.i32.const, ...i32(num)]; 182 | }, 183 | }); 184 | } 185 | 186 | test('toWasm bytecodes - locals & assignment', () => { 187 | assert.deepEqual( 188 | toWasmFlat('func main() { 42 }'), 189 | [[instr.i32.const, 42], instr.end].flat(), 190 | ); 191 | assert.deepEqual( 192 | toWasmFlat('func main() { let x = 0; 42 }'), 193 | [ 194 | [instr.i32.const, 0], 195 | [instr.local.set, 0], 196 | [instr.i32.const, 42], 197 | instr.end, 198 | ].flat(), 199 | ); 200 | assert.deepEqual( 201 | toWasmFlat('func main() { let x = 0; x }'), 202 | [ 203 | [instr.i32.const, 0], 204 | [instr.local.set, 0], 205 | [instr.local.get, 0], 206 | instr.end, 207 | ].flat(), 208 | ); 209 | assert.deepEqual( 210 | toWasmFlat('func f1(a) { let x = 12; x }'), 211 | [ 212 | [instr.i32.const, 12], 213 | [instr.local.set, 1], // set `x` 214 | [instr.local.get, 1], // get `x` 215 | instr.end, 216 | ].flat(), 217 | ); 218 | assert.deepEqual( 219 | toWasmFlat('func f2(a, b) { let x = 12; b }'), 220 | [ 221 | [instr.i32.const, 12], 222 | [instr.local.set, 2], // set `x` 223 | [instr.local.get, 1], // get `b` 224 | instr.end, 225 | ].flat(), 226 | ); 227 | }); 228 | 229 | function toWasmFlat(input) { 230 | const matchResult = wafer.match(input, 'FunctionDecl'); 231 | const symbols = buildSymbolTable(wafer, matchResult); 232 | const semantics = wafer.createSemantics(); 233 | defineToWasm(semantics, symbols); 234 | return semantics(matchResult).toWasm().flat(Infinity); 235 | } 236 | 237 | function buildSymbolTable(grammar, matchResult) { 238 | const tempSemantics = grammar.createSemantics(); 239 | const scopes = [new Map()]; 240 | tempSemantics.addOperation('buildSymbolTable', { 241 | _default(...children) { 242 | return children.forEach((c) => c.buildSymbolTable()); 243 | }, 244 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 245 | const name = ident.sourceString; 246 | const locals = new Map(); 247 | scopes.at(-1).set(name, locals); 248 | scopes.push(locals); 249 | optParams.child(0)?.buildSymbolTable(); 250 | blockExpr.buildSymbolTable(); 251 | scopes.pop(); 252 | }, 253 | Params(ident, _, iterIdent) { 254 | for (const id of [ident, ...iterIdent.children]) { 255 | const name = id.sourceString; 256 | const idx = scopes.at(-1).size; 257 | const info = {name, idx, what: 'param'}; 258 | scopes.at(-1).set(name, info); 259 | } 260 | }, 261 | LetStatement(_let, id, _eq, _expr, _) { 262 | const name = id.sourceString; 263 | const idx = scopes.at(-1).size; 264 | const info = {name, idx, what: 'local'}; 265 | scopes.at(-1).set(name, info); 266 | }, 267 | }); 268 | tempSemantics(matchResult).buildSymbolTable(); 269 | return scopes[0]; 270 | } 271 | -------------------------------------------------------------------------------- /chapter05/07-test-e2e.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | // buildSymbolTable, 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcidx, 13 | funcsec, 14 | functype, 15 | i32, 16 | instr, 17 | loadMod, 18 | localidx, 19 | locals, 20 | makeTestFn, 21 | module, 22 | resolveSymbol, 23 | testExtractedExamples, 24 | typeidx, 25 | typesec, 26 | valtype, 27 | } from '../chapter04.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | function buildModule(functionDecls) { 32 | const types = functionDecls.map((f) => 33 | functype(f.paramTypes, [f.resultType]), 34 | ); 35 | const funcs = functionDecls.map((f, i) => typeidx(i)); 36 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 37 | const exports = functionDecls.map((f, i) => 38 | export_(f.name, exportdesc.func(i)), 39 | ); 40 | 41 | const mod = module([ 42 | typesec(types), 43 | funcsec(funcs), 44 | exportsec(exports), 45 | codesec(codes), 46 | ]); 47 | return Uint8Array.from(mod.flat(Infinity)); 48 | } 49 | 50 | test('buildModule', () => { 51 | const functionDecls = [ 52 | { 53 | name: 'main', 54 | paramTypes: [], 55 | resultType: valtype.i32, 56 | locals: [locals(1, valtype.i32)], 57 | body: [instr.i32.const, i32(42), instr.call, funcidx(1), instr.end], 58 | }, 59 | { 60 | name: 'backup', 61 | paramTypes: [valtype.i32], 62 | resultType: valtype.i32, 63 | locals: [], 64 | body: [instr.i32.const, i32(43), instr.end], 65 | }, 66 | ]; 67 | const exports = loadMod(buildModule(functionDecls)); 68 | assert.strictEqual(exports.main(), 43); 69 | assert.strictEqual(exports.backup(), 43); 70 | }); 71 | 72 | instr.call = 0x10; 73 | 74 | const grammarDef = ` 75 | Wafer { 76 | Module = FunctionDecl* 77 | 78 | Statement = LetStatement 79 | | ExprStatement 80 | 81 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 82 | //- "let y;" 83 | LetStatement = "let" identifier "=" Expr ";" 84 | 85 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 86 | //- "func x", "func x();" 87 | FunctionDecl = "func" identifier "(" Params? ")" BlockExpr 88 | 89 | Params = identifier ("," identifier)* 90 | 91 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 92 | //+ "{ let x = 3; 42 }" 93 | //- "{ 3abc }" 94 | BlockExpr = "{" Statement* Expr "}" 95 | 96 | ExprStatement = Expr ";" 97 | 98 | Expr = AssignmentExpr -- assignment 99 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 100 | 101 | //+ "x := 3", "y := 2 + 1" 102 | AssignmentExpr = identifier ":=" Expr 103 | 104 | PrimaryExpr = "(" Expr ")" -- paren 105 | | number 106 | | identifier -- var 107 | 108 | op = "+" | "-" | "*" | "/" 109 | number = digit+ 110 | 111 | //+ "x", "élan", "_", "_99" 112 | //- "1", "$nope" 113 | identifier = identStart identPart* 114 | identStart = letter | "_" 115 | identPart = identStart | digit 116 | 117 | // Examples: 118 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 119 | //- "42", "let x", "func x {}" 120 | } 121 | `; 122 | 123 | test('extracted examples', () => testExtractedExamples(grammarDef)); 124 | 125 | const wafer = ohm.grammar(grammarDef); 126 | 127 | function defineToWasm(semantics, symbols) { 128 | const scopes = [symbols]; 129 | semantics.addOperation('toWasm', { 130 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 131 | scopes.push(symbols.get(ident.sourceString)); 132 | const result = [blockExpr.toWasm(), instr.end]; 133 | scopes.pop(); 134 | return result; 135 | }, 136 | BlockExpr(_lbrace, iterStatement, expr, _rbrace) { 137 | return [...iterStatement.children, expr].map((c) => c.toWasm()); 138 | }, 139 | LetStatement(_let, ident, _eq, expr, _) { 140 | const info = resolveSymbol(ident, scopes.at(-1)); 141 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 142 | }, 143 | ExprStatement(expr, _) { 144 | return [expr.toWasm(), instr.drop]; 145 | }, 146 | Expr_arithmetic(num, iterOps, iterOperands) { 147 | const result = [num.toWasm()]; 148 | for (let i = 0; i < iterOps.numChildren; i++) { 149 | const op = iterOps.child(i); 150 | const operand = iterOperands.child(i); 151 | result.push(operand.toWasm(), op.toWasm()); 152 | } 153 | return result; 154 | }, 155 | AssignmentExpr(ident, _, expr) { 156 | const info = resolveSymbol(ident, scopes.at(-1)); 157 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 158 | }, 159 | PrimaryExpr_paren(_lparen, expr, _rparen) { 160 | return expr.toWasm(); 161 | }, 162 | PrimaryExpr_var(ident) { 163 | const info = resolveSymbol(ident, scopes.at(-1)); 164 | return [instr.local.get, localidx(info.idx)]; 165 | }, 166 | op(char) { 167 | const op = char.sourceString; 168 | const instructionByOp = { 169 | '+': instr.i32.add, 170 | '-': instr.i32.sub, 171 | '*': instr.i32.mul, 172 | '/': instr.i32.div_s, 173 | }; 174 | if (!Object.hasOwn(instructionByOp, op)) { 175 | throw new Error(`Unhandled operator '${op}'`); 176 | } 177 | return instructionByOp[op]; 178 | }, 179 | number(_digits) { 180 | const num = parseInt(this.sourceString, 10); 181 | return [instr.i32.const, ...i32(num)]; 182 | }, 183 | }); 184 | } 185 | 186 | test('toWasm bytecodes - locals & assignment', () => { 187 | assert.deepEqual( 188 | toWasmFlat('func main() { 42 }'), 189 | [[instr.i32.const, 42], instr.end].flat(), 190 | ); 191 | assert.deepEqual( 192 | toWasmFlat('func main() { let x = 0; 42 }'), 193 | [ 194 | [instr.i32.const, 0], 195 | [instr.local.set, 0], 196 | [instr.i32.const, 42], 197 | instr.end, 198 | ].flat(), 199 | ); 200 | assert.deepEqual( 201 | toWasmFlat('func main() { let x = 0; x }'), 202 | [ 203 | [instr.i32.const, 0], 204 | [instr.local.set, 0], 205 | [instr.local.get, 0], 206 | instr.end, 207 | ].flat(), 208 | ); 209 | assert.deepEqual( 210 | toWasmFlat('func f1(a) { let x = 12; x }'), 211 | [ 212 | [instr.i32.const, 12], 213 | [instr.local.set, 1], // set `x` 214 | [instr.local.get, 1], // get `x` 215 | instr.end, 216 | ].flat(), 217 | ); 218 | assert.deepEqual( 219 | toWasmFlat('func f2(a, b) { let x = 12; b }'), 220 | [ 221 | [instr.i32.const, 12], 222 | [instr.local.set, 2], // set `x` 223 | [instr.local.get, 1], // get `b` 224 | instr.end, 225 | ].flat(), 226 | ); 227 | }); 228 | 229 | function toWasmFlat(input) { 230 | const matchResult = wafer.match(input, 'FunctionDecl'); 231 | const symbols = buildSymbolTable(wafer, matchResult); 232 | const semantics = wafer.createSemantics(); 233 | defineToWasm(semantics, symbols); 234 | return semantics(matchResult).toWasm().flat(Infinity); 235 | } 236 | 237 | function buildSymbolTable(grammar, matchResult) { 238 | const tempSemantics = grammar.createSemantics(); 239 | const scopes = [new Map()]; 240 | tempSemantics.addOperation('buildSymbolTable', { 241 | _default(...children) { 242 | return children.forEach((c) => c.buildSymbolTable()); 243 | }, 244 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 245 | const name = ident.sourceString; 246 | const locals = new Map(); 247 | scopes.at(-1).set(name, locals); 248 | scopes.push(locals); 249 | optParams.child(0)?.buildSymbolTable(); 250 | blockExpr.buildSymbolTable(); 251 | scopes.pop(); 252 | }, 253 | Params(ident, _, iterIdent) { 254 | for (const id of [ident, ...iterIdent.children]) { 255 | const name = id.sourceString; 256 | const idx = scopes.at(-1).size; 257 | const info = {name, idx, what: 'param'}; 258 | scopes.at(-1).set(name, info); 259 | } 260 | }, 261 | LetStatement(_let, id, _eq, _expr, _) { 262 | const name = id.sourceString; 263 | const idx = scopes.at(-1).size; 264 | const info = {name, idx, what: 'local'}; 265 | scopes.at(-1).set(name, info); 266 | }, 267 | }); 268 | tempSemantics(matchResult).buildSymbolTable(); 269 | return scopes[0]; 270 | } 271 | 272 | function compile(source) { 273 | const matchResult = wafer.match(source); 274 | if (!matchResult.succeeded()) { 275 | throw new Error(matchResult.message); 276 | } 277 | 278 | const symbols = buildSymbolTable(wafer, matchResult); 279 | const semantics = wafer.createSemantics(); 280 | defineToWasm(semantics, symbols); 281 | defineFunctionDecls(semantics, symbols); 282 | 283 | const functionDecls = semantics(matchResult).functionDecls(); 284 | return buildModule(functionDecls); 285 | } 286 | 287 | function defineFunctionDecls(semantics, symbols) { 288 | semantics.addOperation('functionDecls', { 289 | _default(...children) { 290 | return children.flatMap((c) => c.functionDecls()); 291 | }, 292 | FunctionDecl(_func, ident, _l, _params, _r, _blockExpr) { 293 | const name = ident.sourceString; 294 | const localVars = Array.from(symbols.get(name).values()); 295 | const params = localVars.filter((info) => info.what === 'param'); 296 | const paramTypes = params.map((_) => valtype.i32); 297 | const varsCount = localVars.filter( 298 | (info) => info.what === 'local', 299 | ).length; 300 | return [ 301 | { 302 | name, 303 | paramTypes, 304 | resultType: valtype.i32, 305 | locals: [locals(varsCount, valtype.i32)], 306 | body: this.toWasm(), 307 | }, 308 | ]; 309 | }, 310 | }); 311 | } 312 | 313 | test('module with multiple functions', () => { 314 | assert.deepEqual( 315 | loadMod(compile('func main() { let x = 42; x }')).main(), 316 | 42, 317 | ); 318 | }); 319 | -------------------------------------------------------------------------------- /chapter05/08-functionCalls.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | // buildSymbolTable, 6 | code, 7 | codesec, 8 | export_, 9 | exportdesc, 10 | exportsec, 11 | func, 12 | funcidx, 13 | funcsec, 14 | functype, 15 | i32, 16 | instr, 17 | loadMod, 18 | localidx, 19 | locals, 20 | makeTestFn, 21 | module, 22 | resolveSymbol, 23 | testExtractedExamples, 24 | typeidx, 25 | typesec, 26 | valtype, 27 | } from '../chapter04.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | function buildModule(functionDecls) { 32 | const types = functionDecls.map((f) => 33 | functype(f.paramTypes, [f.resultType]), 34 | ); 35 | const funcs = functionDecls.map((f, i) => typeidx(i)); 36 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 37 | const exports = functionDecls.map((f, i) => 38 | export_(f.name, exportdesc.func(i)), 39 | ); 40 | 41 | const mod = module([ 42 | typesec(types), 43 | funcsec(funcs), 44 | exportsec(exports), 45 | codesec(codes), 46 | ]); 47 | return Uint8Array.from(mod.flat(Infinity)); 48 | } 49 | 50 | test('buildModule', () => { 51 | const functionDecls = [ 52 | { 53 | name: 'main', 54 | paramTypes: [], 55 | resultType: valtype.i32, 56 | locals: [locals(1, valtype.i32)], 57 | body: [instr.i32.const, i32(42), instr.call, funcidx(1), instr.end], 58 | }, 59 | { 60 | name: 'backup', 61 | paramTypes: [valtype.i32], 62 | resultType: valtype.i32, 63 | locals: [], 64 | body: [instr.i32.const, i32(43), instr.end], 65 | }, 66 | ]; 67 | const exports = loadMod(buildModule(functionDecls)); 68 | assert.strictEqual(exports.main(), 43); 69 | assert.strictEqual(exports.backup(), 43); 70 | }); 71 | 72 | instr.call = 0x10; 73 | 74 | const grammarDef = ` 75 | Wafer { 76 | Module = FunctionDecl* 77 | 78 | Statement = LetStatement 79 | | ExprStatement 80 | 81 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 82 | //- "let y;" 83 | LetStatement = "let" identifier "=" Expr ";" 84 | 85 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 86 | //- "func x", "func x();" 87 | FunctionDecl = "func" identifier "(" Params? ")" BlockExpr 88 | 89 | Params = identifier ("," identifier)* 90 | 91 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 92 | //+ "{ let x = 3; 42 }" 93 | //- "{ 3abc }" 94 | BlockExpr = "{" Statement* Expr "}" 95 | 96 | ExprStatement = Expr ";" 97 | 98 | Expr = AssignmentExpr -- assignment 99 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 100 | 101 | //+ "x := 3", "y := 2 + 1" 102 | AssignmentExpr = identifier ":=" Expr 103 | 104 | PrimaryExpr = "(" Expr ")" -- paren 105 | | number 106 | | CallExpr 107 | | identifier -- var 108 | 109 | CallExpr = identifier "(" Args? ")" 110 | 111 | Args = Expr ("," Expr)* 112 | 113 | op = "+" | "-" | "*" | "/" 114 | number = digit+ 115 | 116 | //+ "x", "élan", "_", "_99" 117 | //- "1", "$nope" 118 | identifier = identStart identPart* 119 | identStart = letter | "_" 120 | identPart = identStart | digit 121 | 122 | // Examples: 123 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 124 | //- "42", "let x", "func x {}" 125 | } 126 | `; 127 | 128 | test('extracted examples', () => testExtractedExamples(grammarDef)); 129 | 130 | const wafer = ohm.grammar(grammarDef); 131 | 132 | function defineToWasm(semantics, symbols) { 133 | const scopes = [symbols]; 134 | semantics.addOperation('toWasm', { 135 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 136 | scopes.push(symbols.get(ident.sourceString)); 137 | const result = [blockExpr.toWasm(), instr.end]; 138 | scopes.pop(); 139 | return result; 140 | }, 141 | BlockExpr(_lbrace, iterStatement, expr, _rbrace) { 142 | return [...iterStatement.children, expr].map((c) => c.toWasm()); 143 | }, 144 | LetStatement(_let, ident, _eq, expr, _) { 145 | const info = resolveSymbol(ident, scopes.at(-1)); 146 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 147 | }, 148 | ExprStatement(expr, _) { 149 | return [expr.toWasm(), instr.drop]; 150 | }, 151 | Expr_arithmetic(num, iterOps, iterOperands) { 152 | const result = [num.toWasm()]; 153 | for (let i = 0; i < iterOps.numChildren; i++) { 154 | const op = iterOps.child(i); 155 | const operand = iterOperands.child(i); 156 | result.push(operand.toWasm(), op.toWasm()); 157 | } 158 | return result; 159 | }, 160 | AssignmentExpr(ident, _, expr) { 161 | const info = resolveSymbol(ident, scopes.at(-1)); 162 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 163 | }, 164 | PrimaryExpr_paren(_lparen, expr, _rparen) { 165 | return expr.toWasm(); 166 | }, 167 | CallExpr(ident, _lparen, optArgs, _rparen) { 168 | const name = ident.sourceString; 169 | const funcNames = Array.from(scopes[0].keys()); 170 | const idx = funcNames.indexOf(name); 171 | return [ 172 | optArgs.children.map((c) => c.toWasm()), 173 | [instr.call, funcidx(idx)], 174 | ]; 175 | }, 176 | Args(exp, _, iterExp) { 177 | return [exp, ...iterExp.children].map((c) => c.toWasm()); 178 | }, 179 | PrimaryExpr_var(ident) { 180 | const info = resolveSymbol(ident, scopes.at(-1)); 181 | return [instr.local.get, localidx(info.idx)]; 182 | }, 183 | op(char) { 184 | const op = char.sourceString; 185 | const instructionByOp = { 186 | '+': instr.i32.add, 187 | '-': instr.i32.sub, 188 | '*': instr.i32.mul, 189 | '/': instr.i32.div_s, 190 | }; 191 | if (!Object.hasOwn(instructionByOp, op)) { 192 | throw new Error(`Unhandled operator '${op}'`); 193 | } 194 | return instructionByOp[op]; 195 | }, 196 | number(_digits) { 197 | const num = parseInt(this.sourceString, 10); 198 | return [instr.i32.const, ...i32(num)]; 199 | }, 200 | }); 201 | } 202 | 203 | test('toWasm bytecodes - locals & assignment', () => { 204 | assert.deepEqual( 205 | toWasmFlat('func main() { 42 }'), 206 | [[instr.i32.const, 42], instr.end].flat(), 207 | ); 208 | assert.deepEqual( 209 | toWasmFlat('func main() { let x = 0; 42 }'), 210 | [ 211 | [instr.i32.const, 0], 212 | [instr.local.set, 0], 213 | [instr.i32.const, 42], 214 | instr.end, 215 | ].flat(), 216 | ); 217 | assert.deepEqual( 218 | toWasmFlat('func main() { let x = 0; x }'), 219 | [ 220 | [instr.i32.const, 0], 221 | [instr.local.set, 0], 222 | [instr.local.get, 0], 223 | instr.end, 224 | ].flat(), 225 | ); 226 | assert.deepEqual( 227 | toWasmFlat('func f1(a) { let x = 12; x }'), 228 | [ 229 | [instr.i32.const, 12], 230 | [instr.local.set, 1], // set `x` 231 | [instr.local.get, 1], // get `x` 232 | instr.end, 233 | ].flat(), 234 | ); 235 | assert.deepEqual( 236 | toWasmFlat('func f2(a, b) { let x = 12; b }'), 237 | [ 238 | [instr.i32.const, 12], 239 | [instr.local.set, 2], // set `x` 240 | [instr.local.get, 1], // get `b` 241 | instr.end, 242 | ].flat(), 243 | ); 244 | }); 245 | 246 | function toWasmFlat(input) { 247 | const matchResult = wafer.match(input, 'FunctionDecl'); 248 | const symbols = buildSymbolTable(wafer, matchResult); 249 | const semantics = wafer.createSemantics(); 250 | defineToWasm(semantics, symbols); 251 | return semantics(matchResult).toWasm().flat(Infinity); 252 | } 253 | 254 | function buildSymbolTable(grammar, matchResult) { 255 | const tempSemantics = grammar.createSemantics(); 256 | const scopes = [new Map()]; 257 | tempSemantics.addOperation('buildSymbolTable', { 258 | _default(...children) { 259 | return children.forEach((c) => c.buildSymbolTable()); 260 | }, 261 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 262 | const name = ident.sourceString; 263 | const locals = new Map(); 264 | scopes.at(-1).set(name, locals); 265 | scopes.push(locals); 266 | optParams.child(0)?.buildSymbolTable(); 267 | blockExpr.buildSymbolTable(); 268 | scopes.pop(); 269 | }, 270 | Params(ident, _, iterIdent) { 271 | for (const id of [ident, ...iterIdent.children]) { 272 | const name = id.sourceString; 273 | const idx = scopes.at(-1).size; 274 | const info = {name, idx, what: 'param'}; 275 | scopes.at(-1).set(name, info); 276 | } 277 | }, 278 | LetStatement(_let, id, _eq, _expr, _) { 279 | const name = id.sourceString; 280 | const idx = scopes.at(-1).size; 281 | const info = {name, idx, what: 'local'}; 282 | scopes.at(-1).set(name, info); 283 | }, 284 | }); 285 | tempSemantics(matchResult).buildSymbolTable(); 286 | return scopes[0]; 287 | } 288 | 289 | function compile(source) { 290 | const matchResult = wafer.match(source); 291 | if (!matchResult.succeeded()) { 292 | throw new Error(matchResult.message); 293 | } 294 | 295 | const symbols = buildSymbolTable(wafer, matchResult); 296 | const semantics = wafer.createSemantics(); 297 | defineToWasm(semantics, symbols); 298 | defineFunctionDecls(semantics, symbols); 299 | 300 | const functionDecls = semantics(matchResult).functionDecls(); 301 | return buildModule(functionDecls); 302 | } 303 | 304 | function defineFunctionDecls(semantics, symbols) { 305 | semantics.addOperation('functionDecls', { 306 | _default(...children) { 307 | return children.flatMap((c) => c.functionDecls()); 308 | }, 309 | FunctionDecl(_func, ident, _l, _params, _r, _blockExpr) { 310 | const name = ident.sourceString; 311 | const localVars = Array.from(symbols.get(name).values()); 312 | const params = localVars.filter((info) => info.what === 'param'); 313 | const paramTypes = params.map((_) => valtype.i32); 314 | const varsCount = localVars.filter( 315 | (info) => info.what === 'local', 316 | ).length; 317 | return [ 318 | { 319 | name, 320 | paramTypes, 321 | resultType: valtype.i32, 322 | locals: [locals(varsCount, valtype.i32)], 323 | body: this.toWasm(), 324 | }, 325 | ]; 326 | }, 327 | }); 328 | } 329 | 330 | test('module with multiple functions', () => { 331 | assert.deepEqual( 332 | loadMod(compile('func main() { let x = 42; x }')).main(), 333 | 42, 334 | ); 335 | assert.deepEqual( 336 | loadMod( 337 | compile('func doIt() { add(1, 2) } func add(x, y) { x + y }'), 338 | ).doIt(), 339 | 3, 340 | ); 341 | }); 342 | -------------------------------------------------------------------------------- /chapter06/01-ifExpr.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildModule, 6 | buildSymbolTable, 7 | defineFunctionDecls, 8 | funcidx, 9 | i32, 10 | instr, 11 | loadMod, 12 | localidx, 13 | makeTestFn, 14 | resolveSymbol, 15 | testExtractedExamples, 16 | u32, 17 | valtype, 18 | } from '../chapter05.js'; 19 | 20 | const test = makeTestFn(import.meta.url); 21 | 22 | const grammarDef = ` 23 | Wafer { 24 | Module = FunctionDecl* 25 | 26 | Statement = LetStatement 27 | | ExprStatement 28 | 29 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 30 | //- "let y;" 31 | LetStatement = let identifier "=" Expr ";" 32 | 33 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 34 | //- "func x", "func x();" 35 | FunctionDecl = func identifier "(" Params? ")" BlockExpr 36 | 37 | Params = identifier ("," identifier)* 38 | 39 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 40 | //+ "{ let x = 3; 42 }" 41 | //- "{ 3abc }" 42 | BlockExpr = "{" Statement* Expr "}" 43 | 44 | ExprStatement = Expr ";" 45 | 46 | Expr = AssignmentExpr -- assignment 47 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 48 | 49 | //+ "x := 3", "y := 2 + 1" 50 | AssignmentExpr = identifier ":=" Expr 51 | 52 | PrimaryExpr = "(" Expr ")" -- paren 53 | | number 54 | | CallExpr 55 | | identifier -- var 56 | | IfExpr 57 | 58 | CallExpr = identifier "(" Args? ")" 59 | 60 | Args = Expr ("," Expr)* 61 | 62 | //+ "if x { 42 } else { 99 }", "if x { 42 } else if y { 99 } else { 0 }" 63 | //- "if x { 42 }" 64 | IfExpr = if Expr BlockExpr else (BlockExpr|IfExpr) 65 | 66 | op = "+" | "-" | "*" | "/" 67 | number = digit+ 68 | 69 | keyword = if | else | func | let 70 | if = "if" ~identPart 71 | else = "else" ~identPart 72 | func = "func" ~identPart 73 | let = "let" ~identPart 74 | 75 | //+ "x", "élan", "_", "_99" 76 | //- "1", "$nope" 77 | identifier = ~keyword identStart identPart* 78 | identStart = letter | "_" 79 | identPart = identStart | digit 80 | 81 | // Examples: 82 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 83 | //- "42", "let x", "func x {}" 84 | } 85 | `; 86 | 87 | test('extracted examples', () => testExtractedExamples(grammarDef)); 88 | 89 | const wafer = ohm.grammar(grammarDef); 90 | -------------------------------------------------------------------------------- /chapter06/02-test-ifExpr.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildModule, 6 | buildSymbolTable, 7 | defineFunctionDecls, 8 | funcidx, 9 | i32, 10 | instr, 11 | loadMod, 12 | localidx, 13 | makeTestFn, 14 | resolveSymbol, 15 | testExtractedExamples, 16 | u32, 17 | valtype, 18 | } from '../chapter05.js'; 19 | 20 | const test = makeTestFn(import.meta.url); 21 | 22 | const grammarDef = ` 23 | Wafer { 24 | Module = FunctionDecl* 25 | 26 | Statement = LetStatement 27 | | ExprStatement 28 | 29 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 30 | //- "let y;" 31 | LetStatement = let identifier "=" Expr ";" 32 | 33 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 34 | //- "func x", "func x();" 35 | FunctionDecl = func identifier "(" Params? ")" BlockExpr 36 | 37 | Params = identifier ("," identifier)* 38 | 39 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 40 | //+ "{ let x = 3; 42 }" 41 | //- "{ 3abc }" 42 | BlockExpr = "{" Statement* Expr "}" 43 | 44 | ExprStatement = Expr ";" 45 | 46 | Expr = AssignmentExpr -- assignment 47 | | PrimaryExpr (op PrimaryExpr)* -- arithmetic 48 | 49 | //+ "x := 3", "y := 2 + 1" 50 | AssignmentExpr = identifier ":=" Expr 51 | 52 | PrimaryExpr = "(" Expr ")" -- paren 53 | | number 54 | | CallExpr 55 | | identifier -- var 56 | | IfExpr 57 | 58 | CallExpr = identifier "(" Args? ")" 59 | 60 | Args = Expr ("," Expr)* 61 | 62 | //+ "if x { 42 } else { 99 }", "if x { 42 } else if y { 99 } else { 0 }" 63 | //- "if x { 42 }" 64 | IfExpr = if Expr BlockExpr else (BlockExpr|IfExpr) 65 | 66 | op = "+" | "-" | "*" | "/" 67 | number = digit+ 68 | 69 | keyword = if | else | func | let 70 | if = "if" ~identPart 71 | else = "else" ~identPart 72 | func = "func" ~identPart 73 | let = "let" ~identPart 74 | 75 | //+ "x", "élan", "_", "_99" 76 | //- "1", "$nope" 77 | identifier = ~keyword identStart identPart* 78 | identStart = letter | "_" 79 | identPart = identStart | digit 80 | 81 | // Examples: 82 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 83 | //- "42", "let x", "func x {}" 84 | } 85 | `; 86 | 87 | test('extracted examples', () => testExtractedExamples(grammarDef)); 88 | 89 | const wafer = ohm.grammar(grammarDef); 90 | 91 | instr.if = 0x04; 92 | instr.else = 0x05; 93 | 94 | const blocktype = {empty: 0x40, ...valtype}; 95 | 96 | function defineToWasm(semantics, symbols) { 97 | const scopes = [symbols]; 98 | semantics.addOperation('toWasm', { 99 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 100 | scopes.push(symbols.get(ident.sourceString)); 101 | const result = [blockExpr.toWasm(), instr.end]; 102 | scopes.pop(); 103 | return result; 104 | }, 105 | BlockExpr(_lbrace, iterStatement, expr, _rbrace) { 106 | return [...iterStatement.children, expr].map((c) => c.toWasm()); 107 | }, 108 | LetStatement(_let, ident, _eq, expr, _) { 109 | const info = resolveSymbol(ident, scopes.at(-1)); 110 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 111 | }, 112 | ExprStatement(expr, _) { 113 | return [expr.toWasm(), instr.drop]; 114 | }, 115 | Expr_arithmetic(num, iterOps, iterOperands) { 116 | const result = [num.toWasm()]; 117 | for (let i = 0; i < iterOps.numChildren; i++) { 118 | const op = iterOps.child(i); 119 | const operand = iterOperands.child(i); 120 | result.push(operand.toWasm(), op.toWasm()); 121 | } 122 | return result; 123 | }, 124 | AssignmentExpr(ident, _, expr) { 125 | const info = resolveSymbol(ident, scopes.at(-1)); 126 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 127 | }, 128 | PrimaryExpr_paren(_lparen, expr, _rparen) { 129 | return expr.toWasm(); 130 | }, 131 | CallExpr(ident, _lparen, optArgs, _rparen) { 132 | const name = ident.sourceString; 133 | const funcNames = Array.from(scopes[0].keys()); 134 | const idx = funcNames.indexOf(name); 135 | return [ 136 | optArgs.children.map((c) => c.toWasm()), 137 | [instr.call, funcidx(idx)], 138 | ]; 139 | }, 140 | Args(exp, _, iterExp) { 141 | return [exp, ...iterExp.children].map((c) => c.toWasm()); 142 | }, 143 | IfExpr(_if, expr, thenBlock, _else, elseBlock) { 144 | return [ 145 | expr.toWasm(), 146 | [instr.if, blocktype.i32], 147 | thenBlock.toWasm(), 148 | instr.else, 149 | elseBlock.toWasm(), 150 | instr.end, 151 | ]; 152 | }, 153 | PrimaryExpr_var(ident) { 154 | const info = resolveSymbol(ident, scopes.at(-1)); 155 | return [instr.local.get, localidx(info.idx)]; 156 | }, 157 | op(char) { 158 | const op = char.sourceString; 159 | const instructionByOp = { 160 | '+': instr.i32.add, 161 | '-': instr.i32.sub, 162 | '*': instr.i32.mul, 163 | '/': instr.i32.div_s, 164 | }; 165 | if (!Object.hasOwn(instructionByOp, op)) { 166 | throw new Error(`Unhandled operator '${op}'`); 167 | } 168 | return instructionByOp[op]; 169 | }, 170 | number(_digits) { 171 | const num = parseInt(this.sourceString, 10); 172 | return [instr.i32.const, ...i32(num)]; 173 | }, 174 | }); 175 | } 176 | 177 | function compile(source) { 178 | const matchResult = wafer.match(source); 179 | if (!matchResult.succeeded()) { 180 | throw new Error(matchResult.message); 181 | } 182 | 183 | const symbols = buildSymbolTable(wafer, matchResult); 184 | const semantics = wafer.createSemantics(); 185 | defineToWasm(semantics, symbols); 186 | defineFunctionDecls(semantics, symbols); 187 | 188 | const functionDecls = semantics(matchResult).functionDecls(); 189 | return buildModule(functionDecls); 190 | } 191 | 192 | test('Wafer if expressions', () => { 193 | let mod = loadMod(compile('func choose(x) { if x { 42 } else { 43 } }')); 194 | assert.strictEqual(mod.choose(1), 42); 195 | assert.strictEqual(mod.choose(0), 43); 196 | 197 | mod = loadMod( 198 | compile(` 199 | func isZero(x) { 200 | let result = if x { 0 } else { 1 }; 201 | result 202 | } 203 | `), 204 | ); 205 | assert.strictEqual(mod.isZero(1), 0); 206 | assert.strictEqual(mod.isZero(0), 1); 207 | }); 208 | -------------------------------------------------------------------------------- /chapter06/03-test-comparison.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildModule, 6 | buildSymbolTable, 7 | defineFunctionDecls, 8 | funcidx, 9 | i32, 10 | instr, 11 | loadMod, 12 | localidx, 13 | makeTestFn, 14 | resolveSymbol, 15 | testExtractedExamples, 16 | u32, 17 | valtype, 18 | } from '../chapter05.js'; 19 | 20 | const test = makeTestFn(import.meta.url); 21 | 22 | const grammarDef = ` 23 | Wafer { 24 | Module = FunctionDecl* 25 | 26 | Statement = LetStatement 27 | | ExprStatement 28 | 29 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 30 | //- "let y;" 31 | LetStatement = let identifier "=" Expr ";" 32 | 33 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 34 | //- "func x", "func x();" 35 | FunctionDecl = func identifier "(" Params? ")" BlockExpr 36 | 37 | Params = identifier ("," identifier)* 38 | 39 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 40 | //+ "{ let x = 3; 42 }" 41 | //- "{ 3abc }" 42 | BlockExpr = "{" Statement* Expr "}" 43 | 44 | ExprStatement = Expr ";" 45 | 46 | Expr = AssignmentExpr -- assignment 47 | | PrimaryExpr (binaryOp PrimaryExpr)* -- binary 48 | 49 | //+ "x := 3", "y := 2 + 1" 50 | AssignmentExpr = identifier ":=" Expr 51 | 52 | PrimaryExpr = "(" Expr ")" -- paren 53 | | number 54 | | CallExpr 55 | | identifier -- var 56 | | IfExpr 57 | 58 | CallExpr = identifier "(" Args? ")" 59 | 60 | Args = Expr ("," Expr)* 61 | 62 | //+ "if x { 42 } else { 99 }", "if x { 42 } else if y { 99 } else { 0 }" 63 | //- "if x { 42 }" 64 | IfExpr = if Expr BlockExpr else (BlockExpr|IfExpr) 65 | 66 | binaryOp = "+" | "-" | "*" | "/" | compareOp | logicalOp 67 | compareOp = "==" | "!=" | "<=" | "<" | ">=" | ">" 68 | logicalOp = and | or 69 | number = digit+ 70 | 71 | keyword = if | else | func | let | and | or 72 | if = "if" ~identPart 73 | else = "else" ~identPart 74 | func = "func" ~identPart 75 | let = "let" ~identPart 76 | and = "and" ~identPart 77 | or = "or" ~identPart 78 | 79 | //+ "x", "élan", "_", "_99" 80 | //- "1", "$nope" 81 | identifier = ~keyword identStart identPart* 82 | identStart = letter | "_" 83 | identPart = identStart | digit 84 | 85 | // Examples: 86 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 87 | //- "42", "let x", "func x {}" 88 | } 89 | `; 90 | 91 | test('extracted examples', () => testExtractedExamples(grammarDef)); 92 | 93 | const wafer = ohm.grammar(grammarDef); 94 | 95 | instr.if = 0x04; 96 | instr.else = 0x05; 97 | 98 | const blocktype = {empty: 0x40, ...valtype}; 99 | 100 | function defineToWasm(semantics, symbols) { 101 | const scopes = [symbols]; 102 | semantics.addOperation('toWasm', { 103 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 104 | scopes.push(symbols.get(ident.sourceString)); 105 | const result = [blockExpr.toWasm(), instr.end]; 106 | scopes.pop(); 107 | return result; 108 | }, 109 | BlockExpr(_lbrace, iterStatement, expr, _rbrace) { 110 | return [...iterStatement.children, expr].map((c) => c.toWasm()); 111 | }, 112 | LetStatement(_let, ident, _eq, expr, _) { 113 | const info = resolveSymbol(ident, scopes.at(-1)); 114 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 115 | }, 116 | ExprStatement(expr, _) { 117 | return [expr.toWasm(), instr.drop]; 118 | }, 119 | Expr_binary(num, iterOps, iterOperands) { 120 | const result = [num.toWasm()]; 121 | for (let i = 0; i < iterOps.numChildren; i++) { 122 | const op = iterOps.child(i); 123 | const operand = iterOperands.child(i); 124 | result.push(operand.toWasm(), op.toWasm()); 125 | } 126 | return result; 127 | }, 128 | AssignmentExpr(ident, _, expr) { 129 | const info = resolveSymbol(ident, scopes.at(-1)); 130 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 131 | }, 132 | PrimaryExpr_paren(_lparen, expr, _rparen) { 133 | return expr.toWasm(); 134 | }, 135 | CallExpr(ident, _lparen, optArgs, _rparen) { 136 | const name = ident.sourceString; 137 | const funcNames = Array.from(scopes[0].keys()); 138 | const idx = funcNames.indexOf(name); 139 | return [ 140 | optArgs.children.map((c) => c.toWasm()), 141 | [instr.call, funcidx(idx)], 142 | ]; 143 | }, 144 | Args(exp, _, iterExp) { 145 | return [exp, ...iterExp.children].map((c) => c.toWasm()); 146 | }, 147 | IfExpr(_if, expr, thenBlock, _else, elseBlock) { 148 | return [ 149 | expr.toWasm(), 150 | [instr.if, blocktype.i32], 151 | thenBlock.toWasm(), 152 | instr.else, 153 | elseBlock.toWasm(), 154 | instr.end, 155 | ]; 156 | }, 157 | PrimaryExpr_var(ident) { 158 | const info = resolveSymbol(ident, scopes.at(-1)); 159 | return [instr.local.get, localidx(info.idx)]; 160 | }, 161 | binaryOp(char) { 162 | const op = char.sourceString; 163 | const instructionByOp = { 164 | // Arithmetic 165 | '+': instr.i32.add, 166 | '-': instr.i32.sub, 167 | '*': instr.i32.mul, 168 | '/': instr.i32.div_s, 169 | // Comparison 170 | '==': instr.i32.eq, 171 | '!=': instr.i32.ne, 172 | '<': instr.i32.lt_s, 173 | '<=': instr.i32.le_s, 174 | '>': instr.i32.gt_s, 175 | '>=': instr.i32.ge_s, 176 | // Logical 177 | 'and': instr.i32.and, 178 | 'or': instr.i32.or, 179 | }; 180 | if (!Object.hasOwn(instructionByOp, op)) { 181 | throw new Error(`Unhandled binary op '${op}'`); 182 | } 183 | return instructionByOp[op]; 184 | }, 185 | number(_digits) { 186 | const num = parseInt(this.sourceString, 10); 187 | return [instr.i32.const, ...i32(num)]; 188 | }, 189 | }); 190 | } 191 | 192 | function compile(source) { 193 | const matchResult = wafer.match(source); 194 | if (!matchResult.succeeded()) { 195 | throw new Error(matchResult.message); 196 | } 197 | 198 | const symbols = buildSymbolTable(wafer, matchResult); 199 | const semantics = wafer.createSemantics(); 200 | defineToWasm(semantics, symbols); 201 | defineFunctionDecls(semantics, symbols); 202 | 203 | const functionDecls = semantics(matchResult).functionDecls(); 204 | return buildModule(functionDecls); 205 | } 206 | 207 | test('Wafer if expressions', () => { 208 | let mod = loadMod(compile('func choose(x) { if x { 42 } else { 43 } }')); 209 | assert.strictEqual(mod.choose(1), 42); 210 | assert.strictEqual(mod.choose(0), 43); 211 | 212 | mod = loadMod( 213 | compile(` 214 | func isZero(x) { 215 | let result = if x { 0 } else { 1 }; 216 | result 217 | } 218 | `), 219 | ); 220 | assert.strictEqual(mod.isZero(1), 0); 221 | assert.strictEqual(mod.isZero(0), 1); 222 | }); 223 | 224 | instr.i32.eq = 0x46; // a == b 225 | instr.i32.ne = 0x47; // a != b 226 | instr.i32.lt_s = 0x48; // a < b (signed) 227 | instr.i32.lt_u = 0x49; // a < b (unsigned) 228 | instr.i32.gt_s = 0x4a; // a > b (signed) 229 | instr.i32.gt_u = 0x4b; // a > b (unsigned) 230 | instr.i32.le_s = 0x4c; // a <= b (signed) 231 | instr.i32.le_u = 0x4d; // a <= b (unsigned) 232 | instr.i32.ge_s = 0x4e; // a >= b (signed) 233 | instr.i32.ge_u = 0x4f; // a >= b (unsigned) 234 | 235 | instr.i32.eqz = 0x45; // a == 0 236 | 237 | instr.i32.and = 0x71; 238 | instr.i32.or = 0x72; 239 | 240 | test('Wafer comparison operators', () => { 241 | const mod = loadMod( 242 | compile(` 243 | func greaterThan(a, b) { a > b } 244 | func lessThan(a, b) { a < b } 245 | func greaterThanOrEq(a, b) { a >= b } 246 | func lessThanOrEq(a, b) { a <= b } 247 | func eq(a, b) { a == b } 248 | func and_(a, b) { a and b } 249 | func or_(a, b) { a or b } 250 | `), 251 | ); 252 | assert.strictEqual(mod.greaterThan(43, 42), 1); 253 | assert.strictEqual(mod.greaterThan(42, 43), 0); 254 | assert.strictEqual(mod.lessThan(43, 42), 0); 255 | assert.strictEqual(mod.greaterThanOrEq(42, 42), 1); 256 | assert.strictEqual(mod.lessThanOrEq(42, 43), 1); 257 | assert.strictEqual(mod.eq(42, 42), 1); 258 | assert.strictEqual(mod.and_(1, 1), 1); 259 | assert.strictEqual(mod.and_(1, 0), 0); 260 | assert.strictEqual(mod.or_(1, 0), 1); 261 | assert.strictEqual(mod.or_(0, 1), 1); 262 | }); 263 | -------------------------------------------------------------------------------- /chapter06/04-test-while.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | buildModule, 6 | buildSymbolTable, 7 | defineFunctionDecls, 8 | funcidx, 9 | i32, 10 | instr, 11 | loadMod, 12 | localidx, 13 | makeTestFn, 14 | resolveSymbol, 15 | testExtractedExamples, 16 | u32, 17 | valtype, 18 | } from '../chapter05.js'; 19 | 20 | const test = makeTestFn(import.meta.url); 21 | 22 | const grammarDef = ` 23 | Wafer { 24 | Module = FunctionDecl* 25 | 26 | Statement = LetStatement 27 | | WhileStatement 28 | | ExprStatement 29 | 30 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 31 | //- "let y;" 32 | LetStatement = let identifier "=" Expr ";" 33 | 34 | //+ "while 0 {}", "while x < 10 { x := x + 1; }" 35 | //- "while 1 { 42 }", "while x < 10 { x := x + 1 }" 36 | WhileStatement = while Expr BlockStatements 37 | 38 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 39 | //- "func x", "func x();" 40 | FunctionDecl = func identifier "(" Params? ")" BlockExpr 41 | 42 | Params = identifier ("," identifier)* 43 | 44 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 45 | //+ "{ let x = 3; 42 }" 46 | //- "{ 3abc }" 47 | BlockExpr = "{" Statement* Expr "}" 48 | 49 | //+ "{}", "{ let x = 3; }", "{ 42; 99; }" 50 | //- "{ 42 }", "{ x := 1 }" 51 | BlockStatements = "{" Statement* "}" 52 | 53 | ExprStatement = Expr ";" 54 | 55 | Expr = AssignmentExpr -- assignment 56 | | PrimaryExpr (binaryOp PrimaryExpr)* -- binary 57 | 58 | //+ "x := 3", "y := 2 + 1" 59 | AssignmentExpr = identifier ":=" Expr 60 | 61 | PrimaryExpr = "(" Expr ")" -- paren 62 | | number 63 | | CallExpr 64 | | identifier -- var 65 | | IfExpr 66 | 67 | CallExpr = identifier "(" Args? ")" 68 | 69 | Args = Expr ("," Expr)* 70 | 71 | //+ "if x { 42 } else { 99 }", "if x { 42 } else if y { 99 } else { 0 }" 72 | //- "if x { 42 }" 73 | IfExpr = if Expr BlockExpr else (BlockExpr|IfExpr) 74 | 75 | binaryOp = "+" | "-" | "*" | "/" | compareOp | logicalOp 76 | compareOp = "==" | "!=" | "<=" | "<" | ">=" | ">" 77 | logicalOp = and | or 78 | number = digit+ 79 | 80 | keyword = if | else | func | let | and | or | while 81 | if = "if" ~identPart 82 | else = "else" ~identPart 83 | func = "func" ~identPart 84 | let = "let" ~identPart 85 | and = "and" ~identPart 86 | or = "or" ~identPart 87 | while = "while" ~identPart 88 | 89 | //+ "x", "élan", "_", "_99" 90 | //- "1", "$nope" 91 | identifier = ~keyword identStart identPart* 92 | identStart = letter | "_" 93 | identPart = identStart | digit 94 | 95 | // Examples: 96 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 97 | //- "42", "let x", "func x {}" 98 | } 99 | `; 100 | 101 | test('extracted examples', () => testExtractedExamples(grammarDef)); 102 | 103 | const wafer = ohm.grammar(grammarDef); 104 | 105 | instr.if = 0x04; 106 | instr.else = 0x05; 107 | 108 | const blocktype = {empty: 0x40, ...valtype}; 109 | 110 | function defineToWasm(semantics, symbols) { 111 | const scopes = [symbols]; 112 | semantics.addOperation('toWasm', { 113 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 114 | scopes.push(symbols.get(ident.sourceString)); 115 | const result = [blockExpr.toWasm(), instr.end]; 116 | scopes.pop(); 117 | return result; 118 | }, 119 | BlockExpr(_lbrace, iterStatement, expr, _rbrace) { 120 | return [...iterStatement.children, expr].map((c) => c.toWasm()); 121 | }, 122 | BlockStatements(_lbrace, iterStatement, _rbrace) { 123 | return iterStatement.children.map((c) => c.toWasm()); 124 | }, 125 | LetStatement(_let, ident, _eq, expr, _) { 126 | const info = resolveSymbol(ident, scopes.at(-1)); 127 | return [expr.toWasm(), instr.local.set, localidx(info.idx)]; 128 | }, 129 | WhileStatement(_while, cond, body) { 130 | return [ 131 | [instr.loop, blocktype.empty], 132 | cond.toWasm(), 133 | [instr.if, blocktype.empty], 134 | body.toWasm(), 135 | [instr.br, labelidx(1)], 136 | instr.end, // end if 137 | instr.end, // end loop 138 | ]; 139 | }, 140 | ExprStatement(expr, _) { 141 | return [expr.toWasm(), instr.drop]; 142 | }, 143 | Expr_binary(num, iterOps, iterOperands) { 144 | const result = [num.toWasm()]; 145 | for (let i = 0; i < iterOps.numChildren; i++) { 146 | const op = iterOps.child(i); 147 | const operand = iterOperands.child(i); 148 | result.push(operand.toWasm(), op.toWasm()); 149 | } 150 | return result; 151 | }, 152 | AssignmentExpr(ident, _, expr) { 153 | const info = resolveSymbol(ident, scopes.at(-1)); 154 | return [expr.toWasm(), instr.local.tee, localidx(info.idx)]; 155 | }, 156 | PrimaryExpr_paren(_lparen, expr, _rparen) { 157 | return expr.toWasm(); 158 | }, 159 | CallExpr(ident, _lparen, optArgs, _rparen) { 160 | const name = ident.sourceString; 161 | const funcNames = Array.from(scopes[0].keys()); 162 | const idx = funcNames.indexOf(name); 163 | return [ 164 | optArgs.children.map((c) => c.toWasm()), 165 | [instr.call, funcidx(idx)], 166 | ]; 167 | }, 168 | Args(exp, _, iterExp) { 169 | return [exp, ...iterExp.children].map((c) => c.toWasm()); 170 | }, 171 | IfExpr(_if, expr, thenBlock, _else, elseBlock) { 172 | return [ 173 | expr.toWasm(), 174 | [instr.if, blocktype.i32], 175 | thenBlock.toWasm(), 176 | instr.else, 177 | elseBlock.toWasm(), 178 | instr.end, 179 | ]; 180 | }, 181 | PrimaryExpr_var(ident) { 182 | const info = resolveSymbol(ident, scopes.at(-1)); 183 | return [instr.local.get, localidx(info.idx)]; 184 | }, 185 | binaryOp(char) { 186 | const op = char.sourceString; 187 | const instructionByOp = { 188 | // Arithmetic 189 | '+': instr.i32.add, 190 | '-': instr.i32.sub, 191 | '*': instr.i32.mul, 192 | '/': instr.i32.div_s, 193 | // Comparison 194 | '==': instr.i32.eq, 195 | '!=': instr.i32.ne, 196 | '<': instr.i32.lt_s, 197 | '<=': instr.i32.le_s, 198 | '>': instr.i32.gt_s, 199 | '>=': instr.i32.ge_s, 200 | // Logical 201 | 'and': instr.i32.and, 202 | 'or': instr.i32.or, 203 | }; 204 | if (!Object.hasOwn(instructionByOp, op)) { 205 | throw new Error(`Unhandle binary op '${op}'`); 206 | } 207 | return instructionByOp[op]; 208 | }, 209 | number(_digits) { 210 | const num = parseInt(this.sourceString, 10); 211 | return [instr.i32.const, ...i32(num)]; 212 | }, 213 | }); 214 | } 215 | 216 | function compile(source) { 217 | const matchResult = wafer.match(source); 218 | if (!matchResult.succeeded()) { 219 | throw new Error(matchResult.message); 220 | } 221 | 222 | const symbols = buildSymbolTable(wafer, matchResult); 223 | const semantics = wafer.createSemantics(); 224 | defineToWasm(semantics, symbols); 225 | defineFunctionDecls(semantics, symbols); 226 | 227 | const functionDecls = semantics(matchResult).functionDecls(); 228 | return buildModule(functionDecls); 229 | } 230 | 231 | test('Wafer if expressions', () => { 232 | let mod = loadMod(compile('func choose(x) { if x { 42 } else { 43 } }')); 233 | assert.strictEqual(mod.choose(1), 42); 234 | assert.strictEqual(mod.choose(0), 43); 235 | 236 | mod = loadMod( 237 | compile(` 238 | func isZero(x) { 239 | let result = if x { 0 } else { 1 }; 240 | result 241 | } 242 | `), 243 | ); 244 | assert.strictEqual(mod.isZero(1), 0); 245 | assert.strictEqual(mod.isZero(0), 1); 246 | }); 247 | 248 | instr.i32.eq = 0x46; // a == b 249 | instr.i32.ne = 0x47; // a != b 250 | instr.i32.lt_s = 0x48; // a < b (signed) 251 | instr.i32.lt_u = 0x49; // a < b (unsigned) 252 | instr.i32.gt_s = 0x4a; // a > b (signed) 253 | instr.i32.gt_u = 0x4b; // a > b (unsigned) 254 | instr.i32.le_s = 0x4c; // a <= b (signed) 255 | instr.i32.le_u = 0x4d; // a <= b (unsigned) 256 | instr.i32.ge_s = 0x4e; // a >= b (signed) 257 | instr.i32.ge_u = 0x4f; // a >= b (unsigned) 258 | 259 | instr.i32.eqz = 0x45; // a == 0 260 | 261 | instr.i32.and = 0x71; 262 | instr.i32.or = 0x72; 263 | 264 | test('Wafer comparison operators', () => { 265 | const mod = loadMod( 266 | compile(` 267 | func greaterThan(a, b) { a > b } 268 | func lessThan(a, b) { a < b } 269 | func greaterThanOrEq(a, b) { a >= b } 270 | func lessThanOrEq(a, b) { a <= b } 271 | func eq(a, b) { a == b } 272 | func and_(a, b) { a and b } 273 | func or_(a, b) { a or b } 274 | `), 275 | ); 276 | assert.strictEqual(mod.greaterThan(43, 42), 1); 277 | assert.strictEqual(mod.greaterThan(42, 43), 0); 278 | assert.strictEqual(mod.lessThan(43, 42), 0); 279 | assert.strictEqual(mod.greaterThanOrEq(42, 42), 1); 280 | assert.strictEqual(mod.lessThanOrEq(42, 43), 1); 281 | assert.strictEqual(mod.eq(42, 42), 1); 282 | assert.strictEqual(mod.and_(1, 1), 1); 283 | assert.strictEqual(mod.and_(1, 0), 0); 284 | assert.strictEqual(mod.or_(1, 0), 1); 285 | assert.strictEqual(mod.or_(0, 1), 1); 286 | }); 287 | 288 | const labelidx = u32; 289 | 290 | instr.block = 0x02; 291 | instr.loop = 0x03; 292 | instr.br = 0x0c; 293 | instr.br_if = 0x0d; 294 | 295 | test('Wafer while loops', () => { 296 | const mod = loadMod( 297 | compile(` 298 | func countTo(n) { 299 | let x = 0; 300 | while x < n { 301 | x := x + 1; 302 | } 303 | x 304 | } 305 | `), 306 | ); 307 | assert.strictEqual(mod.countTo(10), 10); 308 | }); 309 | -------------------------------------------------------------------------------- /chapter07.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | defineFunctionDecls, 8 | defineToWasm, 9 | export_, 10 | exportdesc, 11 | exportsec, 12 | func, 13 | funcidx, 14 | funcsec, 15 | functype, 16 | i32, 17 | instr, 18 | makeTestFn, 19 | module, 20 | name, 21 | section, 22 | testExtractedExamples, 23 | typeidx, 24 | typesec, 25 | valtype, 26 | vec, 27 | } from './chapter06.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | const grammarDef = ` 32 | Wafer { 33 | Module = (FunctionDecl|ExternFunctionDecl)* 34 | 35 | Statement = LetStatement 36 | | IfStatement 37 | | WhileStatement 38 | | ExprStatement 39 | 40 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 41 | //- "let y;" 42 | LetStatement = let identifier "=" Expr ";" 43 | 44 | //+ "if x < 10 {}", "if z { 42; }", "if x {} else if y {} else { 42; }" 45 | //- "if x < 10 { 3 } else {}" 46 | IfStatement = if Expr BlockStatements (else (BlockStatements|IfStatement))? 47 | 48 | //+ "while 0 {}", "while x < 10 { x := x + 1; }" 49 | //- "while 1 { 42 }", "while x < 10 { x := x + 1 }" 50 | WhileStatement = while Expr BlockStatements 51 | 52 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 53 | //- "func x", "func x();" 54 | FunctionDecl = func identifier "(" Params? ")" BlockExpr 55 | 56 | //+ "extern func print(x);" 57 | ExternFunctionDecl = extern func identifier "(" Params? ")" ";" 58 | Params = identifier ("," identifier)* 59 | 60 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 61 | //+ "{ let x = 3; 42 }" 62 | //- "{ 3abc }" 63 | BlockExpr = "{" Statement* Expr "}" 64 | 65 | //+ "{}", "{ let x = 3; }", "{ 42; 99; }" 66 | //- "{ 42 }", "{ x := 1 }" 67 | BlockStatements = "{" Statement* "}" 68 | 69 | ExprStatement = Expr ";" 70 | 71 | Expr = AssignmentExpr -- assignment 72 | | PrimaryExpr (binaryOp PrimaryExpr)* -- binary 73 | 74 | //+ "x := 3", "y := 2 + 1" 75 | AssignmentExpr = identifier ":=" Expr 76 | 77 | PrimaryExpr = "(" Expr ")" -- paren 78 | | number 79 | | CallExpr 80 | | identifier -- var 81 | | IfExpr 82 | 83 | CallExpr = identifier "(" Args? ")" 84 | 85 | Args = Expr ("," Expr)* 86 | 87 | //+ "if x { 42 } else { 99 }", "if x { 42 } else if y { 99 } else { 0 }" 88 | //- "if x { 42 }" 89 | IfExpr = if Expr BlockExpr else (BlockExpr|IfExpr) 90 | 91 | binaryOp = "+" | "-" | "*" | "/" | compareOp | logicalOp 92 | compareOp = "==" | "!=" | "<=" | "<" | ">=" | ">" 93 | logicalOp = and | or 94 | number = digit+ 95 | 96 | keyword = if | else | func | let | while | and | or | extern 97 | if = "if" ~identPart 98 | else = "else" ~identPart 99 | func = "func" ~identPart 100 | let = "let" ~identPart 101 | while = "while" ~identPart 102 | and = "and" ~identPart 103 | or = "or" ~identPart 104 | extern = "extern" ~identPart 105 | 106 | //+ "x", "élan", "_", "_99" 107 | //- "1", "$nope" 108 | identifier = ~keyword identStart identPart* 109 | identStart = letter | "_" 110 | identPart = identStart | digit 111 | 112 | space += singleLineComment | multiLineComment 113 | singleLineComment = "//" (~"\\n" any)* 114 | multiLineComment = "/*" (~"*/" any)* "*/" 115 | 116 | // Examples: 117 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 118 | //- "42", "let x", "func x {}" 119 | } 120 | `; 121 | 122 | test('extracted examples', () => testExtractedExamples(grammarDef)); 123 | 124 | const wafer = ohm.grammar(grammarDef); 125 | 126 | const SECTION_ID_IMPORT = 2; 127 | 128 | // mod:name nm:name d:importdesc 129 | function import_(mod, nm, d) { 130 | return [name(mod), name(nm), d]; 131 | } 132 | 133 | // im*:vec(import) 134 | function importsec(ims) { 135 | return section(SECTION_ID_IMPORT, vec(ims)); 136 | } 137 | 138 | const importdesc = { 139 | // x:typeidx 140 | func(x) { 141 | return [0x00, typeidx(x)]; 142 | }, 143 | }; 144 | 145 | function buildModule(importDecls, functionDecls) { 146 | const types = [...importDecls, ...functionDecls].map((f) => 147 | functype(f.paramTypes, [f.resultType]), 148 | ); 149 | const imports = importDecls.map((f, i) => 150 | import_(f.module, f.name, importdesc.func(i)), 151 | ); 152 | const funcs = functionDecls.map((f, i) => typeidx(i + importDecls.length)); 153 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 154 | const exports = functionDecls.map((f, i) => 155 | export_(f.name, exportdesc.func(i + importDecls.length)), 156 | ); 157 | 158 | const mod = module([ 159 | typesec(types), 160 | importsec(imports), 161 | funcsec(funcs), 162 | exportsec(exports), 163 | codesec(codes), 164 | ]); 165 | return Uint8Array.from(mod.flat(Infinity)); 166 | } 167 | 168 | test('buildModule with imports', () => { 169 | const importDecls = [ 170 | { 171 | module: 'basicMath', 172 | name: 'addOne', 173 | paramTypes: [valtype.i32], 174 | resultType: valtype.i32, 175 | }, 176 | ]; 177 | const functionDecls = [ 178 | { 179 | name: 'main', 180 | paramTypes: [], 181 | resultType: valtype.i32, 182 | locals: [], 183 | body: [instr.i32.const, i32(42), instr.call, funcidx(0), instr.end], 184 | }, 185 | ]; 186 | const exports = loadMod(buildModule(importDecls, functionDecls), { 187 | basicMath: {addOne: (x) => x + 1}, 188 | }); 189 | assert.strictEqual(exports.main(), 43); 190 | }); 191 | 192 | function loadMod(bytes, imports) { 193 | const mod = new WebAssembly.Module(bytes); 194 | return new WebAssembly.Instance(mod, imports).exports; 195 | } 196 | 197 | function compile(source) { 198 | const matchResult = wafer.match(source); 199 | if (!matchResult.succeeded()) { 200 | throw new Error(matchResult.message); 201 | } 202 | 203 | const symbols = buildSymbolTable(wafer, matchResult); 204 | const semantics = wafer.createSemantics(); 205 | defineToWasm(semantics, symbols); 206 | defineImportDecls(semantics); 207 | defineFunctionDecls(semantics, symbols); 208 | 209 | const importDecls = semantics(matchResult).importDecls(); 210 | const functionDecls = semantics(matchResult).functionDecls(); 211 | return buildModule(importDecls, functionDecls); 212 | } 213 | 214 | function defineImportDecls(semantics) { 215 | semantics.addOperation('importDecls', { 216 | _default(...children) { 217 | return children.flatMap((c) => c.importDecls()); 218 | }, 219 | ExternFunctionDecl(_extern, _func, ident, _l, optParams, _r, _) { 220 | const name = ident.sourceString; 221 | const paramTypes = 222 | optParams.numChildren === 0 ? [] : getParamTypes(optParams.child(0)); 223 | return [ 224 | { 225 | module: 'waferImports', 226 | name, 227 | paramTypes, 228 | resultType: valtype.i32, 229 | }, 230 | ]; 231 | }, 232 | }); 233 | } 234 | 235 | function getParamTypes(node) { 236 | assert.strictEqual(node.ctorName, 'Params', 'Wrong node type'); 237 | assert.strictEqual(node.numChildren, 3, 'Wrong number of children'); 238 | const [first, _, iterRest] = node.children; 239 | return new Array(iterRest.numChildren + 1).fill(valtype.i32); 240 | } 241 | 242 | function buildSymbolTable(grammar, matchResult) { 243 | const tempSemantics = grammar.createSemantics(); 244 | const scopes = [new Map()]; 245 | tempSemantics.addOperation('buildSymbolTable', { 246 | _default(...children) { 247 | return children.forEach((c) => c.buildSymbolTable()); 248 | }, 249 | ExternFunctionDecl(_extern, _func, ident, _l, optParams, _r, _) { 250 | const name = ident.sourceString; 251 | scopes.at(-1).set(name, new Map()); 252 | }, 253 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 254 | const name = ident.sourceString; 255 | const locals = new Map(); 256 | scopes.at(-1).set(name, locals); 257 | scopes.push(locals); 258 | optParams.child(0)?.buildSymbolTable(); 259 | blockExpr.buildSymbolTable(); 260 | scopes.pop(); 261 | }, 262 | Params(ident, _, iterIdent) { 263 | for (const id of [ident, ...iterIdent.children]) { 264 | const name = id.sourceString; 265 | const idx = scopes.at(-1).size; 266 | const info = {name, idx, what: 'param'}; 267 | scopes.at(-1).set(name, info); 268 | } 269 | }, 270 | LetStatement(_let, id, _eq, _expr, _) { 271 | const name = id.sourceString; 272 | const idx = scopes.at(-1).size; 273 | const info = {name, idx, what: 'local'}; 274 | scopes.at(-1).set(name, info); 275 | }, 276 | }); 277 | tempSemantics(matchResult).buildSymbolTable(); 278 | return scopes[0]; 279 | } 280 | 281 | test('module with imports', () => { 282 | const imports = { 283 | waferImports: { 284 | add: (a, b) => a + b, 285 | one: () => 1, 286 | }, 287 | }; 288 | const compileAndEval = (source) => loadMod(compile(source), imports).main(); 289 | 290 | // Make sure that code with no imports continues to work. 291 | assert.strictEqual(compileAndEval(`func main() { 2 + 2 }`), 4); 292 | 293 | // Now test some code that uses imports. 294 | assert.strictEqual( 295 | compileAndEval(` 296 | extern func add(a, b); 297 | func main() { 298 | let a = 42; 299 | add(a, 1) 300 | } 301 | `), 302 | 43, 303 | ); 304 | assert.strictEqual( 305 | compileAndEval(` 306 | extern func add(a, b); 307 | extern func one(); 308 | func main() { 309 | add(42, one()) 310 | } 311 | `), 312 | 43, 313 | ); 314 | }); 315 | 316 | export * from './chapter06.js'; 317 | export {import_, importdesc, importsec, SECTION_ID_IMPORT}; 318 | export {buildModule, buildSymbolTable, compile, defineImportDecls, loadMod}; 319 | -------------------------------------------------------------------------------- /chapter07/01-externFunctionDecl.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | defineFunctionDecls, 8 | defineToWasm, 9 | export_, 10 | exportdesc, 11 | exportsec, 12 | func, 13 | funcidx, 14 | funcsec, 15 | functype, 16 | i32, 17 | instr, 18 | makeTestFn, 19 | module, 20 | name, 21 | section, 22 | testExtractedExamples, 23 | typeidx, 24 | typesec, 25 | valtype, 26 | vec, 27 | } from '../chapter06.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | const grammarDef = ` 32 | Wafer { 33 | Module = (FunctionDecl|ExternFunctionDecl)* 34 | 35 | Statement = LetStatement 36 | | IfStatement 37 | | WhileStatement 38 | | ExprStatement 39 | 40 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 41 | //- "let y;" 42 | LetStatement = let identifier "=" Expr ";" 43 | 44 | //+ "if x < 10 {}", "if z { 42; }", "if x {} else if y {} else { 42; }" 45 | //- "if x < 10 { 3 } else {}" 46 | IfStatement = if Expr BlockStatements (else (BlockStatements|IfStatement))? 47 | 48 | //+ "while 0 {}", "while x < 10 { x := x + 1; }" 49 | //- "while 1 { 42 }", "while x < 10 { x := x + 1 }" 50 | WhileStatement = while Expr BlockStatements 51 | 52 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 53 | //- "func x", "func x();" 54 | FunctionDecl = func identifier "(" Params? ")" BlockExpr 55 | 56 | //+ "extern func print(x);" 57 | ExternFunctionDecl = extern func identifier "(" Params? ")" ";" 58 | Params = identifier ("," identifier)* 59 | 60 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 61 | //+ "{ let x = 3; 42 }" 62 | //- "{ 3abc }" 63 | BlockExpr = "{" Statement* Expr "}" 64 | 65 | //+ "{}", "{ let x = 3; }", "{ 42; 99; }" 66 | //- "{ 42 }", "{ x := 1 }" 67 | BlockStatements = "{" Statement* "}" 68 | 69 | ExprStatement = Expr ";" 70 | 71 | Expr = AssignmentExpr -- assignment 72 | | PrimaryExpr (binaryOp PrimaryExpr)* -- binary 73 | 74 | //+ "x := 3", "y := 2 + 1" 75 | AssignmentExpr = identifier ":=" Expr 76 | 77 | PrimaryExpr = "(" Expr ")" -- paren 78 | | number 79 | | CallExpr 80 | | identifier -- var 81 | | IfExpr 82 | 83 | CallExpr = identifier "(" Args? ")" 84 | 85 | Args = Expr ("," Expr)* 86 | 87 | //+ "if x { 42 } else { 99 }", "if x { 42 } else if y { 99 } else { 0 }" 88 | //- "if x { 42 }" 89 | IfExpr = if Expr BlockExpr else (BlockExpr|IfExpr) 90 | 91 | binaryOp = "+" | "-" | "*" | "/" | compareOp | logicalOp 92 | compareOp = "==" | "!=" | "<=" | "<" | ">=" | ">" 93 | logicalOp = and | or 94 | number = digit+ 95 | 96 | keyword = if | else | func | let | while | and | or | extern 97 | if = "if" ~identPart 98 | else = "else" ~identPart 99 | func = "func" ~identPart 100 | let = "let" ~identPart 101 | while = "while" ~identPart 102 | and = "and" ~identPart 103 | or = "or" ~identPart 104 | extern = "extern" ~identPart 105 | 106 | //+ "x", "élan", "_", "_99" 107 | //- "1", "$nope" 108 | identifier = ~keyword identStart identPart* 109 | identStart = letter | "_" 110 | identPart = identStart | digit 111 | 112 | space += singleLineComment | multiLineComment 113 | singleLineComment = "//" (~"\\n" any)* 114 | multiLineComment = "/*" (~"*/" any)* "*/" 115 | 116 | // Examples: 117 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 118 | //- "42", "let x", "func x {}" 119 | } 120 | `; 121 | 122 | test('extracted examples', () => testExtractedExamples(grammarDef)); 123 | 124 | const wafer = ohm.grammar(grammarDef); 125 | -------------------------------------------------------------------------------- /chapter07/02-buildModule-imports.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | defineFunctionDecls, 8 | defineToWasm, 9 | export_, 10 | exportdesc, 11 | exportsec, 12 | func, 13 | funcidx, 14 | funcsec, 15 | functype, 16 | i32, 17 | instr, 18 | makeTestFn, 19 | module, 20 | name, 21 | section, 22 | testExtractedExamples, 23 | typeidx, 24 | typesec, 25 | valtype, 26 | vec, 27 | } from '../chapter06.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | const grammarDef = ` 32 | Wafer { 33 | Module = (FunctionDecl|ExternFunctionDecl)* 34 | 35 | Statement = LetStatement 36 | | IfStatement 37 | | WhileStatement 38 | | ExprStatement 39 | 40 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 41 | //- "let y;" 42 | LetStatement = let identifier "=" Expr ";" 43 | 44 | //+ "if x < 10 {}", "if z { 42; }", "if x {} else if y {} else { 42; }" 45 | //- "if x < 10 { 3 } else {}" 46 | IfStatement = if Expr BlockStatements (else (BlockStatements|IfStatement))? 47 | 48 | //+ "while 0 {}", "while x < 10 { x := x + 1; }" 49 | //- "while 1 { 42 }", "while x < 10 { x := x + 1 }" 50 | WhileStatement = while Expr BlockStatements 51 | 52 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 53 | //- "func x", "func x();" 54 | FunctionDecl = func identifier "(" Params? ")" BlockExpr 55 | 56 | //+ "extern func print(x);" 57 | ExternFunctionDecl = extern func identifier "(" Params? ")" ";" 58 | Params = identifier ("," identifier)* 59 | 60 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 61 | //+ "{ let x = 3; 42 }" 62 | //- "{ 3abc }" 63 | BlockExpr = "{" Statement* Expr "}" 64 | 65 | //+ "{}", "{ let x = 3; }", "{ 42; 99; }" 66 | //- "{ 42 }", "{ x := 1 }" 67 | BlockStatements = "{" Statement* "}" 68 | 69 | ExprStatement = Expr ";" 70 | 71 | Expr = AssignmentExpr -- assignment 72 | | PrimaryExpr (binaryOp PrimaryExpr)* -- binary 73 | 74 | //+ "x := 3", "y := 2 + 1" 75 | AssignmentExpr = identifier ":=" Expr 76 | 77 | PrimaryExpr = "(" Expr ")" -- paren 78 | | number 79 | | CallExpr 80 | | identifier -- var 81 | | IfExpr 82 | 83 | CallExpr = identifier "(" Args? ")" 84 | 85 | Args = Expr ("," Expr)* 86 | 87 | //+ "if x { 42 } else { 99 }", "if x { 42 } else if y { 99 } else { 0 }" 88 | //- "if x { 42 }" 89 | IfExpr = if Expr BlockExpr else (BlockExpr|IfExpr) 90 | 91 | binaryOp = "+" | "-" | "*" | "/" | compareOp | logicalOp 92 | compareOp = "==" | "!=" | "<=" | "<" | ">=" | ">" 93 | logicalOp = and | or 94 | number = digit+ 95 | 96 | keyword = if | else | func | let | while | and | or | extern 97 | if = "if" ~identPart 98 | else = "else" ~identPart 99 | func = "func" ~identPart 100 | let = "let" ~identPart 101 | while = "while" ~identPart 102 | and = "and" ~identPart 103 | or = "or" ~identPart 104 | extern = "extern" ~identPart 105 | 106 | //+ "x", "élan", "_", "_99" 107 | //- "1", "$nope" 108 | identifier = ~keyword identStart identPart* 109 | identStart = letter | "_" 110 | identPart = identStart | digit 111 | 112 | space += singleLineComment | multiLineComment 113 | singleLineComment = "//" (~"\\n" any)* 114 | multiLineComment = "/*" (~"*/" any)* "*/" 115 | 116 | // Examples: 117 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 118 | //- "42", "let x", "func x {}" 119 | } 120 | `; 121 | 122 | test('extracted examples', () => testExtractedExamples(grammarDef)); 123 | 124 | const wafer = ohm.grammar(grammarDef); 125 | 126 | const SECTION_ID_IMPORT = 2; 127 | 128 | // mod:name nm:name d:importdesc 129 | function import_(mod, nm, d) { 130 | return [name(mod), name(nm), d]; 131 | } 132 | 133 | // im*:vec(import) 134 | function importsec(ims) { 135 | return section(SECTION_ID_IMPORT, vec(ims)); 136 | } 137 | 138 | const importdesc = { 139 | // x:typeidx 140 | func(x) { 141 | return [0x00, typeidx(x)]; 142 | }, 143 | }; 144 | 145 | function buildModule(importDecls, functionDecls) { 146 | const types = [...importDecls, ...functionDecls].map((f) => 147 | functype(f.paramTypes, [f.resultType]), 148 | ); 149 | const imports = importDecls.map((f, i) => 150 | import_(f.module, f.name, importdesc.func(i)), 151 | ); 152 | const funcs = functionDecls.map((f, i) => typeidx(i + importDecls.length)); 153 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 154 | const exports = functionDecls.map((f, i) => 155 | export_(f.name, exportdesc.func(i + importDecls.length)), 156 | ); 157 | 158 | const mod = module([ 159 | typesec(types), 160 | importsec(imports), 161 | funcsec(funcs), 162 | exportsec(exports), 163 | codesec(codes), 164 | ]); 165 | return Uint8Array.from(mod.flat(Infinity)); 166 | } 167 | 168 | test('buildModule with imports', () => { 169 | const importDecls = [ 170 | { 171 | module: 'basicMath', 172 | name: 'addOne', 173 | paramTypes: [valtype.i32], 174 | resultType: valtype.i32, 175 | }, 176 | ]; 177 | const functionDecls = [ 178 | { 179 | name: 'main', 180 | paramTypes: [], 181 | resultType: valtype.i32, 182 | locals: [], 183 | body: [instr.i32.const, i32(42), instr.call, funcidx(0), instr.end], 184 | }, 185 | ]; 186 | const exports = loadMod(buildModule(importDecls, functionDecls), { 187 | basicMath: {addOne: (x) => x + 1}, 188 | }); 189 | assert.strictEqual(exports.main(), 43); 190 | }); 191 | 192 | function loadMod(bytes, imports) { 193 | const mod = new WebAssembly.Module(bytes); 194 | return new WebAssembly.Instance(mod, imports).exports; 195 | } 196 | -------------------------------------------------------------------------------- /chapter07/03-test-e2e.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | code, 6 | codesec, 7 | defineFunctionDecls, 8 | defineToWasm, 9 | export_, 10 | exportdesc, 11 | exportsec, 12 | func, 13 | funcidx, 14 | funcsec, 15 | functype, 16 | i32, 17 | instr, 18 | makeTestFn, 19 | module, 20 | name, 21 | section, 22 | testExtractedExamples, 23 | typeidx, 24 | typesec, 25 | valtype, 26 | vec, 27 | } from '../chapter06.js'; 28 | 29 | const test = makeTestFn(import.meta.url); 30 | 31 | const grammarDef = ` 32 | Wafer { 33 | Module = (FunctionDecl|ExternFunctionDecl)* 34 | 35 | Statement = LetStatement 36 | | IfStatement 37 | | WhileStatement 38 | | ExprStatement 39 | 40 | //+ "let x = 3 + 4;", "let distance = 100 + 2;" 41 | //- "let y;" 42 | LetStatement = let identifier "=" Expr ";" 43 | 44 | //+ "if x < 10 {}", "if z { 42; }", "if x {} else if y {} else { 42; }" 45 | //- "if x < 10 { 3 } else {}" 46 | IfStatement = if Expr BlockStatements (else (BlockStatements|IfStatement))? 47 | 48 | //+ "while 0 {}", "while x < 10 { x := x + 1; }" 49 | //- "while 1 { 42 }", "while x < 10 { x := x + 1 }" 50 | WhileStatement = while Expr BlockStatements 51 | 52 | //+ "func zero() { 0 }", "func add(x, y) { x + y }" 53 | //- "func x", "func x();" 54 | FunctionDecl = func identifier "(" Params? ")" BlockExpr 55 | 56 | //+ "extern func print(x);" 57 | ExternFunctionDecl = extern func identifier "(" Params? ")" ";" 58 | Params = identifier ("," identifier)* 59 | 60 | //+ "{ 42 }", "{ 66 + 99 }", "{ 1 + 2 - 3 }" 61 | //+ "{ let x = 3; 42 }" 62 | //- "{ 3abc }" 63 | BlockExpr = "{" Statement* Expr "}" 64 | 65 | //+ "{}", "{ let x = 3; }", "{ 42; 99; }" 66 | //- "{ 42 }", "{ x := 1 }" 67 | BlockStatements = "{" Statement* "}" 68 | 69 | ExprStatement = Expr ";" 70 | 71 | Expr = AssignmentExpr -- assignment 72 | | PrimaryExpr (binaryOp PrimaryExpr)* -- binary 73 | 74 | //+ "x := 3", "y := 2 + 1" 75 | AssignmentExpr = identifier ":=" Expr 76 | 77 | PrimaryExpr = "(" Expr ")" -- paren 78 | | number 79 | | CallExpr 80 | | identifier -- var 81 | | IfExpr 82 | 83 | CallExpr = identifier "(" Args? ")" 84 | 85 | Args = Expr ("," Expr)* 86 | 87 | //+ "if x { 42 } else { 99 }", "if x { 42 } else if y { 99 } else { 0 }" 88 | //- "if x { 42 }" 89 | IfExpr = if Expr BlockExpr else (BlockExpr|IfExpr) 90 | 91 | binaryOp = "+" | "-" | "*" | "/" | compareOp | logicalOp 92 | compareOp = "==" | "!=" | "<=" | "<" | ">=" | ">" 93 | logicalOp = and | or 94 | number = digit+ 95 | 96 | keyword = if | else | func | let | while | and | or | extern 97 | if = "if" ~identPart 98 | else = "else" ~identPart 99 | func = "func" ~identPart 100 | let = "let" ~identPart 101 | while = "while" ~identPart 102 | and = "and" ~identPart 103 | or = "or" ~identPart 104 | extern = "extern" ~identPart 105 | 106 | //+ "x", "élan", "_", "_99" 107 | //- "1", "$nope" 108 | identifier = ~keyword identStart identPart* 109 | identStart = letter | "_" 110 | identPart = identStart | digit 111 | 112 | space += singleLineComment | multiLineComment 113 | singleLineComment = "//" (~"\\n" any)* 114 | multiLineComment = "/*" (~"*/" any)* "*/" 115 | 116 | // Examples: 117 | //+ "func addOne(x) { x + one }", "func one() { 1 } func two() { 2 }" 118 | //- "42", "let x", "func x {}" 119 | } 120 | `; 121 | 122 | test('extracted examples', () => testExtractedExamples(grammarDef)); 123 | 124 | const wafer = ohm.grammar(grammarDef); 125 | 126 | const SECTION_ID_IMPORT = 2; 127 | 128 | // mod:name nm:name d:importdesc 129 | function import_(mod, nm, d) { 130 | return [name(mod), name(nm), d]; 131 | } 132 | 133 | // im*:vec(import) 134 | function importsec(ims) { 135 | return section(SECTION_ID_IMPORT, vec(ims)); 136 | } 137 | 138 | const importdesc = { 139 | // x:typeidx 140 | func(x) { 141 | return [0x00, typeidx(x)]; 142 | }, 143 | }; 144 | 145 | function buildModule(importDecls, functionDecls) { 146 | const types = [...importDecls, ...functionDecls].map((f) => 147 | functype(f.paramTypes, [f.resultType]), 148 | ); 149 | const imports = importDecls.map((f, i) => 150 | import_(f.module, f.name, importdesc.func(i)), 151 | ); 152 | const funcs = functionDecls.map((f, i) => typeidx(i + importDecls.length)); 153 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 154 | const exports = functionDecls.map((f, i) => 155 | export_(f.name, exportdesc.func(i + importDecls.length)), 156 | ); 157 | 158 | const mod = module([ 159 | typesec(types), 160 | importsec(imports), 161 | funcsec(funcs), 162 | exportsec(exports), 163 | codesec(codes), 164 | ]); 165 | return Uint8Array.from(mod.flat(Infinity)); 166 | } 167 | 168 | test('buildModule with imports', () => { 169 | const importDecls = [ 170 | { 171 | module: 'basicMath', 172 | name: 'addOne', 173 | paramTypes: [valtype.i32], 174 | resultType: valtype.i32, 175 | }, 176 | ]; 177 | const functionDecls = [ 178 | { 179 | name: 'main', 180 | paramTypes: [], 181 | resultType: valtype.i32, 182 | locals: [], 183 | body: [instr.i32.const, i32(42), instr.call, funcidx(0), instr.end], 184 | }, 185 | ]; 186 | const exports = loadMod(buildModule(importDecls, functionDecls), { 187 | basicMath: {addOne: (x) => x + 1}, 188 | }); 189 | assert.strictEqual(exports.main(), 43); 190 | }); 191 | 192 | function loadMod(bytes, imports) { 193 | const mod = new WebAssembly.Module(bytes); 194 | return new WebAssembly.Instance(mod, imports).exports; 195 | } 196 | 197 | function compile(source) { 198 | const matchResult = wafer.match(source); 199 | if (!matchResult.succeeded()) { 200 | throw new Error(matchResult.message); 201 | } 202 | 203 | const symbols = buildSymbolTable(wafer, matchResult); 204 | const semantics = wafer.createSemantics(); 205 | defineToWasm(semantics, symbols); 206 | defineImportDecls(semantics); 207 | defineFunctionDecls(semantics, symbols); 208 | 209 | const importDecls = semantics(matchResult).importDecls(); 210 | const functionDecls = semantics(matchResult).functionDecls(); 211 | return buildModule(importDecls, functionDecls); 212 | } 213 | 214 | function defineImportDecls(semantics) { 215 | semantics.addOperation('importDecls', { 216 | _default(...children) { 217 | return children.flatMap((c) => c.importDecls()); 218 | }, 219 | ExternFunctionDecl(_extern, _func, ident, _l, optParams, _r, _) { 220 | const name = ident.sourceString; 221 | const paramTypes = 222 | optParams.numChildren === 0 ? [] : getParamTypes(optParams.child(0)); 223 | return [ 224 | { 225 | module: 'waferImports', 226 | name, 227 | paramTypes, 228 | resultType: valtype.i32, 229 | }, 230 | ]; 231 | }, 232 | }); 233 | } 234 | 235 | function getParamTypes(node) { 236 | assert.strictEqual(node.ctorName, 'Params', 'Wrong node type'); 237 | assert.strictEqual(node.numChildren, 3, 'Wrong number of children'); 238 | const [first, _, iterRest] = node.children; 239 | return new Array(iterRest.numChildren + 1).fill(valtype.i32); 240 | } 241 | 242 | function buildSymbolTable(grammar, matchResult) { 243 | const tempSemantics = grammar.createSemantics(); 244 | const scopes = [new Map()]; 245 | tempSemantics.addOperation('buildSymbolTable', { 246 | _default(...children) { 247 | return children.forEach((c) => c.buildSymbolTable()); 248 | }, 249 | ExternFunctionDecl(_extern, _func, ident, _l, optParams, _r, _) { 250 | const name = ident.sourceString; 251 | scopes.at(-1).set(name, new Map()); 252 | }, 253 | FunctionDecl(_func, ident, _lparen, optParams, _rparen, blockExpr) { 254 | const name = ident.sourceString; 255 | const locals = new Map(); 256 | scopes.at(-1).set(name, locals); 257 | scopes.push(locals); 258 | optParams.child(0)?.buildSymbolTable(); 259 | blockExpr.buildSymbolTable(); 260 | scopes.pop(); 261 | }, 262 | Params(ident, _, iterIdent) { 263 | for (const id of [ident, ...iterIdent.children]) { 264 | const name = id.sourceString; 265 | const idx = scopes.at(-1).size; 266 | const info = {name, idx, what: 'param'}; 267 | scopes.at(-1).set(name, info); 268 | } 269 | }, 270 | LetStatement(_let, id, _eq, _expr, _) { 271 | const name = id.sourceString; 272 | const idx = scopes.at(-1).size; 273 | const info = {name, idx, what: 'local'}; 274 | scopes.at(-1).set(name, info); 275 | }, 276 | }); 277 | tempSemantics(matchResult).buildSymbolTable(); 278 | return scopes[0]; 279 | } 280 | 281 | test('module with imports', () => { 282 | const imports = { 283 | waferImports: { 284 | add: (a, b) => a + b, 285 | one: () => 1, 286 | }, 287 | }; 288 | const compileAndEval = (source) => loadMod(compile(source), imports).main(); 289 | 290 | // Make sure that code with no imports continues to work. 291 | assert.strictEqual(compileAndEval(`func main() { 2 + 2 }`), 4); 292 | 293 | // Now test some code that uses imports. 294 | assert.strictEqual( 295 | compileAndEval(` 296 | extern func add(a, b); 297 | func main() { 298 | let a = 42; 299 | add(a, 1) 300 | } 301 | `), 302 | 43, 303 | ); 304 | assert.strictEqual( 305 | compileAndEval(` 306 | extern func add(a, b); 307 | extern func one(); 308 | func main() { 309 | add(42, one()) 310 | } 311 | `), 312 | 43, 313 | ); 314 | }); 315 | -------------------------------------------------------------------------------- /chapter08.js: -------------------------------------------------------------------------------- 1 | import {makeTestFn} from './chapter07.js'; 2 | 3 | const test = makeTestFn(import.meta.url); 4 | 5 | export * from './chapter07.js'; 6 | -------------------------------------------------------------------------------- /chapter08/draw.wafer: -------------------------------------------------------------------------------- 1 | extern func setPixel(x, y, r, g, b, a); 2 | 3 | func draw(width, height, t) { 4 | let y = 0; 5 | while y < height { 6 | let x = 0; 7 | while x < width { 8 | let r = t; 9 | let g = x; 10 | let b = y; 11 | let a = 255; 12 | setPixel(x, y, r, g, b, a); 13 | x := x + 1; 14 | } 15 | y := y + 1; 16 | } 17 | 0 18 | } 19 | -------------------------------------------------------------------------------- /chapter08/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WasmDRAW! 4 | 5 | 6 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /chapter08/waferc.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import {basename, dirname, extname, join} from 'node:path'; 3 | 4 | import {compile} from '../chapter07.js'; 5 | 6 | const filePath = process.argv[2]; 7 | const ext = extname(filePath); 8 | if (!filePath || ext !== '.wafer') { 9 | console.error('Usage: node waferc.js '); 10 | process.exit(1); 11 | } 12 | 13 | const source = fs.readFileSync(filePath, 'utf8'); 14 | const wasmBytes = compile(source); 15 | const outputPath = join(dirname(filePath), basename(filePath, ext) + '.wasm'); 16 | fs.writeFileSync(outputPath, wasmBytes); 17 | -------------------------------------------------------------------------------- /chapter09/01-buildModule-memory.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | blocktype, 6 | code, 7 | codesec, 8 | defineFunctionDecls, 9 | defineImportDecls, 10 | export_, 11 | exportdesc, 12 | exportsec, 13 | func, 14 | funcidx, 15 | funcsec, 16 | functype, 17 | i32, 18 | import_, 19 | importdesc, 20 | importsec, 21 | instr, 22 | labelidx, 23 | localidx, 24 | loadMod, 25 | makeTestFn, 26 | module, 27 | resolveSymbol, 28 | section, 29 | testExtractedExamples, 30 | typeidx, 31 | typesec, 32 | u32, 33 | valtype, 34 | vec, 35 | } from '../chapter08.js'; 36 | 37 | const test = makeTestFn(import.meta.url); 38 | 39 | const SECTION_ID_MEMORY = 5; 40 | 41 | function memsec(mems) { 42 | return section(SECTION_ID_MEMORY, vec(mems)); 43 | } 44 | 45 | function mem(memtype) { 46 | return memtype; 47 | } 48 | 49 | function memtype(limits) { 50 | return limits; 51 | } 52 | 53 | const limits = { 54 | // n:u32 55 | min(n) { 56 | return [0x00, u32(n)]; 57 | }, 58 | // n:u32, m:u32 59 | minmax(n, m) { 60 | return [0x01, u32(n), u32(m)]; 61 | }, 62 | }; 63 | 64 | const memidx = u32; 65 | 66 | exportdesc.mem = (idx) => [0x02, memidx(idx)]; 67 | 68 | function buildModule(importDecls, functionDecls) { 69 | const types = [...importDecls, ...functionDecls].map((f) => 70 | functype(f.paramTypes, [f.resultType]), 71 | ); 72 | const imports = importDecls.map((f, i) => 73 | import_(f.module, f.name, importdesc.func(i)), 74 | ); 75 | const funcs = functionDecls.map((f, i) => typeidx(i + importDecls.length)); 76 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 77 | const exports = functionDecls.map((f, i) => 78 | export_(f.name, exportdesc.func(i + importDecls.length)), 79 | ); 80 | exports.push(export_('$waferMemory', exportdesc.mem(0))); 81 | 82 | const mod = module([ 83 | typesec(types), 84 | importsec(imports), 85 | funcsec(funcs), 86 | memsec([mem(memtype(limits.min(1)))]), 87 | exportsec(exports), 88 | codesec(codes), 89 | ]); 90 | return Uint8Array.from(mod.flat(Infinity)); 91 | } 92 | 93 | test('buildModule with memory', () => { 94 | const importDecls = []; 95 | const functionDecls = [ 96 | { 97 | name: 'main', 98 | paramTypes: [], 99 | resultType: valtype.i32, 100 | locals: [], 101 | body: [[instr.i32.const, i32(42)], instr.end], 102 | }, 103 | ]; 104 | const exports = loadMod(buildModule(importDecls, functionDecls)); 105 | assert.ok(exports.$waferMemory); 106 | assert.strictEqual(exports.main(), 42); 107 | }); 108 | -------------------------------------------------------------------------------- /chapter09/02-test-load-and-store.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import * as ohm from 'ohm-js'; 3 | 4 | import { 5 | blocktype, 6 | code, 7 | codesec, 8 | defineFunctionDecls, 9 | defineImportDecls, 10 | export_, 11 | exportdesc, 12 | exportsec, 13 | func, 14 | funcidx, 15 | funcsec, 16 | functype, 17 | i32, 18 | import_, 19 | importdesc, 20 | importsec, 21 | instr, 22 | labelidx, 23 | localidx, 24 | loadMod, 25 | makeTestFn, 26 | module, 27 | resolveSymbol, 28 | section, 29 | testExtractedExamples, 30 | typeidx, 31 | typesec, 32 | u32, 33 | valtype, 34 | vec, 35 | } from '../chapter08.js'; 36 | 37 | const test = makeTestFn(import.meta.url); 38 | 39 | const SECTION_ID_MEMORY = 5; 40 | 41 | function memsec(mems) { 42 | return section(SECTION_ID_MEMORY, vec(mems)); 43 | } 44 | 45 | function mem(memtype) { 46 | return memtype; 47 | } 48 | 49 | function memtype(limits) { 50 | return limits; 51 | } 52 | 53 | const limits = { 54 | // n:u32 55 | min(n) { 56 | return [0x00, u32(n)]; 57 | }, 58 | // n:u32, m:u32 59 | minmax(n, m) { 60 | return [0x01, u32(n), u32(m)]; 61 | }, 62 | }; 63 | 64 | const memidx = u32; 65 | 66 | exportdesc.mem = (idx) => [0x02, memidx(idx)]; 67 | 68 | function buildModule(importDecls, functionDecls) { 69 | const types = [...importDecls, ...functionDecls].map((f) => 70 | functype(f.paramTypes, [f.resultType]), 71 | ); 72 | const imports = importDecls.map((f, i) => 73 | import_(f.module, f.name, importdesc.func(i)), 74 | ); 75 | const funcs = functionDecls.map((f, i) => typeidx(i + importDecls.length)); 76 | const codes = functionDecls.map((f) => code(func(f.locals, f.body))); 77 | const exports = functionDecls.map((f, i) => 78 | export_(f.name, exportdesc.func(i + importDecls.length)), 79 | ); 80 | exports.push(export_('$waferMemory', exportdesc.mem(0))); 81 | 82 | const mod = module([ 83 | typesec(types), 84 | importsec(imports), 85 | funcsec(funcs), 86 | memsec([mem(memtype(limits.min(1)))]), 87 | exportsec(exports), 88 | codesec(codes), 89 | ]); 90 | return Uint8Array.from(mod.flat(Infinity)); 91 | } 92 | 93 | test('buildModule with memory', () => { 94 | const importDecls = []; 95 | const functionDecls = [ 96 | { 97 | name: 'main', 98 | paramTypes: [], 99 | resultType: valtype.i32, 100 | locals: [], 101 | body: [ 102 | [instr.i32.const, i32(40), [instr.memory.grow, memidx(0)]], 103 | [instr.memory.size, memidx(0)], 104 | instr.i32.add, 105 | instr.end, 106 | ], 107 | }, 108 | ]; 109 | const exports = loadMod(buildModule(importDecls, functionDecls)); 110 | assert.ok(exports.$waferMemory); 111 | assert.strictEqual(exports.main(), 42); 112 | 113 | const PAGE_SIZE_IN_BYTES = 64 * 1024; 114 | assert.strictEqual( 115 | exports.$waferMemory.buffer.byteLength, 116 | PAGE_SIZE_IN_BYTES * 41, 117 | ); 118 | }); 119 | 120 | instr.memory = { 121 | size: 0x3f, // [] -> [i32] 122 | grow: 0x40, // [i32] -> [i32] 123 | }; 124 | 125 | instr.i32.load = 0x28; // [i32] -> [i32] 126 | instr.i32.store = 0x36; // [i32, i32] -> [] 127 | 128 | // align:u32, offset:u32 129 | function memarg(align, offset) { 130 | return [u32(align), u32(offset)]; 131 | } 132 | 133 | test('load and store', () => { 134 | const importDecls = []; 135 | const functionDecls = [ 136 | { 137 | name: 'main', 138 | paramTypes: [], 139 | resultType: valtype.i32, 140 | locals: [], 141 | body: [ 142 | [instr.i32.const, i32(4)], // offset (destination) 143 | [instr.i32.const, i32(42)], // value 144 | [instr.i32.store, memarg(0, 0)], 145 | [instr.i32.const, i32(4)], 146 | [instr.i32.load, memarg(0, 0)], 147 | instr.end, 148 | ], 149 | }, 150 | ]; 151 | const exports = loadMod(buildModule(importDecls, functionDecls)); 152 | assert.equal(exports.main(), 42); 153 | 154 | const view = new DataView(exports.$waferMemory.buffer); 155 | assert.equal(view.getInt32(4, true), 42); 156 | }); 157 | -------------------------------------------------------------------------------- /chapter11.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import {mock} from 'node:test'; 3 | 4 | import { 5 | buildModule, 6 | buildStringTable, 7 | buildSymbolTable, 8 | defineFunctionDecls, 9 | defineImportDecls, 10 | defineToWasm, 11 | int32ToBytes, 12 | makeTestFn, 13 | wafer, 14 | } from './chapter10.js'; 15 | 16 | const test = makeTestFn(import.meta.url); 17 | 18 | const waferPrelude = ` 19 | extern func __consoleLog(str); 20 | 21 | func newInt32Array(len) { 22 | let freeOffset = __mem[__heap_base]; 23 | __mem[__heap_base] := freeOffset + (len * 4) + 4; 24 | __mem[freeOffset] := len; 25 | freeOffset 26 | } 27 | 28 | func __readInt32Array(arr, idx) { 29 | if idx < 0 or idx >= __mem[arr] { 30 | __trap(); 31 | } 32 | __mem[arr + 4 + (idx * 4)] 33 | } 34 | 35 | func __writeInt32Array(arr, idx, val) { 36 | if idx < 0 or idx >= __mem[arr] { 37 | __trap(); 38 | } 39 | __mem[arr + 4 + (idx * 4)] := val 40 | } 41 | 42 | func print(str) { 43 | __consoleLog(str) 44 | } 45 | `; 46 | 47 | function compile(source) { 48 | const matchResult = wafer.match(waferPrelude + source); 49 | if (!matchResult.succeeded()) { 50 | throw new Error(matchResult.message); 51 | } 52 | 53 | const symbols = buildSymbolTable(wafer, matchResult); 54 | const strings = buildStringTable(wafer, matchResult); 55 | const semantics = wafer.createSemantics(); 56 | defineToWasm(semantics, symbols, strings); 57 | defineImportDecls(semantics); 58 | defineFunctionDecls(semantics, symbols); 59 | 60 | const importDecls = semantics(matchResult).importDecls(); 61 | const functionDecls = semantics(matchResult).functionDecls(); 62 | const heapBase = strings.data.length; 63 | const dataSegs = [ 64 | {offset: 0, bytes: strings.data}, 65 | {offset: heapBase, bytes: int32ToBytes(heapBase + 4)}, 66 | ]; 67 | return buildModule(importDecls, functionDecls, dataSegs); 68 | } 69 | 70 | function loadMod(bytes, imports) { 71 | const mod = new WebAssembly.Module(bytes); 72 | let memory = undefined; 73 | const waferImports = { 74 | ...imports.waferImports, 75 | __consoleLog: (waferStr) => { 76 | console.log(waferStringToJS(memory, waferStr)); 77 | }, 78 | }; 79 | const fullImports = {...imports, waferImports}; 80 | const {exports} = new WebAssembly.Instance(mod, fullImports); 81 | memory = exports.$waferMemory; 82 | return exports; 83 | } 84 | 85 | function waferStringToJS(mem, waferStr) { 86 | const int32View = new DataView(mem.buffer); 87 | const chars = []; 88 | const len = int32View.getUint32(waferStr, true); 89 | for (let i = 0; i < len; i++) { 90 | chars.push(int32View.getUint32(waferStr + (i + 1) * 4, true)); 91 | } 92 | return String.fromCharCode(...chars); 93 | } 94 | 95 | test('print', () => { 96 | const waferSrc = ` 97 | func sayHello() { 98 | print("Hello from Wafer!!") 99 | } 100 | `; 101 | const exports = loadMod(compile(waferSrc), {}); 102 | const consoleLog = mock.method(console, 'log'); 103 | exports.sayHello(); 104 | assert.strictEqual(consoleLog.mock.callCount(), 1); 105 | assert.deepEqual(consoleLog.mock.calls[0].arguments, ['Hello from Wafer!!']); 106 | }); 107 | 108 | export * from './chapter10.js'; 109 | -------------------------------------------------------------------------------- /chapter11/01-hello-world.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import {mock} from 'node:test'; 3 | 4 | import { 5 | buildModule, 6 | buildStringTable, 7 | buildSymbolTable, 8 | defineFunctionDecls, 9 | defineImportDecls, 10 | defineToWasm, 11 | int32ToBytes, 12 | makeTestFn, 13 | wafer, 14 | } from '../chapter10.js'; 15 | 16 | const test = makeTestFn(import.meta.url); 17 | 18 | const waferPrelude = ` 19 | extern func __consoleLog(str); 20 | 21 | func newInt32Array(len) { 22 | let freeOffset = __mem[__heap_base]; 23 | __mem[__heap_base] := freeOffset + (len * 4) + 4; 24 | __mem[freeOffset] := len; 25 | freeOffset 26 | } 27 | 28 | func __readInt32Array(arr, idx) { 29 | if idx < 0 or idx >= __mem[arr] { 30 | __trap(); 31 | } 32 | __mem[arr + 4 + (idx * 4)] 33 | } 34 | 35 | func __writeInt32Array(arr, idx, val) { 36 | if idx < 0 or idx >= __mem[arr] { 37 | __trap(); 38 | } 39 | __mem[arr + 4 + (idx * 4)] := val 40 | } 41 | 42 | func print(str) { 43 | __consoleLog(str) 44 | } 45 | `; 46 | 47 | function compile(source) { 48 | const matchResult = wafer.match(waferPrelude + source); 49 | if (!matchResult.succeeded()) { 50 | throw new Error(matchResult.message); 51 | } 52 | 53 | const symbols = buildSymbolTable(wafer, matchResult); 54 | const strings = buildStringTable(wafer, matchResult); 55 | const semantics = wafer.createSemantics(); 56 | defineToWasm(semantics, symbols, strings); 57 | defineImportDecls(semantics); 58 | defineFunctionDecls(semantics, symbols); 59 | 60 | const importDecls = semantics(matchResult).importDecls(); 61 | const functionDecls = semantics(matchResult).functionDecls(); 62 | const heapBase = strings.data.length; 63 | const dataSegs = [ 64 | {offset: 0, bytes: strings.data}, 65 | {offset: heapBase, bytes: int32ToBytes(heapBase + 4)}, 66 | ]; 67 | return buildModule(importDecls, functionDecls, dataSegs); 68 | } 69 | 70 | function loadMod(bytes, imports) { 71 | const mod = new WebAssembly.Module(bytes); 72 | let memory = undefined; 73 | const waferImports = { 74 | ...imports.waferImports, 75 | __consoleLog: (waferStr) => { 76 | console.log(waferStringToJS(memory, waferStr)); 77 | }, 78 | }; 79 | const fullImports = {...imports, waferImports}; 80 | const {exports} = new WebAssembly.Instance(mod, fullImports); 81 | memory = exports.$waferMemory; 82 | return exports; 83 | } 84 | 85 | function waferStringToJS(mem, waferStr) { 86 | const int32View = new DataView(mem.buffer); 87 | const chars = []; 88 | const len = int32View.getUint32(waferStr, true); 89 | for (let i = 0; i < len; i++) { 90 | chars.push(int32View.getUint32(waferStr + (i + 1) * 4, true)); 91 | } 92 | return String.fromCharCode(...chars); 93 | } 94 | 95 | test('print', () => { 96 | const waferSrc = ` 97 | func sayHello() { 98 | print("Hello from Wafer!!") 99 | } 100 | `; 101 | const exports = loadMod(compile(waferSrc), {}); 102 | const consoleLog = mock.method(console, 'log'); 103 | exports.sayHello(); 104 | assert.strictEqual(consoleLog.mock.callCount(), 1); 105 | assert.deepEqual(consoleLog.mock.calls[0].arguments, ['Hello from Wafer!!']); 106 | }); 107 | -------------------------------------------------------------------------------- /chapterd1.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | import { 4 | code, 5 | codesec, 6 | export_, 7 | exportdesc, 8 | exportsec, 9 | func, 10 | funcsec, 11 | functype, 12 | instr, 13 | makeTestFn, 14 | module, 15 | typeidx, 16 | typesec, 17 | valtype, 18 | } from './chapter02.js'; 19 | 20 | const test = makeTestFn(import.meta.url); 21 | 22 | const CONTINUATION_BIT = 0b10000000; 23 | 24 | const SEVEN_BIT_MASK_BIG_INT = 0b01111111n; 25 | 26 | function leb128(v) { 27 | let val = BigInt(v); 28 | let more = true; 29 | const r = []; 30 | 31 | while (more) { 32 | const b = Number(val & SEVEN_BIT_MASK_BIG_INT); 33 | val = val >> 7n; 34 | more = val !== 0n; 35 | if (more) { 36 | r.push(b | CONTINUATION_BIT); 37 | } else { 38 | r.push(b); 39 | } 40 | } 41 | 42 | return r; 43 | } 44 | 45 | function sleb128(v) { 46 | let val = BigInt(v); 47 | let more = true; 48 | const r = []; 49 | 50 | while (more) { 51 | const b = Number(val & SEVEN_BIT_MASK_BIG_INT); 52 | const signBitSet = !!(b & 0x40); 53 | 54 | val = val >> 7n; 55 | 56 | if ((val === 0n && !signBitSet) || (val === -1n && signBitSet)) { 57 | more = false; 58 | r.push(b); 59 | } else { 60 | r.push(b | CONTINUATION_BIT); 61 | } 62 | } 63 | 64 | return r; 65 | } 66 | 67 | instr.i64 ??= {}; 68 | instr.i64.const = 0x42; 69 | 70 | async function buildSLEB128Body(v) { 71 | const mod = module([ 72 | typesec([functype([], [valtype.i64])]), 73 | funcsec([typeidx(0)]), 74 | exportsec([export_('main', exportdesc.func(0))]), 75 | codesec([code(func([], [[instr.i64.const, i64(v)], instr.end]))]), 76 | ]); 77 | const bin = Uint8Array.from(mod.flat(Infinity)); 78 | 79 | return (await WebAssembly.instantiate(bin)).instance.exports.main(); 80 | } 81 | 82 | test('test sleb128 encoder', async () => { 83 | async function test(v) { 84 | const r = await buildSLEB128Body(v); 85 | assert.strictEqual(v, r); 86 | } 87 | 88 | for (let i = 0n; i < 62n; i++) { 89 | const v = 2n << i; 90 | await test(-v - 1n); 91 | await test(-v); 92 | await test(-v + 1n); 93 | 94 | await test(v - 1n); 95 | await test(v); 96 | await test(v + 1n); 97 | } 98 | }); 99 | 100 | async function buildLEB128Body(bodyLenBytes) { 101 | if (bodyLenBytes < 2) { 102 | throw new Error("Can't generate body that small"); 103 | } 104 | 105 | let bytesRemaining = bodyLenBytes - 3; 106 | const adds = []; 107 | const oneAdd = [[instr.i64.const, 1], instr.i64.add]; 108 | // 3 bytes per increment 109 | for (; bytesRemaining >= 3; bytesRemaining -= 3) { 110 | adds.push(oneAdd); 111 | } 112 | 113 | // Add nops to match expected body length 114 | for (let i = 0; i < bytesRemaining; i++) { 115 | adds.push(instr.nop); 116 | } 117 | 118 | const mod = module([ 119 | typesec([functype([], [valtype.i64])]), 120 | funcsec([typeidx(0)]), 121 | exportsec([export_('main', exportdesc.func(0))]), 122 | codesec([code(func([], [[instr.i64.const, 0], adds, instr.end]))]), 123 | ]); 124 | const bin = Uint8Array.from(mod.flat(Infinity)); 125 | 126 | return (await WebAssembly.instantiate(bin)).instance.exports.main(); 127 | } 128 | 129 | instr.i64.add = 0x7c; 130 | instr.nop = 0x01; 131 | 132 | test('test leb128 encoder', async () => { 133 | function getIncrResultForBytes(len) { 134 | return BigInt(Math.floor((len - 3) / 3)); 135 | } 136 | 137 | async function test(bodyLenBytes) { 138 | const expected = getIncrResultForBytes(bodyLenBytes); 139 | const result = await buildLEB128Body(bodyLenBytes); 140 | assert.strictEqual(expected, result); 141 | } 142 | 143 | await test(3); 144 | await test(6); 145 | await test(2 ** 7); 146 | await test(2 ** 14); 147 | await test(2 ** 21); 148 | }); 149 | 150 | const MIN_U32 = 0; 151 | const MAX_U32 = 2 ** 32 - 1; 152 | function u32(v) { 153 | if (v < MIN_U32 || v > MAX_U32) { 154 | throw Error(`Value out of range for u32: ${v}`); 155 | } 156 | 157 | return leb128(v); 158 | } 159 | 160 | const MIN_I32 = -(2 ** 32 / 2); 161 | const MAX_I32 = 2 ** 32 / 2 - 1; 162 | const I32_NEG_OFFSET = 2 ** 32; 163 | function i32(v) { 164 | if (v < MIN_I32 || v > MAX_U32) { 165 | throw Error(`Value out of range for i32: ${v}`); 166 | } 167 | 168 | if (v > MAX_I32) { 169 | return sleb128(v - I32_NEG_OFFSET); 170 | } 171 | 172 | return sleb128(v); 173 | } 174 | 175 | test('i32/u32 encode difference for numbers with most significant bit set', () => { 176 | assert.deepEqual(i32(MIN_I32), i32(2 ** 31)); 177 | assert.deepEqual(i32(-1), i32(MAX_U32)); 178 | assert.throws(() => i32(MAX_U32 + 1)); 179 | assert.throws(() => i32(MIN_I32 - 1)); 180 | }); 181 | 182 | const MIN_U64 = 0n; 183 | const MAX_U64 = 2n ** 64n - 1n; 184 | function u64(v) { 185 | if (v < MIN_U64 || v > MAX_U64) { 186 | throw Error(`Value out of range for u64: ${v}`); 187 | } 188 | 189 | return leb128(v); 190 | } 191 | 192 | const MIN_I64 = -(2n ** 64n / 2n); 193 | const MAX_I64 = 2n ** 64n / 2n - 1n; 194 | const I64_NEG_OFFSET = 2n ** 64n; 195 | function i64(v0) { 196 | const v = BigInt(v0); 197 | 198 | if (v < MIN_I64 || v > MAX_U64) { 199 | throw Error(`Value out of range for i64: ${v}`); 200 | } 201 | 202 | if (v > MAX_I64) { 203 | return sleb128(v - I64_NEG_OFFSET); 204 | } 205 | 206 | return sleb128(v); 207 | } 208 | 209 | test('i64/u64 encode difference for numbers with most significant bit set', () => { 210 | assert.deepEqual(i64(MIN_I64), i64(2n ** 63n)); 211 | assert.deepEqual(i64(-1n), i64(MAX_U64)); 212 | assert.throws(() => i64(MAX_U64 + 1n)); 213 | assert.throws(() => i64(MIN_I64 - 1n)); 214 | }); 215 | -------------------------------------------------------------------------------- /chapterd3.js: -------------------------------------------------------------------------------- 1 | import { 2 | code, 3 | codesec, 4 | func, 5 | funcidx, 6 | funcsec, 7 | functype, 8 | instr, 9 | locals, 10 | makeTestFn, 11 | module, 12 | name, 13 | section, 14 | typeidx, 15 | typesec, 16 | u32, 17 | valtype, 18 | vec, 19 | } from './chapter05.js'; 20 | 21 | const test = makeTestFn(import.meta.url); 22 | 23 | const SECTION_ID_CUSTOM = 0; 24 | 25 | function custom(name, bytes) { 26 | return [name, bytes]; 27 | } 28 | 29 | function customsec(custom) { 30 | return section(SECTION_ID_CUSTOM, custom); 31 | } 32 | 33 | function namesec(namedata) { 34 | return customsec(custom(name('name'), namedata)); 35 | } 36 | 37 | // n:name 38 | function namedata(modulenamesubsec, funcnamesubsec, localnamesubsec) { 39 | return [modulenamesubsec, funcnamesubsec, localnamesubsec]; 40 | } 41 | 42 | const CUSTOM_NAME_SUB_SEC_MODULE = 0; 43 | function modulenamesubsec(n) { 44 | return namesubsection(CUSTOM_NAME_SUB_SEC_MODULE, name(n)); 45 | } 46 | 47 | const CUSTOM_NAME_SUB_SEC_FUNC = 1; 48 | function funcnamesubsec(namemap) { 49 | return namesubsection(CUSTOM_NAME_SUB_SEC_FUNC, namemap); 50 | } 51 | 52 | // N:byte 53 | function namesubsection(N, B) { 54 | const flatB = B.flat(Infinity); 55 | const size = u32(flatB.length); 56 | return [N, size, flatB]; 57 | } 58 | 59 | function namemap(nameassocs) { 60 | return vec(nameassocs); 61 | } 62 | 63 | function nameassoc(idx, n) { 64 | return [idx, name(n)]; 65 | } 66 | 67 | const CUSTOM_NAME_SUB_SEC_LOCAL = 2; 68 | function localnamesubsec(indirectnamemap) { 69 | return namesubsection(CUSTOM_NAME_SUB_SEC_LOCAL, indirectnamemap); 70 | } 71 | 72 | function indirectnamemap(indirectnameassocs) { 73 | return vec(indirectnameassocs); 74 | } 75 | 76 | function indirectnameassoc(idx, namemap) { 77 | return [idx, namemap]; 78 | } 79 | 80 | export * from './chapter11.js'; 81 | export { 82 | SECTION_ID_CUSTOM, 83 | custom, 84 | customsec, 85 | namesec, 86 | namedata, 87 | CUSTOM_NAME_SUB_SEC_MODULE, 88 | modulenamesubsec, 89 | CUSTOM_NAME_SUB_SEC_FUNC, 90 | funcnamesubsec, 91 | namesubsection, 92 | namemap, 93 | nameassoc, 94 | CUSTOM_NAME_SUB_SEC_LOCAL, 95 | localnamesubsec, 96 | indirectnamemap, 97 | indirectnameassoc, 98 | }; 99 | -------------------------------------------------------------------------------- /chapterd4.js: -------------------------------------------------------------------------------- 1 | class ModuleInstance { 2 | constructor() { 3 | this.pc = 0; 4 | this.opC = 0; 5 | 6 | this.stack = []; 7 | 8 | this.c = null; 9 | this.c1 = null; 10 | this.c2 = null; 11 | } 12 | 13 | step(code) { 14 | if (this.pc >= code.length) { 15 | return; 16 | } 17 | 18 | const instr = code[this.pc]; 19 | 20 | if (this.opC < instr.ops.length) { 21 | const op = instr.ops[this.opC]; 22 | op.run(this); 23 | } 24 | 25 | this.opC += 1; 26 | if (this.opC >= instr.ops.length) { 27 | this.pc += 1; 28 | this.opC = 0; 29 | } 30 | } 31 | 32 | pushValue(v) { 33 | this.stack.push(v); 34 | } 35 | 36 | popValue() { 37 | return this.stack.pop(); 38 | } 39 | 40 | peekValue() { 41 | return this.stack.at(-1); 42 | } 43 | 44 | assertTopValueOfType(Class) { 45 | return this.peekValue() instanceof Class; 46 | } 47 | 48 | popValueIntoC1() { 49 | this.c1 = this.popValue(); 50 | } 51 | 52 | popValueIntoC2() { 53 | this.c2 = this.popValue(); 54 | } 55 | 56 | applyUnOp(fn) { 57 | this.c = this.c1.applyUnOp(fn); 58 | } 59 | 60 | pushC() { 61 | this.pushValue(this.c); 62 | } 63 | 64 | applyBinOp(fn) { 65 | this.c = this.c1.applyBinOp(fn, this.c2); 66 | } 67 | 68 | clearC() { 69 | this.c = null; 70 | } 71 | 72 | clearC1() { 73 | this.c1 = null; 74 | } 75 | 76 | clearC2() { 77 | this.c2 = null; 78 | } 79 | 80 | clearCs() { 81 | this.clearC(); 82 | this.clearC1(); 83 | this.clearC2(); 84 | } 85 | 86 | reset() { 87 | this.pc = 0; 88 | this.opC = 0; 89 | this.clearCs(); 90 | this.stack = []; 91 | } 92 | } 93 | 94 | class Instruction { 95 | constructor(name, ops) { 96 | this.name = name; 97 | this.ops = ops; 98 | } 99 | } 100 | 101 | const nop = new Instruction('nop', []); 102 | 103 | class Module { 104 | constructor(code) { 105 | this.code = code; 106 | } 107 | } 108 | 109 | class Value { 110 | constructor(value) { 111 | this.value = value; 112 | } 113 | 114 | getTypeName() { 115 | throw new Error('not implemented'); 116 | } 117 | 118 | applyUnOp(fn) { 119 | return new this.constructor(fn(this.value)); 120 | } 121 | 122 | applyBinOp(fn, other) { 123 | return new this.constructor(fn(this.value, other.value)); 124 | } 125 | } 126 | 127 | class I32 extends Value { 128 | getTypeName() { 129 | return 'i32'; 130 | } 131 | } 132 | 133 | function i32(v) { 134 | return new I32(v); 135 | } 136 | 137 | class Op { 138 | constructor(name, fn) { 139 | this.name = name; 140 | this.fn = fn; 141 | } 142 | 143 | run(vm) { 144 | this.fn(vm); 145 | } 146 | } 147 | 148 | class ConstOp extends Op { 149 | constructor(name, value) { 150 | super(name, (vm) => vm.pushValue(value)); 151 | } 152 | } 153 | 154 | i32.const = (v) => 155 | new Instruction(`i32.const`, [new ConstOp(`Push value ${v}`, i32(v))]); 156 | 157 | const ASSERT_TOP_I32 = new Op('Assert Value Type i32', (vm) => 158 | vm.assertTopValueOfType(I32), 159 | ); 160 | 161 | const POP_TO_C1 = new Op('Pop to c1', (vm) => vm.popValueIntoC1()); 162 | 163 | const APPLY_UN_OP = (opName, fn) => 164 | new Op('Apply UnOp: ' + opName, (vm) => vm.applyUnOp(fn)); 165 | 166 | const PUSH_C = new Op('Push C', (vm) => vm.pushC()); 167 | 168 | i32.eqz = new Instruction('i32.eqz', [ 169 | ASSERT_TOP_I32, 170 | POP_TO_C1, 171 | APPLY_UN_OP('eqz', (c1) => (c1 === 0 ? 1 : 0)), 172 | PUSH_C, 173 | ]); 174 | 175 | const POP_TO_C2 = new Op('Pop to c2', (vm) => vm.popValueIntoC2()); 176 | 177 | const APPLY_BIN_OP = (opName, fn) => 178 | new Op('Apply BinOp: ' + opName, (vm) => vm.applyBinOp(fn)); 179 | 180 | i32.add = new Instruction('i32.add', [ 181 | ASSERT_TOP_I32, 182 | POP_TO_C2, 183 | ASSERT_TOP_I32, 184 | POP_TO_C1, 185 | APPLY_BIN_OP('+', (c1, c2) => c1 + c2), 186 | PUSH_C, 187 | ]); 188 | 189 | i32.sub = new Instruction('i32.sub ', [ 190 | ASSERT_TOP_I32, 191 | POP_TO_C2, 192 | ASSERT_TOP_I32, 193 | POP_TO_C1, 194 | APPLY_BIN_OP('-', (c1, c2) => c1 - c2), 195 | PUSH_C, 196 | ]); 197 | 198 | function makeI32BinOp(name, symbol, fn) { 199 | return new Instruction(name, [ 200 | ASSERT_TOP_I32, 201 | POP_TO_C2, 202 | ASSERT_TOP_I32, 203 | POP_TO_C1, 204 | APPLY_BIN_OP(symbol, fn), 205 | PUSH_C, 206 | ]); 207 | } 208 | 209 | i32.mul = makeI32BinOp('i32.mul', '*', (c1, c2) => c1 * c2); 210 | 211 | i32.div = makeI32BinOp('i32.div', '/', (c1, c2) => Math.trunc(c1 / c2)); 212 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "hasInstallScript": true, 9 | "devDependencies": { 10 | "ohm-js": "^17.1.0", 11 | "uvu": "^0.5.6" 12 | } 13 | }, 14 | "node_modules/dequal": { 15 | "version": "2.0.3", 16 | "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", 17 | "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 18 | "dev": true, 19 | "engines": { 20 | "node": ">=6" 21 | } 22 | }, 23 | "node_modules/diff": { 24 | "version": "5.1.0", 25 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", 26 | "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", 27 | "dev": true, 28 | "engines": { 29 | "node": ">=0.3.1" 30 | } 31 | }, 32 | "node_modules/kleur": { 33 | "version": "4.1.5", 34 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 35 | "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", 36 | "dev": true, 37 | "engines": { 38 | "node": ">=6" 39 | } 40 | }, 41 | "node_modules/mri": { 42 | "version": "1.2.0", 43 | "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", 44 | "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", 45 | "dev": true, 46 | "engines": { 47 | "node": ">=4" 48 | } 49 | }, 50 | "node_modules/ohm-js": { 51 | "version": "17.1.0", 52 | "resolved": "https://registry.npmjs.org/ohm-js/-/ohm-js-17.1.0.tgz", 53 | "integrity": "sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q==", 54 | "dev": true, 55 | "engines": { 56 | "node": ">=0.12.1" 57 | } 58 | }, 59 | "node_modules/sade": { 60 | "version": "1.8.1", 61 | "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", 62 | "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", 63 | "dev": true, 64 | "dependencies": { 65 | "mri": "^1.1.0" 66 | }, 67 | "engines": { 68 | "node": ">=6" 69 | } 70 | }, 71 | "node_modules/uvu": { 72 | "version": "0.5.6", 73 | "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", 74 | "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", 75 | "dev": true, 76 | "dependencies": { 77 | "dequal": "^2.0.0", 78 | "diff": "^5.0.0", 79 | "kleur": "^4.0.3", 80 | "sade": "^1.7.3" 81 | }, 82 | "bin": { 83 | "uvu": "bin.js" 84 | }, 85 | "engines": { 86 | "node": ">=8" 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "test": "node --test chapter*/*[0-9]*.js chapter??.js", 6 | "postinstall": "cat README.md" 7 | }, 8 | "devDependencies": { 9 | "ohm-js": "^17.1.0" 10 | } 11 | } --------------------------------------------------------------------------------