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/CHANGELOG.md +28 -0
- package/README.MD +16 -10
- package/babel-plugin-inline-runtime.cjs +34 -0
- package/babel.config.json +2 -3
- package/dist/compiler.js +687 -421
- package/dist/index.js +2 -1
- package/dist/options.js +1 -1
- package/dist/runtime.js +598 -463
- package/dist/runtimeObf.js +26 -6
- package/dist/transforms/controlFlowFlattening.js +22 -0
- package/dist/transforms/resolveContants.js +33 -0
- package/dist/transforms/resolveLabels.js +59 -0
- package/dist/transforms/selfModifying.js +107 -0
- package/dist/types.js +13 -0
- package/index.ts +0 -6
- package/jest.config.js +10 -0
- package/package.json +1 -1
- package/src/compiler.ts +876 -487
- package/src/index.ts +2 -1
- package/src/options.ts +2 -0
- package/src/runtime.ts +589 -455
- package/src/runtimeObf.ts +22 -8
- package/src/transforms/controlFlowFlattening.ts +30 -0
- package/src/transforms/resolveContants.ts +42 -0
- package/src/transforms/resolveLabels.ts +83 -0
- package/src/transforms/selfModifying.ts +124 -0
- package/src/types.ts +24 -0
- package/dist/minify_empty_externs.js +0 -4
package/dist/compiler.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const
|
|
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
|
|
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
|
-
|
|
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 = []; //
|
|
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 =
|
|
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
|
|
357
|
-
ctx.bc
|
|
358
|
-
ctx.bc
|
|
359
|
-
ctx.bc
|
|
360
|
-
|
|
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
|
|
363
|
-
|
|
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
|
|
371
|
-
ctx.bc
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
457
|
+
this.emit(bc, [this.OP.RETURN], null); // end program
|
|
416
458
|
|
|
417
|
-
//
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
510
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
485
511
|
} else {
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
527
561
|
} else {
|
|
528
|
-
|
|
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
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
//
|
|
558
|
-
bc[
|
|
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 --
|
|
561
|
-
bc[
|
|
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
|
-
|
|
573
|
-
|
|
622
|
+
breakLabel: exitLabel,
|
|
623
|
+
continueLabel: loopTopLabel // continue re-evaluates the test
|
|
574
624
|
});
|
|
575
|
-
|
|
576
|
-
|
|
625
|
+
this.emit(bc, [null, {
|
|
626
|
+
type: "defineLabel",
|
|
627
|
+
label: loopTopLabel
|
|
628
|
+
}], node);
|
|
577
629
|
this._compileExpr(node.test, scope, bc);
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
602
|
-
|
|
659
|
+
breakLabel: exitLabel,
|
|
660
|
+
continueLabel: continueLabel // continue falls to the test
|
|
603
661
|
});
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
613
|
-
|
|
672
|
+
this.emit(bc, [null, {
|
|
673
|
+
type: "defineLabel",
|
|
674
|
+
label: continueLabel
|
|
675
|
+
}], node);
|
|
614
676
|
this._compileExpr(node.test, scope, bc);
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
bc[
|
|
620
|
-
|
|
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
|
-
|
|
632
|
-
|
|
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
|
-
|
|
711
|
+
this.emit(bc, [this.OP.POP], node);
|
|
641
712
|
}
|
|
642
713
|
}
|
|
643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
|
|
732
|
+
this.emit(bc, [null, {
|
|
733
|
+
type: "defineLabel",
|
|
734
|
+
label: updateLabel
|
|
735
|
+
}], node);
|
|
658
736
|
this._compileExpr(node.update, scope, bc);
|
|
659
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
674
|
-
|
|
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
|
-
|
|
758
|
+
_bTargetIdx = _bi;
|
|
681
759
|
break;
|
|
682
760
|
}
|
|
683
761
|
}
|
|
684
|
-
if (
|
|
685
|
-
this._loopStack[_bFound].breakJumps.push(_bJumpIdx);
|
|
762
|
+
if (_bTargetIdx === -1) throw new Error(`Label '${node.label.name}' not found`);
|
|
686
763
|
} else {
|
|
687
|
-
|
|
688
|
-
this._loopStack
|
|
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
|
-
|
|
695
|
-
|
|
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
|
-
|
|
793
|
+
_cTargetIdx = _ci;
|
|
702
794
|
break;
|
|
703
795
|
}
|
|
704
796
|
}
|
|
705
|
-
if (
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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 (
|
|
718
|
-
|
|
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
|
-
|
|
730
|
-
|
|
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
|
-
//
|
|
740
|
-
const
|
|
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
|
|
743
|
-
|
|
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
|
-
|
|
746
|
-
|
|
845
|
+
const nextCheckLabel = this._makeLabel("sw_next");
|
|
846
|
+
this.emit(bc, [this.OP.DUP], node);
|
|
747
847
|
this._compileExpr(cas.test, scope, bc);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
//
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
|
|
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
|
-
|
|
817
|
-
|
|
908
|
+
breakLabel: blockBreakLabel,
|
|
909
|
+
continueLabel: blockBreakLabel // unused
|
|
818
910
|
});
|
|
819
911
|
this._compileStatement(_lBody, scope, bc);
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
843
|
-
emitStoreIter = () =>
|
|
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 =
|
|
847
|
-
emitLoadIter = () =>
|
|
848
|
-
emitStoreIter = () =>
|
|
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
|
-
|
|
855
|
-
|
|
950
|
+
breakLabel: exitLabel,
|
|
951
|
+
continueLabel: loopTopLabel // continue re-checks the iterator
|
|
856
952
|
});
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
863
|
-
|
|
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
|
-
|
|
972
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
873
973
|
} else {
|
|
874
|
-
|
|
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
|
-
|
|
979
|
+
this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
|
|
880
980
|
} else if (res.kind === "upvalue") {
|
|
881
|
-
|
|
981
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
882
982
|
} else {
|
|
883
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1118
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
940
1119
|
} else if (res.kind === "upvalue") {
|
|
941
|
-
|
|
1120
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
942
1121
|
} else {
|
|
943
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1153
|
+
const elseLabel = this._makeLabel("ternary_else");
|
|
1154
|
+
const endLabel = this._makeLabel("ternary_end");
|
|
976
1155
|
this._compileExpr(node.test, scope, bc);
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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[
|
|
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
|
-
|
|
999
|
-
|
|
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[
|
|
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
|
-
|
|
1005
|
-
|
|
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[
|
|
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
|
-
|
|
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 =
|
|
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")
|
|
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")
|
|
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)
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1310
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)], node);
|
|
1110
1311
|
}
|
|
1111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1327
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
1127
1328
|
} else if (res.kind === "upvalue") {
|
|
1128
|
-
|
|
1329
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1129
1330
|
} else {
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
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
|
-
|
|
1145
|
-
|
|
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 =
|
|
1148
|
-
|
|
1149
|
-
|
|
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 =
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
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
|
-
|
|
1398
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(arg.property.name)], node);
|
|
1200
1399
|
}
|
|
1201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1412
|
+
this.emit(bc, [this.OP.UNARY_NEG], node);
|
|
1214
1413
|
break;
|
|
1215
1414
|
case "+":
|
|
1216
|
-
|
|
1415
|
+
this.emit(bc, [this.OP.UNARY_POS], node);
|
|
1217
1416
|
break;
|
|
1218
1417
|
case "!":
|
|
1219
|
-
|
|
1418
|
+
this.emit(bc, [this.OP.UNARY_NOT], node);
|
|
1220
1419
|
break;
|
|
1221
1420
|
case "~":
|
|
1222
|
-
|
|
1421
|
+
this.emit(bc, [this.OP.UNARY_BITNOT], node);
|
|
1223
1422
|
break;
|
|
1224
1423
|
case "typeof":
|
|
1225
|
-
|
|
1424
|
+
this.emit(bc, [this.OP.TYPEOF], node);
|
|
1226
1425
|
break;
|
|
1227
1426
|
case "void":
|
|
1228
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1486
|
+
this.emit(bc, [this.OP.BUILD_ARRAY, node.elements.length], node);
|
|
1284
1487
|
break;
|
|
1285
1488
|
}
|
|
1286
1489
|
case "ObjectExpression":
|
|
1287
1490
|
{
|
|
1288
|
-
//
|
|
1289
|
-
|
|
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
|
-
|
|
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;
|
|
1517
|
+
keyStr = key.name;
|
|
1299
1518
|
} else if (key.type === "StringLiteral" || key.type === "NumericLiteral") {
|
|
1300
|
-
keyStr = String(key.value);
|
|
1519
|
+
keyStr = String(key.value);
|
|
1301
1520
|
} else {
|
|
1302
1521
|
throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1303
1522
|
}
|
|
1304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
const [op,
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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:
|
|
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
|
|
1430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1446
|
-
|
|
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
|
-
|
|
1464
|
-
return b64;
|
|
1712
|
+
return Buffer.from(buf).toString("base64");
|
|
1465
1713
|
}
|
|
1466
|
-
serialize(bytecode,
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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
|
-
|
|
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.
|
|
1726
|
+
sections.push(`var BYTECODE = "${this.__serializeBytecode(bytecodeResult.bytecode, constants)}";`);
|
|
1481
1727
|
} else {
|
|
1482
|
-
sections.push(`var BYTECODE = [
|
|
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
|
-
|
|
1500
|
-
|
|
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
|