js-confuser-vm 0.0.2 → 0.0.3

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