|
import * as util from 'util'; |
|
|
|
const consts = { |
|
'i32.const': Number, |
|
'i64.const': BigInt, |
|
'f32.const': Number, |
|
'f64.const': Number, |
|
} |
|
|
|
const overflow = (x, n) => { |
|
const result = x % BigInt(2 ** n); |
|
if (result >= BigInt(2 ** (n - 1))) { |
|
return result - BigInt(2 ** n); |
|
} else if (result < -BigInt(2 ** (n - 1))) { |
|
return BigInt(2 ** n) + result; |
|
} |
|
return result; |
|
} |
|
|
|
const i32Unsign = (x) => |
|
new DataView(new Int32Array([x]).buffer).getUint32(0, true); |
|
|
|
const binary = { |
|
'i32.add': (x, y) => x + y, |
|
'i32.sub': (x, y) => x - y, |
|
'i32.mul': (x, y) => x * y, |
|
'i32.div_s': (x, y) => Math.round(x / y), |
|
'i32.div_u': (x, y) => Math.round(x / y), |
|
'i32.and': (x, y) => x & y, |
|
'i32.or': (x, y) => x | y, |
|
'i32.xor': (x, y) => x ^ y, |
|
'i32.rem_s': (x, y) => x % y, |
|
'i32.rem_u': (x, y) => x % y, |
|
'i32.eq': (x, y) => Number(x === y), |
|
'i32.ne': (x, y) => Number(x != y), |
|
'i32.lt_s': (x, y) => Number(x < y), |
|
'i32.le_s': (x, y) => Number(x <= y), |
|
'i32.gt_s': (x, y) => Number(x > y), |
|
'i32.ge_s': (x, y) => Number(x >= y), |
|
'i32.lt_u': (x, y) => Number(i32Unsign(x) < i32Unsign(y)), |
|
'i32.le_u': (x, y) => Number(i32Unsign(x) <= i32Unsign(y)), |
|
'i32.gt_u': (x, y) => Number(i32Unsign(x) > i32Unsign(y)), |
|
'i32.ge_u': (x, y) => Number(i32Unsign(x) >= i32Unsign(y)), |
|
'i32.rotr': (x, y) => (x >> y) | ((x & ((2 ** y) - 1)) << (32 - y)), |
|
'i32.rotl': (x, y) => (x << y) | ((x & ((2 ** y) - 1)) >> (32 - y)), |
|
'i32.shr_u': (x, y) => x >> y, |
|
'i32.shl': (x, y) => x << y, |
|
|
|
'i64.add': (x, y) => BigInt(x) + BigInt(y) % BigInt(2 ** 64), |
|
'i64.sub': (x, y) => BigInt(x) - BigInt(y), |
|
'i64.mul': (x, y) => overflow(BigInt(x) * BigInt(y), 64), |
|
'i64.div_s': (x, y) => Math.round(x / y), |
|
'i64.div_u': (x, y) => Math.round(x / y), |
|
'i64.and': (x, y) => BigInt(x) & BigInt(y), |
|
'i64.or': (x, y) => BigInt(x) | BigInt(y), |
|
'i64.xor': (x, y) => BigInt(x) ^ BigInt(y), |
|
'i64.rem_s': (x, y) => x % y, |
|
'i64.rem_u': (x, y) => x % y, |
|
'i64.eq': (x, y) => x === y, |
|
'i64.ne': (x, y) => x != y, |
|
'i64.lt_s': (x, y) => x < y, |
|
'i64.le_s': (x, y) => x <= y, |
|
'i64.gt_s': (x, y) => x > y, |
|
'i64.ge_s': (x, y) => x >= y, |
|
'i64.lt_u': (x, y) => x < y, |
|
'i64.le_u': (x, y) => x <= y, |
|
'i64.gt_u': (x, y) => x > y, |
|
'i64.ge_u': (x, y) => x >= y, |
|
'i64.rotr': (x, y) => (x >> y) | ((x & ((2 ** y) - 1)) << (64 - y)), |
|
'i64.rotl': (x, y) => (x << y) | ((x & ((2 ** y) - 1)) >> (64 - y)), |
|
'i64.shr_u': (x, y) => new DataView(new BigInt64Array([x]).buffer).getBigUint64(0, true) >> BigInt(y), |
|
'i64.shl': (x, y) => overflow(BigInt(x) << BigInt(y), 64), |
|
|
|
'f64.add': (x, y) => x + y, |
|
'f64.sub': (x, y) => x - y, |
|
'f64.mul': (x, y) => x * y, |
|
'f64.div': (x, y) => x / y, |
|
'f64.eq': (x, y) => Number(x === y), |
|
'f64.ne': (x, y) => Number(x != y), |
|
'f64.lt': (x, y) => Number(x < y), |
|
'f64.le': (x, y) => Number(x <= y), |
|
'f64.gt': (x, y) => Number(x > y), |
|
'f64.ge': (x, y) => Number(x >= y), |
|
}; |
|
|
|
const unary = { |
|
'i32.eqz': (x) => Number(x === 0), |
|
'i32.clz': x => Math.clz32(x), |
|
'i32.ctz': x => 31 - Math.clz32(x ^ (x - 1)), |
|
'i64.extend_u/i32': x => BigInt(x >>> 0), |
|
'f64.reinterpret/i64': x => new DataView(new BigUint64Array([x]).buffer).getFloat64(0, true), |
|
'f64.sqrt': x => Math.sqrt(x), |
|
'f64.convert_u/i32': i32Unsign, |
|
'i32.wrap/i64': x => Number(overflow(x, 32)), |
|
} |
|
|
|
const load = { |
|
'i32.load': DataView.prototype.getInt32, |
|
'i64.load': DataView.prototype.getBigInt64, |
|
'f64.load': DataView.prototype.getFloat64, |
|
'i32.load8_s': DataView.prototype.getInt8, |
|
'i32.load8_u': DataView.prototype.getUint8, |
|
'i32.load16_s': DataView.prototype.getInt16, |
|
'i32.load16_u': DataView.prototype.getUint16, |
|
'i64.load8_s': DataView.prototype.getInt8, |
|
'i64.load8_u': DataView.prototype.getUint8, |
|
'i64.load16_s': DataView.prototype.getInt16, |
|
'i64.load16_u': DataView.prototype.getUint16, |
|
'i64.load32_s': DataView.prototype.getInt32, |
|
'i64.load32_u': DataView.prototype.getUint32, |
|
}; |
|
|
|
const store = { |
|
'i32.store': DataView.prototype.setInt32, |
|
'i64.store': DataView.prototype.setBigInt64, |
|
'f64.store': DataView.prototype.setFloat64, |
|
'i32.store8': DataView.prototype.setInt8, |
|
'i32.store16': DataView.prototype.setInt16, |
|
'i64.store8': DataView.prototype.setInt8, |
|
'i64.store16': DataView.prototype.setInt16, |
|
'i64.store32': DataView.prototype.setInt32, |
|
}; |
|
|
|
export class Func { |
|
constructor(public params, public returns, public code) { |
|
} |
|
} |
|
|
|
export class ImportFunc { |
|
constructor(public params, public returns, public call: Function) { |
|
} |
|
} |
|
|
|
class Break { |
|
constructor(public level) { |
|
} |
|
} |
|
|
|
class Return { |
|
|
|
} |
|
|
|
export class Machine { |
|
public memory: DataView; |
|
private readonly items: Array<any>; |
|
|
|
constructor(private functions: Array<Func | ImportFunc> = [], memSize = 65536) { |
|
this.memory = new DataView(new ArrayBuffer(memSize)); |
|
this.items = [] |
|
} |
|
|
|
push(item) { |
|
this.items.push(item); |
|
} |
|
|
|
pop() { |
|
return this.items.pop(); |
|
} |
|
|
|
call(func: Func | ImportFunc, args) { |
|
if (func instanceof Func) { |
|
try { |
|
this.execute(func.code, args); |
|
} catch (err) { |
|
if (!(err instanceof Return)) { |
|
throw err; |
|
} |
|
} |
|
if (func.returns) { |
|
return this.pop(); |
|
} |
|
} else { |
|
// console.dir({ func, args }); |
|
return func.call.apply(null, args); |
|
} |
|
} |
|
|
|
formatArray(arr) { |
|
return '[' + arr.join(', ') + ']'; |
|
} |
|
|
|
execute(ops, locals = []) { |
|
for (const [op, ...rest] of ops) { |
|
// if (this.items.length > 0) { |
|
// console.log(op, this.formatArray(rest), this.formatArray(this.items)); |
|
// } else { |
|
// console.log(op); |
|
// } |
|
if (consts[op]) { |
|
this.push(consts[op](rest[0])); |
|
} else if (binary[op]) { |
|
const right = this.pop(); |
|
const left = this.pop(); |
|
this.push(binary[op](left, right)); |
|
} else if (unary[op]) { |
|
const value = this.pop(); |
|
this.push(unary[op](value)); |
|
} |
|
else if (load[op]) { |
|
const [, offset] = rest; |
|
const addr = this.pop() + offset; |
|
this.push(load[op].call(this.memory, addr, true)); |
|
} else if (store[op]) { |
|
let value = this.pop(); |
|
if (op === 'i64.store') { |
|
value = BigInt(value); |
|
} |
|
const [, offset] = rest; |
|
const addr = this.pop() + offset; |
|
store[op].call(this.memory, addr, value, true); |
|
} |
|
|
|
else if (op === 'memory.size' || op === 'current_memory') { |
|
this.push(Math.round(this.memory.byteLength / 65536)); |
|
} else if (op === 'memory.grow' || op === 'grow_memory') { |
|
const pages = this.pop(); |
|
|
|
const newBuffer = new ArrayBuffer(this.memory.byteLength + pages * 65536); |
|
const newMemory = new Uint8Array(newBuffer); |
|
newMemory.set(new Uint8Array(this.memory.buffer)) |
|
this.memory = new DataView(newMemory.buffer); |
|
} |
|
|
|
|
|
else if (op === 'get_local') { |
|
this.push(locals[rest[0]]); |
|
} else if (op === 'set_local') { |
|
locals[rest[0]] = this.pop(); |
|
} else if (op === 'tee_local') { |
|
locals[rest[0]] = this.items[this.items.length - 1]; |
|
} |
|
|
|
else if (op === 'drop') { |
|
this.pop(); |
|
} |
|
|
|
else if (op === 'select') { |
|
const cond = this.pop(); |
|
const v2 = this.pop(); |
|
const v1 = this.pop(); |
|
|
|
this.push(cond ? v1 : v2); |
|
} |
|
|
|
|
|
else if (op === 'call') { |
|
// console.dir({ funcs: this.functions, args: rest }) |
|
const func = this.functions[rest[0]]; |
|
const args = Array.from(Array(func.params).keys()).map(p => this.pop()).reverse(); |
|
const result = this.call(func, args); |
|
if (func.returns) { |
|
this.push(result); |
|
} |
|
} else if (op === 'br') { |
|
throw new Break(rest[0]); |
|
} else if (op === 'br_if') { |
|
if (this.pop()) { |
|
throw new Break(rest[0]); |
|
} |
|
} else if (op === 'br_table') { |
|
const n = this.pop(); |
|
if (n < rest[0].length) { |
|
throw new Break(rest[0][n]); |
|
} else { |
|
throw new Break(rest[1]); |
|
} |
|
} else if (op === 'block') { |
|
try { |
|
this.execute(rest[1], locals); |
|
} catch (err) { |
|
if (err instanceof Break) { |
|
if (err.level > 0) { |
|
err.level--; |
|
throw err; |
|
} |
|
} else { |
|
throw err; |
|
} |
|
} |
|
} else if (op === 'loop') { |
|
while (true) { |
|
try { |
|
this.execute(rest[1], locals); |
|
break; |
|
} catch (err) { |
|
if (err instanceof Break) { |
|
if (err.level > 0) { |
|
err.level--; |
|
throw err; |
|
} |
|
} else { |
|
throw err; |
|
} |
|
} |
|
} |
|
} else if (op === 'return') { |
|
throw new Return(); |
|
} else if (op === 'local' || op === 'unreachable' || op === 'end' || op === 'call_indirect') { |
|
// enjoy |
|
} else { |
|
console.error('=== Unknown op ===', op); |
|
} |
|
} |
|
} |
|
} |