diff options
Diffstat (limited to 'src/js/tinyvm.ts')
| -rw-r--r-- | src/js/tinyvm.ts | 946 |
1 files changed, 946 insertions, 0 deletions
diff --git a/src/js/tinyvm.ts b/src/js/tinyvm.ts new file mode 100644 index 0000000..ab1e4da --- /dev/null +++ b/src/js/tinyvm.ts @@ -0,0 +1,946 @@ +/** + * 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 src/core/memmap.h at build time (src/js/layout.ts) +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<Layout>; + /** 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 <eval> 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; + 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]); + } + } + + 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 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: <eval> 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) ? '<eval>' : 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<string, number>(); + 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<number, bigint>(); + 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) { + 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 src/js/tinyvm.js <file.zc>, or the source on stdin +if (typeof require === 'function' && typeof module !== 'undefined' && require.main === module) { + const genomePath = path.join(__dirname, 'genome.json'); + if (!fs.existsSync(genomePath)) { + console.error('Error: genome.json not found. Run extract_genome.js first.'); + process.exit(1); + } + + const genomeData = JSON.parse(fs.readFileSync(genomePath, 'utf8')); + 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 <file.zc>'); + 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.loadGenome(genomeData.words, genomeData.gorg); + vm.setInput(inputSource); + const code = vm.run(genomeData.gorg); + process.exit(code); +} + +if (typeof module !== 'undefined') module.exports = { TinyVM, DEFAULT_MW, GORG }; |
