UNPKG

alan-js-runtime

Version:

The runtime component for alan-js. Separately licensed from the compiler.

1,348 lines (1,287 loc) 40.3 kB
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')