js-confuser-vm 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1761 @@
1
+ import { parse } from "@babel/parser";
2
+ import traverseImport from "@babel/traverse";
3
+ import { generate } from "@babel/generator";
4
+ import { stripTypeScriptTypes } from "module";
5
+ import * as t from "@babel/types";
6
+ import { ok } from "assert";
7
+ import { obfuscateRuntime } from "./build-runtime.js";
8
+ import { DEFAULT_OPTIONS } from "./options.js";
9
+ import { resolveLabels } from "./transforms/bytecode/resolveLabels.js";
10
+ import { resolveConstants } from "./transforms/bytecode/resolveContants.js";
11
+ import { selfModifying } from "./transforms/bytecode/selfModifying.js";
12
+ import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
13
+ import * as b from "./types.js";
14
+ import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
15
+ import { getRandomInt } from "./transforms/utils/random-utils.js";
16
+ import { U16_MAX } from "./transforms/utils/op-utils.js";
17
+ const traverse = traverseImport.default || traverseImport;
18
+ const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\r\nconst BYTECODE = [];\r\nconst MAIN_START_PC = 0;\r\nconst CONSTANTS = [];\r\nconst ENCODE_BYTECODE = false;\r\nconst TIMING_CHECKS = false;\r\n// The text above is not included in the compiled output - for type intellisense only\r\n// @START\r\n\r\nfunction decodeBytecode(s) {\r\n if (!ENCODE_BYTECODE) return s;\r\n\r\n var b =\r\n typeof Buffer !== \"undefined\"\r\n ? Buffer.from(s, \"base64\")\r\n : Uint8Array.from(atob(s), function (c) {\r\n return c.charCodeAt(0);\r\n });\r\n // Each slot is a u16 stored as 2 little-endian bytes.\r\n var r = new Uint16Array(b.length / 2);\r\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\r\n return r;\r\n}\r\n\r\n// Closure symbol\r\n// Used to tag shell functions so the VM can fast-path back to the\r\n// inner Closure instead of going through a sub-VM on internal calls.\r\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\r\n\r\n// Upvalue\r\n// While the outer frame is alive: reads/writes go to frame.locals[slot].\r\n// After the outer frame returns (closed): reads/writes hit this.value.\r\nfunction Upvalue(frame, slot) {\r\n this._frame = frame;\r\n this._slot = slot;\r\n this._closed = false;\r\n this._value = undefined;\r\n}\r\nUpvalue.prototype._read = function () {\r\n return this._closed ? this._value : this._frame.locals[this._slot];\r\n};\r\nUpvalue.prototype._write = function (v) {\r\n if (this._closed) this._value = v;\r\n else this._frame.locals[this._slot] = v;\r\n};\r\nUpvalue.prototype._close = function () {\r\n this._value = this._frame.locals[this._slot];\r\n this._closed = true;\r\n};\r\n\r\n// Closure & Frame\r\nfunction Closure(fn) {\r\n this.fn = fn;\r\n this.upvalues = [];\r\n this.prototype = {}; // <- default prototype object for \\`new\\`\r\n}\r\n\r\nfunction Frame(closure, returnPc, parent, thisVal ) {\r\n this.closure = closure;\r\n this.locals = new Array(closure.fn.localCount).fill(undefined);\r\n this._pc = closure.fn.startPc; // <- initialize from fn descriptor\r\n this._returnPc = returnPc; // pc to resume in parent frame after RETURN\r\n this._parent = parent;\r\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\r\n this._newObj = null; // <- set by NEW so RETURN can see it\r\n this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP\r\n}\r\n\r\n// VM\r\nfunction VM(bytecode, mainStartPc, constants, globals) {\r\n this.bytecode = bytecode;\r\n this.constants = constants;\r\n this.globals = globals;\r\n this._stack = [];\r\n this._frameStack = [];\r\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\r\n\r\n var mainFn = {\r\n paramCount: 0,\r\n localCount: 0,\r\n startPc: mainStartPc, // <- where main begins\r\n };\r\n this._currentFrame = new Frame(new Closure(mainFn), null, null);\r\n}\r\n\r\nVM.prototype._push = function (v) {\r\n this._stack.push(v);\r\n};\r\nVM.prototype._pop = function () {\r\n return this._stack.pop();\r\n};\r\nVM.prototype.peek = function () {\r\n return this._stack[this._stack.length - 1];\r\n};\r\n\r\n// Consume the next slot from the flat bytecode stream and advance the PC.\r\n// Called by opcode handlers to read each of their operands in order.\r\nVM.prototype._operand = function () {\r\n return this.bytecode[this._currentFrame._pc++];\r\n};\r\n\r\nVM.prototype.captureUpvalue = function (frame, slot) {\r\n // Reuse existing open upvalue for this frame+slot if one exists.\r\n // This is what makes two closures share the same mutable cell.\r\n for (var i = 0; i < this._openUpvalues.length; i++) {\r\n var uv = this._openUpvalues[i];\r\n if (uv._frame === frame && uv._slot === slot) return uv;\r\n }\r\n var uv = new Upvalue(frame, slot);\r\n this._openUpvalues.push(uv);\r\n return uv;\r\n};\r\n\r\nVM.prototype._closeUpvaluesFor = function (frame) {\r\n // Called on RETURN - close every upvalue that was pointing into this frame.\r\n // After this, closures that captured from the frame read from upvalue.value.\r\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\r\n if (uv._frame === frame) {\r\n uv._close();\r\n return false;\r\n }\r\n return true;\r\n });\r\n};\r\n\r\nVM.prototype.run = function () {\r\n var now = () => {\r\n return performance.now();\r\n };\r\n\r\n var lastTime = now();\r\n\r\n while (true) {\r\n var frame = this._currentFrame;\r\n var bc = this.bytecode;\r\n if (frame._pc >= bc.length) break;\r\n\r\n var op = this.bytecode[frame._pc++];\r\n\r\n // console.log(frame._pc - 1, op);\r\n\r\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\r\n if (TIMING_CHECKS) {\r\n var currentTime = now();\r\n var isTamper = currentTime - lastTime > 1000;\r\n lastTime = currentTime;\r\n if (isTamper) {\r\n // Poison the bytecode\r\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\r\n // Break the current state\r\n op = OP.POP;\r\n this._stack = [];\r\n }\r\n }\r\n\r\n try {\r\n /* @SWITCH */\r\n switch (op) {\r\n case OP.LOAD_CONST:\r\n this._push(this.constants[this._operand()]);\r\n break;\r\n\r\n case OP.LOAD_INT:\r\n this._push(this._operand());\r\n break;\r\n\r\n case OP.LOAD_LOCAL:\r\n this._push(frame.locals[this._operand()]);\r\n break;\r\n\r\n case OP.STORE_LOCAL:\r\n frame.locals[this._operand()] = this._pop();\r\n break;\r\n\r\n case OP.LOAD_GLOBAL:\r\n this._push(this.globals[this.constants[this._operand()]]);\r\n break;\r\n\r\n case OP.STORE_GLOBAL:\r\n this.globals[this.constants[this._operand()]] = this._pop();\r\n break;\r\n\r\n case OP.GET_PROP: {\r\n // Stack: [..., obj, key] -> [..., obj, obj[key]]\r\n // obj is PEEKED (not popped) - CALL_METHOD needs it as receiver\r\n var key = this._pop();\r\n var obj = this.peek();\r\n this._push(obj[key]);\r\n break;\r\n }\r\n\r\n case OP.ADD: {\r\n var b = this._pop();\r\n this._push(this._pop() + b);\r\n break;\r\n }\r\n case OP.SUB: {\r\n var b = this._pop();\r\n this._push(this._pop() - b);\r\n break;\r\n }\r\n case OP.MUL: {\r\n var b = this._pop();\r\n this._push(this._pop() * b);\r\n break;\r\n }\r\n case OP.DIV: {\r\n var b = this._pop();\r\n this._push(this._pop() / b);\r\n break;\r\n }\r\n case OP.MOD: {\r\n var b = this._pop();\r\n this._push(this._pop() % b);\r\n break;\r\n }\r\n case OP.BAND: {\r\n var b = this._pop();\r\n this._push(this._pop() & b);\r\n break;\r\n }\r\n case OP.BOR: {\r\n var b = this._pop();\r\n this._push(this._pop() | b);\r\n break;\r\n }\r\n case OP.BXOR: {\r\n var b = this._pop();\r\n this._push(this._pop() ^ b);\r\n break;\r\n }\r\n case OP.SHL: {\r\n var b = this._pop();\r\n this._push(this._pop() << b);\r\n break;\r\n }\r\n case OP.SHR: {\r\n var b = this._pop();\r\n this._push(this._pop() >> b);\r\n break;\r\n }\r\n case OP.USHR: {\r\n var b = this._pop();\r\n this._push(this._pop() >>> b);\r\n break;\r\n }\r\n\r\n case OP.LT: {\r\n var b = this._pop();\r\n this._push(this._pop() < b);\r\n break;\r\n }\r\n case OP.GT: {\r\n var b = this._pop();\r\n this._push(this._pop() > b);\r\n break;\r\n }\r\n case OP.EQ: {\r\n var b = this._pop();\r\n this._push(this._pop() === b);\r\n break;\r\n }\r\n\r\n case OP.LTE: {\r\n var b = this._pop();\r\n this._push(this._pop() <= b);\r\n break;\r\n }\r\n case OP.GTE: {\r\n var b = this._pop();\r\n this._push(this._pop() >= b);\r\n break;\r\n }\r\n case OP.NEQ: {\r\n var b = this._pop();\r\n this._push(this._pop() !== b);\r\n break;\r\n }\r\n case OP.LOOSE_EQ: {\r\n var b = this._pop();\r\n this._push(this._pop() == b);\r\n break;\r\n }\r\n case OP.LOOSE_NEQ: {\r\n var b = this._pop();\r\n this._push(this._pop() != b);\r\n break;\r\n }\r\n\r\n case OP.IN: {\r\n var b = this._pop();\r\n this._push(this._pop() in b);\r\n break;\r\n }\r\n\r\n case OP.INSTANCEOF: {\r\n var ctor = this._pop();\r\n var obj = this._pop();\r\n if (typeof ctor === \"function\") {\r\n // Native constructor (e.g. Array, Date) - native instanceof is fine\r\n this._push(obj instanceof ctor);\r\n } else {\r\n // VM Closure - ctor.prototype was set by MAKE_CLOSURE / user assignment.\r\n // Walk obj's prototype chain looking for identity with ctor.prototype.\r\n var proto = ctor.prototype; // the .prototype property on the Closure\r\n var target = Object.getPrototypeOf(obj);\r\n var result = false;\r\n while (target !== null) {\r\n if (target === proto) {\r\n result = true;\r\n break;\r\n }\r\n target = Object.getPrototypeOf(target);\r\n }\r\n this._push(result);\r\n }\r\n break;\r\n }\r\n\r\n case OP.UNARY_NEG:\r\n this._push(-this._pop());\r\n break;\r\n case OP.UNARY_POS:\r\n this._push(this._pop());\r\n break;\r\n case OP.UNARY_NOT:\r\n this._push(!this._pop());\r\n break;\r\n case OP.UNARY_BITNOT:\r\n this._push(~this._pop());\r\n break;\r\n case OP.TYPEOF:\r\n this._push(typeof this._pop());\r\n break;\r\n case OP.VOID:\r\n this._pop();\r\n this._push(undefined);\r\n break;\r\n\r\n case OP.TYPEOF_SAFE: {\r\n // operand is a const index holding the variable name string.\r\n // Mimics JS semantics: typeof undeclaredVar === \"undefined\" (no throw).\r\n var name = this._pop(); // LOAD_CONST pushed the name - consume it\r\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\r\n ? this.globals[name]\r\n : undefined;\r\n this._push(typeof val);\r\n break;\r\n }\r\n\r\n case OP.JUMP:\r\n frame._pc = this._operand();\r\n break;\r\n\r\n case OP.JUMP_IF_FALSE: {\r\n var target = this._operand();\r\n if (!this._pop()) frame._pc = target;\r\n break;\r\n }\r\n\r\n case OP.JUMP_IF_TRUE_OR_POP: {\r\n // || semantics: if truthy, we're done - leave value, jump over RHS.\r\n // If falsy, discard it and fall through to evaluate RHS.\r\n var target = this._operand();\r\n if (this.peek()) {\r\n frame._pc = target;\r\n } else {\r\n this._pop();\r\n }\r\n break;\r\n }\r\n\r\n case OP.JUMP_IF_FALSE_OR_POP: {\r\n // && semantics: if falsy, we're done - leave value, jump over RHS.\r\n // If truthy, discard it and fall through to evaluate RHS.\r\n var target = this._operand();\r\n if (!this.peek()) {\r\n frame._pc = target;\r\n } else {\r\n this._pop();\r\n }\r\n break;\r\n }\r\n\r\n case OP.MAKE_CLOSURE: {\r\n // Inline operands: startPc, paramCount, localCount, uvCount,\r\n // [isLocal_0, idx_0, isLocal_1, idx_1, ...]\r\n var startPc = this._operand();\r\n var paramCount = this._operand();\r\n var localCount = this._operand();\r\n var uvCount = this._operand();\r\n\r\n var uvDescs = new Array(uvCount);\r\n for (var i = 0; i < uvCount; i++) {\r\n var isLocalRaw = this._operand();\r\n var uvIndex = this._operand();\r\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\r\n }\r\n\r\n var fn = {\r\n paramCount: paramCount,\r\n localCount: localCount,\r\n startPc: startPc,\r\n upvalueDescriptors: uvDescs,\r\n };\r\n\r\n var closure = new Closure(fn);\r\n for (var i = 0; i < uvDescs.length; i++) {\r\n var uvd = uvDescs[i];\r\n if (uvd.isLocal) {\r\n // Capture directly from current frame's local slot\r\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\r\n } else {\r\n // Relay - take upvalue from the enclosing closure's list\r\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\r\n }\r\n }\r\n // Wrap in a native callable shell so host code (array methods,\r\n // test assertions, setTimeout, etc.) can invoke VM closures.\r\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\r\n var self = this;\r\n var shell = (function (c) {\r\n return function () {\r\n var args = Array.prototype.slice.call(arguments);\r\n var sub = new VM(self.bytecode, 0, self.constants, self.globals);\r\n // Sloppy-mode: null/undefined thisArg \u2192 global object\r\n var f = new Frame(\r\n c,\r\n null,\r\n null,\r\n this == null ? self.globals : this,\r\n );\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n sub._currentFrame = f;\r\n return sub.run();\r\n };\r\n })(closure);\r\n shell[CLOSURE_SYM] = closure;\r\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\r\n this._push(shell);\r\n break;\r\n }\r\n\r\n case OP.LOAD_UPVALUE:\r\n this._push(frame.closure.upvalues[this._operand()]._read());\r\n break;\r\n\r\n case OP.STORE_UPVALUE:\r\n frame.closure.upvalues[this._operand()]._write(this._pop());\r\n break;\r\n\r\n case OP.BUILD_ARRAY: {\r\n var elems = this._stack.splice(this._stack.length - this._operand());\r\n this._push(elems);\r\n break;\r\n }\r\n\r\n case OP.BUILD_OBJECT: {\r\n // Stack has: key0, val0, key1, val1 ... keyN, valN (pushed left->right)\r\n // Pop all pairs and build the object.\r\n var pairs = this._stack.splice(\r\n this._stack.length - this._operand() * 2,\r\n );\r\n var o = {};\r\n for (var i = 0; i < pairs.length; i += 2) {\r\n o[pairs[i]] = pairs[i + 1]; // key at even index, val at odd\r\n }\r\n this._push(o);\r\n break;\r\n }\r\n case OP.SET_PROP: {\r\n // Stack: [..., obj, key, val]\r\n // Leaves val on stack - assignment is an expression in JS.\r\n var val = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n // Reflect.set performs [[Set]] without throwing on failure,\r\n // correctly simulating sloppy-mode assignment from a strict-mode host\r\n // (output.js is an ES module). This also properly invokes inherited\r\n // or prototype-chain setter functions.\r\n Reflect.set(obj, key, val);\r\n this._push(val); // assignment expression evaluates to the assigned value\r\n break;\r\n }\r\n case OP.GET_PROP_COMPUTED: {\r\n // Stack: [..., obj, key] - key is a runtime value (nums[i])\r\n // Mirrors GET_PROP but pops the key that was pushed dynamically.\r\n var key = this._pop();\r\n var obj = this._pop();\r\n this._push(obj[key]);\r\n break;\r\n }\r\n case OP.DELETE_PROP: {\r\n var key = this._pop();\r\n var obj = this._pop();\r\n this._push(delete obj[key]);\r\n break;\r\n }\r\n\r\n case OP.CALL: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure - run directly in this VM, no sub-VM overhead\r\n var c = callee[CLOSURE_SYM];\r\n // Sloppy-mode: plain function call \u2192 global object as this\r\n var f = new Frame(c, frame._pc, frame, this.globals);\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native function\r\n this._push(callee.apply(null, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.CALL_METHOD: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n var receiver = this._pop(); // left on stack by GET_PROP\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure - run directly in this VM with receiver as this\r\n var c = callee[CLOSURE_SYM];\r\n var f = new Frame(c, frame._pc, frame, receiver);\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native method\r\n this._push(callee.apply(receiver, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.LOAD_THIS:\r\n this._push(frame.thisVal);\r\n break;\r\n\r\n case OP.NEW: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure constructor - prototype is unified via shell.prototype = closure.prototype\r\n var c = callee[CLOSURE_SYM];\r\n var newObj = Object.create(c.prototype || null);\r\n var f = new Frame(c, frame._pc, frame, newObj);\r\n f._newObj = newObj;\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native constructor (e.g. new Error(), new Date()).\r\n // Reflect.construct is required - Object.create+apply does NOT set\r\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\r\n this._push(Reflect.construct(callee, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.RETURN: {\r\n var retVal = this._pop();\r\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\r\n if (this._frameStack.length === 0) return retVal;\r\n\r\n // new-call rule: primitive return -> discard, use the constructed object instead\r\n if (frame._newObj !== null) {\r\n if (typeof retVal !== \"object\" || retVal === null)\r\n retVal = frame._newObj;\r\n }\r\n\r\n this._currentFrame = this._frameStack.pop();\r\n this._push(retVal);\r\n break;\r\n }\r\n\r\n case OP.POP:\r\n this._pop();\r\n break;\r\n\r\n case OP.DUP:\r\n this._push(this.peek());\r\n break;\r\n\r\n case OP.THROW:\r\n throw this._pop();\r\n\r\n case OP.FOR_IN_SETUP: {\r\n // Pop the object; build an ordered list of all enumerable own+inherited\r\n // string keys by walking the prototype chain manually.\r\n // Uses getOwnPropertyNames (includes non-enumerable) + descriptor check,\r\n // so we never rely on Object.keys() and we handle inheritance correctly.\r\n var obj = this._pop();\r\n var keys = [];\r\n if (obj !== null && obj !== undefined) {\r\n var seen = Object.create(null);\r\n var cur = Object(obj); // box primitives\r\n while (cur !== null) {\r\n var ownNames = Object.getOwnPropertyNames(cur);\r\n for (var i = 0; i < ownNames.length; i++) {\r\n var k = ownNames[i];\r\n if (!(k in seen)) {\r\n seen[k] = true;\r\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\r\n if (propDesc && propDesc.enumerable) {\r\n keys.push(k);\r\n }\r\n }\r\n }\r\n cur = Object.getPrototypeOf(cur);\r\n }\r\n }\r\n this._push({ _keys: keys, i: 0 });\r\n break;\r\n }\r\n\r\n case OP.FOR_IN_NEXT: {\r\n // Operand = jump target for the done case. Must be read before the\r\n // conditional so the PC stays correctly aligned either way.\r\n var target = this._operand();\r\n var iter = this._pop();\r\n if (iter.i >= iter._keys.length) {\r\n frame._pc = target;\r\n } else {\r\n this._push(iter._keys[iter.i++]);\r\n }\r\n break;\r\n }\r\n\r\n case OP.PATCH: {\r\n // Inline operands: destPc, sliceStart, sliceEnd\r\n // Copies bytecode[sliceStart..sliceEnd) flat u16 slots to destPc.\r\n var destPc = this._operand();\r\n var sliceStart = this._operand();\r\n var sliceEnd = this._operand();\r\n\r\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\r\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\r\n }\r\n break;\r\n }\r\n\r\n case OP.TRY_SETUP: {\r\n // Push an exception handler record onto the current frame.\r\n // Saves: catch PC (operand), current stack depth, current frame-stack depth.\r\n // If an exception is thrown before TRY_END fires, the VM jumps here.\r\n frame._handlerStack.push({\r\n handlerPc: this._operand(),\r\n stackDepth: this._stack.length,\r\n frameStackDepth: this._frameStack.length,\r\n });\r\n break;\r\n }\r\n\r\n case OP.TRY_END: {\r\n // Normal exit from a try block \u2014 disarm the exception handler.\r\n frame._handlerStack.pop();\r\n break;\r\n }\r\n\r\n case OP.DEFINE_GETTER: {\r\n // Stack: [..., obj, key, getterFn]\r\n // Pops all three; defines an enumerable, configurable getter on obj.\r\n // If a setter was already defined for this key, it is preserved.\r\n var getterFn = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\r\n var getDesc = {\r\n get: getterFn,\r\n configurable: true,\r\n enumerable: true,\r\n };\r\n if (existingDesc && typeof existingDesc.set === \"function\") {\r\n getDesc.set = existingDesc.set;\r\n }\r\n Object.defineProperty(obj, key, getDesc);\r\n break;\r\n }\r\n\r\n case OP.DEFINE_SETTER: {\r\n // Stack: [..., obj, key, setterFn]\r\n // Pops all three; defines an enumerable, configurable setter on obj.\r\n // If a getter was already defined for this key, it is preserved.\r\n var setterFn = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\r\n var setDesc = {\r\n set: setterFn,\r\n configurable: true,\r\n enumerable: true,\r\n };\r\n if (existingDesc && typeof existingDesc.get === \"function\") {\r\n setDesc.get = existingDesc.get;\r\n }\r\n Object.defineProperty(obj, key, setDesc);\r\n break;\r\n }\r\n\r\n case OP.DEBUGGER: {\r\n debugger;\r\n break;\r\n }\r\n\r\n default:\r\n throw new Error(\r\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\r\n );\r\n }\r\n } catch (err) {\r\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\r\n // Walk from the current frame upward until we find a frame that has an open\r\n // exception handler (TRY_SETUP without a matching TRY_END).\r\n // For every frame we abandon along the way, close its captured upvalues.\r\n var handledFrame = null;\r\n var searchFrame = this._currentFrame;\r\n while (true) {\r\n if (searchFrame._handlerStack.length > 0) {\r\n handledFrame = searchFrame;\r\n break;\r\n }\r\n // No handler in this frame \u2014 abandon it and walk up.\r\n this._closeUpvaluesFor(searchFrame);\r\n if (this._frameStack.length === 0) break;\r\n searchFrame = this._frameStack.pop();\r\n this._currentFrame = searchFrame;\r\n }\r\n\r\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\r\n\r\n var h = handledFrame._handlerStack.pop();\r\n // Restore the VM value stack to the depth recorded at TRY_SETUP time,\r\n // then push the caught exception so the catch binding can store it.\r\n this._stack.length = h.stackDepth;\r\n this._push(err);\r\n // Discard any call-frames that were pushed inside the try body\r\n // (functions called from within the try block that are still live).\r\n this._frameStack.length = h.frameStackDepth;\r\n // Jump to the catch block.\r\n handledFrame._pc = h.handlerPc;\r\n this._currentFrame = handledFrame;\r\n }\r\n }\r\n};\r\n\r\n// Boot\r\nvar globals = {}; // global object for globals\r\n\r\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\r\n// with a local `window` variable (e.g. the test harness fake window).\r\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\r\n globals[k] = globalThis[k];\r\n}\r\n// If a window object is in scope (browser or test harness), capture it\r\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\r\nif (typeof window !== \"undefined\") {\r\n globals[\"window\"] = window;\r\n}\r\n\r\n// Transfer common primitives\r\nglobals.undefined = undefined;\r\nglobals.Infinity = Infinity;\r\nglobals.NaN = NaN;\r\n\r\nvar vm = new VM(decodeBytecode(BYTECODE), MAIN_START_PC, CONSTANTS, globals);\r\nvm.run();\r\n";
19
+ export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
20
+ export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE"); // Attach source node location to pseudo bytecode instructions
21
+
22
+ // Opcodes
23
+ export const OP_ORIGINAL = {
24
+ LOAD_CONST: 0,
25
+ LOAD_LOCAL: 1,
26
+ STORE_LOCAL: 2,
27
+ LOAD_GLOBAL: 3,
28
+ STORE_GLOBAL: 4,
29
+ GET_PROP: 5,
30
+ ADD: 6,
31
+ // a + b (both are popped)
32
+ SUB: 7,
33
+ // a - b
34
+ MUL: 8,
35
+ // a * b
36
+ DIV: 9,
37
+ // a / b
38
+ MAKE_CLOSURE: 10,
39
+ CALL: 11,
40
+ CALL_METHOD: 12,
41
+ RETURN: 13,
42
+ POP: 14,
43
+ // discard top of stack
44
+ LT: 15,
45
+ // pop b, pop a -> push (a < b)
46
+ GT: 16,
47
+ // pop b, pop a -> push (a > b)
48
+ EQ: 17,
49
+ // pop b, pop a -> push (a === b)
50
+ JUMP: 18,
51
+ // unconditional - operand = absolute bytecode index
52
+ JUMP_IF_FALSE: 19,
53
+ // pop value; jump if falsy
54
+ LTE: 20,
55
+ // a <= b
56
+ GTE: 21,
57
+ // a >= b
58
+ NEQ: 22,
59
+ // a !== b
60
+ LOAD_UPVALUE: 23,
61
+ // push frame.closure.upvalues[operand].read()
62
+ STORE_UPVALUE: 24,
63
+ // frame.closure.upvalues[operand].write(pop())
64
+
65
+ // Unary
66
+ UNARY_NEG: 25,
67
+ // -x
68
+ UNARY_POS: 26,
69
+ // +x
70
+ UNARY_NOT: 27,
71
+ // !x
72
+ UNARY_BITNOT: 28,
73
+ // ~x
74
+ TYPEOF: 29,
75
+ // typeof x
76
+ VOID: 30,
77
+ // void x -> always undefined
78
+
79
+ TYPEOF_SAFE: 31,
80
+ // operand = name constIdx - typeof guard for undeclared globals
81
+ BUILD_ARRAY: 32,
82
+ // operand = element count - pops N values -> pushes array
83
+ BUILD_OBJECT: 33,
84
+ // operand = pair count - pops N*2 (key,val) -> pushes object
85
+ SET_PROP: 34,
86
+ // pop val, pop key, peek obj -> obj[key] = val (obj stays on stack)
87
+ GET_PROP_COMPUTED: 35,
88
+ // pop key, peek obj -> push obj[key] (computed: nums[i])
89
+
90
+ MOD: 36,
91
+ // a % b
92
+ BAND: 37,
93
+ // a & b
94
+ BOR: 38,
95
+ // a | b
96
+ BXOR: 39,
97
+ // a ^ b
98
+ SHL: 40,
99
+ // a << b
100
+ SHR: 41,
101
+ // a >> b
102
+ USHR: 42,
103
+ // a >>> b
104
+
105
+ JUMP_IF_FALSE_OR_POP: 43,
106
+ // && - if top falsy: jump (keep), else: pop, eval RHS
107
+ JUMP_IF_TRUE_OR_POP: 44,
108
+ // || - if top truthy: jump (keep), else: pop, eval RHS
109
+
110
+ DELETE_PROP: 45,
111
+ IN: 46,
112
+ // a in b
113
+ INSTANCEOF: 47,
114
+ // a instanceof b
115
+
116
+ // NEW
117
+ LOAD_THIS: 48,
118
+ // push frame.thisVal
119
+ NEW: 49,
120
+ // operand = argCount - construct a new object
121
+ DUP: 50,
122
+ // duplicate top of stack
123
+ THROW: 51,
124
+ // pop value, throw it
125
+ LOOSE_EQ: 52,
126
+ // a == b (abstract equality)
127
+ LOOSE_NEQ: 53,
128
+ // a != b (abstract inequality)
129
+
130
+ FOR_IN_SETUP: 54,
131
+ // pop obj -> build enumerable-key iterator -> push {keys,i}
132
+ FOR_IN_NEXT: 55,
133
+ // operand=exit_pc; pop iter; if done->jump; else push next key
134
+
135
+ // Self-modifying bytecode
136
+ PATCH: 56,
137
+ // pop destPc; constants[operand]=word[]; write words into bytecode[destPc..]
138
+
139
+ // Try-Catch
140
+ TRY_SETUP: 57,
141
+ // operand = catch_pc; push exception handler onto frame._handlerStack
142
+ TRY_END: 58,
143
+ // pop exception handler (normal exit from try body)
144
+
145
+ // Getter / Setter (ES5 object literal accessor syntax)
146
+ DEFINE_GETTER: 59,
147
+ // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {get: fn})
148
+ DEFINE_SETTER: 60,
149
+ // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {set: fn})
150
+
151
+ DEBUGGER: 61,
152
+ // emits a "debugger" statement
153
+
154
+ // Push the raw integer operand directly onto the stack (no constant pool lookup).
155
+ // Identical pipeline to JUMP ops: {type:"label"} pseudo-operands resolve to a
156
+ // raw PC number that becomes the operand, which is pushed as-is at runtime.
157
+ LOAD_INT: 62
158
+ };
159
+
160
+ // Scope
161
+ // Each function call gets its own Scope. Locals are resolved to
162
+ // numeric slots at compile time -- zero name lookups at runtime.
163
+ class Scope {
164
+ constructor(parent = null) {
165
+ this.parent = parent;
166
+ this._locals = new Map(); // name -> slot index
167
+ this._next = 0;
168
+ }
169
+ define(name) {
170
+ if (!this._locals.has(name)) {
171
+ this._locals.set(name, this._next++);
172
+ }
173
+ return this._locals.get(name);
174
+ }
175
+
176
+ // Walk up scope chain. If we fall off the top -> global.
177
+ resolve(name) {
178
+ if (this._locals.has(name)) {
179
+ return {
180
+ kind: "local",
181
+ slot: this._locals.get(name)
182
+ };
183
+ }
184
+ if (this.parent) return this.parent.resolve(name);
185
+ return {
186
+ kind: "global"
187
+ };
188
+ }
189
+ get localCount() {
190
+ return this._next;
191
+ }
192
+ }
193
+
194
+ // FnContext
195
+ // Compiler-side state for the function currently being compiled.
196
+ // Distinct from runtime Frame -- this is compile-time only.
197
+ class FnContext {
198
+ constructor(compiler, parentCtx = null) {
199
+ this.compiler = compiler;
200
+ this.parentCtx = parentCtx;
201
+ this.scope = new Scope();
202
+ this.bc = [];
203
+ this.upvalues = []; // { name, isLocal, index }
204
+ }
205
+
206
+ // Find or register a captured variable as an upvalue.
207
+ // isLocal=true -> captured directly from parent's locals[index]
208
+ // isLocal=false -> relayed from parent's own upvalue list[index]
209
+ addUpvalue(name, isLocal, index) {
210
+ const existing = this.upvalues.findIndex(u => u.name === name);
211
+ if (existing !== -1) return existing;
212
+ const idx = this.upvalues.length;
213
+ this.upvalues.push({
214
+ name,
215
+ isLocal,
216
+ index: index
217
+ });
218
+ return idx;
219
+ }
220
+ }
221
+
222
+ // Compiler
223
+ export class Compiler {
224
+ emit(bc, instr, node) {
225
+ bc.push(instr);
226
+ instr[SOURCE_NODE_SYM] = node;
227
+ }
228
+
229
+ // DO NOT USE THIS KEY UNLESS YOU ARE "RESOLVE CONSTANTS"
230
+ // CONSTANTS DURING COMPILATION MUST BE USED BY REFERENCE WITH b.constantOperand("myConstantHere")
231
+
232
+ constructor(options = DEFAULT_OPTIONS) {
233
+ this.options = options;
234
+ this.fnDescriptors = []; // populated in pass 1
235
+ this.bytecode = [];
236
+ this.mainStartPc = 0;
237
+ this._currentCtx = null; // FnContext of the function being compiled, null at top-level
238
+ this._loopStack = []; // per active loop/switch/block/try
239
+ this._pendingLabel = null;
240
+ this._forInCount = 0; // counter for synthetic for-in iterator global names
241
+ this._labelCount = 0; // monotonically increasing counter for unique label names
242
+
243
+ this.serializer = new Serializer(this);
244
+ this.MACRO_OPS = {};
245
+ this.SPECIALIZED_OPS = {};
246
+ this.OP = {
247
+ ...OP_ORIGINAL
248
+ };
249
+
250
+ // Construct randomized opcode mapping
251
+ if (this.options.randomizeOpcodes) {
252
+ let usedNumbers = new Set();
253
+ for (const key in this.OP) {
254
+ let val;
255
+ do {
256
+ val = getRandomInt(0, U16_MAX);
257
+ } while (usedNumbers.has(val));
258
+ usedNumbers.add(val);
259
+ this.OP[key] = val;
260
+ }
261
+ }
262
+
263
+ // Reverse map for comment generation
264
+ this.OP_NAME = Object.fromEntries(Object.entries(this.OP).map(([k, v]) => [v, k]));
265
+ this.JUMP_OPS = new Set([this.OP.JUMP, this.OP.JUMP_IF_FALSE, this.OP.JUMP_IF_TRUE_OR_POP, this.OP.JUMP_IF_FALSE_OR_POP, this.OP.FOR_IN_NEXT, this.OP.TRY_SETUP // catch_pc operand needs offset adjustment like jump targets
266
+ ]);
267
+ }
268
+
269
+ // Generate a globally unique label string with an optional hint for readability.
270
+ _makeLabel(hint = "") {
271
+ var id = this._labelCount++;
272
+ return `${hint || "L"}_${id}`;
273
+ }
274
+
275
+ // Variable resolution
276
+ // Walks up the FnContext chain. Crossing a context boundary means
277
+ // we're capturing from an outer function - register an upvalue.
278
+ _resolve(name, ctx) {
279
+ if (!ctx) return {
280
+ kind: "global"
281
+ };
282
+
283
+ // 1. Own locals
284
+ if (ctx.scope._locals.has(name)) {
285
+ return {
286
+ kind: "local",
287
+ slot: ctx.scope._locals.get(name)
288
+ };
289
+ }
290
+
291
+ // 2. No parent context -> must be global
292
+ if (!ctx.parentCtx) return {
293
+ kind: "global"
294
+ };
295
+
296
+ // 3. Ask parent -- recurse up the chain
297
+ const parentResult = this._resolve(name, ctx.parentCtx);
298
+ if (parentResult.kind === "global") return {
299
+ kind: "global"
300
+ };
301
+
302
+ // 4. Parent has it (as local or upvalue) -- register an upvalue here.
303
+ // isLocal=true means "take it straight from parent's locals[index]"
304
+ // isLocal=false means "relay parent's upvalue[index]" (multi-level capture)
305
+ const isLocal = parentResult.kind === "local";
306
+ const index = isLocal ? parentResult.slot : parentResult.index;
307
+ const uvIdx = ctx.addUpvalue(name, isLocal, index);
308
+ return {
309
+ kind: "upvalue",
310
+ index: uvIdx
311
+ };
312
+ }
313
+
314
+ // Entry point
315
+ compile(source) {
316
+ const ast = parse(source, {
317
+ sourceType: "script"
318
+ });
319
+ return this.compileAST(ast);
320
+ }
321
+ compileAST(ast) {
322
+ // Pass 1 - compile every FunctionDeclaration into a descriptor.
323
+ // Traverse finds them regardless of nesting depth.
324
+ traverse(ast, {
325
+ FunctionDeclaration: path => {
326
+ // Only handle top-level functions for this MVP.
327
+ // (Parent is Program node)
328
+ if (path.parent.type !== "Program") return;
329
+ this._compileFunctionDecl(path.node);
330
+ path.skip(); // don't recurse into nested functions
331
+ }
332
+ });
333
+
334
+ // Pass 2 -- compile top-level statements into BYTECODE.
335
+ this._compileMain(ast.program.body);
336
+ return this.bytecode;
337
+ }
338
+
339
+ // Function Declaration
340
+
341
+ _compileFunctionDecl(node) {
342
+ // Reserve a slot in fnDescriptors NOW, before compiling the body, so that
343
+ // any nested _compileFunctionDecl calls see the correct .length and get a
344
+ // distinct _fnIdx. The placeholder object is mutated in-place below once
345
+ // the body and header are ready.
346
+ var fnIdx = this.fnDescriptors.length;
347
+ const entryLabel = this._makeLabel(`fn_${fnIdx}`);
348
+ var desc = {}; // placeholder — filled in after compilation
349
+ this.fnDescriptors.push(desc);
350
+
351
+ // Create a context whose parent is whatever we're currently compiling.
352
+ // This is what lets _resolve cross function boundaries correctly.
353
+ const ctx = new FnContext(this, this._currentCtx);
354
+ const savedCtx = this._currentCtx;
355
+ this._currentCtx = ctx;
356
+
357
+ // Isolate the loop stack so that try/loop entries from the outer scope
358
+ // don't cause spurious TRY_END / extra jumps inside this function body.
359
+ const savedLoopStack = this._loopStack;
360
+ this._loopStack = [];
361
+
362
+ // Params occupy the first N local slots (args are copied in on CALL)
363
+ for (const param of node.params) {
364
+ let identifier = param.type === "AssignmentPattern" ? param.left : param;
365
+ ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
366
+ ctx.scope.define(identifier.name);
367
+ }
368
+
369
+ // Reserve the next slot for the implicit `arguments` object.
370
+ // Slot index will always equal paramCount (params are 0..paramCount-1).
371
+ ctx.scope.define("arguments");
372
+
373
+ // Pass 2: emit default-value guards at top of fn body
374
+ // Mirrors what JS engines do: if the caller passed undefined (or
375
+ // nothing), evaluate the default expression and overwrite the slot.
376
+ for (const param of node.params) {
377
+ if (param.type !== "AssignmentPattern") continue;
378
+ const slot = ctx.scope._locals.get(param.left.name);
379
+ const skipLabel = this._makeLabel("param_skip");
380
+
381
+ // if (param === undefined) param = <default expr>
382
+ this.emit(ctx.bc, [this.OP.LOAD_LOCAL, slot], param);
383
+ this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], param);
384
+ this.emit(ctx.bc, [this.OP.EQ], param);
385
+ this.emit(ctx.bc, [this.OP.JUMP_IF_FALSE, {
386
+ type: "label",
387
+ label: skipLabel
388
+ }], param);
389
+ this._compileExpr(param.right, ctx.scope, ctx.bc); // eval default
390
+ this.emit(ctx.bc, [this.OP.STORE_LOCAL, slot], param);
391
+ this.emit(ctx.bc, [null, {
392
+ type: "defineLabel",
393
+ label: skipLabel
394
+ }], param);
395
+ }
396
+ for (const stmt of node.body.body) {
397
+ this._compileStatement(stmt, ctx.scope, ctx.bc);
398
+ }
399
+
400
+ // If we fall off the end of the function, implicitly return undefined.
401
+ this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
402
+ this.emit(ctx.bc, [this.OP.RETURN], node);
403
+ this._currentCtx = savedCtx; // restore before touching fnDescriptors
404
+ this._loopStack = savedLoopStack;
405
+ node._fnIdx = fnIdx;
406
+
407
+ // Fill the placeholder that was reserved at the top of this function.
408
+ // Metadata (paramCount, localCount, upvalues) is stored on desc and emitted
409
+ // as inline operands on the MAKE_CLOSURE instruction via _emitMakeClosure.
410
+ desc.name = node.id?.name || "<anonymous>";
411
+ desc.entryLabel = entryLabel;
412
+ desc.bytecode = ctx.bc;
413
+ desc._fnIdx = fnIdx;
414
+ desc.paramCount = node.params.length;
415
+ desc.localCount = ctx.scope.localCount;
416
+ desc.upvalues = ctx.upvalues.slice();
417
+ return desc;
418
+ }
419
+
420
+ // Emit a single MAKE_CLOSURE instruction with all closure metadata packed
421
+ // as inline operands. The runtime reads them via _operand() — no stack
422
+ // shuffling needed.
423
+ //
424
+ // Flat operand layout: startPc, paramCount, localCount, uvCount,
425
+ // [isLocal_0, idx_0, isLocal_1, idx_1, ...]
426
+ _emitMakeClosure(desc, node, bc) {
427
+ const uvOperands = [];
428
+ for (const uv of desc.upvalues) {
429
+ uvOperands.push(uv.isLocal ? 1 : 0);
430
+ uvOperands.push(uv.index);
431
+ }
432
+ this.emit(bc, [this.OP.MAKE_CLOSURE, {
433
+ type: "label",
434
+ label: desc.entryLabel
435
+ }, desc.paramCount, desc.localCount, desc.upvalues.length, ...uvOperands], node);
436
+ }
437
+
438
+ // Main (top-level)
439
+ _compileMain(body) {
440
+ const bc = this.bytecode;
441
+
442
+ // Hoist all FunctionDeclarations: MAKE_CLOSURE -> STORE_GLOBAL
443
+ // (mirrors JS hoisting -- functions are available before other code)
444
+ for (const node of body) {
445
+ if (node.type !== "FunctionDeclaration") continue;
446
+ const desc = this.fnDescriptors.find(d => d._fnIdx === node._fnIdx);
447
+ const nameRef = b.constantOperand(node.id.name);
448
+ this._emitMakeClosure(desc, node, bc);
449
+ this.emit(bc, [this.OP.STORE_GLOBAL, nameRef], node);
450
+ }
451
+
452
+ // Compile everything else in order
453
+ for (const node of body) {
454
+ if (node.type === "FunctionDeclaration") continue;
455
+ this._compileStatement(node, null, bc); // null scope -> global context
456
+ }
457
+ this.emit(bc, [this.OP.RETURN], null); // end program
458
+
459
+ // Append all function bodies. Each function's entryLabel (already generated
460
+ // in _compileFunctionDecl) points directly to the first body instruction;
461
+ // metadata is pushed onto the stack at each call site, not stored inline.
462
+ for (const descriptor of this.fnDescriptors) {
463
+ this.bytecode.push([null, {
464
+ type: "defineLabel",
465
+ label: descriptor.entryLabel
466
+ }]);
467
+ for (const instr of descriptor.bytecode) {
468
+ this.bytecode.push(instr);
469
+ }
470
+ }
471
+ }
472
+
473
+ // Statements
474
+ _compileStatement(node, scope, bc) {
475
+ switch (node.type) {
476
+ case "EmptyStatement":
477
+ {
478
+ // nothing to emit -- bare semicolon is a no-op
479
+ break;
480
+ }
481
+ case "DebuggerStatement":
482
+ this.emit(bc, [this.OP.DEBUGGER], node);
483
+ break;
484
+ case "BlockStatement":
485
+ {
486
+ for (const stmt of node.body) {
487
+ this._compileStatement(stmt, scope, bc);
488
+ }
489
+ break;
490
+ }
491
+ case "FunctionDeclaration":
492
+ {
493
+ // Nested function -- compile it into a descriptor, then emit
494
+ // MAKE_CLOSURE so it's captured as a live closure at runtime.
495
+ // (_compileFunctionDecl pushes/pops _currentCtx internally)
496
+ const desc = this._compileFunctionDecl(node);
497
+ this._emitMakeClosure(desc, node, bc);
498
+ if (scope) {
499
+ const slot = scope.define(node.id.name);
500
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
501
+ } else {
502
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.id.name)], node);
503
+ }
504
+ break;
505
+ }
506
+ case "ThrowStatement":
507
+ {
508
+ this._compileExpr(node.argument, scope, bc);
509
+ this.emit(bc, [this.OP.THROW], node);
510
+ break;
511
+ }
512
+ case "ReturnStatement":
513
+ {
514
+ if (node.argument) {
515
+ this._compileExpr(node.argument, scope, bc);
516
+ } else {
517
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
518
+ }
519
+ // Disarm any open try handlers before leaving the function.
520
+ // TRY_END only touches frame._handlerStack, not the value stack,
521
+ // so the return value sitting on top is safe.
522
+ for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
523
+ if (this._loopStack[_ri].type === "try") {
524
+ this.emit(bc, [this.OP.TRY_END], node);
525
+ }
526
+ }
527
+ this.emit(bc, [this.OP.RETURN], node);
528
+ break;
529
+ }
530
+ case "ExpressionStatement":
531
+ {
532
+ this._compileExpr(node.expression, scope, bc);
533
+ this.emit(bc, [this.OP.POP], node); // discard return value of statement-level expressions
534
+ break;
535
+ }
536
+ case "VariableDeclaration":
537
+ {
538
+ for (const decl of node.declarations) {
539
+ // Push the initialiser (or undefined if absent)
540
+ if (decl.init) {
541
+ this._compileExpr(decl.init, scope, bc);
542
+ } else {
543
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
544
+ }
545
+ ok(decl.id.type === "Identifier", "Only simple identifiers can be declared");
546
+
547
+ // Store: local slot if inside a function, global name otherwise
548
+ if (scope) {
549
+ const slot = scope.define(decl.id.name);
550
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
551
+ } else {
552
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(decl.id.name)], node);
553
+ }
554
+ }
555
+ break;
556
+ }
557
+ case "IfStatement":
558
+ {
559
+ const elseOrEndLabel = this._makeLabel("if_else");
560
+ // 1. Compile the test expression -> leaves a value on the stack
561
+ this._compileExpr(node.test, scope, bc);
562
+ // 2. Emit JUMP_IF_FALSE to the else branch (or end if no else)
563
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, {
564
+ type: "label",
565
+ label: elseOrEndLabel
566
+ }], node);
567
+ // 3. Compile the consequent block (the "then" branch)
568
+ const consequentBody = node.consequent.type === "BlockStatement" ? node.consequent.body : [node.consequent];
569
+ for (const stmt of consequentBody) {
570
+ this._compileStatement(stmt, scope, bc);
571
+ }
572
+ if (node.alternate) {
573
+ // 4a. Consequent needs to jump OVER the else block when done
574
+ const endLabel = this._makeLabel("if_end");
575
+ this.emit(bc, [this.OP.JUMP, {
576
+ type: "label",
577
+ label: endLabel
578
+ }], node);
579
+ // Mark start of else
580
+ this.emit(bc, [null, {
581
+ type: "defineLabel",
582
+ label: elseOrEndLabel
583
+ }], node);
584
+ // 5. Compile the alternate (else) block
585
+ const altBody = node.alternate.type === "BlockStatement" ? node.alternate.body : [node.alternate]; // handles `else if` -- it's just a nested IfStatement
586
+ for (const stmt of altBody) {
587
+ this._compileStatement(stmt, scope, bc);
588
+ }
589
+ // Mark end (consequent's jump lands here)
590
+ this.emit(bc, [null, {
591
+ type: "defineLabel",
592
+ label: endLabel
593
+ }], node);
594
+ } else {
595
+ // 4b. No else -- label lands right after the then block
596
+ this.emit(bc, [null, {
597
+ type: "defineLabel",
598
+ label: elseOrEndLabel
599
+ }], node);
600
+ }
601
+ break;
602
+ }
603
+ case "WhileStatement":
604
+ {
605
+ const _wLabel = this._pendingLabel;
606
+ this._pendingLabel = null;
607
+ const loopTopLabel = this._makeLabel("while_top");
608
+ const exitLabel = this._makeLabel("while_exit");
609
+ this._loopStack.push({
610
+ type: "loop",
611
+ label: _wLabel,
612
+ breakLabel: exitLabel,
613
+ continueLabel: loopTopLabel // continue re-evaluates the test
614
+ });
615
+ this.emit(bc, [null, {
616
+ type: "defineLabel",
617
+ label: loopTopLabel
618
+ }], node);
619
+ this._compileExpr(node.test, scope, bc);
620
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, {
621
+ type: "label",
622
+ label: exitLabel
623
+ }], node);
624
+ const whileBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
625
+ for (const stmt of whileBody) {
626
+ this._compileStatement(stmt, scope, bc);
627
+ }
628
+ this.emit(bc, [this.OP.JUMP, {
629
+ type: "label",
630
+ label: loopTopLabel
631
+ }], node);
632
+ this.emit(bc, [null, {
633
+ type: "defineLabel",
634
+ label: exitLabel
635
+ }], node);
636
+ this._loopStack.pop();
637
+ break;
638
+ }
639
+ case "DoWhileStatement":
640
+ {
641
+ const _dwLabel = this._pendingLabel;
642
+ this._pendingLabel = null;
643
+ const loopTopLabel = this._makeLabel("dowhile_top");
644
+ const continueLabel = this._makeLabel("dowhile_cont");
645
+ const exitLabel = this._makeLabel("dowhile_exit");
646
+ this._loopStack.push({
647
+ type: "loop",
648
+ label: _dwLabel,
649
+ breakLabel: exitLabel,
650
+ continueLabel: continueLabel // continue falls to the test
651
+ });
652
+ this.emit(bc, [null, {
653
+ type: "defineLabel",
654
+ label: loopTopLabel
655
+ }], node);
656
+ const doWhileBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
657
+ for (const stmt of doWhileBody) {
658
+ this._compileStatement(stmt, scope, bc);
659
+ }
660
+
661
+ // continue -> skip rest of body, fall through to test
662
+ this.emit(bc, [null, {
663
+ type: "defineLabel",
664
+ label: continueLabel
665
+ }], node);
666
+ this._compileExpr(node.test, scope, bc);
667
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, {
668
+ type: "label",
669
+ label: exitLabel
670
+ }], node);
671
+ this.emit(bc, [this.OP.JUMP, {
672
+ type: "label",
673
+ label: loopTopLabel
674
+ }], node);
675
+ this.emit(bc, [null, {
676
+ type: "defineLabel",
677
+ label: exitLabel
678
+ }], node);
679
+ this._loopStack.pop();
680
+ break;
681
+ }
682
+ case "ForStatement":
683
+ {
684
+ const _fLabel = this._pendingLabel;
685
+ this._pendingLabel = null;
686
+ const loopTopLabel = this._makeLabel("for_top");
687
+ const exitLabel = this._makeLabel("for_exit");
688
+ // continue jumps to the update clause if present, else straight to test
689
+ const updateLabel = node.update ? this._makeLabel("for_update") : loopTopLabel;
690
+ this._loopStack.push({
691
+ type: "loop",
692
+ label: _fLabel,
693
+ breakLabel: exitLabel,
694
+ continueLabel: updateLabel
695
+ });
696
+ if (node.init) {
697
+ if (node.init.type === "VariableDeclaration") {
698
+ this._compileStatement(node.init, scope, bc);
699
+ } else {
700
+ this._compileExpr(node.init, scope, bc);
701
+ this.emit(bc, [this.OP.POP], node);
702
+ }
703
+ }
704
+ this.emit(bc, [null, {
705
+ type: "defineLabel",
706
+ label: loopTopLabel
707
+ }], node);
708
+ if (node.test) {
709
+ this._compileExpr(node.test, scope, bc);
710
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, {
711
+ type: "label",
712
+ label: exitLabel
713
+ }], node);
714
+ }
715
+ const forBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
716
+ for (const stmt of forBody) {
717
+ this._compileStatement(stmt, scope, bc);
718
+ }
719
+
720
+ // continue -> run update (if any) then back to test
721
+ if (node.update) {
722
+ this.emit(bc, [null, {
723
+ type: "defineLabel",
724
+ label: updateLabel
725
+ }], node);
726
+ this._compileExpr(node.update, scope, bc);
727
+ this.emit(bc, [this.OP.POP], node);
728
+ }
729
+ this.emit(bc, [this.OP.JUMP, {
730
+ type: "label",
731
+ label: loopTopLabel
732
+ }], node);
733
+ this.emit(bc, [null, {
734
+ type: "defineLabel",
735
+ label: exitLabel
736
+ }], node);
737
+ this._loopStack.pop();
738
+ break;
739
+ }
740
+ case "BreakStatement":
741
+ {
742
+ // Find the jump target in the loop stack.
743
+ let _bTargetIdx = -1;
744
+ if (node.label) {
745
+ const _bLabelName = node.label.name;
746
+ for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
747
+ if (this._loopStack[_bi].label === _bLabelName) {
748
+ _bTargetIdx = _bi;
749
+ break;
750
+ }
751
+ }
752
+ if (_bTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found`);
753
+ } else {
754
+ // Find innermost loop/switch/block (skip "try" entries)
755
+ for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
756
+ if (this._loopStack[_bi].type !== "try") {
757
+ _bTargetIdx = _bi;
758
+ break;
759
+ }
760
+ }
761
+ if (_bTargetIdx === -1) throw new Error("break outside loop");
762
+ }
763
+ // Emit TRY_END for every open try block between here and the target.
764
+ for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
765
+ if (this._loopStack[_bi].type === "try") {
766
+ this.emit(bc, [this.OP.TRY_END], node);
767
+ }
768
+ }
769
+ this.emit(bc, [this.OP.JUMP, {
770
+ type: "label",
771
+ label: this._loopStack[_bTargetIdx].breakLabel
772
+ }], node);
773
+ break;
774
+ }
775
+ case "ContinueStatement":
776
+ {
777
+ // Find the target loop in the loop stack.
778
+ let _cTargetIdx = -1;
779
+ if (node.label) {
780
+ const _cLabelName = node.label.name;
781
+ for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
782
+ if (this._loopStack[_ci].label === _cLabelName && this._loopStack[_ci].type === "loop") {
783
+ _cTargetIdx = _ci;
784
+ break;
785
+ }
786
+ }
787
+ if (_cTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found for continue`);
788
+ } else {
789
+ // Find the innermost loop (skip switch, block, and try contexts)
790
+ for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
791
+ if (this._loopStack[_ci].type === "loop") {
792
+ _cTargetIdx = _ci;
793
+ break;
794
+ }
795
+ }
796
+ if (_cTargetIdx === -1) throw new Error("continue outside loop");
797
+ }
798
+ // Emit TRY_END for every open try block between here and the target loop.
799
+ for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
800
+ if (this._loopStack[_ci].type === "try") {
801
+ this.emit(bc, [this.OP.TRY_END], node);
802
+ }
803
+ }
804
+ this.emit(bc, [this.OP.JUMP, {
805
+ type: "label",
806
+ label: this._loopStack[_cTargetIdx].continueLabel
807
+ }], node);
808
+ break;
809
+ }
810
+ case "SwitchStatement":
811
+ {
812
+ const _swLabel = this._pendingLabel;
813
+ this._pendingLabel = null;
814
+ const switchBreakLabel = this._makeLabel("sw_break");
815
+ this._loopStack.push({
816
+ type: "switch",
817
+ label: _swLabel,
818
+ breakLabel: switchBreakLabel,
819
+ continueLabel: switchBreakLabel // not used for switch
820
+ });
821
+
822
+ // Compile the discriminant and leave it on the stack
823
+ this._compileExpr(node.discriminant, scope, bc);
824
+ const cases = node.cases;
825
+ const defaultIdx = cases.findIndex(c => c.test === null);
826
+
827
+ // Pre-allocate a label for each case body so dispatch can reference them
828
+ const caseLabels = cases.map((_, i) => this._makeLabel(`sw_case_${i}`));
829
+
830
+ // Dispatch section: for each non-default case, check and jump to its body
831
+ for (let i = 0; i < cases.length; i++) {
832
+ const cas = cases[i];
833
+ if (cas.test === null) continue; // skip default in dispatch
834
+
835
+ const nextCheckLabel = this._makeLabel("sw_next");
836
+ this.emit(bc, [this.OP.DUP], node);
837
+ this._compileExpr(cas.test, scope, bc);
838
+ this.emit(bc, [this.OP.EQ], node);
839
+ // If not matched, fall through to the next check
840
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, {
841
+ type: "label",
842
+ label: nextCheckLabel
843
+ }], node);
844
+ // If matched, jump directly to this case's body
845
+ this.emit(bc, [this.OP.JUMP, {
846
+ type: "label",
847
+ label: caseLabels[i]
848
+ }], node);
849
+ this.emit(bc, [null, {
850
+ type: "defineLabel",
851
+ label: nextCheckLabel
852
+ }], node);
853
+ }
854
+
855
+ // No case matched: jump to default body or exit (which pops discriminant)
856
+ this.emit(bc, [this.OP.JUMP, {
857
+ type: "label",
858
+ label: defaultIdx !== -1 ? caseLabels[defaultIdx] : switchBreakLabel
859
+ }], node);
860
+
861
+ // Body section: compile all case bodies in source order (fallthrough intact)
862
+ for (let i = 0; i < cases.length; i++) {
863
+ this.emit(bc, [null, {
864
+ type: "defineLabel",
865
+ label: caseLabels[i]
866
+ }], node);
867
+ for (const stmt of cases[i].consequent) {
868
+ this._compileStatement(stmt, scope, bc);
869
+ }
870
+ }
871
+
872
+ // break label lands here; pop the discriminant and continue after switch
873
+ this.emit(bc, [null, {
874
+ type: "defineLabel",
875
+ label: switchBreakLabel
876
+ }], node);
877
+ this.emit(bc, [this.OP.POP], node);
878
+ this._loopStack.pop();
879
+ break;
880
+ }
881
+ case "LabeledStatement":
882
+ {
883
+ const _lName = node.label.name;
884
+ const _lBody = node.body;
885
+ const _lIsLoop = _lBody.type === "ForStatement" || _lBody.type === "WhileStatement" || _lBody.type === "DoWhileStatement" || _lBody.type === "ForInStatement";
886
+ const _lIsSwitch = _lBody.type === "SwitchStatement";
887
+ if (_lIsLoop || _lIsSwitch) {
888
+ // Pass label down to the loop/switch handler via _pendingLabel
889
+ this._pendingLabel = _lName;
890
+ this._compileStatement(_lBody, scope, bc);
891
+ this._pendingLabel = null; // safety clear if handler didn't consume it
892
+ } else {
893
+ // Non-loop labeled statement (e.g. labeled block) -- only break is valid
894
+ const blockBreakLabel = this._makeLabel("block_break");
895
+ this._loopStack.push({
896
+ type: "block",
897
+ label: _lName,
898
+ breakLabel: blockBreakLabel,
899
+ continueLabel: blockBreakLabel // unused
900
+ });
901
+ this._compileStatement(_lBody, scope, bc);
902
+ this._loopStack.pop();
903
+ this.emit(bc, [null, {
904
+ type: "defineLabel",
905
+ label: blockBreakLabel
906
+ }], node);
907
+ }
908
+ break;
909
+ }
910
+ case "ForInStatement":
911
+ {
912
+ const _fiLabel = this._pendingLabel;
913
+ this._pendingLabel = null;
914
+
915
+ // Evaluate the object expression -> on stack
916
+ this._compileExpr(node.right, scope, bc);
917
+ // FOR_IN_SETUP: pops obj, pushes iterator {keys, i}
918
+ this.emit(bc, [this.OP.FOR_IN_SETUP], node);
919
+
920
+ // Store iterator in a hidden slot so break/continue need no cleanup
921
+ let emitLoadIter;
922
+ let emitStoreIter;
923
+ if (scope) {
924
+ // Reserve a hidden local slot (no name mapping needed)
925
+ const iterSlot = scope._next++;
926
+ emitLoadIter = () => this.emit(bc, [this.OP.LOAD_LOCAL, iterSlot], node);
927
+ emitStoreIter = () => this.emit(bc, [this.OP.STORE_LOCAL, iterSlot], node);
928
+ } else {
929
+ // Top level -- use a synthetic global that won't collide with user code
930
+ const iterNameIdx = b.constantOperand("__fi" + this._forInCount++);
931
+ emitLoadIter = () => this.emit(bc, [this.OP.LOAD_GLOBAL, iterNameIdx], node);
932
+ emitStoreIter = () => this.emit(bc, [this.OP.STORE_GLOBAL, iterNameIdx], node);
933
+ }
934
+ emitStoreIter();
935
+ const loopTopLabel = this._makeLabel("forin_top");
936
+ const exitLabel = this._makeLabel("forin_exit");
937
+ this._loopStack.push({
938
+ type: "loop",
939
+ label: _fiLabel,
940
+ breakLabel: exitLabel,
941
+ continueLabel: loopTopLabel // continue re-checks the iterator
942
+ });
943
+ this.emit(bc, [null, {
944
+ type: "defineLabel",
945
+ label: loopTopLabel
946
+ }], node);
947
+
948
+ // Load iterator, attempt to get next key
949
+ emitLoadIter();
950
+ this.emit(bc, [this.OP.FOR_IN_NEXT, {
951
+ type: "label",
952
+ label: exitLabel
953
+ }], node);
954
+
955
+ // Assign the key (now on top of stack) to the loop variable
956
+ if (node.left.type === "VariableDeclaration") {
957
+ const identifier = node.left.declarations[0].id;
958
+ ok(identifier.type === "Identifier", "Only simple identifiers can be declared in for-in loops");
959
+ const name = identifier.name;
960
+ if (scope) {
961
+ const slot = scope.define(name);
962
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
963
+ } else {
964
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name)], node);
965
+ }
966
+ } else if (node.left.type === "Identifier") {
967
+ const res = this._resolve(node.left.name, this._currentCtx);
968
+ if (res.kind === "local") {
969
+ this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
970
+ } else if (res.kind === "upvalue") {
971
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
972
+ } else {
973
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.left.name)], node);
974
+ }
975
+ } else {
976
+ const src = generate(node.left).code;
977
+ throw new Error(`Unsupported for-in left-hand side: ${node.left.type}\n -> ${src}`);
978
+ }
979
+
980
+ // Compile the loop body
981
+ const fiBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
982
+ for (const stmt of fiBody) {
983
+ this._compileStatement(stmt, scope, bc);
984
+ }
985
+ this.emit(bc, [this.OP.JUMP, {
986
+ type: "label",
987
+ label: loopTopLabel
988
+ }], node);
989
+ this.emit(bc, [null, {
990
+ type: "defineLabel",
991
+ label: exitLabel
992
+ }], node);
993
+ this._loopStack.pop();
994
+ break;
995
+ }
996
+ case "TryStatement":
997
+ {
998
+ if (node.finalizer) {
999
+ throw new Error("try..finally is not supported. Use a helper function instead");
1000
+ }
1001
+ if (!node.handler) {
1002
+ // try without catch requires finally — not supported
1003
+ throw new Error("try without catch is not supported (requires finally).");
1004
+ }
1005
+ const catchLabel = this._makeLabel("catch");
1006
+ const afterCatchLabel = this._makeLabel("after_catch");
1007
+
1008
+ // Emit TRY_SETUP with the catch block's label as the handler PC.
1009
+ // At runtime: saves stack depth + frame stack depth, pushes handler.
1010
+ this.emit(bc, [this.OP.TRY_SETUP, {
1011
+ type: "label",
1012
+ label: catchLabel
1013
+ }], node);
1014
+
1015
+ // Track the open try block so that break/continue/return inside the
1016
+ // try body can emit the matching TRY_END before their jump.
1017
+ this._loopStack.push({
1018
+ type: "try",
1019
+ label: null,
1020
+ breakLabel: "",
1021
+ // unused
1022
+ continueLabel: "" // unused
1023
+ });
1024
+
1025
+ // Compile try body
1026
+ for (const stmt of node.block.body) {
1027
+ this._compileStatement(stmt, scope, bc);
1028
+ }
1029
+
1030
+ // Done compiling the try body — pop the tracking entry.
1031
+ this._loopStack.pop();
1032
+
1033
+ // Normal exit: disarm the exception handler.
1034
+ this.emit(bc, [this.OP.TRY_END], node);
1035
+
1036
+ // Jump over the catch block on normal path.
1037
+ this.emit(bc, [this.OP.JUMP, {
1038
+ type: "label",
1039
+ label: afterCatchLabel
1040
+ }], node);
1041
+
1042
+ // Catch block: exception is on top of the stack (pushed by the VM).
1043
+ this.emit(bc, [null, {
1044
+ type: "defineLabel",
1045
+ label: catchLabel
1046
+ }], node);
1047
+ const handler = node.handler;
1048
+ if (handler.param) {
1049
+ // Bind the exception value to the catch variable.
1050
+ const name = handler.param.name;
1051
+ if (scope) {
1052
+ const slot = scope.define(name);
1053
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
1054
+ } else {
1055
+ this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(name)], node);
1056
+ }
1057
+ } else {
1058
+ // Optional catch binding (catch without a variable — ES2019+)
1059
+ this.emit(bc, [this.OP.POP], node);
1060
+ }
1061
+
1062
+ // Compile catch body
1063
+ for (const stmt of handler.body.body) {
1064
+ this._compileStatement(stmt, scope, bc);
1065
+ }
1066
+
1067
+ // Normal-path jump lands here (after the catch block).
1068
+ this.emit(bc, [null, {
1069
+ type: "defineLabel",
1070
+ label: afterCatchLabel
1071
+ }], node);
1072
+ break;
1073
+ }
1074
+ default:
1075
+ {
1076
+ // Use @babel/generator to reproduce the source of unsupported nodes
1077
+ // so we can emit a clear error with context.
1078
+ const src = generate(node).code;
1079
+ throw new Error(`Unsupported statement: ${node.type}\n -> ${src}`);
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ // Expressions
1085
+ _compileExpr(node, scope, bc) {
1086
+ switch (node.type) {
1087
+ case "NumericLiteral":
1088
+ case "StringLiteral":
1089
+ {
1090
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.value)], node);
1091
+ break;
1092
+ }
1093
+ case "BooleanLiteral":
1094
+ {
1095
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.value)], node);
1096
+ break;
1097
+ }
1098
+ case "NullLiteral":
1099
+ {
1100
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(null)], node);
1101
+ break;
1102
+ }
1103
+ case "Identifier":
1104
+ {
1105
+ // scope=null means we're at the top-level -> always global
1106
+ const res = this._resolve(node.name, this._currentCtx);
1107
+ if (res.kind === "local") {
1108
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1109
+ } else if (res.kind === "upvalue") {
1110
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1111
+ } else {
1112
+ this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand(node.name)], node);
1113
+ }
1114
+ break;
1115
+ }
1116
+ case "ThisExpression":
1117
+ {
1118
+ this.emit(bc, [this.OP.LOAD_THIS], node);
1119
+ break;
1120
+ }
1121
+ case "NewExpression":
1122
+ {
1123
+ // Push callee, then args -- identical layout to CALL but uses NEW opcode
1124
+ this._compileExpr(node.callee, scope, bc);
1125
+ for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1126
+ this.emit(bc, [this.OP.NEW, node.arguments.length], node);
1127
+ break;
1128
+ }
1129
+ case "SequenceExpression":
1130
+ {
1131
+ // (a, b, c) -> eval a -> POP, eval b -> POP, eval c -> leave on stack
1132
+ for (let i = 0; i < node.expressions.length - 1; i++) {
1133
+ this._compileExpr(node.expressions[i], scope, bc);
1134
+ this.emit(bc, [this.OP.POP], node); // discard intermediate result
1135
+ }
1136
+ // Last expression -- its value is the result of the whole sequence
1137
+ this._compileExpr(node.expressions[node.expressions.length - 1], scope, bc);
1138
+ break;
1139
+ }
1140
+ case "ConditionalExpression":
1141
+ {
1142
+ // test ? consequent : alternate
1143
+ const elseLabel = this._makeLabel("ternary_else");
1144
+ const endLabel = this._makeLabel("ternary_end");
1145
+ this._compileExpr(node.test, scope, bc);
1146
+ this.emit(bc, [this.OP.JUMP_IF_FALSE, {
1147
+ type: "label",
1148
+ label: elseLabel
1149
+ }], node);
1150
+ this._compileExpr(node.consequent, scope, bc);
1151
+ this.emit(bc, [this.OP.JUMP, {
1152
+ type: "label",
1153
+ label: endLabel
1154
+ }], node);
1155
+ this.emit(bc, [null, {
1156
+ type: "defineLabel",
1157
+ label: elseLabel
1158
+ }], node);
1159
+ this._compileExpr(node.alternate, scope, bc);
1160
+ this.emit(bc, [null, {
1161
+ type: "defineLabel",
1162
+ label: endLabel
1163
+ }], node);
1164
+ break;
1165
+ }
1166
+ case "LogicalExpression":
1167
+ {
1168
+ // Pattern (CPython-style):
1169
+ // eval LHS
1170
+ // JUMP_IF_*_OR_POP -> target (past RHS)
1171
+ // eval RHS ← only reached if LHS didn't short-circuit
1172
+ // [target lands here, stack top is the result either way]
1173
+
1174
+ this._compileExpr(node.left, scope, bc);
1175
+ if (node.operator === "||") {
1176
+ // Short-circuit if LHS is TRUTHY -- keep it, skip RHS
1177
+ const endLabel = this._makeLabel("or_end");
1178
+ this.emit(bc, [this.OP.JUMP_IF_TRUE_OR_POP, {
1179
+ type: "label",
1180
+ label: endLabel
1181
+ }], node);
1182
+ this._compileExpr(node.right, scope, bc);
1183
+ this.emit(bc, [null, {
1184
+ type: "defineLabel",
1185
+ label: endLabel
1186
+ }], node);
1187
+ } else if (node.operator === "&&") {
1188
+ // Short-circuit if LHS is FALSY -- keep it, skip RHS
1189
+ const endLabel = this._makeLabel("and_end");
1190
+ this.emit(bc, [this.OP.JUMP_IF_FALSE_OR_POP, {
1191
+ type: "label",
1192
+ label: endLabel
1193
+ }], node);
1194
+ this._compileExpr(node.right, scope, bc);
1195
+ this.emit(bc, [null, {
1196
+ type: "defineLabel",
1197
+ label: endLabel
1198
+ }], node);
1199
+ } else {
1200
+ throw new Error(`Unsupported logical operator: ${node.operator}`);
1201
+ }
1202
+ break;
1203
+ }
1204
+ case "BinaryExpression":
1205
+ {
1206
+ this._compileExpr(node.left, scope, bc);
1207
+ this._compileExpr(node.right, scope, bc);
1208
+ const arithOp = {
1209
+ "+": this.OP.ADD,
1210
+ "-": this.OP.SUB,
1211
+ "*": this.OP.MUL,
1212
+ "/": this.OP.DIV,
1213
+ "%": this.OP.MOD,
1214
+ "&": this.OP.BAND,
1215
+ "|": this.OP.BOR,
1216
+ "^": this.OP.BXOR,
1217
+ "<<": this.OP.SHL,
1218
+ ">>": this.OP.SHR,
1219
+ ">>>": this.OP.USHR
1220
+ }[node.operator];
1221
+ const cmpOp = {
1222
+ "<": this.OP.LT,
1223
+ ">": this.OP.GT,
1224
+ "===": this.OP.EQ,
1225
+ "==": this.OP.LOOSE_EQ,
1226
+ "<=": this.OP.LTE,
1227
+ ">=": this.OP.GTE,
1228
+ "!==": this.OP.NEQ,
1229
+ "!=": this.OP.LOOSE_NEQ,
1230
+ in: this.OP.IN,
1231
+ // ← add
1232
+ instanceof: this.OP.INSTANCEOF // ← add
1233
+ }[node.operator];
1234
+ const resolvedOp = arithOp ?? cmpOp;
1235
+ if (resolvedOp === undefined) throw new Error(`Unsupported operator: ${node.operator}`);
1236
+ this.emit(bc, [resolvedOp], node);
1237
+ break;
1238
+ }
1239
+ case "UpdateExpression":
1240
+ {
1241
+ const res = this._resolve(node.argument.name, this._currentCtx);
1242
+ const bumpOp = node.operator === "++" ? this.OP.ADD : this.OP.SUB;
1243
+ const one = b.constantOperand(1);
1244
+
1245
+ // Helper closures: emit load / store for whichever resolution kind we have
1246
+ const emitLoad = () => {
1247
+ if (res.kind === "local") this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);else if (res.kind === "upvalue") this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);else this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand(node.argument.name)], node);
1248
+ };
1249
+ const emitStore = () => {
1250
+ if (res.kind === "local") this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);else if (res.kind === "upvalue") this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);else this.emit(bc, [this.OP.STORE_GLOBAL, b.constantOperand(node.argument.name)], node);
1251
+ };
1252
+ emitLoad();
1253
+ if (!node.prefix) this.emit(bc, [this.OP.DUP], node); // post: save old value before mutating
1254
+ this.emit(bc, [this.OP.LOAD_CONST, one], node);
1255
+ this.emit(bc, [bumpOp], node);
1256
+ emitStore();
1257
+ if (node.prefix) emitLoad(); // pre: reload new value as result
1258
+
1259
+ break;
1260
+ }
1261
+ case "AssignmentExpression":
1262
+ {
1263
+ const compoundOp = {
1264
+ "+=": this.OP.ADD,
1265
+ "-=": this.OP.SUB,
1266
+ "*=": this.OP.MUL,
1267
+ "/=": this.OP.DIV,
1268
+ "%=": this.OP.MOD,
1269
+ "&=": this.OP.BAND,
1270
+ "|=": this.OP.BOR,
1271
+ "^=": this.OP.BXOR,
1272
+ "<<=": this.OP.SHL,
1273
+ ">>=": this.OP.SHR,
1274
+ ">>>=": this.OP.USHR
1275
+ }[node.operator];
1276
+ const isCompound = compoundOp !== undefined;
1277
+ if (node.operator !== "=" && !isCompound) {
1278
+ throw new Error(`Unsupported assignment operator: ${node.operator}`);
1279
+ }
1280
+
1281
+ // Member assignment: obj.x = val or arr[i] = val
1282
+ if (node.left.type === "MemberExpression") {
1283
+ this._compileExpr(node.left.object, scope, bc); // push obj
1284
+
1285
+ if (node.left.computed) {
1286
+ this._compileExpr(node.left.property, scope, bc); // push key (runtime)
1287
+ } else {
1288
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)], node);
1289
+ }
1290
+ if (isCompound) {
1291
+ // Duplicate obj+key on the stack so we can read before we write.
1292
+ // Stack before DUP2: [..., obj, key]
1293
+ // We need: [..., obj, key, obj, key] -> GET_PROP_COMPUTED -> [..., obj, key, currentVal]
1294
+ // Cheapest approach without a DUP opcode: re-compile the member read.
1295
+ // (emits obj + key again; a future peephole pass could DUP instead)
1296
+ this._compileExpr(node.left.object, scope, bc);
1297
+ if (node.left.computed) {
1298
+ this._compileExpr(node.left.property, scope, bc);
1299
+ } else {
1300
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)], node);
1301
+ }
1302
+ this.emit(bc, [this.OP.GET_PROP_COMPUTED], node); // [..., obj, key, currentVal]
1303
+ this._compileExpr(node.right, scope, bc); // [..., obj, key, currentVal, rhs]
1304
+ this.emit(bc, [compoundOp], node); // [..., obj, key, newVal]
1305
+ } else {
1306
+ this._compileExpr(node.right, scope, bc); // [..., obj, key, val]
1307
+ }
1308
+ this.emit(bc, [this.OP.SET_PROP], node); // obj[key] = val, leaves val on stack
1309
+ break;
1310
+ }
1311
+
1312
+ // Plain identifier assignment
1313
+ const res = this._resolve(node.left.name, this._currentCtx);
1314
+ if (isCompound) {
1315
+ // Load the current value of the target first
1316
+ if (res.kind === "local") {
1317
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1318
+ } else if (res.kind === "upvalue") {
1319
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1320
+ } else {
1321
+ this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand(node.left.name)], node);
1322
+ }
1323
+ }
1324
+ this._compileExpr(node.right, scope, bc); // push RHS
1325
+
1326
+ if (isCompound) {
1327
+ this.emit(bc, [compoundOp], node); // apply binary op -> leaves newVal on stack
1328
+ }
1329
+
1330
+ // Store & leave value on stack (assignment is an expression)
1331
+ if (res.kind === "local") {
1332
+ this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
1333
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1334
+ } else if (res.kind === "upvalue") {
1335
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
1336
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1337
+ } else {
1338
+ const nameIdx = b.constantOperand(node.left.name);
1339
+ this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx], node);
1340
+ this.emit(bc, [this.OP.LOAD_GLOBAL, nameIdx], node);
1341
+ }
1342
+ break;
1343
+ }
1344
+ case "CallExpression":
1345
+ {
1346
+ if (node.callee.type === "MemberExpression") {
1347
+ // ── Method call: console.log(...)
1348
+ // Push receiver first (GET_PROP leaves it; CALL_METHOD pops it as `this`)
1349
+ this._compileExpr(node.callee.object, scope, bc);
1350
+ const prop = node.callee.property.name;
1351
+ const propIdx = b.constantOperand(prop);
1352
+ this.emit(bc, [this.OP.LOAD_CONST, propIdx], node);
1353
+ this.emit(bc, [this.OP.GET_PROP], node);
1354
+ for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1355
+ this.emit(bc, [this.OP.CALL_METHOD, node.arguments.length], node);
1356
+ } else {
1357
+ // ── Plain call: add(5, 10)
1358
+ this._compileExpr(node.callee, scope, bc);
1359
+ for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1360
+ this.emit(bc, [this.OP.CALL, node.arguments.length], node);
1361
+ }
1362
+ break;
1363
+ }
1364
+ case "UnaryExpression":
1365
+ {
1366
+ // Special case: typeof on a bare identifier must not throw if undeclared.
1367
+ // We emit TYPEOF_SAFE (operand = name constant index) instead of
1368
+ // compiling the argument first. The VM does the guard itself.
1369
+ if (node.operator === "typeof" && node.argument.type === "Identifier") {
1370
+ const res = this._resolve(node.argument.name, this._currentCtx);
1371
+ if (res.kind === "global") {
1372
+ // Potentially undeclared -- let VM guard it
1373
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.argument.name)], node);
1374
+ this.emit(bc, [this.OP.TYPEOF_SAFE], node);
1375
+ break;
1376
+ }
1377
+ // Known local or upvalue -- safe to load first, then typeof
1378
+ }
1379
+
1380
+ // Special case: delete -- argument must NOT be pre-evaluated.
1381
+ if (node.operator === "delete") {
1382
+ const arg = node.argument;
1383
+ if (arg.type === "MemberExpression") {
1384
+ this._compileExpr(arg.object, scope, bc);
1385
+ if (arg.computed) {
1386
+ this._compileExpr(arg.property, scope, bc);
1387
+ } else {
1388
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(arg.property.name)], node);
1389
+ }
1390
+ this.emit(bc, [this.OP.DELETE_PROP], node);
1391
+ } else {
1392
+ // delete x, delete 0, etc. -- always true in non-strict, just push true
1393
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(true)], node);
1394
+ }
1395
+ break;
1396
+ }
1397
+
1398
+ // All other unary ops: compile argument first, then apply operator
1399
+ this._compileExpr(node.argument, scope, bc);
1400
+ switch (node.operator) {
1401
+ case "-":
1402
+ this.emit(bc, [this.OP.UNARY_NEG], node);
1403
+ break;
1404
+ case "+":
1405
+ this.emit(bc, [this.OP.UNARY_POS], node);
1406
+ break;
1407
+ case "!":
1408
+ this.emit(bc, [this.OP.UNARY_NOT], node);
1409
+ break;
1410
+ case "~":
1411
+ this.emit(bc, [this.OP.UNARY_BITNOT], node);
1412
+ break;
1413
+ case "typeof":
1414
+ this.emit(bc, [this.OP.TYPEOF], node);
1415
+ break;
1416
+ case "void":
1417
+ this.emit(bc, [this.OP.VOID], node);
1418
+ break;
1419
+ default:
1420
+ throw new Error(`Unsupported unary operator: ${node.operator}`);
1421
+ }
1422
+ break;
1423
+ }
1424
+ case "RegExpLiteral":
1425
+ {
1426
+ // Emit: new RegExp(pattern, flags)
1427
+ // Fresh object per evaluation -- correct for stateful g/y flags.
1428
+ this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand("RegExp")], node);
1429
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.pattern)], node);
1430
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.flags)], node);
1431
+ this.emit(bc, [this.OP.NEW, 2], node);
1432
+ break;
1433
+ }
1434
+ case "FunctionExpression":
1435
+ {
1436
+ // Compile into a descriptor exactly like a declaration,
1437
+ // but leave the resulting closure ON THE STACK -- no store.
1438
+ // The surrounding expression (assignment, call arg, return) consumes it.
1439
+ const desc = this._compileFunctionDecl(node);
1440
+ this._emitMakeClosure(desc, node, bc);
1441
+ break;
1442
+ }
1443
+ case "MemberExpression":
1444
+ {
1445
+ this._compileExpr(node.object, scope, bc);
1446
+ if (node.computed) {
1447
+ // nums[i] -- key is runtime value
1448
+ this._compileExpr(node.property, scope, bc);
1449
+ } else {
1450
+ // point.x -- push key as string, same opcode handles both
1451
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.property.name)], node);
1452
+ }
1453
+
1454
+ // GET_PROP_COMPUTED pops the object -- correct for value access.
1455
+ // GET_PROP (peek) is only used in CallExpression's method call path
1456
+ // where the receiver must survive on the stack for CALL_METHOD.
1457
+ this.emit(bc, [this.OP.GET_PROP_COMPUTED], node);
1458
+ break;
1459
+ }
1460
+ case "ArrayExpression":
1461
+ {
1462
+ // Compile each element left->right, then BUILD_ARRAY collapses them.
1463
+ // Sparse arrays (holes) get explicit undefined per slot.
1464
+ for (const el of node.elements) {
1465
+ if (el === null) {
1466
+ // hole: e.g. [1,,3]
1467
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
1468
+ } else {
1469
+ this._compileExpr(el, scope, bc);
1470
+ }
1471
+ }
1472
+ this.emit(bc, [this.OP.BUILD_ARRAY, node.elements.length], node);
1473
+ break;
1474
+ }
1475
+ case "ObjectExpression":
1476
+ {
1477
+ // Separate regular data properties from ES5 accessor methods (get/set).
1478
+ const regularProps = [];
1479
+ const accessorProps = [];
1480
+ for (const prop of node.properties) {
1481
+ if (prop.type === "SpreadElement") {
1482
+ throw new Error("Object spread not supported");
1483
+ }
1484
+ if (prop.type === "ObjectMethod") {
1485
+ if (prop.kind === "get" || prop.kind === "set") {
1486
+ if (prop.computed) {
1487
+ throw new Error("Computed getter/setter keys are not supported");
1488
+ }
1489
+ accessorProps.push(prop);
1490
+ } else {
1491
+ throw new Error(`Shorthand method syntax is not supported`);
1492
+ }
1493
+ } else {
1494
+ regularProps.push(prop);
1495
+ }
1496
+ }
1497
+
1498
+ // Build the base object from data properties.
1499
+ for (const prop of regularProps) {
1500
+ const key = prop.key;
1501
+ let keyStr;
1502
+ if (key.type === "Identifier") {
1503
+ keyStr = key.name;
1504
+ } else if (key.type === "StringLiteral" || key.type === "NumericLiteral") {
1505
+ keyStr = String(key.value);
1506
+ } else {
1507
+ throw new Error(`Unsupported object key type: ${key.type}`);
1508
+ }
1509
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
1510
+ this._compileExpr(prop.value, scope, bc);
1511
+ }
1512
+ this.emit(bc, [this.OP.BUILD_OBJECT, regularProps.length], node);
1513
+
1514
+ // Define each accessor on the object that is now on top of the stack.
1515
+ // Stack after BUILD_OBJECT: [..., obj]
1516
+ // For each accessor: DUP obj, push key, compile fn, DEFINE_GETTER/DEFINE_SETTER
1517
+ // DEFINE_GETTER/DEFINE_SETTER pops fn+key+obj, leaving the original obj.
1518
+ for (const prop of accessorProps) {
1519
+ const key = prop.key;
1520
+ let keyStr;
1521
+ if (key.type === "Identifier") {
1522
+ keyStr = key.name;
1523
+ } else if (key.type === "StringLiteral" || key.type === "NumericLiteral") {
1524
+ keyStr = String(key.value);
1525
+ } else {
1526
+ throw new Error(`Unsupported object key type: ${key.type}`);
1527
+ }
1528
+ this.emit(bc, [this.OP.DUP], node); // dup so the original obj stays after the define
1529
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
1530
+
1531
+ // Compile the accessor body as an anonymous function descriptor.
1532
+ const desc = this._compileFunctionDecl(prop);
1533
+ this._emitMakeClosure(desc, prop, bc);
1534
+ this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER], node);
1535
+ }
1536
+ break;
1537
+ }
1538
+ default:
1539
+ {
1540
+ throw new Error(`Unsupported expression: ${node.type}`);
1541
+ }
1542
+ }
1543
+ }
1544
+ }
1545
+
1546
+ // Serializer
1547
+ // Turns the compiled output into a commented JS source string.
1548
+ // Expects fully-resolved bytecode (all label refs and constant refs already
1549
+ // converted to plain integers by resolveLabels + resolveConstants passes).
1550
+ class Serializer {
1551
+ constructor(compiler) {
1552
+ this.compiler = compiler;
1553
+ }
1554
+ get options() {
1555
+ return this.compiler.options;
1556
+ }
1557
+ get OP() {
1558
+ return this.compiler.OP;
1559
+ }
1560
+ get OP_NAME() {
1561
+ return this.compiler.OP_NAME;
1562
+ }
1563
+ get JUMP_OPS() {
1564
+ return this.compiler.JUMP_OPS;
1565
+ }
1566
+
1567
+ // Produce a JS literal for a constant pool entry
1568
+ _serializeConst(val) {
1569
+ if (val === null) return "null";
1570
+ if (val === undefined) return "undefined";
1571
+ return JSON.stringify(val); // number / string / bool
1572
+ }
1573
+
1574
+ // One instruction -> "[op, op1, op2, ...] // MNEMONIC description"
1575
+ // Expects a fully-resolved instruction: all operands are plain numbers.
1576
+ // Returns { text, values } where values is the flat u16 slots for this
1577
+ // instruction (opcode first, then one entry per operand).
1578
+ _serializeInstr(instr, constants) {
1579
+ const op = instr[0];
1580
+ const operands = instr.slice(1);
1581
+ const resolvedOperands = operands.filter(operand => operand?.placeholder !== true).map(o => o?.resolvedValue ?? o);
1582
+ for (const o of resolvedOperands) {
1583
+ ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
1584
+ ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
1585
+ }
1586
+ ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
1587
+ const operand = resolvedOperands[0]; // first operand for single-operand comment cases
1588
+ const name = this.OP_NAME[op] || `OP_${op}`;
1589
+ let comment = name;
1590
+ const sourceNode = instr[SOURCE_NODE_SYM];
1591
+ const sourceLocation = sourceNode ? sourceNode.loc.start?.line + ":" + sourceNode.loc.start?.column + "-" + (sourceNode.loc.end?.line + ":" + sourceNode.loc.end?.column) : "";
1592
+
1593
+ // Annotate with human-readable operand meaning
1594
+ if (resolvedOperands.length > 0) {
1595
+ switch (op) {
1596
+ case this.OP.LOAD_CONST:
1597
+ {
1598
+ const val = constants[operand];
1599
+ comment += ` ${this._serializeConst(val)}`;
1600
+ break;
1601
+ }
1602
+ case this.OP.MAKE_CLOSURE:
1603
+ {
1604
+ comment += ` PC ${operand} (params=${resolvedOperands[1]} locals=${resolvedOperands[2]} upvalues=${resolvedOperands[3]})`;
1605
+ break;
1606
+ }
1607
+ case this.OP.LOAD_LOCAL:
1608
+ case this.OP.STORE_LOCAL:
1609
+ comment += ` slot[${operand}]`;
1610
+ break;
1611
+ case this.OP.LOAD_UPVALUE:
1612
+ case this.OP.STORE_UPVALUE:
1613
+ comment += ` upvalue[${operand}]`;
1614
+ break;
1615
+ case this.OP.LOAD_GLOBAL:
1616
+ case this.OP.STORE_GLOBAL:
1617
+ comment += ` "${constants[operand]}"`;
1618
+ break;
1619
+ case this.OP.CALL:
1620
+ case this.OP.CALL_METHOD:
1621
+ comment += ` (${operand} args)`;
1622
+ break;
1623
+ case this.OP.BUILD_ARRAY:
1624
+ comment += ` (${operand} elements)`;
1625
+ break;
1626
+ case this.OP.BUILD_OBJECT:
1627
+ comment += ` (${operand} pairs)`;
1628
+ break;
1629
+ case this.OP.NEW:
1630
+ comment += ` (${operand} args)`;
1631
+ break;
1632
+ default:
1633
+ comment += resolvedOperands.length === 1 ? ` ${operand}` : ` [${resolvedOperands.join(", ")}]`;
1634
+ }
1635
+ }
1636
+ comment = comment.padEnd(40) + sourceLocation;
1637
+ const values = [op, ...resolvedOperands];
1638
+ const instrText = `[${values.join(", ")}]`;
1639
+ const text = `${(instrText + ",").padEnd(12)} ${comment}`;
1640
+ return {
1641
+ text,
1642
+ values
1643
+ };
1644
+ }
1645
+
1646
+ // Serialize the CONSTANTS array
1647
+ _serializeConstants(constants) {
1648
+ const lines = ["var CONSTANTS = ["];
1649
+ constants.forEach((val, idx) => {
1650
+ lines.push(` /* ${idx} */ ${this._serializeConst(val)},`);
1651
+ });
1652
+ lines.push("];");
1653
+ return lines.join("\n");
1654
+ }
1655
+
1656
+ // Filter out any remaining null-opcode pseudo-instructions.
1657
+ // (defineLabel pseudo-ops are already stripped by resolveLabels.)
1658
+ _serializeBytecode(bytecode, compiler) {
1659
+ const serialized = [];
1660
+ for (const instr of bytecode) {
1661
+ if (instr[0] === null) continue;
1662
+ const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
1663
+ if (specializedOpInfo) {
1664
+ const resolvedValue = instr[1]?.resolvedValue ?? instr[1];
1665
+ const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
1666
+ compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValue}`;
1667
+ specializedOpInfo.resolvedOperand = instr[1];
1668
+ }
1669
+ serialized.push(instr);
1670
+ }
1671
+ return {
1672
+ bytecode: serialized
1673
+ };
1674
+ }
1675
+ _encodeBytecode(flat) {
1676
+ // Encode as little-endian Uint16Array -> base64.
1677
+ const buf = new Uint8Array(flat.length * 2);
1678
+ flat.forEach((w, i) => {
1679
+ buf[i * 2] = w & 0xff;
1680
+ buf[i * 2 + 1] = w >>> 8 & 0xff;
1681
+ });
1682
+ return Buffer.from(buf).toString("base64");
1683
+ }
1684
+ serialize(bytecode, constants, compiler) {
1685
+ const mainStartPc = compiler.mainStartPc;
1686
+ let sections = [];
1687
+ var textForm = [];
1688
+ var initBody = [];
1689
+ var bytecodeResult = this._serializeBytecode(bytecode, compiler);
1690
+ for (const instr of bytecodeResult.bytecode) {
1691
+ const serialized = this._serializeInstr(instr, constants);
1692
+ textForm.push(serialized.text);
1693
+ }
1694
+ initBody.push(textForm.map(line => `// ${line}`).join("\n"));
1695
+ const flat = bytecodeResult.bytecode.flatMap(instr => {
1696
+ let filtered = instr.filter(x => x?.placeholder !== true);
1697
+ let resolved = filtered.map(x => x?.resolvedValue ?? x);
1698
+ return resolved;
1699
+ });
1700
+ if (this.options.encodeBytecode) {
1701
+ sections.push(`var BYTECODE = "${this._encodeBytecode(flat)}";`);
1702
+ } else {
1703
+ // Flatten each [op, ...operands] instruction into individual u16 slots.
1704
+
1705
+ sections.push(`var BYTECODE = [${flat.join(",")}]`);
1706
+ }
1707
+
1708
+ // MAIN_START_PC
1709
+ sections.push(`var MAIN_START_PC = ${mainStartPc};`);
1710
+ sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
1711
+ sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
1712
+ // Opcodes
1713
+ const object = t.objectExpression(Object.entries(this.OP).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
1714
+ sections.push(`var OP = ${generate(object).code};`);
1715
+
1716
+ // Constants must be defined before the bytecode
1717
+ initBody.push(this._serializeConstants(constants));
1718
+ sections = [...initBody, ...sections];
1719
+
1720
+ // VM runtime
1721
+ sections.push(VM_RUNTIME);
1722
+ return sections.join("\n\n");
1723
+ }
1724
+ }
1725
+ export async function compileAndSerialize(sourceCode, options) {
1726
+ const compiler = new Compiler(options);
1727
+ let bytecode = compiler.compile(sourceCode);
1728
+
1729
+ // User transform passes (operate on unresolved IR with label/constant refs)
1730
+ // macroOpcodes must run after selfModifying (so PATCH-stub bodies are in place)
1731
+ const passes = [];
1732
+
1733
+ // Due to current implementation, specialized must run BEFORE macroOpcodes
1734
+ if (options.specializedOpcodes) {
1735
+ passes.push(specializedOpcodes);
1736
+ }
1737
+ if (options.macroOpcodes) {
1738
+ passes.push(macroOpcodes);
1739
+ }
1740
+ if (options.selfModifying) {
1741
+ passes.push(selfModifying);
1742
+ }
1743
+ for (const pass of passes) {
1744
+ const passResult = pass(bytecode, compiler);
1745
+ bytecode = passResult.bytecode;
1746
+ }
1747
+
1748
+ // Assembler phases: resolve IR operands to plain integers before printing
1749
+ const {
1750
+ bytecode: labelResolved
1751
+ } = resolveLabels(bytecode, compiler);
1752
+ let {
1753
+ bytecode: finalBytecode,
1754
+ constants
1755
+ } = resolveConstants(labelResolved);
1756
+ const output = compiler.serializer.serialize(finalBytecode, constants, compiler);
1757
+ const finalOutput = await obfuscateRuntime(output, finalBytecode, options, compiler);
1758
+ return {
1759
+ code: finalOutput
1760
+ };
1761
+ }