Last active
November 20, 2020 01:39
-
-
Save tyfkda/9c9ec5713245529f6408de4a3f290d91 to your computer and use it in GitHub Desktop.
JIT WASM Calculator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no" /> | |
| <title>JIT WASM Calculator</title> | |
| <style type="text/css"> | |
| .code { | |
| font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; | |
| } | |
| </style> | |
| <script src="main.js"></script> | |
| <script type="text/javascript"> | |
| /*window.addEventListener('load', function() { | |
| const importObject = {} | |
| WebAssembly.instantiateStreaming(fetch('a.wasm'), importObject) | |
| .then(results => { | |
| const main = results.instance.exports.main | |
| const res = main() | |
| console.log(`main: result=${res}`) | |
| }) | |
| })*/ | |
| function dumpCode(code) { | |
| const NCOL = 8 | |
| const rows = [] | |
| let i = 0 | |
| for (let i = 0; i < code.length; i += NCOL) { | |
| const row = code.slice(i, i + NCOL) | |
| const hexRow = Array.from(row) | |
| .map(x => `0x${x.toString(16).padStart(2, '0')}`) | |
| .join(', ') | |
| rows.push(hexRow) | |
| } | |
| const codeArea = document.getElementById('code') | |
| codeArea.value = rows.join(',\n') + ',' | |
| } | |
| async function calculate(value) { | |
| document.getElementById('error').innerText = '' | |
| document.getElementById('code').value = '' | |
| const compiler = new Compiler() | |
| try { | |
| const code = compiler.compile(value) | |
| const funcName = 'calc' | |
| const wasm = generateWasm(funcName, code) | |
| dumpCode(wasm) | |
| const importObject = {} | |
| const module = await WebAssembly.instantiate(wasm, importObject) | |
| const func = module.instance.exports[funcName] | |
| const result = func() | |
| document.getElementById('answer').value = `${result}` | |
| } catch (e) { | |
| document.getElementById('error').innerText = e.toString() | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <h1>JIT WASM Calculator</h1> | |
| <form onsubmit="calculate(this.num.value); return false"> | |
| <div> | |
| <input id="num" type="text" value="-(1 + 2) * 3"> | |
| <input type="submit" value="Calc"> | |
| <div id="error" style="color:red"></div> | |
| </div> | |
| <div>Answer: <input id="answer" type="text" size="30" readonly></div> | |
| </form> | |
| <section> | |
| <h4>WASM Code</h4> | |
| <textarea id="code" class="code" wrap="off" cols="50" rows="8" readonly></textarea> | |
| </section> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use strict' | |
| function tokenize(src) { | |
| const tokens = [] | |
| while ((src = src.trimStart()) !== '') { | |
| let m | |
| let token | |
| if ((m = src.match(/^(\d+)/)) != null) { | |
| token = parseInt(m[1]) | |
| } else if ((m = src.match(/^([+\-*\/%()])/)) != null) { | |
| token = m[1] | |
| } else { | |
| throw `Unknown token: ${src}` | |
| } | |
| tokens.push(token) | |
| src = RegExp.rightContext | |
| } | |
| return tokens | |
| } | |
| function isNumber(value) { | |
| return typeof(value) === 'number' | |
| } | |
| function uint32(val) { | |
| return [...Array(4)].map((_, i) => val >> (i * 8)) | |
| } | |
| function varuint32(val) { | |
| const I32MAX = 0x7fffffff | |
| if (val > I32MAX) | |
| val &= I32MAX | |
| else if (val < -I32MAX - 1) | |
| val = ~(~val & I32MAX) | |
| const array = [] | |
| const MAX = 1 << 6 | |
| for (;;) { | |
| if (val < MAX && val >= -MAX) { | |
| array.push(val & 0x7f) | |
| break | |
| } | |
| array.push((val & 0x7f) | 0x80) | |
| val >>= 7 | |
| } | |
| return array | |
| } | |
| function chars(str) { | |
| return [...Array(str.length)].map((_, i) => str.charCodeAt(i)) | |
| } | |
| const OP_END = 0x0b | |
| const OP_I32_CONST = 0x41 | |
| const OP_I32_ADD = 0x6a | |
| const OP_I32_SUB = 0x6b | |
| const OP_I32_MUL = 0x6c | |
| const OP_I32_DIV_S = 0x6d | |
| const OP_I32_REM_S = 0x6f | |
| class Compiler { | |
| compile(src) { | |
| this.funcBody = [] | |
| this.tokens = tokenize(src) | |
| this.parseExpr() | |
| this.appendCode(OP_END) | |
| if (this.tokens.length > 0) | |
| throw `Syntax error: ${this.tokens}` | |
| const localCount = varuint32(0) | |
| return [ | |
| ...varuint32(localCount.length + this.funcBody.length), | |
| ...localCount, // local decl count | |
| ...this.funcBody, | |
| ] | |
| } | |
| parseExpr() { | |
| this.parseTerm() | |
| for (;;) { | |
| if (!(this.tokens.length > 0 && | |
| (this.tokens[0] === '+' || this.tokens[0] === '-'))) | |
| break | |
| const op = this.tokens.shift() | |
| this.parseTerm() | |
| switch (op) { | |
| case '+': this.appendCode(OP_I32_ADD); break | |
| case '-': this.appendCode(OP_I32_SUB); break | |
| } | |
| } | |
| } | |
| parseTerm() { | |
| this.parsePrim() | |
| for (;;) { | |
| if (!(this.tokens.length > 0 && | |
| (this.tokens[0] === '*' || this.tokens[0] === '/' || this.tokens[0] === '%'))) | |
| break | |
| const op = this.tokens.shift() | |
| this.parsePrim() | |
| switch (op) { | |
| case '*': this.appendCode(OP_I32_MUL); break | |
| case '/': this.appendCode(OP_I32_DIV_S); break | |
| case '%': this.appendCode(OP_i32_REM_S); break | |
| } | |
| } | |
| } | |
| parsePrim() { | |
| switch (this.tokens.length > 0 && this.tokens[0]) { | |
| case '(': | |
| this.tokens.shift() | |
| this.parseExpr() | |
| if (this.tokens.length <= 0 || this.tokens[0] !== ')') | |
| throw `Close paren expected: ${this.tokens[0]}` | |
| this.tokens.shift() | |
| return | |
| case '+': | |
| this.tokens.shift() | |
| this.parsePrim() | |
| return | |
| case '-': | |
| this.tokens.shift() | |
| this.appendCode([OP_I32_CONST, ...varuint32(0)]) | |
| this.parsePrim() | |
| this.appendCode(OP_I32_SUB) | |
| return | |
| default: | |
| if (isNumber(this.tokens[0])) { | |
| const num = this.tokens.shift() | |
| this.appendCode([OP_I32_CONST, ...varuint32(num)]) | |
| return | |
| } | |
| break | |
| } | |
| throw `Number expected: ${this.tokens[0]}` | |
| } | |
| appendCode(code) { | |
| if (Array.isArray(code)) | |
| code.forEach(c => this.funcBody.push(c)) | |
| else | |
| this.funcBody.push(code) | |
| } | |
| } | |
| function generateWasm(funcName, funcBody) { | |
| const SEC_TYPE = 1 | |
| const SEC_FUNC = 3 | |
| const SEC_EXPORT = 7 | |
| const SEC_CODE = 10 | |
| const TY_I32 = 0x7f | |
| const exports = [ | |
| [ | |
| varuint32(funcName.length), // string length | |
| ...chars(funcName), // export name | |
| 0x00, // export kind | |
| 0x00, // export func index | |
| ], | |
| ] | |
| const numExports = varuint32(exports.length) | |
| const flatExports = exports.flat() | |
| const bin = [ | |
| ...chars('\x00asm'), // WASM_BINARY_MAGIC | |
| ...uint32(1), // WASM_BINARY_VERSION | |
| // Types | |
| SEC_TYPE, // Section "Type" (1) | |
| 0x05, // Size | |
| 0x01, // num types | |
| // type 0 | |
| 0x60, // func | |
| 0x00, // num params | |
| 0x01, // num results | |
| TY_I32, // i32 | |
| // Functions | |
| SEC_FUNC, // Section "Function" (3) | |
| 0x02, // Size | |
| 0x01, // num functions | |
| 0x00, // function 0 signature index | |
| // Exports | |
| SEC_EXPORT, // Section "Export" (7) | |
| ...varuint32(flatExports.length + numExports.length), // Size | |
| ...numExports, // num exports | |
| ...flatExports, | |
| // Code | |
| SEC_CODE, // Section "Code" (10) | |
| funcBody.length + 1, // Size | |
| 0x01, // num types | |
| // function body 0 | |
| ...funcBody, | |
| ] | |
| return Uint8Array.from(bin) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment