Skip to content

Instantly share code, notes, and snippets.

@tyfkda
Last active November 20, 2020 01:39
Show Gist options
  • Save tyfkda/9c9ec5713245529f6408de4a3f290d91 to your computer and use it in GitHub Desktop.
Save tyfkda/9c9ec5713245529f6408de4a3f290d91 to your computer and use it in GitHub Desktop.
JIT WASM Calculator
<!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>
'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