alan-js-runtime
Version:
The runtime component for alan-js. Separately licensed from the compiler.
1,348 lines (1,287 loc) • 40.3 kB
JavaScript
require('cross-fetch/polyfill')
const EventEmitter = require('events')
const http = require('http')
const net = require('net')
const util = require('util')
const xxh = require('xxhashjs')
const exec = util.promisify ? util.promisify(require('child_process').exec) : () => {} // browsers
const e = new EventEmitter()
const INT8MAX = 2 ** 7 - 1
const INT8MIN = -(2 ** 7)
const INT16MAX = 2 ** 15 - 1
const INT16MIN = -(2 ** 15)
const INT32MAX = 2 ** 31 - 1
const INT32MIN = -(2 ** 31)
const INT64MAX = 2n ** 63n - 1n
const INT64MIN = -(2n ** 63n)
// JS really doesn't have BigInt equivalents of these?
const BigMin = (a, b) => a < b ? a : b
const BigMax = (a, b) => a > b ? a : b
const BigAbs = a => a > 0n ? a : -a // A six pack?
// Hashing opcodes (hashv is recursive, needs to be defined outside of the export object)
const hashcore = (hasher, a) => {
// TODO: We have to turn these values into ArrayBuffers of the right type. There's currently an
// issue if a floating point number that is also an integer is provided -- the duck typing here
// will treat it as an i64 instead of an f64 so the hash will be different between the JS and
// Rust implementations. There are a few ways to solve this, but they all have tradeoffs. Will
// revisit this in the future.
let buffer = new ArrayBuffer(8)
if (typeof a === 'number') {
if (a === parseInt(a)) {
const view = new BigInt64Array(buffer)
view.set([BigInt(a)], 0)
} else {
const view = new Float64Array(buffer)
view.set([a], 0)
}
} else if (typeof a === 'bigint') {
const view = new BigInt64Array(buffer)
view.set([a], 0)
} else if (typeof a === 'string') {
// If it's a string, we treat it like an array of 64-bit integers with a prefixed 64-bit length
// to match the behavior of the Rust runtime
const len = a.length
const len8 = Math.ceil(len / 8) * 8
buffer = new ArrayBuffer(8 + len8)
const lenview = new BigInt64Array(buffer)
lenview.set([BigInt(len)], 0)
const strview = new Int8Array(buffer)
// The following only works in the ASCII subset for now, since JS chose to use utf16 instead of
// utf8. TODO: Find a pure Javascript library that converts utf16 codepoints to utf8, or write
// one. :/
strview.set(a.split('').map(s => s.charCodeAt(0)), 8)
} else {
// Booleans are treated as if they are 64-bit integers
const val = a ? BigInt(1) : BigInt(0)
const view = new BigInt64Array(buffer)
view.set([val], 0)
}
for (let i = 0; i < buffer.byteLength; i += 8) {
const piece = buffer.slice(i, i + 8)
hasher.update(piece)
}
return hasher
}
const hashf = a => BigInt.asIntN(64, hashcore(xxh.h64().init(0xfa57), a).digest())
const hashv = arr => {
// The Rust runtime considers strings a variable type, but they are more like a fixed type for JS
if (typeof arr === 'string') return hashf(arr)
const hasher = xxh.h64().init(0xfa57)
let stack = [arr]
while (stack.length > 0) {
let arr = stack.pop()
for (const elem of arr) {
if (elem instanceof Array) {
stack.push(elem)
} else {
hashcore(hasher, elem)
}
}
}
return BigInt.asIntN(64, hasher.digest())
}
const copyarr = a => {
try {
return JSON.parse(JSON.stringify(a))
} catch (e) {
if (typeof a[0] === 'bigint') {
return a.map(v => BigInt(v))
} else {
return a.map(v => copyarr(v))
}
}
}
// Not very OOP, but since the HTTP server is a singleton right now, store open connections here
const httpConns = {}
// Same justification for the TCP server
const tcpConns = {}
// The shared mutable state for the datastore library
const ds = {}
module.exports = {
// Type conversion opcodes (mostly no-ops in JS, unless we implement a strict mode)
i8f64: a => a,
i16f64: a => a,
i32f64: a => a,
i64f64: a => parseFloat(a.toString()),
f32f64: a => a,
strf64: a => parseFloat(a),
boolf64: a => a ? 1.0 : 0.0,
i8f32: a => a,
i16f32: a => a,
i32f32: a => a,
i64f32: a => parseFloat(a.toString()),
f64f32: a => a,
strf32: a => parseFloat(a),
boolf32: a => a ? 1.0 : 0.0,
i8i64: a => BigInt(a),
i16i64: a => BigInt(a),
i32i64: a => BigInt(a),
f32i64: a => BigInt(Math.floor(a)),
f64i64: a => BigInt(Math.floor(a)),
stri64: a => BigInt(parseInt(a)), // intentionally allowing other bases here
booli64: a => a ? 1n : 0n,
i8i32: a => a,
i16i32: a => a,
i64i32: a => Number(BigInt.asIntN(32, a)),
f32i32: a => Math.floor(a),
f64i32: a => Math.floor(a),
stri32: a => parseInt(a),
booli32: a => a ? 1 : 0,
i8i16: a => a,
i32i16: a => a,
i64i16: a => Number(BigInt.asIntN(16, a)),
f32i16: a => Math.floor(a),
f64i16: a => Math.floor(a),
stri16: a => parseInt(a),
booli16: a => a ? 1 : 0,
i16i8: a => a,
i32i8: a => a,
i64i8: a => Number(BigInt.asIntN(8, a)),
f32i8: a => Math.floor(a),
f64i8: a => Math.floor(a),
stri8: a => parseInt(a),
booli8: a => a ? 1 : 0,
i8bool: a => a !== 0,
i16bool: a => a !== 0,
i32bool: a => a !== 0,
i64bool: a => a !== 0n,
f32bool: a => a !== 0.0,
f64bool: a => a !== 0.0,
strbool: a => a === "true",
i8str: a => a.toString(),
i16str: a => a.toString(),
i32str: a => a.toString(),
i64str: a => a.toString(),
f32str: a => a.toString(),
f64str: a => a.toString(),
boolstr: a => a.toString(),
// Arithmetic opcodes
addi8: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 0 && a > INT8MAX - b) return [false, 'overflow']
if (a < 0 && b < 0 && a < INT8MIN - b) return [false, 'underflow']
return [true, a + b]
},
addi16: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 0 && a > INT16MAX - b) return [false, 'overflow']
if (a < 0 && b < 0 && a < INT16MIN - b) return [false, 'underflow']
return [true, a + b]
},
addi32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 0 && a > INT32MAX - b) return [false, 'overflow']
if (a < 0 && b < 0 && a < INT32MIN - b) return [false, 'underflow']
return [true, a + b]
},
addi64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0n && b > 0n && a > INT64MAX - b) return [false, 'overflow']
if (a < 0n && b < 0n && a < INT64MIN - b) return [false, 'underflow']
return [true, a + b]
},
addf32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
const out = a + b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
addf64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
const out = a + b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
subi8: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b < 0 && a > INT8MAX + b) return [false, 'overflow']
if (a < 0 && b > 0 && a < INT8MIN + b) return [false, 'underflow']
return [true, a - b]
},
subi16: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b < 0 && a > INT16MAX + b) return [false, 'overflow']
if (a < 0 && b > 0 && a < INT16MIN + b) return [false, 'underflow']
return [true, a - b]
},
subi32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b < 0 && a > INT32MAX + b) return [false, 'overflow']
if (a < 0 && b > 0 && a < INT32MIN + b) return [false, 'underflow']
return [true, a - b]
},
subi64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0n && b < 0n && a > INT64MAX + b) return [false, 'overflow']
if (a < 0n && b > 0n && a < INT64MIN + b) return [false, 'underflow']
return [true, a - b]
},
subf32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
const out = a - b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
subf64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
const out = a - b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
negi8: ra => !ra[0] ? ra : (0 - ra[1] <= INT8MAX ? [true, 0 - ra[1]] : [false, 'overflow']),
negi16: ra => !ra[0] ? ra : (0 - ra[1] <= INT16MAX ? [true, 0 - ra[1]] : [false, 'overflow']),
negi32: ra => !ra[0] ? ra : (0 - ra[1] <= INT32MAX ? [true, 0 - ra[1]] : [false, 'overflow']),
negi64: ra => !ra[0] ? ra : (0n - ra[1] <= INT64MAX ? [true, 0n - ra[1]] : [false, 'overflow']),
negf32: ra => !ra[0] ? ra : [true, 0.0 - a],
negf64: ra => !ra[0] ? ra : [true, 0.0 - a],
absi8: ra => !ra[0] ? ra : (Math.abs(ra[1]) <= INT8MAX ? [true, Math.abs(ra[1])] : [false, 'overflow']),
absi16: ra => !ra[0] ? ra : (Math.abs(ra[1]) <= INT16MAX ? [true, Math.abs(ra[1])] : [false, 'overflow']),
absi32: ra => !ra[0] ? ra : (Math.abs(ra[1]) <= INT32MAX ? [true, Math.abs(ra[1])] : [false, 'overflow']),
absi64: ra => !ra[0] ? ra : (BigAbs(ra[1]) <= INT64MAX ? [true, BigAbs(ra[1])] : [false, 'overflow']),
absf32: ra => !ra[0] ? ra : [true, Math.abs(ra[1])],
absf64: ra => !ra[0] ? ra : [true, Math.abs(ra[1])],
muli8: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 0 && a > INT8MAX / b) return [false, 'overflow']
if (a < 0 && b < 0 && a < INT8MIN / b) return [false, 'underflow']
return [true, a * b]
},
muli16: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 0 && a > INT16MAX / b) return [false, 'overflow']
if (a < 0 && b < 0 && a < INT16MIN / b) return [false, 'underflow']
return [true, a * b]
},
muli32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 0 && a > INT32MAX / b) return [false, 'overflow']
if (a < 0 && b < 0 && a < INT32MIN / b) return [false, 'underflow']
return [true, a * b]
},
muli64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0n && b > 0n && a > INT64MAX / b) return [false, 'overflow']
if (a < 0n && b < 0n && a < INT64MIN / b) return [false, 'underflow']
return [true, a * b]
},
mulf32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
const out = a * b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
mulf64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
const out = a * b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
divi8: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (b === 0) return [false, 'divide-by-zero']
return [true, Math.floor(a / b)]
},
divi16: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (b === 0) return [false, 'divide-by-zero']
return [true, Math.floor(a / b)]
},
divi32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (b === 0) return [false, 'divide-by-zero']
return [true, Math.floor(a / b)]
},
divi64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (b === 0n) return [false, 'divide-by-zero']
return [true, a / b]
},
divf32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (b === 0.0) return [false, 'divide-by-zero']
const out = a / b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
divf64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (b === 0.0) return [false, 'divide-by-zero']
const out = a / b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
modi8: (a, b) => a % b,
modi16: (a, b) => a % b,
modi32: (a, b) => a % b,
modi64: (a, b) => a % b,
powi8: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 1 && a > INT8MAX ** (1 / b)) return [false, 'overflow']
if (a < 0 && b > 1 && a < INT8MIN ** (1 / b)) return [false, 'underflow']
return [true, Math.floor(a ** b)]
},
powi16: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 1 && a > INT16MAX ** (1 / b)) return [false, 'overflow']
if (a < 0 && b > 1 && a < INT16MIN ** (1 / b)) return [false, 'underflow']
return [true, Math.floor(a ** b)]
},
powi32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 1 && a > INT32MAX ** (1 / b)) return [false, 'overflow']
if (a < 0 && b > 1 && a < INT32MIN ** (1 / b)) return [false, 'underflow']
return [true, Math.floor(a ** b)]
},
powi64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
if (a > 0 && b > 1n) {
const af = parseFloat(a.toString())
const bf = parseFloat(b.toString())
const maxf = parseFloat(INT64MAX.toString())
if (af > maxf ** (1 / bf)) return [false, 'overflow']
}
if (a < 0n && b > 1n) {
const af = parseFloat(a.toString())
const bf = parseFloat(b.toString())
const minf = parseFloat(INT64MIN.toString())
if (af < minf ** (1 / bf)) return [false, 'underflow']
}
return [true, a ** b]
},
powf32: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
const out = a ** b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
powf64: (ra, rb) => {
if (!ra[0]) return ra
if (!rb[0]) return rb
const a = ra[1]
const b = rb[1]
const out = a ** b
if (out === Number.POSITIVE_INFINITY) return [false, 'overflow']
if (out === Number.NEGATIVE_INFINITY) return [false, 'underflow']
return [true, out]
},
sqrtf32: a => Math.sqrt(a),
sqrtf64: a => Math.sqrt(a),
// Saturating arithmetic
saddi8: (a, b) => {
if (a > 0 && b > 0 && a > INT8MAX - b) return INT8MAX
if (a < 0 && b < 0 && a < INT8MIN - b) return INT8MIN
return a + b
},
saddi16: (a, b) => {
if (a > 0 && b > 0 && a > INT16MAX - b) return INT16MAX
if (a < 0 && b < 0 && a < INT16MIN - b) return INT16MIN
return a + b
},
saddi32: (a, b) => {
if (a > 0 && b > 0 && a > INT32MAX - b) return INT32MAX
if (a < 0 && b < 0 && a < INT32MIN - b) return INT32MIN
return a + b
},
saddi64: (a, b) => {
if (a > 0n && b > 0n && a > INT64MAX - b) return INT64MAX
if (a < 0n && b < 0n && a < INT64MIN - b) return INT64MIN
return a + b
},
saddf32: (a, b) => a + b,
saddf64: (a, b) => a + b,
ssubi8: (a, b) => {
if (a > 0 && b < 0 && a > INT8MAX + b) return INT8MAX
if (a < 0 && b > 0 && a < INT8MIN + b) return INT8MIN
return a - b
},
ssubi16: (a, b) => {
if (a > 0 && b < 0 && a > INT16MAX + b) return INT16MAX
if (a < 0 && b > 0 && a < INT16MIN + b) return INT16MIN
return a - b
},
ssubi32: (a, b) => {
if (a > 0 && b < 0 && a > INT32MAX + b) return INT32MAX
if (a < 0 && b > 0 && a < INT32MIN + b) return INT32MIN
return a - b
},
ssubi64: (a, b) => {
if (a > 0n && b < 0n && a > INT64MAX + b) return INT64MAX
if (a < 0n && b > 0n && a < INT64MIN + b) return INT64MIN
return a - b
},
ssubf32: (a, b) => a - b,
ssubf64: (a, b) => a - b,
snegi8: a => Math.min(0 - a, INT8MAX),
snegi16: a => Math.min(0 - a, INT16MAX),
snegi32: a => Math.min(0 - a, INT32MAX),
snegi64: a => BigMin(0n - a, INT64MAX),
snegf32: a => 0.0 - a,
snegf64: a => 0.0 - a,
sabsi8: a => Math.min(Math.abs(a), INT8MAX),
sabsi16: a => Math.min(Math.abs(a), INT16MAX),
sabsi32: a => Math.min(Math.abs(a), INT32MAX),
sabsi64: a => BigMin(a > 0n ? a : -a, INT64MAX),
sabsf32: a => Math.abs(a),
sabsf64: a => Math.abs(a),
smuli8: (a, b) => {
if (a > 0 && b > 0 && a > INT8MAX / b) return INT8MAX
if (a < 0 && b < 0 && a < INT8MIN / b) return INT8MIN
return a * b
},
smuli16: (a, b) => {
if (a > 0 && b > 0 && a > INT16MAX / b) return INT16MAX
if (a < 0 && b < 0 && a < INT16MIN / b) return INT16MIN
return a * b
},
smuli32: (a, b) => {
if (a > 0 && b > 0 && a > INT32MAX / b) return INT32MAX
if (a < 0 && b < 0 && a < INT32MIN / b) return INT32MIN
return a * b
},
smuli64: (a, b) => {
if (a > 0n && b > 0n && a > INT64MAX / b) return INT64MAX
if (a < 0n && b < 0n && a < INT64MIN / b) return INT64MIN
return a * b
},
smulf32: (a, b) => a * b,
smulf64: (a, b) => a * b,
sdivi8: (a, b) => {
if (b === 0) return a > 0 ? INT8MAX : INT8MIN
return Math.floor(a / b)
},
sdivi16: (a, b) => {
if (b === 0) return a > 0 ? INT16MAX : INT16MIN
return Math.floor(a / b)
},
sdivi32: (a, b) => {
if (b === 0) return a > 0 ? INT32MAX : INT32MIN
return Math.floor(a / b)
},
sdivi64: (a, b) => {
if (b === 0n) return a > 0n ? INT64MAX : INT64MIN
return a / b
},
sdivf32: (a, b) => a / b,
sdivf64: (a, b) => a / b,
spowi8: (a, b) => {
if (a > 0 && b > 1 && a > INT8MAX ** (1 / b)) return INT8MAX
if (a < 0 && b > 1 && a < INT8MIN ** (1 / b)) return INT8MIN
return Math.floor(a ** b)
},
spowi16: (a, b) => {
if (a > 0 && b > 1 && a > INT16MAX ** (1 / b)) return INT16MAX
if (a < 0 && b > 1 && a < INT16MIN ** (1 / b)) return INT16MIN
return Math.floor(a ** b)
},
spowi32: (a, b) => {
if (a > 0 && b > 1 && a > INT32MAX ** (1 / b)) return INT32MAX
if (a < 0 && b > 1 && a < INT32MIN ** (1 / b)) return INT32MIN
return Math.floor(a ** b)
},
spowi64: (a, b) => {
if (a > 0 && b > 1n) {
const af = parseFloat(a.toString())
const bf = parseFloat(b.toString())
const maxf = parseFloat(INT64MAX.toString())
if (af > maxf ** (1 / bf)) return INT64MAX
}
if (a < 0n && b > 1n) {
const af = parseFloat(a.toString())
const bf = parseFloat(b.toString())
const minf = parseFloat(INT64MIN.toString())
if (af < minf ** (1 / bf)) return INT64MIN
}
return a ** b
},
spowf32: (a, b) => a ** b,
spowf64: (a, b) => a ** b,
// Boolean and bitwise opcodes
andi8: (a, b) => a & b,
andi16: (a, b) => a & b,
andi32: (a, b) => a & b,
andi64: (a, b) => a & b,
andbool: (a, b) => a && b,
ori8: (a, b) => a | b,
ori16: (a, b) => a | b,
ori32: (a, b) => a | b,
ori64: (a, b) => a | b,
orbool: (a, b) => a || b,
xori8: (a, b) => a ^ b,
xori16: (a, b) => a ^ b,
xori32: (a, b) => a ^ b,
xori64: (a, b) => a ^ b,
xorbool: (a, b) => !!(a ^ b),
noti8: a => ~a,
noti16: a => ~a,
noti32: a => ~a,
noti64: a => ~a,
notbool: a => !a,
nandi8: (a, b) => ~(a & b),
nandi16: (a, b) => ~(a & b),
nandi32: (a, b) => ~(a & b),
nandi64: (a, b) => ~(a & b),
nandboo: (a, b) => !(a && b),
nori8: (a, b) => ~(a | b),
nori16: (a, b) => ~(a | b),
nori32: (a, b) => ~(a | b),
nori64: (a, b) => ~(a | b),
norbool: (a, b) => !(a || b),
xnori8: (a, b) => ~(a ^ b),
xnori16: (a, b) => ~(a ^ b),
xnori32: (a, b) => ~(a ^ b),
xnori64: (a, b) => ~(a ^ b),
xnorboo: (a, b) => !(a ^ b),
// Equality and order opcodes
eqi8: (a, b) => a === b,
eqi16: (a, b) => a === b,
eqi32: (a, b) => a === b,
eqi64: (a, b) => a === b,
eqf32: (a, b) => a === b,
eqf64: (a, b) => a === b,
eqstr: (a, b) => a === b,
eqbool: (a, b) => a === b,
neqi8: (a, b) => a !== b,
neqi16: (a, b) => a !== b,
neqi32: (a, b) => a !== b,
neqi64: (a, b) => a !== b,
neqf32: (a, b) => a !== b,
neqf64: (a, b) => a !== b,
neqstr: (a, b) => a !== b,
neqbool: (a, b) => a !== b,
lti8: (a, b) => a < b,
lti16: (a, b) => a < b,
lti32: (a, b) => a < b,
lti64: (a, b) => a < b,
ltf32: (a, b) => a < b,
ltf64: (a, b) => a < b,
ltstr: (a, b) => a < b,
ltei8: (a, b) => a <= b,
ltei16: (a, b) => a <= b,
ltei32: (a, b) => a <= b,
ltei64: (a, b) => a <= b,
ltef32: (a, b) => a <= b,
ltef64: (a, b) => a <= b,
ltestr: (a, b) => a <= b,
gti8: (a, b) => a > b,
gti16: (a, b) => a > b,
gti32: (a, b) => a > b,
gti64: (a, b) => a > b,
gtf32: (a, b) => a > b,
gtf64: (a, b) => a > b,
gtstr: (a, b) => a > b,
gtei8: (a, b) => a >= b,
gtei16: (a, b) => a >= b,
gtei32: (a, b) => a >= b,
gtei64: (a, b) => a >= b,
gtef32: (a, b) => a >= b,
gtef64: (a, b) => a >= b,
gtestr: (a, b) => a >= b,
// String opcodes
catstr: (a, b) => a.concat(b),
split: (a, b) => a.split(b),
repstr: (a, b) => new Array(parseInt(b.toString())).fill(a).join(''),
// TODO: templ, after maps are figured out
matches: (a, b) => RegExp(b).test(a),
indstr: (a, b) => {
const ind = BigInt(a.indexOf(b))
return ind > -1 ? [ true, ind, ] : [ false, 'substring not found', ]
},
lenstr: a => BigInt(a.length),
trim: a => a.trim(),
copyfrom:(arr, ind) => JSON.parse(JSON.stringify(arr[ind])),
copytof: (arr, ind, val) => { arr[ind] = val }, // These do the same thing in JS
copytov: (arr, ind, val) => { arr[ind] = val },
register:(arr, ind) => arr[ind], // Only on references to inner arrays
// Array opcodes TODO more to come
newarr: size => new Array(), // Ignored because JS push doesn't behave as desired
pusharr: (arr, val, size) => arr.push(val),
pushf: (arr, val) => arr.push(val),
pushv: (arr, val) => arr.push(val),
poparr: arr => arr.length > 0 ? [ true, arr.pop(), ] : [ false, 'cannot pop empty array', ],
lenarr: arr => BigInt(arr.length),
indarrf: (arr, val) => {
const ind = BigInt(arr.indexOf(val))
return ind > -1 ? [ true, ind, ] : [ false, 'element not found', ]
},
indarrv: (arr, val) => {
const ind = BigInt(arr.indexOf(val))
return ind > -1 ? [ true, ind, ] : [ false, 'element not found', ]
},
delindx: (arr, idx) => {
const spliced = arr.splice(parseInt(idx.toString()), 1)
if (spliced.length === 1 && parseInt(idx.toString()) >= 0) {
return [ true, spliced[0] ]
} else {
return [ false, `cannot remove idx ${idx} from array with length ${arr.length}` ]
}
},
join: (arr, sep) => arr.join(sep),
map: async (arr, fn) => await Promise.all(arr.map((v, i) => fn(v, BigInt(i)))),
mapl: async (arr, fn) => await Promise.all(arr.map((v, i) => fn(v, BigInt(i)))),
reparr: (arr, n) => Array.from(new Array(parseInt(n.toString()) * arr.length))
.map((_, i) => typeof arr[i % arr.length] === 'bigint' ?
BigInt(arr[i % arr.length]) :
JSON.parse(JSON.stringify(arr[i % arr.length]))
),
each: async (arr, fn) => { // Thrown away but awaited to maintain consistent execution
await Promise.all(arr.map((v, i) => fn(v, BigInt(i))))
},
eachl: async (arr, fn) => { // Thrown away but awaited to maintain consistent execution
await Promise.all(arr.map((v, i) => fn(v, BigInt(i))))
},
find: async (arr, fn) => {
let val = undefined
const len = arr.length
for (let i = 0; i < len && val === undefined; i++) {
if (await fn(arr[i])) {
val = arr[i]
}
}
if (val === undefined) {
return [
false,
'no element matches',
]
} else {
return [
true,
val,
]
}
},
findl: async (arr, fn) => {
let val = undefined
const len = arr.length
for (let i = 0; i < len && val === undefined; i++) {
if (await fn(arr[i])) {
val = arr[i]
}
}
if (val === undefined) {
return [
false,
'no element matches',
]
} else {
return [
true,
val,
]
}
},
every: async (arr, fn) => {
const len = arr.length
for (let i = 0; i < len; i++) {
if (!await fn(arr[i])) return false
}
return true
},
everyl: async (arr, fn) => {
const len = arr.length
for (let i = 0; i < len; i++) {
if (!await fn(arr[i])) return false
}
return true
},
some: async (arr, fn) => {
const len = arr.length
for (let i = 0; i < len; i++) {
if (await fn(arr[i])) return true
}
return false
},
somel: async (arr, fn) => {
const len = arr.length
for (let i = 0; i < len; i++) {
if (await fn(arr[i])) return true
}
return false
},
filter: async (arr, fn) => {
let out = []
let len = arr.length
for (let i = 0; i < len; i++) {
if (await fn(arr[i])) out.push(arr[i])
}
return out
},
filterl: async (arr, fn) => {
let out = []
let len = arr.length
for (let i = 0; i < len; i++) {
if (await fn(arr[i])) out.push(arr[i])
}
return out
},
reducel: async (arr, fn) => {
let cumu = arr[0]
let len = arr.length
for (let i = 1; i < len; i++) {
cumu = await fn(cumu, arr[i])
}
return cumu
},
reducep: async (arr, fn) => {
let cumu = arr[0]
let len = arr.length
for (let i = 1; i < len; i++) {
cumu = await fn(cumu, arr[i])
}
return cumu
},
foldl: async (obj, fn) => {
const [arr, init] = obj
let cumu = init
let len = arr.length
for (let i = 0; i < len; i++) {
cumu = await fn(cumu, arr[i])
}
return cumu
},
foldp: async (obj, fn) => {
const [arr, init] = obj
let cumu = init
let len = arr.length
for (let i = 0; i < len; i++) {
cumu = await fn(cumu, arr[i])
}
return [cumu] // This path is expected to return an array of folded values per thread
},
catarr: (a, b) => [...a, ...b],
// Map opcodes TODO after maps are figured out
// Ternary functions
// TODO: pair and condarr after arrays are figured out
condfn: async (cond, fn) => cond ? await fn() : undefined,
// Copy opcodes (for let reassignment)
copyi8: a => JSON.parse(JSON.stringify(a)),
copyi16: a => JSON.parse(JSON.stringify(a)),
copyi32: a => JSON.parse(JSON.stringify(a)),
copyi64: a => BigInt(a),
copyvoid: a => JSON.parse(JSON.stringify(a)),
copyf32: a => JSON.parse(JSON.stringify(a)),
copyf64: a => JSON.parse(JSON.stringify(a)),
copybool: a => JSON.parse(JSON.stringify(a)),
copystr: a => JSON.parse(JSON.stringify(a)),
// Actually the recommended deep clone mechanism: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone
// Doesn't work with BigInt :(
// copyarr: a => JSON.parse(JSON.stringify(a)),
// Implementation is now recursive with a try-catch wrapper, so not great for perf
copyarr,
zeroed: () => null,
// Trig opcodes
lnf64: a => Math.log(a),
logf64: a => Math.log(a) / Math.log(10),
sinf64: a => Math.sin(a),
cosf64: a => Math.cos(a),
tanf64: a => Math.tan(a),
asinf64: a => Math.asin(a),
acosf64: a => Math.acos(a),
atanf64: a => Math.atan(a),
sinhf64: a => Math.sinh(a),
coshf64: a => Math.cosh(a),
tanhf64: a => Math.tanh(a),
// Error, Maybe, Result, Either opcodes
error: a => a,
reff: a => a, // Just an alias for error but without the type mangling in the compiler
refv: a => a, // Just an alias for error but without the type mangling in the compiler
noerr: () => '',
errorstr: a => a.toString(),
someM: a => [
true,
a,
],
noneM: () => [
false,
],
isSome: a => a[0],
isNone: a => !a[0],
getOrM: (a, b) => a[0] ? a[1] : b,
getMaybe: a => {
if (a[0]) {
return a[1]
} else {
throw new Error('runtime error: illegal access')
}
},
okR: a => [
true,
a,
],
err: a => [
false,
a,
],
isOk: a => a[0],
isErr: a => !a[0],
getOrR: (a, b) => a[0] ? a[1] : b,
getOrRS: (a, b) => a[0] ? a[1] : b,
getR: (a) => {
if (a[0]) {
return a[1]
} else {
throw new Error('runtime error: illegal access')
}
},
getErr: (a, b) => a[0] ? b : a[1],
resfrom: (arr, rind) => {
if (!rind[0]) return rind
const ind = rind[1]
if (ind >= 0 && ind < arr.length) {
return [
true,
arr[ind],
]
} else {
return [
false,
'out-of-bounds access',
]
}
},
mainE: a => [
true,
a,
],
altE: a => [
false,
a,
],
isMain: a => a[0],
isAlt: a => !a[0],
mainOr: (a, b) => a[0] ? a[1] : b,
altOr: (a, b) => a[0] ? b : a[1],
getMain: a => {
if (a[0]) {
return a[1]
} else {
throw new Error('runtime error: illegal access')
}
},
getAlt: a => {
if (!a[0]) {
return a[1]
} else {
throw new Error('runtime error: illegal access')
}
},
// Hashing opcodes (hashv is recursive, needs to be defined elsewhere)
hashf,
hashv,
// In Node.js the datastore opcodes don't have to be IO opcodes, but in the Rust runtime they do,
// because of the multithreaded nature of the Rust runtime. Not sure if they should be "fake"
// async here or not.
dssetf: (ns, key, val) => {
ds[`${ns}:${key}`] = val
},
dssetv: (ns, key, val) => {
ds[`${ns}:${key}`] = val
},
dshas: (ns, key) => ds.hasOwnProperty(`${ns}:${key}`),
dsdel: (ns, key) => {
const fullKey = `${ns}:${key}`
const toDelete = ds.hasOwnProperty(fullKey)
if (toDelete) delete ds[fullKey]
return toDelete
},
dsgetf: (ns, key) => {
const fullKey = `${ns}:${key}`
if (ds.hasOwnProperty(fullKey)) {
return [ true, ds[`${ns}:${key}`], ]
} else {
return [ false, 'namespace-key pair not found', ]
}
},
dsgetv: (ns, key) => {
const fullKey = `${ns}:${key}`
if (ds.hasOwnProperty(fullKey)) {
return [ true, ds[`${ns}:${key}`], ]
} else {
return [ false, 'namespace-key pair not found', ]
}
},
dsrrun: async (nskey, func) => {
const val = ds[`${nskey[0]}:${nskey[1]}`];
return [true, await func(val)];
},
dsmrun: async (nskey, func) => {
let val = ds[`${nskey[0]}:${nskey[1]}`];
const [out, newval] = await func(val);
ds[`${nskey[0]}:${nskey[1]}`] = newval;
return [true, out];
},
dsrwith: async (wth, func) => {
const nskey = wth[0];
const a = wth[1];
const b = ds[`${nskey[0]}:${nskey[1]}`];
return [true, await func(b, a)];
},
dsmwith: async (wth, func) => {
const nskey = wth[0];
const a = wth[1];
let b = ds[`${nskey[0]}:${nskey[1]}`];
// Get out, newb!
const [out, newb] = await func(b, a);
ds[`${nskey[0]}:${nskey[1]}`] = newb;
return [true, out];
},
dsmonly: async (nskey, func) => {
let val = ds[`${nskey[0]}:${nskey[1]}`];
const newval = await func(val);
ds[`${nskey[0]}:${nskey[1]}`] = newval;
},
dswonly: async (wth, func) => {
const nskey = wth[0];
const a = wth[1];
const b = ds[`${nskey[0]}:${nskey[1]}`];
const newb = await func(b, a);
ds[`${nskey[0]}:${nskey[1]}`] = newb;
},
dsrclos: async (nskey, func) => {
const val = ds[`${nskey[0]}:${nskey[1]}`];
return [true, await func(val)];
},
dsmclos: async (nskey, func) => {
let val = ds[`${nskey[0]}:${nskey[1]}`];
const [out, newval] = await func(val);
ds[`${nskey[0]}:${nskey[1]}`] = newval;
return [true, out];
},
getcs: () => [false],
newseq: (limit) => [0n, limit],
seqnext: (seq) => {
if (seq[0] < seq[1]) {
const out = [true, seq[0]]
seq[0]++
return out
} else {
return [false, 'error: sequence out-of-bounds']
}
},
seqeach: async (seq, func) => {
while (seq[0] < seq[1]) {
await func(seq[0])
seq[0]++
}
},
seqwhile:async (seq, condFn, bodyFn) => {
while (seq[0] < seq[1] && await condFn()) {
await bodyFn()
seq[0]++
}
},
seqdo: async (seq, bodyFn) => {
let ok = true
do {
ok = await bodyFn()
seq[0]++
} while (seq[0] < seq[1] && ok)
},
selfrec: async (self, arg) => {
const [seq, recurseFn] = self
if (seq[0] < seq[1]) {
seq[0]++
return recurseFn(self, arg)
} else {
return [false, 'error: sequence out-of-bounds']
}
},
seqrec: (seq, recurseFn) => [seq, recurseFn],
// IO opcodes
httpreq: async req => {
const [ method, url, headers, body, ] = req
try {
const response = await fetch(url, {
method,
headers,
body: body.length > 0 ? body : undefined,
});
const rstatus = response.status
const rheaders = [...response.headers.entries()].map(kv => [kv[0] + '', kv[1] + ''])
const rbody = await response.text()
return [ true, [ rstatus, rheaders, rbody, 0n ] ]
} catch (e) {
return [ false, e.toString() ]
}
},
httplsn: async () => {
const server = http.createServer((req, res) => {
const connId = Number(hashf(Math.random().toString()))
httpConns[connId] = {
req,
res,
}
let body = ''
req.on('data', d => {
body += d
})
req.on('end', () => {
e.emit('__conn', [
req.method,
req.url,
Object.entries(req.headers),
body,
connId,
])
})
})
const listenResult = await new Promise(resolve => {
server.on('error', e => resolve(e))
server.listen({
port: 8000,
}, () => resolve(true))
})
if (listenResult === true) {
console.log("HTTP server listening on port 8000")
} else {
console.error(`HTTP server failed to listen to port 8000: ${listenResult}`)
}
},
httpsend: async (ires) => {
const [ status, headers, body, connId, ] = ires
const conn = httpConns[connId]
if (!conn) return [ false, 'connection not found', ]
delete httpConns[connId]
return new Promise(resolve => {
conn.res.on('close', () => resolve([ false, 'client hangup', ]))
conn.res
.writeHead(Number(status), headers.reduce((acc, kv) => {
acc[kv[0]] = kv[1]
return acc
}, {}))
.end(body, () => resolve([ true, 'ok', ]))
})
},
waitop: async (a) => await new Promise(resolve => setTimeout(resolve, Number(a))),
syncop: async (f, a) => await f(a),
execop: async (cmd) => {
try {
const res = await exec(cmd)
const { stdout, stderr } = res
return [ 0, stdout, stderr ]
} catch (e) {
return [ e.signal, e.stdout, e.stderr ]
}
},
tcplsn: async () => {
const server = net.createServer((c) => {
// To allow the `tcpConn` event to set up anything it needs
c.pause()
const connId = Number(hashf(Math.random().toString()))
const context = [connId, undefined, {
c,
dataArr: [],
state: 'paused',
}]
tcpConns[connId] = context
c.on('data', (buf) => {
context[2].dataArr.push(buf)
e.emit('chunk', context)
})
c.on('error', () => context[2].state = 'error')
c.on('timeout', () => {
context[2].state = 'timeout'
c.end() // Node.js doesn't automatically do this on timeout for some reason?
})
c.on('close', () => {
if (context[2].state === 'open') context[2].state = 'closed'
e.emit('tcpClose', context)
delete tcpConns[connId]
})
e.emit('tcpConn', connId)
})
const listenResult = await new Promise(resolve => {
server.on('error', e => resolve(e))
server.listen({
port: 8000,
}, () => resolve(true))
})
if (listenResult === true) {
console.log("TCP server listening on port 8000")
} else {
console.error(`TCP server failed to listen on port 8000: ${listenResult}`)
}
},
tcptun: async (port) => {
const server = net.createServer((c) => {
// To set up the connection to the server to forward this data to.
c.pause();
const t = net.createConnection({ port, }, () => {
c.resume();
});
c.on('data', (data) => t.write(data));
t.on('data', (data) => c.write(data));
c.on('end', () => t.end());
t.on('end', () => c.end());
});
return await new Promise((resolve) => {
server.on('error', e => resolve(e));
server.listen({
port: 8000,
}, () => resolve(true));
});
},
tcpconn: async (host, port) => {
return new Promise(resolve => {
const c = net.createConnection(port, host, () => {
const connId = Number(hashf(Math.random().toString()))
const context = [connId, undefined, {
connId,
c,
dataArr: [],
state: 'paused',
}]
tcpConns[connId] = context
c.on('data', (buf) => {
context[2].dataArr.push(buf)
e.emit('chunk', context)
})
c.on('error', () => context[2].state = 'error')
c.on('timeout', () => {
context[2].state = 'timeout'
c.end() // Node.js doesn't automatically do this on timeout for some reason
})
c.on('close', () => {
if (context[2].state === 'open') context[2].state = 'closed'
e.emit('tcpClose', context)
delete tcpConns[connId]
})
resolve(connId)
})
// To allow the setup of everything needed
c.pause()
})
},
tcpAddC: (connId, context) => {
tcpConns[connId][1] = context
return connId
},
tcpReady: (connId) => {
const channel = tcpConns[connId]
if (!channel) return connId
channel[2].state = 'open'
channel[2].c.resume()
return connId
},
tcpRead: (connId) => {
const channel = tcpConns[connId]
if (!channel) return new Buffer()
const chunk = channel[2].dataArr.shift()
return chunk
},
tcpWrite: (connId, chunk) => {
const channel = tcpConns[connId]
if (!channel) return connId
channel[2].c.write(chunk)
return connId
},
tcpTerm: (connId) => {
const channel = tcpConns[connId]
if (!channel) return undefined
channel[2].c.end()
},
// "Special" opcodes
stdoutp: out => process.stdout.write(out),
stderrp: err => process.stderr.write(err),
exitop: code => process.exit(parseInt(code.toString())),
// Event bookkeeping
emit: (name, payload) => e.emit(name, payload),
on: (name, cb) => e.on(name, cb),
emitter: e,
}
module.exports.asyncopcodes = Object.keys(module.exports).filter(k => module.exports[k].constructor.name === 'AsyncFunction')