aboutsummaryrefslogtreecommitdiff
path: root/src/js
diff options
context:
space:
mode:
authorGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
committerGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
commitecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch)
tree70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /src/js
a
Diffstat (limited to 'src/js')
-rw-r--r--src/js/extract_genome.js45
-rw-r--r--src/js/extract_lsp.js21
-rw-r--r--src/js/mapvalues.js22
-rw-r--r--src/js/tinyvm.ts946
4 files changed, 1034 insertions, 0 deletions
diff --git a/src/js/extract_genome.js b/src/js/extract_genome.js
new file mode 100644
index 0000000..5cb2b38
--- /dev/null
+++ b/src/js/extract_genome.js
@@ -0,0 +1,45 @@
+const fs = require('fs');
+const path = require('path');
+
+const srcPath = path.join(__dirname, '..', 'core', 'genome.h');
+const outPath = path.join(__dirname, 'genome.json');
+
+const text = fs.readFileSync(srcPath, 'utf8');
+const gorgMatch = text.match(/#define\s+GORG\s+(\d+)/);
+const glenMatch = text.match(/#define\s+GLEN\s+(\d+)/);
+
+const gorg = parseInt(gorgMatch[1], 10);
+const glen = parseInt(glenMatch[1], 10);
+
+const arrMatch = text.match(/genome\[\]\s*=\s*\{([\s\S]*?)\};/);
+if (!arrMatch) {
+ console.error('Could not find genome array in genome.h');
+ process.exit(1);
+}
+
+const words = arrMatch[1]
+ .split(',')
+ .map(s => s.trim())
+ .filter(s => s.length > 0)
+ .map(s => s); // keep as string numbers
+
+console.log(`Parsed genome: GORG=${gorg}, GLEN=${words.length}`);
+
+if (words.length !== glen) throw new Error('genome length does not match GLEN');
+
+// the compiler's map, from src/core/memmap.h, as typescript constants: the js vm reads
+// them instead of its own copies. the values are computed here, from the map.
+const { mapValues } = require('./mapvalues.js');
+const names = ['MW', 'PW', 'ZFP', 'GORG', 'DS0', 'DSLIM', 'RS0', 'RSLIM', 'SYSGATE', 'HEAP_LO', 'HEAP_HI'];
+const map = mapValues(path.join(__dirname, '..', 'core', 'memmap.h'), names);
+if (map.GORG !== gorg) throw new Error('genome GORG ' + gorg + ' is not the map GORG ' + map.GORG);
+fs.writeFileSync(outPath, JSON.stringify({ gorg, glen, words }));
+console.log(`saved genome to ${outPath}`);
+const lines = names.map(k => ` ${k}: ${map[k]},`).join('\n');
+fs.writeFileSync(path.join(__dirname, 'layout.ts'),
+ '// generated by src/js/extract_genome.js from src/core/memmap.h. do not edit.\n' +
+ 'export const MAP = {\n' + lines + '\n} as const;\n');
+console.log(`Saved the map to ${path.join(__dirname, 'layout.ts')}`);
+fs.writeFileSync(path.join(__dirname, 'layout.js'),
+ '// generated by src/js/extract_genome.js from src/core/memmap.h. do not edit.\n' +
+ 'exports.MAP = ' + JSON.stringify(map, null, 4) + ';\n');
diff --git a/src/js/extract_lsp.js b/src/js/extract_lsp.js
new file mode 100644
index 0000000..6f53ebc
--- /dev/null
+++ b/src/js/extract_lsp.js
@@ -0,0 +1,21 @@
+// writes src/js/lsp.json: the language server's image (build/lsp/genome.h, made by
+// src/tools/mklsp.sh) and the map it was built for, read from build/lsp/map/memmap.h.
+// the vm needs the layout, because the server's map is not the compiler's.
+const fs = require('fs');
+const path = require('path');
+
+const root = path.join(__dirname, '..', '..');
+const genome = fs.readFileSync(path.join(root, 'build', 'lsp', 'genome.h'), 'utf8');
+
+const { mapValues } = require('./mapvalues.js');
+const layout = mapValues(path.join(root, 'build', 'lsp', 'map', 'memmap.h'), ['GORG', 'DS0', 'DSLIM', 'RS0', 'RSLIM', 'SYSGATE', 'ZFP', 'HEAP_LO', 'HEAP_HI']);
+
+const gorg = parseInt(genome.match(/#define\s+GORG\s+(\d+)/)[1], 10);
+const glen = parseInt(genome.match(/#define\s+GLEN\s+(\d+)/)[1], 10);
+const arr = genome.match(/genome\[\]\s*=\s*\{([\s\S]*?)\};/);
+const words = arr[1].split(',').map(s => s.trim()).filter(s => s.length > 0);
+if (words.length !== glen) throw new Error('genome has ' + words.length + ' words, GLEN says ' + glen);
+if (gorg !== layout.GORG) throw new Error('genome GORG ' + gorg + ' is not the map GORG ' + layout.GORG);
+
+fs.writeFileSync(path.join(__dirname, 'lsp.json'), JSON.stringify({ gorg, glen, layout, words }));
+console.log('wrote src/js/lsp.json: genome ' + glen + ' words at ' + gorg + ', heap ' + layout.HEAP_LO + '..' + layout.HEAP_HI);
diff --git a/src/js/mapvalues.js b/src/js/mapvalues.js
new file mode 100644
index 0000000..f701ba2
--- /dev/null
+++ b/src/js/mapvalues.js
@@ -0,0 +1,22 @@
+// reads the values of a machine map from its memmap.h. the #define lines may use
+// other names and small sums (ZFP + 2, DSLO + 1100), so each value is evaluated
+// with its names substituted. shared by extract_genome.js and extract_lsp.js.
+const fs = require('fs');
+
+function mapValues(mapPath, names) {
+ const text = fs.readFileSync(mapPath, 'utf8');
+ const defs = {};
+ for (const m of text.matchAll(/^#define[ \t]+(\w+)[ \t]+(.+)$/gm)) defs[m[1]] = m[2].trim();
+ function value(name, depth = 0) {
+ if (depth > 20) throw new Error('map loop at ' + name);
+ const expr = defs[name];
+ if (expr === undefined) throw new Error(mapPath + ' has no ' + name);
+ const sub = expr.replace(/\b[A-Za-z_]\w*\b/g, id => '(' + value(id, depth + 1) + ')');
+ return Function('return (' + sub + ');')();
+ }
+ const out = {};
+ for (const k of names) out[k] = value(k);
+ return out;
+}
+
+module.exports = { mapValues };
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 };