/** * the typescript and javascript vm follows the portable microcore. * it uses 64-bit word memory, * stacks, opcodes, hostcalls, enhancements, float and byte operations. */ // node's own globals, for the command line runner at the end of this file. they are // declared here and required where used, so building needs no @types/node declare const process: any, require: any, module: any, __dirname: string, Buffer: any; const fs: any = typeof require === 'function' ? require('fs') : undefined; const path: any = typeof require === 'function' ? require('path') : undefined; // the compiler's map, computed from build/core/map/memmap.h at build time (build/js/layout.js) import { MAP } from './layout'; // float bits stay in vm words; arithmetic uses javascript's binary64 numbers. const floatView = new DataView(new ArrayBuffer(8)); function floatNumber(bits: bigint): number { floatView.setBigInt64(0, bits, true); return floatView.getFloat64(0, true); } function floatBits(value: number): bigint { floatView.setFloat64(0, value, true); return floatView.getBigInt64(0, true); } function floatInteger(bits: bigint): bigint { const value = floatNumber(bits); if (Number.isNaN(value)) return 0n; if (value >= 9223372036854775808) return 9223372036854775807n; if (value <= -9223372036854775808) return -9223372036854775808n; return BigInt(Math.trunc(value)); } function floatSingle(bits: bigint): bigint { const raw = BigInt.asUintN(64, bits), frac = raw & 0xfffffffffffffn; if (((raw >> 52n) & 2047n) === 2047n && frac) return ((raw >> 63n) << 31n) | 0x7fc00000n | (frac >> 29n); floatView.setFloat32(0, floatNumber(bits), true); return BigInt(floatView.getUint32(0, true)); } function floatDouble(bits: bigint): bigint { const raw = bits & 0xffffffffn, frac = raw & 0x7fffffn; if (((raw >> 23n) & 255n) === 255n && frac) return BigInt.asIntN(64, ((raw >> 31n) << 63n) | 0x7ff8000000000000n | (frac << 29n)); floatView.setUint32(0, Number(raw), true); return floatBits(floatView.getFloat32(0, true)); } function parseFloatBits(text: string): bigint { if (/^[+-]?inf$/i.test(text)) return floatBits(text[0] === '-' ? -Infinity : Infinity); if (/^[+-]?nan$/i.test(text)) return BigInt.asIntN(64, text[0] === '-' ? 0xfff8000000000000n : 0x7ff8000000000000n); const match = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(text); return floatBits(match ? Number(match[0]) : 0); } // exact decimal scaling keeps fixed and scientific output consistent with fops.h. function formatFloat(bits: bigint, mode: bigint, precision: bigint): string { const raw = BigInt.asUintN(64, bits), negative = (raw >> 63n) !== 0n; const sign = negative ? '-' : '', exponent = Number((raw >> 52n) & 2047n), fraction = raw & 0xfffffffffffffn; if (exponent === 2047) return fraction ? 'nan' : sign + 'inf'; const sig = exponent ? fraction | (1n << 52n) : fraction, shift = exponent ? exponent - 1075 : -1074; const scale = (places: number): string => { let numerator = sig, denominator = 1n; if (shift >= 0) numerator <<= BigInt(shift); else denominator <<= BigInt(-shift); if (places >= 0) numerator *= 10n ** BigInt(places); else denominator *= 10n ** BigInt(-places); return ((numerator * 2n + denominator) / (denominator * 2n)).toString(); }; if (mode === 1n) { const places = Number(precision < 0n ? 0n : precision > 17n ? 17n : precision); const digits = scale(places).padStart(places + 1, '0'); return sign + (places ? digits.slice(0, -places) + '.' + digits.slice(-places) : digits); } const scientific = mode === 2n; const places = Number(precision < 1n ? 1n : precision > 17n ? 17n : precision); if (!sig) return sign + (scientific ? '0' + (places > 1 ? '.' + '0'.repeat(places - 1) : '') + 'e+0' : '0'); const spell = (digits: string, decimal: number, useExponent: boolean): string => { if (useExponent) return sign + digits[0] + (digits.length > 1 ? '.' + digits.slice(1) : '') + 'e' + (decimal < 0 ? '-' : '+') + Math.abs(decimal); if (decimal < 0) return sign + '0.' + '0'.repeat(-decimal - 1) + digits; const whole = decimal + 1; return sign + (digits.length <= whole ? digits + '0'.repeat(whole - digits.length) : digits.slice(0, whole) + '.' + digits.slice(whole)); }; for (let count = scientific ? places : 1; count <= (scientific ? places : 18); count++) { let decimal = Math.floor(Math.log10(Math.abs(floatNumber(bits)))); let digits = scale(count - 1 - decimal); while (digits.length !== count) { decimal += digits.length - count; digits = scale(count - 1 - decimal); } const text = spell(digits, decimal, scientific || decimal < -4 || decimal > 15); if (scientific || floatBits(Number(text)) === bits || count === 18) return text; } return sign + '0'; } // the machine starts at the initial heap's end and grows on the heap's request (syscall 9, 4) export const DEFAULT_MW = MAP.HEAP_HI; export const GORG = MAP.GORG; export const DS0 = MAP.DS0; export const DSLIM = MAP.DSLIM; export const RS0 = MAP.RS0; export const RSLIM = MAP.RSLIM; export const SYSGATE = MAP.SYSGATE; export const XOP0 = 22; export const NENH = 512; // micro-op opcode constants export const U_LI = 0, U_MOV = 1, U_ADD = 2, U_SUB = 3, U_MUL = 4, U_SDIV = 5, U_UDIV = 6, U_SREM = 7, U_UREM = 8; export const U_AND = 9, U_OR = 10, U_XOR = 11, U_SHL = 12, U_SHR = 13, U_SAR = 14, U_NEG = 15, U_NOT = 16; export const U_LT = 17, U_LTU = 18, U_EQ = 19, U_NE = 20, U_MIN = 21, U_MAX = 22, U_MINU = 23, U_MAXU = 24; export const U_MULHS = 25, U_MULHU = 26, U_CLZ = 27, U_ABS = 28, U_SEL = 29, U_LD = 30, U_ST = 31, U_CTZ = 32, U_POPCNT = 33, U_BSWAP = 34, U_ROTL = 35, U_ROTR = 36, U_F2S = 37, U_S2F = 38; export const UNAMES = [ "li", "mov", "add", "sub", "mul", "sdiv", "udiv", "srem", "urem", "and", "or", "xor", "shl", "shr", "sar", "neg", "not", "lt", "ltu", "eq", "ne", "min", "max", "minu", "maxu", "mulhs", "mulhu", "clz", "abs", "sel", "ld", "st", "ctz", "popcnt", "bswap", "rotl", "rotr", "f2s", "s2f" ]; /** where the image sits and how the map is laid out. the compiler's own genome (the default) and the language server's have different maps, so the layout is data, not constants: a program built for another map passes its own. */ export interface Layout { GORG: number; DS0: number; DSLIM: number; RS0: number; RSLIM: number; SYSGATE: number; ZFP: number; HEAP_LO: number; HEAP_HI: number; } export const DEFAULT_LAYOUT: Layout = { GORG, DS0, DSLIM, RS0, RSLIM, SYSGATE, ZFP: MAP.ZFP, HEAP_LO: MAP.HEAP_LO, HEAP_HI: MAP.HEAP_HI, }; /** run() returns this when a streaming program waits for input. */ export const BLOCKED = 3; export interface VMOptions { memoryWords?: number; maxMemoryWords?: number; /** the map the program was built for; the default is the compiler's. */ layout?: Partial; /** streaming: input arrives in pieces (feed) and a get_c with nothing buffered suspends the program instead of reading the end. resume() goes on from there. eof() is true only once close() has been called. */ streaming?: boolean; input?: string | Uint8Array; onOutput?: (byte: number) => void; onMessage?: (msg: string) => void; /** command line for the zc program, ops 21 and 22. empty means the program was read from stdin, so argv[0] is on its own. */ args?: string[]; /** environment entries, ops 23 and 24, each a NAME=value string. */ env?: string[]; } export interface Enhancement { live: boolean; nin: number; nout: number; ops: MicroOp[]; out: number[]; nv: number; } export interface MicroOp { op: number; d: number; a: number; b: number; c: number; imm: bigint; } export class TinyVM { public mem: BigInt64Array; public bytes: Uint8Array; public view: DataView; public mw: number; private inputBuffer: Uint8Array = new Uint8Array(0); private inputPos: number = 0; /** true while a streaming run is suspended by a poll (syscall 42) rather than by a read: the host should resume it after its event loop has run, whether or not input came. */ public polling: boolean = false; private outputBuffer: number[] = []; private onOutput?: (byte: number) => void; private onMessage?: (msg: string) => void; private enh: Enhancement[] = []; public L: Layout; private streaming: boolean = false; private closed: boolean = false; private blocked: boolean = false; private rp: number = 0; private rs: number = 0; private rr: number = 0; private memoryLimit: number; private stackControl = 0; private args: string[]; private env: string[]; constructor(options: VMOptions = {}) { this.L = Object.assign({}, DEFAULT_LAYOUT, options.layout || {}); const minimumWords = Math.max(this.L.HEAP_HI, this.L.DS0 + 16); this.mw = options.memoryWords ?? minimumWords; this.memoryLimit = options.maxMemoryWords ?? 0x7fffffff; if (!Number.isSafeInteger(this.mw) || this.mw < minimumWords || !Number.isSafeInteger(this.memoryLimit) || this.memoryLimit < this.mw || this.memoryLimit > 0x7fffffff) { throw new RangeError('invalid initial memory or growth limit'); } this.streaming = !!options.streaming; const buffer = new ArrayBuffer(this.mw * 8); this.mem = new BigInt64Array(buffer); this.bytes = new Uint8Array(buffer); this.view = new DataView(buffer); this.onOutput = options.onOutput; this.onMessage = options.onMessage; this.args = options.args || []; this.env = options.env || []; if (options.input) { this.setInput(options.input); } for (let i = 0; i < NENH; i++) { this.enh.push({ live: false, nin: 0, nout: 0, ops: [], out: [], nv: 0 }); } } public setInput(input: string | Uint8Array): void { if (typeof input === 'string') { this.inputBuffer = new TextEncoder().encode(input); } else { this.inputBuffer = input; } this.inputPos = 0; } /** streaming input: more bytes after what is already here. */ public feed(input: string | Uint8Array): void { const more = typeof input === 'string' ? new TextEncoder().encode(input) : input; const rest = this.inputBuffer.subarray(this.inputPos); const joined = new Uint8Array(rest.length + more.length); joined.set(rest, 0); joined.set(more, rest.length); this.inputBuffer = joined; this.inputPos = 0; } /** no more input will come: a streaming program now reads the end. */ public close(): void { this.closed = true; } /** true once the input is used up and nothing more can come. */ private atEof(): boolean { if (this.streaming && !this.closed) return false; return this.inputPos >= this.inputBuffer.length; } public loadGenome(words: (number | string | bigint)[], org: number = this.L.GORG): void { for (let i = 0; i < words.length; i++) { this.mem[org + i] = BigInt(words[i]); } } /** load an image file's bytes (little-endian 64-bit words, the format the c cores #embed) at org. returns the number of words. */ public loadImage(bytes: Uint8Array, org: number = this.L.GORG): number { if (bytes.length % 8) throw new RangeError('an image is a whole number of 64-bit words'); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length); const n = bytes.length / 8; for (let i = 0; i < n; i++) this.mem[org + i] = view.getBigInt64(i * 8, true); return n; } public getOutput(): string { return new TextDecoder().decode(new Uint8Array(this.outputBuffer)); } // stack controls and allocator headers use the same layout as stack.h. private stackGet(slot: number): number { return Number(this.mem[Number(this.mem[this.stackControl + slot])]); } private stackSet(slot: number, value: number): void { this.mem[Number(this.mem[this.stackControl + slot])] = BigInt(value); } private stackRegister(control: number): boolean { if (!Number.isSafeInteger(control) || control <= 0 || control + 16 >= this.mw) return false; for (let i = 0; i < 17; i++) { const address = Number(this.mem[control + i]); if (!Number.isSafeInteger(address) || address < 0 || address >= this.mw) return false; } if (Number(this.mem[control + 9]) + 31 >= this.mw) return false; this.stackControl = control; return true; } private stackPush(block: number, sizeClass: number): void { const list = Number(this.mem[this.stackControl + 9]), head = this.mem[list + sizeClass]; this.mem[block] = BigInt(2 ** sizeClass); this.mem[block + 1] = BigInt(sizeClass * 65536); this.mem[block + 2] = this.mem[block + 3] = this.mem[block + 4] = 0n; this.mem[block + 5] = head; this.mem[block + 6] = 0n; if (head) this.mem[Number(head) + 6] = BigInt(block); this.mem[list + sizeClass] = BigInt(block); } private stackHeapGrow(sizeClass: number): boolean { const heap = this.stackGet(10) - this.L.HEAP_LO, block = 2 ** sizeClass; const need = (Math.ceil(heap / block) + 1) * block; let size = Math.max(heap * 2, need); if (!this.stackGet(12) || this.stackGet(15) || this.stackGet(14)) return false; try { if (this.grow(this.L.HEAP_LO + size) < 0) throw new RangeError('stack growth limit'); } catch { size = need; try { if (this.grow(this.L.HEAP_LO + size) < 0) return false; } catch { return false; } } this.stackSet(10, this.L.HEAP_LO + size); this.stackSet(11, this.L.HEAP_LO + size); this.stackSet(12, this.L.HEAP_LO + size); let offset = heap; while (offset < size) { let part = 8, k = 3; while (part * 2 <= size - offset && offset % (part * 2) === 0) { part *= 2; k++; } this.stackPush(this.L.HEAP_LO + offset, k); offset += part; } return true; } private stackAllocate(words: number): number { if (!this.stackControl || words < 0 || words > 0x3ffffffa) return 0; let k = 3, grown = false; while (2 ** k < words + 5) k++; while (true) { const list = Number(this.mem[this.stackControl + 9]); let j = k; while (j < 31 && !this.mem[list + j]) j++; if (j === 31) { if (grown || !this.stackHeapGrow(k)) return 0; grown = true; continue; } const block = Number(this.mem[list + j]); if (block < 0 || block + 2 ** j > this.mw || this.mem[block] !== BigInt(2 ** j) || (this.mem[block + 1] & 65535n)) return 0; const next = this.mem[block + 5]; this.mem[list + j] = next; if (next) { if (Number(next) + 6 >= this.mw) return 0; this.mem[Number(next) + 6] = 0n; } while (j > k) { j--; this.stackPush(block + 2 ** j, j); } if (this.stackGet(14) && block < this.stackGet(14) && block + 2 ** k > this.stackGet(13)) { this.mem[block] = BigInt(2 ** k); this.mem[block + 1] = BigInt(k * 65536 + 2); this.mem[block + 2] = this.mem[block + 5] = this.mem[block + 6] = 0n; continue; } const pointer = block + 5; this.mem[block] = BigInt(2 ** k); this.mem[block + 1] = BigInt(k * 65536 + 1537 + (this.stackGet(16) ? 2 : 0)); this.mem[block + 2] = BigInt(pointer) ^ 0x2B7E1516n; this.mem[block + 3] = 0n; this.mem[block + 4] = -BigInt(words) - 1n; this.mem.fill(0n, pointer, block + 2 ** k); return pointer; } } private stackExpand(isReturn: boolean, minimum: number, s: number, r: number): number[] | null { if (!this.stackControl || !Number.isSafeInteger(minimum) || minimum < 0 || minimum > 0x3ffffff0) return null; const slot = isReturn ? 6 : 3, low = this.stackGet(slot), top = this.stackGet(slot + 1); const capacity = top - low, used = isReturn ? r - low : top - s; if (capacity <= 0 || used < 0 || used > capacity + (isReturn ? 8 : 0)) return null; if (minimum <= capacity) return [s, r, capacity]; minimum = Math.max(minimum, capacity * 2); const node = this.stackAllocate(minimum + 16); if (!node) return null; const source = isReturn ? low : s, destination = isReturn ? node : node + minimum - used; this.mem.copyWithin(destination, source, source + used); const old = this.stackGet(slot + 2); this.stackSet(slot, node); this.stackSet(slot + 1, node + minimum); this.stackSet(slot + 2, node); if (old) this.stackPush(old - 5, Number((this.mem[old - 4] >> 16n) & 31n)); this.L.DSLIM = this.stackGet(3) + 1100; this.L.RSLIM = this.stackGet(7); return isReturn ? [s, node + used, minimum] : [node + minimum - used, r, minimum]; } /** the next byte, -1 at the end, or BLOCKED when a streaming program has to wait for more input. */ private get_c(): number { if (this.inputPos >= this.inputBuffer.length) { if (this.streaming && !this.closed) return BLOCKED; return -1; } return this.inputBuffer[this.inputPos++]; } private put_c(c: number): void { const b = c & 0xFF; this.outputBuffer.push(b); if (this.onOutput) { this.onOutput(b); } } /** the machine's memory grows to words; the old words stay where they were. returns the new size. */ private grow(words: number): number { if (!Number.isSafeInteger(words) || words < 0 || words > this.memoryLimit) return -1; if (words <= this.mw) return this.mw; const buf = new ArrayBuffer(words * 8); new Uint8Array(buf).set(this.bytes.subarray(0, this.mw * 8)); this.mem = new BigInt64Array(buf); this.bytes = new Uint8Array(buf); this.view = new DataView(buf); this.mw = words; return words; } private syscall(op: bigint, a: bigint, b: bigint, c: bigint): bigint { const opNum = Number(op); switch (opNum) { case 1: // open return -1n; case 2: // close return 0n; case 3: // read return 0n; case 4: { // write(fd, buf, n) const n = Number(c); const bufAddr = Number(b); for (let i = 0; i < n; i++) { const ch = Number(this.mem[bufAddr + i] & 0xFFn); this.put_c(ch); } return c; } case 5: // getenv return -1n; case 6: // time return BigInt(Math.floor(Date.now() / 1000)); case 7: // exit return 0n; case 8: // rand return BigInt(Math.floor(Math.random() * 0x7FFFFFFF)); case 9: { // memory info; a = 4 grows the machine to b words (the heap asks for it) if (a === 4n) { try { return BigInt(this.grow(Number(b))); } catch { return -1n; } } if (a === 0n) return BigInt(this.mw); if (a === 1n) return BigInt(this.L.HEAP_LO); if (a === 2n) return BigInt(this.L.HEAP_HI); if (a === 3n) return BigInt(this.mw * 8); if (a === 5n) { const words = Number(b); if (!Number.isSafeInteger(words) || words < this.mw || words > 0x7fffffff) return -1n; this.memoryLimit = words; return b; } if (a === 9n) return b > 0n && b <= BigInt(this.mw) ? BigInt(this.mw) : -1n; if (a === 6n) return this.stackRegister(Number(b)) ? 0n : -1n; if (a === 7n || a === 8n) { const moved = this.stackExpand(a === 8n, Number(b), this.rs, this.rr); if (!moved) return -1n; [this.rs, this.rr] = moved; return BigInt(moved[2]); } return -1n; } case 42: // poll: input is waiting return this.inputPos < this.inputBuffer.length ? 1n : 0n; case 41: { // shortest float text into a word buffer const text = formatFloat(a, 0n, 17n); if (b < 0n || c <= BigInt(text.length) || b > BigInt(this.mw) || c > BigInt(this.mw) - b) return -1n; for (let i = 0; i < text.length; i++) this.mem[Number(b) + i] = BigInt(text.charCodeAt(i)); this.mem[Number(b) + text.length] = 0n; return BigInt(text.length); } case 18: { // fill heap words without running one vm store per word if (a < BigInt(this.L.HEAP_LO) || c <= 0n || a > BigInt(this.mw) || c > BigInt(this.mw) - a) return -1n; this.mem.fill(b, Number(a), Number(a + c)); return c; } case 19: { // forward word copy; a forward overlap keeps the vm-loop path if (a < BigInt(this.L.HEAP_LO) || b < 0n || c <= 0n || a > BigInt(this.mw) || b > BigInt(this.mw) || c > BigInt(this.mw) - a || c > BigInt(this.mw) - b) return -1n; if (a > b && a < b + c) return -1n; this.mem.copyWithin(Number(a), Number(b), Number(b + c)); return c; } case 20: { // skip words that cannot be live heap pointers during collection if (a < 0n || b < 0n || a > BigInt(this.mw) || b > BigInt(this.mw) - a) return -1n; const begin = Number(a), count = Number(b), low = BigInt(this.L.HEAP_LO + 5); for (let i = 0; i < count; i++) { const word = this.mem[begin + i]; if (word >= low && word < c && word < BigInt(this.mw) && this.mem[Number(word) - 3] === (word ^ 0x2b7e1516n)) return BigInt(i); } return b; } case 21: // argc: on its own when there is no program file name return BigInt(this.args.length ? this.args.length : 1); case 22: { // argv(i, buf, cap): a zero buffer asks for the length only const i = Number(a); const s = (i === 0 && this.args.length === 0) ? '' : this.args[i]; if (s === undefined) return -1n; if (b === 0n) return BigInt(s.length); const addr = Number(b); const cap = Number(c); if (cap < 1 || addr < 0 || addr + cap > this.mw) return -1n; const n = Math.min(s.length, cap - 1); for (let k = 0; k < n; k++) this.mem[addr + k] = BigInt(s.charCodeAt(k)); this.mem[addr + n] = 0n; return BigInt(n); } case 23: // envc return BigInt(this.env.length); case 24: { // envp, shaped like argv const s = this.env[Number(a)]; if (s === undefined) return -1n; if (b === 0n) return BigInt(s.length); const addr = Number(b); const cap = Number(c); if (cap < 1 || addr < 0 || addr + cap > this.mw) return -1n; const n = Math.min(s.length, cap - 1); for (let k = 0; k < n; k++) this.mem[addr + k] = BigInt(s.charCodeAt(k)); this.mem[addr + n] = 0n; return BigInt(n); } default: return -1n; } } private evMicroOp(op: number, a: bigint, b: bigint, c: bigint): bigint { const ua = BigInt.asUintN(64, a); const ub = BigInt.asUintN(64, b); const sh = Number(b & 63n); switch (op) { case U_LI: return a; case U_MOV: return a; case U_ADD: return BigInt.asIntN(64, a + b); case U_SUB: return BigInt.asIntN(64, a - b); case U_MUL: return BigInt.asIntN(64, a * b); case U_SDIV: return b === 0n ? 0n : BigInt.asIntN(64, a / b); case U_UDIV: return ub === 0n ? 0n : BigInt.asIntN(64, BigInt(ua / ub)); case U_SREM: return b === 0n ? a : BigInt.asIntN(64, a % b); case U_UREM: return ub === 0n ? a : BigInt.asIntN(64, BigInt(ua % ub)); case U_AND: return a & b; case U_OR: return a | b; case U_XOR: return a ^ b; case U_SHL: return BigInt.asIntN(64, a << BigInt(sh)); case U_SHR: return BigInt.asIntN(64, BigInt(ua >> BigInt(sh))); case U_SAR: return a >> BigInt(sh); case U_NEG: return BigInt.asIntN(64, -a); case U_NOT: return ~a; case U_LT: return a < b ? 1n : 0n; case U_LTU: return ua < ub ? 1n : 0n; case U_EQ: return a === b ? 1n : 0n; case U_NE: return a !== b ? 1n : 0n; case U_MIN: return a < b ? a : b; case U_MAX: return a > b ? a : b; case U_MINU: return ua < ub ? a : b; case U_MAXU: return ua > ub ? a : b; case U_MULHS: return BigInt.asIntN(64, (a * b) >> 64n); case U_MULHU: return BigInt.asIntN(64, (ua * ub) >> 64n); case U_CLZ: return ua === 0n ? 64n : BigInt(64 - ua.toString(2).length); case U_CTZ: { if (ua === 0n) return 64n; let n = 0n, x = ua; while (!(x & 1n)) { x >>= 1n; n++; } return n; } case U_POPCNT: { let n = 0n, x = ua; while (x) { x &= x - 1n; n++; } return n; } case U_BSWAP: { let r = 0n, x = ua; for (let i = 0; i < 8; i++) { r = (r << 8n) | (x & 0xffn); x >>= 8n; } return BigInt.asIntN(64, r); } case U_ROTL: return BigInt.asIntN(64, (ua << BigInt(sh)) | (ua >> BigInt((64 - sh) & 63))); case U_ROTR: return BigInt.asIntN(64, (ua >> BigInt(sh)) | (ua << BigInt((64 - sh) & 63))); case U_F2S: return floatSingle(a); case U_S2F: return floatDouble(a); case U_ABS: return a < 0n ? BigInt.asIntN(64, -a) : a; case U_SEL: return a !== 0n ? b : c; default: return 0n; } } private wordText(address: number): string { let text = ''; while (address >= 0 && address < this.mw) { const value = this.mem[address++]; if (!value) return text; text += String.fromCharCode(Number(value & 255n)); } throw new Error('recipe string outside memory'); } private parseRecipe(pay: number, namesAddr: number, nin: number, nout: number): { ops: MicroOp[], out: number[], nv: number } { const ops: MicroOp[] = [], names = new Map(); const current = Array.from({ length: 16 }, (_, i) => i < nin ? i : -1); const arity = [0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 3, 1, 2, 1, 1, 1, 2, 2, 1, 1]; let next = nin; const append = (op: number, args: number[], imm = 0n): number => { const d = next++; ops.push({ op, d, a: args[0] ?? 0, b: args[1] ?? 0, c: args[2] ?? 0, imm }); return d; }; for (let i = 0; i < nin; i++) names.set(this.wordText(Number(this.mem[namesAddr + i])), i); const register = (name: string): number => { const match = /^r(\d+)$/.exec(name); const index = match ? Number(match[1]) : names.get(name); if (index === undefined || index < 0 || index > 15) throw new Error('unknown recipe register'); return index; }; const value = (index: number): number => { if (current[index] < 0) current[index] = append(U_LI, [], 0n); return current[index]; }; const operand = (token: string): number => { const numeric = /^([+-]?)(0[xX][0-9a-fA-F]+|\d+)$/.exec(token); if (numeric) return append(U_LI, [], BigInt.asIntN(64, (numeric[1] === '-' ? -1n : 1n) * BigInt(numeric[2]))); return value(register(token)); }; for (const statement of this.wordText(pay).split(/[;\n]/).map(text => text.trim()).filter(Boolean)) { const store = /^st(?:\s|$)/.test(statement); const match = store ? null : /^(\S+)\s*=\s*(.+)$/.exec(statement); if (!store && !match) throw new Error('malformed recipe assignment'); const destination = store ? -1 : register(match![1]); const tokens = (store ? statement.slice(2) : match![2]).trim().split(/\s+/); let op = store ? U_ST : -1; if (!store && tokens.length > 1) { op = UNAMES.indexOf(tokens.shift()!); if (op < 0) throw new Error('unknown recipe operation'); } const args = tokens.map(operand); if (op < 0 || op === U_MOV || op === U_LI) { if (args.length !== 1) throw new Error('wrong recipe arity'); current[destination] = args[0]; } else { if (args.length !== arity[op]) throw new Error('wrong recipe arity'); const result = append(op, args); if (!store) current[destination] = result; } } return { ops, out: Array.from({ length: nout }, (_, i) => value(i)), nv: next }; } private lowerVM(pay: number, length: number, nin: number, nout: number): { ops: MicroOp[], out: number[], nv: number } { const ops: MicroOp[] = [], stack = Array.from({ length: nin }, (_, i) => i); const map = [U_ADD, U_SUB, U_MUL, U_AND, U_OR, U_XOR, U_SAR, U_LT]; const constants = new Map(); let next = nin; const append = (op: number, args: number[], imm = 0n): number => { const d = next++; ops.push({ op, d, a: args[0] ?? 0, b: args[1] ?? 0, c: 0, imm }); if (op === U_LI) constants.set(d, imm); return d; }; const require = (count: number): void => { if (stack.length < count) throw new Error('invalid vm recipe stack'); }; for (let i = 0; i < length; i++) { const word = this.mem[pay + i]; if (word & 1n) { stack.push(append(U_LI, [], word >> 1n)); continue; } const op = Number(word >> 1n); if (op === 9) break; if (op === 1) { require(1); stack.push(append(U_LD, [stack.pop()!])); } else if (op === 2) { require(2); const address = stack.pop()!, value = stack.pop()!; append(U_ST, [address, value]); } else if (op === 3) { require(1); const depth = constants.get(stack.pop()!); if (depth === undefined || depth < 0n || depth >= BigInt(stack.length)) throw new Error('invalid vm recipe pick'); stack.push(stack[stack.length - 1 - Number(depth)]); } else if (op === 4) { require(1); stack.pop(); } else if (op === 5) { require(2); const a = stack.pop()!, b = stack.pop()!; stack.push(a, b); } else if (op >= 13 && op <= 20) { require(2); const a = stack.pop()!, b = stack.pop()!; stack.push(append(map[op - 13], [b, a])); } else throw new Error('unsupported vm recipe operation'); } if (stack.length !== nout) throw new Error('wrong vm recipe output count'); return { ops, out: stack, nv: next }; } private install(descAddr: bigint): bigint { const addr = Number(descAddr); if (!Number.isSafeInteger(addr) || addr < 0 || addr + 6 >= this.mw) return -1n; const slot = Number(this.mem[addr]), kind = Number(this.mem[addr + 1]); // optimizer-region descriptors leave micro-op slots intact on portable hosts. if (kind === 2) return 0n; const nin = Number(this.mem[addr + 2]), nout = Number(this.mem[addr + 3]); const pay = Number(this.mem[addr + 4]), length = Number(this.mem[addr + 5]), names = Number(this.mem[addr + 6]); if (slot < 0 || slot >= NENH || nin < 0 || nin > 8 || nout < 0 || nout > 8 || (kind !== 0 && kind !== 1)) return -1n; if (pay < 0 || pay >= this.mw || names < 0 || names + nin > this.mw) return -1n; if (kind === 1 && (length < 0 || length > 100000 || length > this.mw - pay)) return -5n; try { const parsed = kind === 0 ? this.parseRecipe(pay, names, nin, nout) : this.lowerVM(pay, length, nin, nout); this.enh[slot] = { live: true, nin, nout, ...parsed }; return 0n; } catch { return kind === 0 ? -2n : -4n; } } private xrun(enh: Enhancement, s: number): number { const xv: bigint[] = new Array(enh.nv).fill(0n); for (let j = 0; j < enh.nin; j++) { xv[j] = this.mem[s + enh.nin - 1 - j]; } for (const u of enh.ops) { if (u.op === U_LI) { xv[u.d] = u.imm; } else if (u.op === U_LD) { const addr = Number(xv[u.a]); xv[u.d] = (addr >= 0 && addr < this.mw) ? this.mem[addr] : 0n; } else if (u.op === U_ST) { const addr = Number(xv[u.a]); if (addr >= 0 && addr < this.mw) this.mem[addr] = xv[u.b]; } else { xv[u.d] = this.evMicroOp(u.op, xv[u.a], xv[u.b], xv[u.c]); } } s = s + enh.nin - enh.nout; for (let j = 0; j < enh.nout; j++) { this.mem[s + enh.nout - 1 - j] = xv[enh.out[j]]; } return s; } /** runs the image from entryPc. returns 0 when it halts, BLOCKED when a streaming program waits for input (then feed() and resume()). */ public run(entryPc: number = this.L.GORG): number { return this.exec(entryPc, this.L.DS0, this.L.RS0); } /** goes on from a BLOCKED run, once input has been fed. */ public resume(): number { if (!this.blocked) return 0; this.blocked = false; return this.exec(this.rp, this.rs, this.rr); } private exec(p0: number, s0: number, r0: number): number { let p = p0; let s = s0; let r = r0; // the machine's memory can grow in a syscall, so these are read again after one let mem = this.mem; let bytes = this.bytes; let view = this.view; let mw = this.mw; while (p < mw) { if (s < this.L.DSLIM || r > this.L.RSLIM) { const isReturn = r > this.L.RSLIM, slot = isReturn ? 6 : 3; const moved = this.stackControl ? this.stackExpand(isReturn, this.stackGet(slot + 1) - this.stackGet(slot) + 1, s, r) : null; if (!moved) { if (this.onMessage) this.onMessage(isReturn ? "zy: return stack overflow\n" : "zy: data stack overflow\n"); return 2; } [s, r] = moved; mem = this.mem; bytes = this.bytes; view = this.view; mw = this.mw; } const w = mem[p++]; if ((w & 1n) !== 0n) { mem[--s] = w >> 1n; continue; } const op = Number(w >> 1n); const a = mem[s]; const b = mem[s + 1]; switch (op) { case 1: // ld mem[s] = (Number(a) >= 0 && Number(a) < mw) ? mem[Number(a)] : 0n; break; case 2: // st if (Number(a) === this.L.SYSGATE) { if (b === 42n && this.streaming) { // poll: suspend once so the host can deliver what has arrived, then say if input waits if (!this.polling) { this.polling = true; this.rp = p - 1; this.rs = s; this.rr = r; this.blocked = true; return BLOCKED; } this.polling = false; mem[Number(a)] = this.inputPos < this.inputBuffer.length ? 1n : 0n; s += 2; break; } this.rs = s + 2; this.rr = r; const result = this.syscall(b, mem[Number(a) + 1], mem[Number(a) + 2], mem[Number(a) + 3]); mem = this.mem; bytes = this.bytes; view = this.view; mw = this.mw; // a grow may have moved them mem[Number(a)] = result; s = this.rs; r = this.rr; break; } else if (Number(a) >= 0 && Number(a) < mw) { mem[Number(a)] = b; } s += 2; break; case 3: { // pick follows the same bounds as a word load const address = s + 1 + Number(a); mem[s] = address >= 0 && address < mw ? mem[address] : 0n; break; } case 4: // drop s++; break; case 5: // swap mem[s] = b; mem[s + 1] = a; break; case 6: // call mem[r++] = BigInt(p); p = Number(a); s++; break; case 7: // jmp p = Number(a); s++; break; case 8: // jz if (b === 0n) p = Number(a); s += 2; break; case 9: // ret p = Number(mem[--r]); break; case 10: { // get_c const ch = this.get_c(); if (ch === BLOCKED) { // the get_c op runs again when there is input: back to it, stacks as they are this.rp = p - 1; this.rs = s; this.rr = r; this.blocked = true; return BLOCKED; } mem[--s] = BigInt(ch); break; } case 11: // eof mem[--s] = this.atEof() ? 1n : 0n; break; case 12: // put_c this.put_c(Number(a & 0xFFn)); s++; break; case 13: // add mem[++s] = BigInt.asIntN(64, b + a); break; case 14: // sub mem[++s] = BigInt.asIntN(64, b - a); break; case 15: // mul mem[++s] = BigInt.asIntN(64, b * a); break; case 16: // and mem[++s] = b & a; break; case 17: // or mem[++s] = b | a; break; case 18: // xor mem[++s] = b ^ a; break; case 19: // sar mem[++s] = b >> (a & 63n); break; case 20: // lt mem[++s] = b < a ? 1n : 0n; break; case 21: // install mem[s] = this.install(a); break; // float operations use the same stack effects as the portable core. case 600: if (p >= mw) return 0; mem[--s] = mem[p++]; break; case 601: mem[++s] = floatBits(floatNumber(b) + floatNumber(a)); break; case 602: mem[++s] = floatBits(floatNumber(b) - floatNumber(a)); break; case 603: mem[++s] = floatBits(floatNumber(b) * floatNumber(a)); break; case 604: mem[++s] = floatBits(floatNumber(b) / floatNumber(a)); break; case 605: mem[++s] = floatNumber(b) === floatNumber(a) ? 1n : 0n; break; case 606: mem[++s] = floatNumber(b) < floatNumber(a) ? 1n : 0n; break; case 607: mem[++s] = floatNumber(b) <= floatNumber(a) ? 1n : 0n; break; case 608: mem[s] = floatBits(Number(a)); break; case 609: mem[s] = floatInteger(a); break; case 610: { const text = formatFloat(a, 1n, 6n); for (const char of text) this.put_c(char.charCodeAt(0)); s++; break; } case 611: { let text = '', address = Number(a); while (address >= 0 && address < mw && mem[address]) text += String.fromCharCode(Number(mem[address++] & 255n)); mem[s] = parseFloatBits(text); break; } case 618: { const text = formatFloat(mem[s + 2], a, b); for (const char of text) this.put_c(char.charCodeAt(0)); s += 3; break; } // byte ops case 612: { // ld8 const addr = Number(a); mem[s] = (addr >= 0 && addr < mw * 8) ? BigInt(bytes[addr]) : 0n; break; } case 613: { // st8 const addr = Number(a); if (addr >= 0 && addr < mw * 8) bytes[addr] = Number(b & 0xFFn); s += 2; break; } case 614: { // ld16 const addr = Number(a); mem[s] = (addr >= 0 && addr + 1 < mw * 8) ? BigInt(view.getUint16(addr, true)) : 0n; break; } case 615: { // st16 const addr = Number(a); if (addr >= 0 && addr + 1 < mw * 8) view.setUint16(addr, Number(b & 0xFFFFn), true); s += 2; break; } case 616: { // ld32 const addr = Number(a); mem[s] = (addr >= 0 && addr + 3 < mw * 8) ? BigInt(view.getUint32(addr, true)) : 0n; break; } case 617: { // st32 const addr = Number(a); if (addr >= 0 && addr + 3 < mw * 8) view.setUint32(addr, Number(b & 0xFFFFFFFFn), true); s += 2; break; } case 619: // push s mem[--s] = BigInt(s + 1); break; case 620: { // a native call: no native code here, pop the arguments and answer -1 const blk = Number(a); const sig = blk >= 0 && blk + 1 < mw ? BigInt.asUintN(64, mem[blk + 1]) : 0n; let n = 0; while (n < 15 && ((sig >> BigInt(4 * (n + 1))) & 15n)) n++; s += n; mem[s] = -1n; break; } default: // custom enhancement if (op >= XOP0 && op < XOP0 + NENH && this.enh[op - XOP0].live) { s = this.xrun(this.enh[op - XOP0], s); } else { p = mw; } break; } } return 0; } } // command line runner: node build/js/tinyvm.js , or the source on stdin if (typeof require === 'function' && typeof module !== 'undefined' && require.main === module) { const option = process.argv[2]; if (option === '--help' || option === '-h') { process.stdout.write('usage: node build/js/tinyvm.js [file.zc]\n' + 'compiles and runs the file, or reads source from stdin when no file is supplied.\n' + 'input after a backtick is program input.\n' + 'options:\n --help, -h show this help and exit\n' + 'environment:\n TINYVM_MEMORY_WORDS initial vm memory in words\n' + ' TINYVM_MAX_MEMORY_WORDS maximum vm memory in words\n'); process.exit(0); } if (option && option.startsWith('-')) { process.stderr.write('tinyvm.js: unknown option ' + option + '; use --help for usage\n'); process.exit(2); } const genomePath = path.join(__dirname, 'genome.bin'); if (!fs.existsSync(genomePath)) { console.error('Error: genome.bin not found beside tinyvm.js. Run make js first.'); process.exit(1); } const genome: Uint8Array = fs.readFileSync(genomePath); let inputSource = ''; if (process.argv.length > 2) { const filePath = process.argv[2]; if (fs.existsSync(filePath)) { inputSource = fs.readFileSync(filePath, 'utf8'); } else { console.error(`Error: File not found: ${filePath}`); process.exit(1); } } else { try { inputSource = fs.readFileSync(0, 'utf8'); } catch (e) { console.error('Usage: node tinyvm.js '); process.exit(1); } } const environmentWords = (name: string): number | undefined => { const value = process.env[name]; if (value === undefined) return undefined; if (!/^(?:[0-9]+|0x[0-9a-f]+)$/i.test(value)) throw new RangeError('invalid ' + name); return Number(value); }; const vm = new TinyVM({ memoryWords: environmentWords('TINYVM_MEMORY_WORDS'), maxMemoryWords: environmentWords('TINYVM_MAX_MEMORY_WORDS'), onOutput: (byte: number) => process.stdout.write(Buffer.from([byte])), onMessage: (msg: string) => process.stderr.write(msg) }); vm.loadImage(genome, GORG); vm.setInput(inputSource); const code = vm.run(GORG); process.exit(code); } if (typeof module !== 'undefined') module.exports = { TinyVM, DEFAULT_MW, GORG };