js-confuser-vm 0.0.9 → 0.1.1
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/.gitmodules +4 -0
- package/CHANGELOG.md +125 -2
- package/README.md +128 -53
- package/bench.ts +146 -0
- package/disassemble.ts +12 -0
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +328 -181
- package/dist/disassembler.js +317 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +255 -176
- package/dist/template.js +258 -0
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
- package/dist/transforms/bytecode/dispatcher.js +266 -0
- package/dist/transforms/bytecode/macroOpcodes.js +3 -3
- package/dist/transforms/bytecode/resolveConstants.js +100 -0
- package/dist/transforms/bytecode/resolveLabels.js +21 -18
- package/dist/transforms/bytecode/resolveRegisters.js +216 -0
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +22 -12
- package/dist/transforms/bytecode/stringConcealing.js +110 -0
- package/dist/transforms/runtime/classObfuscation.js +43 -0
- package/dist/transforms/runtime/handlerTable.js +91 -0
- package/dist/transforms/runtime/semanticOpcodes.js +35 -0
- package/dist/transforms/runtime/specializedOpcodes.js +11 -5
- package/dist/types.js +42 -1
- package/dist/utils/ast-utils.js +14 -0
- package/dist/utils/op-utils.js +1 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/index.ts +22 -16
- package/jest.config.js +19 -2
- package/output.disassembled.js +41 -0
- package/package.json +2 -1
- package/src/build-runtime.ts +113 -78
- package/src/compiler.ts +2703 -2482
- package/src/disassembler.ts +329 -0
- package/src/index.ts +12 -2
- package/src/options.ts +8 -1
- package/src/runtime.ts +294 -180
- package/src/template.ts +265 -0
- package/src/transforms/bytecode/aliasedOpcodes.ts +5 -2
- package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
- package/src/transforms/bytecode/dispatcher.ts +292 -0
- package/src/transforms/bytecode/macroOpcodes.ts +4 -4
- package/src/transforms/bytecode/resolveLabels.ts +31 -27
- package/src/transforms/bytecode/resolveRegisters.ts +226 -0
- package/src/transforms/bytecode/specializedOpcodes.ts +27 -20
- package/src/transforms/bytecode/stringConcealing.ts +130 -0
- package/src/transforms/runtime/classObfuscation.ts +59 -0
- package/src/transforms/runtime/specializedOpcodes.ts +14 -9
- package/src/types.ts +106 -5
- package/src/utils/ast-utils.ts +19 -0
- package/src/utils/op-utils.ts +2 -2
- package/src/utils/pass-utils.ts +126 -0
- package/src/utils/profile-utils.ts +3 -0
- package/tsconfig.json +1 -1
- package/dist/transforms/utils/op-utils.js +0 -25
- package/dist/transforms/utils/random-utils.js +0 -27
- package/dist/utilts.js +0 -3
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
package/dist/compiler.js
CHANGED
|
@@ -5,20 +5,24 @@ import traverseImport from "@babel/traverse";
|
|
|
5
5
|
import { generate } from "@babel/generator";
|
|
6
6
|
import { stripTypeScriptTypes } from "module";
|
|
7
7
|
import { ok } from "assert";
|
|
8
|
-
import {
|
|
8
|
+
import { buildRuntime } from "./build-runtime.js";
|
|
9
9
|
import { DEFAULT_OPTIONS } from "./options.js";
|
|
10
10
|
import { resolveLabels } from "./transforms/bytecode/resolveLabels.js";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.js";
|
|
12
|
+
import { resolveConstants } from "./transforms/bytecode/resolveConstants.js";
|
|
12
13
|
import { selfModifying } from "./transforms/bytecode/selfModifying.js";
|
|
13
14
|
import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
|
|
14
|
-
import { microOpcodes } from "./transforms/bytecode/microOpcodes.js";
|
|
15
15
|
import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
|
|
16
16
|
import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.js";
|
|
17
17
|
import { getRandomInt } from "./utils/random-utils.js";
|
|
18
18
|
import { U16_MAX } from "./utils/op-utils.js";
|
|
19
19
|
import { concealConstants } from "./transforms/bytecode/concealConstants.js";
|
|
20
|
+
import { dispatcher } from "./transforms/bytecode/dispatcher.js";
|
|
21
|
+
import { controlFlowFlattening } from "./transforms/bytecode/controlFlowFlattening.js";
|
|
22
|
+
import { stringConcealing } from "./transforms/bytecode/stringConcealing.js";
|
|
23
|
+
import { now } from "./utils/profile-utils.js";
|
|
20
24
|
const traverse = traverseImport.default || traverseImport;
|
|
21
|
-
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 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 // Each slot is a u16 stored as 2 little-endian bytes.\n var r = new Uint16Array(b.length / 2);\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\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.regs[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.regs[this._slot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._frame.regs[this._slot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._frame.regs[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, retDstReg) {\n this.closure = closure;\n this.regs = new Array(closure.fn.regCount).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._retDstReg = retDstReg !== undefined ? retDstReg : 0; // register in parent to write return value\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, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc, // <- where main begins\n };\n this._currentFrame = new Frame(new Closure(mainFn), null, null, undefined, 0);\n this._internals = {};\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\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\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(v, \"base64\")\n : Uint8Array.from(atob(v), function (c) {\n return c.charCodeAt(0);\n });\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\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 lastTime = 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 pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n frame.regs.fill(undefined);\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n frame.regs[dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n frame.regs[dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n frame.regs[dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n frame.regs[dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n frame.regs[dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n frame.regs[dst] = frame.regs[this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // nameIdx and key are consumed inline so the concealConstants runtime\n // transform can rewrite this._constant() consistently.\n this.globals[this._constant()] = frame.regs[this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(frame.regs[this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n frame.regs[dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var val = frame.regs[this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure,\n // correctly simulating sloppy-mode assignment from a strict-mode host.\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n frame.regs[dst] = delete obj[key];\n break;\n }\n\n // \u2500\u2500 Arithmetic (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.ADD: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a + frame.regs[this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a - frame.regs[this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a * frame.regs[this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a / frame.regs[this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a % frame.regs[this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a & frame.regs[this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a | frame.regs[this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a ^ frame.regs[this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a << frame.regs[this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >> frame.regs[this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >>> frame.regs[this._operand()];\n break;\n }\n\n // \u2500\u2500 Comparison (dst, src1, src2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.LT: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a < frame.regs[this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a > frame.regs[this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a <= frame.regs[this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a >= frame.regs[this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a === frame.regs[this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a !== frame.regs[this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a == frame.regs[this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a != frame.regs[this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = frame.regs[this._operand()];\n frame.regs[dst] = a in frame.regs[this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\n var ctor = frame.regs[this._operand()];\n if (typeof ctor === \"function\") {\n frame.regs[dst] = obj instanceof ctor;\n } else {\n // VM Closure - walk prototype chain for identity with ctor.prototype.\n var proto = ctor.prototype;\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 frame.regs[dst] = result;\n }\n break;\n }\n\n // \u2500\u2500 Unary (dst, src) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.UNARY_NEG: {\n var dst = this._operand();\n frame.regs[dst] = -frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n frame.regs[dst] = +frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n frame.regs[dst] = !frame.regs[this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n frame.regs[dst] = ~frame.regs[this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n frame.regs[dst] = typeof frame.regs[this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consume src \u2014 evaluated for side-effects by compiler\n frame.regs[dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // dst, nameConstIdx \u2014 safe typeof for potentially-undeclared globals.\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n frame.regs[dst] = typeof val;\n break;\n }\n\n // \u2500\u2500 Control flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!frame.regs[src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (frame.regs[src]) frame._pc = target;\n break;\n }\n\n // \u2500\u2500 Calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, this.globals, dst);\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n frame.regs[dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = frame.regs[this._operand()];\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, receiver, dst);\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n frame.regs[dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = frame.regs[this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = frame.regs[this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var c = callee[CLOSURE_SYM];\n var newObj = Object.create(c.prototype || null);\n var f = new Frame(c, frame._pc, frame, newObj, dst);\n f._newObj = newObj;\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n frame.regs[dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = frame.regs[this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n\n if (this._frameStack.length === 0) return retVal; // main script returning\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 var parentFrame = this._frameStack.pop();\n parentFrame.regs[frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw frame.regs[this._operand()];\n\n // \u2500\u2500 Closures \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\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 closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\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(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n );\n for (var i = 0; i < args.length; i++) f.regs[i] = args[i];\n f.regs[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 frame.regs[dst] = shell;\n break;\n }\n\n // \u2500\u2500 Collections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = frame.regs[this._operand()];\n frame.regs[dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = frame.regs[this._operand()];\n var val = frame.regs[this._operand()];\n o[key] = val;\n }\n frame.regs[dst] = o;\n break;\n }\n\n // \u2500\u2500 Property definitions (getters / setters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var getterFn = frame.regs[this._operand()];\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 // obj, key, fn\n var obj = frame.regs[this._operand()];\n var key = frame.regs[this._operand()];\n var setterFn = frame.regs[this._operand()];\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 // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = frame.regs[this._operand()];\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 frame.regs[dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = frame.regs[this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n frame.regs[dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\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 // \u2500\u2500 Self-modifying bytecode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\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 // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n handledFrame.regs[h.exceptionReg] = err;\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 for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
|
|
25
|
+
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst MAIN_REG_COUNT = 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 base64ToBytes(s) {\n return typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n}\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b = base64ToBytes(s);\n // Each slot is a u32 stored as 4 little-endian bytes.\n var r = new Uint32Array(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 0;\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 \u2014 Lua/CPython style.\n// While the outer frame is alive: reads/writes go to vm._regs[_absSlot].\n// After the outer frame returns (closed): reads/writes hit this._value.\n// _absSlot is the absolute index in VM._regs (frame._base + local slot).\nfunction Upvalue(regs, absSlot) {\n this._regs = regs; // shared reference to VM._regs flat array\n this._absSlot = absSlot; // absolute index; stable as long as frame is alive\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._regs[this._absSlot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._regs[this._absSlot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._regs[this._absSlot];\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\n// Frame \u2014 analogous to Lua CallInfo / CPython PyFrameObject.\n// Does NOT own a register array; registers live in VM._regs[_base .. _base+regCount).\nfunction Frame(closure, returnPc, parent, thisVal, retDstReg, base) {\n this.closure = closure;\n this._base = base; // absolute offset into VM._regs for this frame's r0\n this._pc = closure.fn.startPc;\n this._returnPc = returnPc;\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._retDstReg = retDstReg !== undefined ? retDstReg : 0;\n this._newObj = null;\n this._handlerStack = [];\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, mainRegCount, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._frameStack = [];\n this._openUpvalues = [];\n\n // \u2500\u2500 Flat register file (Lua-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // All frames share a single array. Each Frame records its _base offset.\n // _regsTop is the next free slot (= base of the hypothetical next frame).\n // On CALL: newBase = _regsTop; _regsTop += fn.regCount\n // On RETURN: _regsTop = frame._base (pop the frame's register window)\n this._regs = new Array(mainRegCount).fill(undefined);\n this._regsTop = mainRegCount; // main frame occupies [0, mainRegCount)\n\n var mainFn = {\n paramCount: 0,\n regCount: mainRegCount,\n startPc: mainStartPc,\n };\n this._currentFrame = new Frame(\n new Closure(mainFn),\n null,\n null,\n undefined,\n 0,\n 0,\n );\n}\n\n// Consume the next slot from the flat bytecode stream and advance the PC.\n// Called by opcode handlers to read each of their operands in order.\nVM.prototype._operand = function () {\n return this.bytecode[this._currentFrame._pc++];\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Dedup by absolute slot \u2014 two closures capturing the same local share one Upvalue.\n var absSlot = frame._base + slot;\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (!uv._closed && uv._absSlot === absSlot) return uv;\n }\n var uv = new Upvalue(this._regs, absSlot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\n// Reads and decodes a constant from the pool.\n// idx \u2014 pool index (first operand of the constant pair emitted by resolveConstants).\n// key \u2014 conceal key (second operand). 0 means no concealment.\n//\n// For integers: stored value is (original ^ key); XOR again to recover.\n// For strings: stored value is a base64 string containing u16 LE byte pairs.\n// Mirrors decodeBytecode: base64 \u2192 bytes \u2192 u16 LE \u2192 XOR with\n// (key + i) & 0xFFFF to recover the original char codes.\n// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time.\nVM.prototype._constant = function (idxIn, keyIn) {\n var idx = idxIn ?? this._operand();\n var key = keyIn ?? this._operand();\n\n var v = this.constants[idx];\n if (!key) return v;\n if (typeof v === \"number\") return v ^ key;\n // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).\n var b = base64ToBytes(v);\n var out = \"\";\n for (var i = 0; i < b.length / 2; i++) {\n var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE\n out += String.fromCharCode(code ^ ((key + i) & 0xffff));\n }\n return out;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN \u2014 close every upvalue whose absolute slot falls within\n // this frame's register window [_base, _base + regCount).\n var lo = frame._base;\n var hi = frame._base + frame.closure.fn.regCount;\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (!uv._closed && uv._absSlot >= lo && uv._absSlot < hi) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype._ensureRegisterWindow = function (base, regCount) {\n var end = base + regCount;\n while (this._regs.length < end) this._regs.push(undefined);\n for (var i = base; i < end; i++) this._regs[i] = undefined;\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var lastTime = 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 pc = frame._pc++;\n var op = this.bytecode[pc];\n var opcode = this.bytecode[pc];\n // console.log(\n // \"pc=\" + pc,\n // \"opcode=\" + opcode,\n // Object.keys(OP).find((key) => OP[key] === opcode),\n // );\n\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\n if (TIMING_CHECKS) {\n var currentTime = now();\n var isTamper = currentTime - lastTime > 1000;\n lastTime = currentTime;\n if (isTamper) {\n // Poison the bytecode\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\n // Break the current state\n for (var i2 = frame._base; i2 < this._regsTop; i2++)\n this._regs[i2] = undefined;\n op = OP.JUMP;\n frame._pc = this.bytecode.length; // jump past end to halt\n }\n }\n\n try {\n var regs = this._regs;\n var base = frame._base;\n\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST: {\n var dst = this._operand();\n regs[base + dst] = this._constant();\n break;\n }\n\n case OP.LOAD_INT: {\n var dst = this._operand();\n regs[base + dst] = this._operand();\n break;\n }\n\n case OP.LOAD_GLOBAL: {\n var dst = this._operand();\n var globalName = this._constant();\n\n if (!(globalName in this.globals)) {\n throw new ReferenceError(`${globalName} is not defined`);\n }\n\n regs[base + dst] = this.globals[globalName];\n break;\n }\n\n case OP.LOAD_UPVALUE: {\n var dst = this._operand();\n regs[base + dst] = frame.closure.upvalues[this._operand()]._read();\n break;\n }\n\n case OP.LOAD_THIS: {\n var dst = this._operand();\n regs[base + dst] = frame.thisVal;\n break;\n }\n\n case OP.MOVE: {\n var dst = this._operand();\n regs[base + dst] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_GLOBAL: {\n // globals[globalName] = regs[src]\n this.globals[this._constant()] = regs[base + this._operand()];\n break;\n }\n\n case OP.STORE_UPVALUE: {\n var uvIdx = this._operand();\n frame.closure.upvalues[uvIdx]._write(regs[base + this._operand()]);\n break;\n }\n\n case OP.GET_PROP: {\n // dst = regs[obj][regs[key]]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = obj[key];\n break;\n }\n\n case OP.SET_PROP: {\n // regs[obj][regs[key]] = regs[val]\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)\n Reflect.set(obj, key, val);\n break;\n }\n\n case OP.DELETE_PROP: {\n // regs[dst] = delete regs[obj][regs[key]]\n // The delete operator returns true if successful which is most cases\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n regs[base + dst] = delete obj[key];\n break;\n }\n\n // Arithmetic (dst, src1, src2)\n case OP.ADD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a + regs[base + this._operand()];\n break;\n }\n case OP.SUB: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a - regs[base + this._operand()];\n break;\n }\n case OP.MUL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a * regs[base + this._operand()];\n break;\n }\n case OP.DIV: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a / regs[base + this._operand()];\n break;\n }\n case OP.MOD: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a % regs[base + this._operand()];\n break;\n }\n case OP.BAND: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a & regs[base + this._operand()];\n break;\n }\n case OP.BOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a | regs[base + this._operand()];\n break;\n }\n case OP.BXOR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a ^ regs[base + this._operand()];\n break;\n }\n case OP.SHL: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a << regs[base + this._operand()];\n break;\n }\n case OP.SHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >> regs[base + this._operand()];\n break;\n }\n case OP.USHR: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >>> regs[base + this._operand()];\n break;\n }\n\n // Comparison (dst, src1, src2)\n case OP.LT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a < regs[base + this._operand()];\n break;\n }\n case OP.GT: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a > regs[base + this._operand()];\n break;\n }\n case OP.LTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a <= regs[base + this._operand()];\n break;\n }\n case OP.GTE: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a >= regs[base + this._operand()];\n break;\n }\n case OP.EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a === regs[base + this._operand()];\n break;\n }\n case OP.NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a !== regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_EQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a == regs[base + this._operand()];\n break;\n }\n case OP.LOOSE_NEQ: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a != regs[base + this._operand()];\n break;\n }\n case OP.IN: {\n var dst = this._operand();\n var a = regs[base + this._operand()];\n regs[base + dst] = a in regs[base + this._operand()];\n break;\n }\n case OP.INSTANCEOF: {\n // regs[dst] = regs[obj] instanceof regs[ctor]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\n var ctor = regs[base + this._operand()];\n if (typeof ctor === \"function\") {\n regs[base + dst] = obj instanceof ctor;\n } else {\n // TODO: Why is this needed?\n var proto = ctor.prototype;\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 regs[base + dst] = result;\n }\n break;\n }\n\n // Unary (dst, src)\n case OP.UNARY_NEG: {\n var dst = this._operand();\n regs[base + dst] = -regs[base + this._operand()];\n break;\n }\n case OP.UNARY_POS: {\n var dst = this._operand();\n regs[base + dst] = +regs[base + this._operand()];\n break;\n }\n case OP.UNARY_NOT: {\n var dst = this._operand();\n regs[base + dst] = !regs[base + this._operand()];\n break;\n }\n case OP.UNARY_BITNOT: {\n var dst = this._operand();\n regs[base + dst] = ~regs[base + this._operand()];\n break;\n }\n case OP.TYPEOF: {\n var dst = this._operand();\n regs[base + dst] = typeof regs[base + this._operand()];\n break;\n }\n case OP.VOID: {\n var dst = this._operand();\n this._operand(); // consumes argument (intended)\n regs[base + dst] = undefined;\n break;\n }\n case OP.TYPEOF_SAFE: {\n // regs[dst] = typeof window[name]\n // Never throws ReferenceError, instead returns undefined for undeclared variables\n var dst = this._operand();\n var name = this._constant();\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n regs[base + dst] = typeof val;\n break;\n }\n\n // Control flow\n case OP.JUMP:\n frame._pc = this._operand();\n break;\n\n case OP.JUMP_IF_FALSE: {\n var src = this._operand();\n var target = this._operand();\n if (!regs[base + src]) frame._pc = target;\n break;\n }\n\n case OP.JUMP_IF_TRUE: {\n // || short-circuit: if truthy, jump over RHS.\n var src = this._operand();\n var target = this._operand();\n if (regs[base + src]) frame._pc = target;\n break;\n }\n\n // Calls\n case OP.CALL: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n this.globals,\n dst,\n newBase,\n );\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(null, args);\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n // dst, receiverReg, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var receiver = regs[base + this._operand()];\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(\n closure,\n frame._pc,\n frame,\n receiver,\n dst,\n newBase,\n );\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n regs[base + dst] = callee.apply(receiver, args);\n }\n break;\n }\n\n case OP.NEW: {\n // dst, calleeReg, argc, [argReg...]\n var dst = this._operand();\n var callee = regs[base + this._operand()];\n var argc = this._operand();\n var args = new Array(argc);\n for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()];\n\n if (callee && callee[CLOSURE_SYM]) {\n var closure = callee[CLOSURE_SYM];\n var newObj = Object.create(closure.prototype || null);\n var newBase = this._regsTop;\n this._ensureRegisterWindow(newBase, closure.fn.regCount);\n this._regsTop = newBase + closure.fn.regCount;\n var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);\n if (closure.fn.hasRest) {\n var restSlot = closure.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n this._regs[newBase + i] = i < args.length ? args[i] : undefined;\n this._regs[newBase + restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < closure.fn.regCount; i++)\n this._regs[newBase + i] = args[i];\n }\n if (closure.fn.paramCount < closure.fn.regCount) {\n this._regs[newBase + closure.fn.paramCount] = args;\n }\n f._newObj = newObj;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n regs[base + dst] = Reflect.construct(callee, args);\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = regs[base + this._operand()];\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n\n // Zero out callee's register window to limit exposing runtime values\n var hi = frame._base + frame.closure.fn.regCount;\n for (var i = frame._base ; i < hi; i++)\n this._regs[i] = undefined;\n this._regsTop = frame._base;\n\n if (this._frameStack.length === 0) return retVal; // main script returning\n\n // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n var parentFrame = this._frameStack.pop();\n this._regs[parentFrame._base + frame._retDstReg] = retVal;\n this._currentFrame = parentFrame;\n break;\n }\n\n case OP.THROW:\n throw regs[base + this._operand()];\n\n // Closures\n case OP.MAKE_CLOSURE: {\n // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]\n var dst = this._operand();\n var startPc = this._operand();\n var paramCount = this._operand();\n var regCount = this._operand();\n var uvCount = this._operand();\n var hasRest = this._operand(); // 1 if last param is a rest element\n\n var uvDescs = new Array(uvCount);\n for (var i = 0; i < uvCount; i++) {\n var isLocalRaw = this._operand();\n var uvIndex = this._operand();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n regCount: regCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n hasRest: hasRest,\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 closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\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(\n self.bytecode,\n 0,\n c.fn.regCount,\n self.constants,\n self.globals,\n );\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n 0,\n 0,\n );\n sub._currentFrame = f;\n if (c.fn.hasRest) {\n var restSlot = c.fn.paramCount - 1;\n for (var i = 0; i < restSlot; i++)\n sub._regs[i] = i < args.length ? args[i] : undefined;\n sub._regs[restSlot] = args.slice(restSlot);\n } else {\n for (var i = 0; i < args.length && i < c.fn.regCount; i++)\n sub._regs[i] = args[i];\n }\n if (c.fn.paramCount < c.fn.regCount) {\n sub._regs[c.fn.paramCount] = args;\n }\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n regs[base + dst] = shell;\n break;\n }\n\n // Collections\n case OP.BUILD_ARRAY: {\n // dst, count, [elemReg...]\n var dst = this._operand();\n var count = this._operand();\n var elems = new Array(count);\n for (var i = 0; i < count; i++)\n elems[i] = regs[base + this._operand()];\n regs[base + dst] = elems;\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // dst, pairCount, [keyReg, valReg, ...]\n var dst = this._operand();\n var pairCount = this._operand();\n var o = {};\n for (var i = 0; i < pairCount; i++) {\n var key = regs[base + this._operand()];\n var val = regs[base + this._operand()];\n o[key] = val;\n }\n regs[base + dst] = o;\n break;\n }\n\n // Object methods (getters / setters)\n case OP.DEFINE_GETTER: {\n // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var getterFn = regs[base + this._operand()];\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 // obj, key, fn\n var obj = regs[base + this._operand()];\n var key = regs[base + this._operand()];\n var setterFn = regs[base + this._operand()];\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 // \u2500\u2500 For-in iteration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.FOR_IN_SETUP: {\n // dst, src \u2014 build iterator object from enumerable keys of regs[src]\n var dst = this._operand();\n var obj = regs[base + this._operand()];\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 regs[base + dst] = { _keys: keys, i: 0 };\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // dst, iterReg, exitTarget\n // Advances iterator; writes next key to dst, or jumps to exitTarget when done.\n var dst = this._operand();\n var iter = regs[base + this._operand()];\n var exitTarget = this._operand();\n if (iter.i >= iter._keys.length) {\n frame._pc = exitTarget;\n } else {\n regs[base + dst] = iter._keys[iter.i++];\n }\n break;\n }\n\n // \u2500\u2500 Exception handling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n case OP.TRY_SETUP: {\n // handlerPc, exceptionReg \u2014 push exception handler record onto current frame.\n frame._handlerStack.push({\n handlerPc: this._operand(),\n exceptionReg: this._operand(),\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 // Self-modifying bytecode\n case OP.PATCH: {\n // destPc, sliceStart, sliceEnd\n var destPc = this._operand();\n var sliceStart = this._operand();\n var sliceEnd = this._operand();\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\n }\n break;\n }\n\n case OP.JUMP_REG: {\n // Indirect jump: allows VM to jump based on runtime values.\n frame._pc = regs[base + this._operand()];\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\n // Walk from the current frame upward until we find a frame that has an open 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 this._regsTop = searchFrame._base;\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // if there's no handler, propagate back to host\n\n var h = handledFrame._handlerStack.pop();\n // Discard any call-frames that were pushed inside the try body.\n this._frameStack.length = h.frameStackDepth;\n // Write the caught exception directly into the designated register.\n this._regs[handledFrame._base + h.exceptionReg] = err;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._regsTop = handledFrame._base + handledFrame.closure.fn.regCount;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n/* @BOOT */ // <- This comment can't be removed!\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 for (var k of Object.getOwnPropertyNames(window)) {\n globals[k] = window[k];\n }\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(\n decodeBytecode(BYTECODE),\n MAIN_START_PC,\n MAIN_REG_COUNT,\n CONSTANTS,\n globals,\n);\nvm.run();\n";
|
|
22
26
|
export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
|
|
23
27
|
export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
|
|
24
28
|
|
|
@@ -149,21 +153,26 @@ export const OP_ORIGINAL = {
|
|
|
149
153
|
// destPc, sliceStart, sliceEnd
|
|
150
154
|
|
|
151
155
|
// ── Debug ─────────────────────────────────────────────────────────────────
|
|
152
|
-
DEBUGGER: 57
|
|
156
|
+
DEBUGGER: 57,
|
|
157
|
+
// ── Indirect jump (register-addressed) ───────────────────────────────────
|
|
158
|
+
// Used by Dispatcher pass. The target PC is read from a register
|
|
159
|
+
// rather than encoded as a bytecode immediate, so static analysis cannot
|
|
160
|
+
// determine the destination without tracking register values at runtime.
|
|
161
|
+
JUMP_REG: 58 // src — frame._pc = regs[src]
|
|
153
162
|
};
|
|
154
163
|
|
|
155
164
|
// ── Scope ─────────────────────────────────────────────────────────────────────
|
|
156
|
-
// Maps variable names to
|
|
157
|
-
// Locals are allocated at compile time; zero name lookups at runtime.
|
|
165
|
+
// Maps variable names to virtual RegisterOperands.
|
|
166
|
+
// Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
|
|
167
|
+
// resolveRegisters() assigns concrete slot indices before serialization.
|
|
158
168
|
class Scope {
|
|
159
169
|
constructor(parent = null) {
|
|
160
170
|
this.parent = parent;
|
|
161
171
|
this._locals = new Map();
|
|
162
|
-
this._next = 0;
|
|
163
172
|
}
|
|
164
|
-
define(name) {
|
|
173
|
+
define(name, ctx) {
|
|
165
174
|
if (!this._locals.has(name)) {
|
|
166
|
-
this._locals.set(name,
|
|
175
|
+
this._locals.set(name, ctx._newReg());
|
|
167
176
|
}
|
|
168
177
|
return this._locals.get(name);
|
|
169
178
|
}
|
|
@@ -171,7 +180,7 @@ class Scope {
|
|
|
171
180
|
if (this._locals.has(name)) {
|
|
172
181
|
return {
|
|
173
182
|
kind: "local",
|
|
174
|
-
|
|
183
|
+
reg: this._locals.get(name)
|
|
175
184
|
};
|
|
176
185
|
}
|
|
177
186
|
if (this.parent) return this.parent.resolve(name);
|
|
@@ -179,38 +188,68 @@ class Scope {
|
|
|
179
188
|
kind: "global"
|
|
180
189
|
};
|
|
181
190
|
}
|
|
182
|
-
get localCount() {
|
|
183
|
-
return this._next;
|
|
184
|
-
}
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
// ── FnContext ─────────────────────────────────────────────────────────────────
|
|
188
194
|
// Compiler-side state for the function currently being compiled.
|
|
189
195
|
// Distinct from the runtime Frame — this is compile-time only.
|
|
196
|
+
//
|
|
197
|
+
// Virtual-register model (Lua/LLVM style):
|
|
198
|
+
// Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
|
|
199
|
+
// unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
|
|
200
|
+
// liveness-aware slot assignment and sets desc.regCount at the end of the
|
|
201
|
+
// pipeline, just like resolveLabels() fills in jump targets.
|
|
190
202
|
class FnContext {
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
// index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
|
|
204
|
+
|
|
205
|
+
// Unique ID for this function — matches the index in compiler.fnDescriptors.
|
|
206
|
+
|
|
207
|
+
// Monotonically increasing counter; each call to _newReg() bumps it.
|
|
208
|
+
_nextId = 0;
|
|
209
|
+
constructor(compiler, parentCtx = null, fnId = 0) {
|
|
196
210
|
this.compiler = compiler;
|
|
197
211
|
this.parentCtx = parentCtx;
|
|
198
212
|
this.scope = new Scope();
|
|
199
213
|
this.bc = [];
|
|
200
214
|
this.upvalues = [];
|
|
215
|
+
this._fnId = fnId;
|
|
201
216
|
}
|
|
202
217
|
|
|
203
|
-
/**
|
|
218
|
+
/** Create a new virtual register owned by this function. */
|
|
219
|
+
_newReg() {
|
|
220
|
+
return b.registerOperand(this._nextId++, this._fnId);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Allocate a short-lived temporary register (pool "temp::").
|
|
225
|
+
* resolveRegisters() will reuse its concrete slot once its live range ends.
|
|
226
|
+
* Do NOT use for named locals or upvalue-captured variables — use _newReg()
|
|
227
|
+
* via scope.define() for those, so they stay in the stable "local::" pool.
|
|
228
|
+
*/
|
|
204
229
|
allocReg() {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
230
|
+
return b.registerOperand(this._nextId++, this._fnId, {
|
|
231
|
+
kind: "temp"
|
|
232
|
+
});
|
|
208
233
|
}
|
|
209
234
|
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
|
|
237
|
+
*
|
|
238
|
+
* NOTE: This is extraneous for any programmatically generated IR.
|
|
239
|
+
* resolveRegisters() already computes lastUse as the last instruction index
|
|
240
|
+
* where the register appears as a real operand — which is always the tightest
|
|
241
|
+
* correct bound when you stop emitting a register after its last logical use.
|
|
242
|
+
* freeReg is only needed in the rare case where a register has a late syntactic
|
|
243
|
+
* appearance that does NOT represent its true logical death (e.g. a dummy read
|
|
244
|
+
* emitted for side-effects long after the value is logically dead). No current
|
|
245
|
+
* pass in this codebase uses it; it is kept as an extension point only.
|
|
246
|
+
*/
|
|
247
|
+
freeReg(bc, reg) {
|
|
248
|
+
bc.push([null, b.freeRegOperand(reg)]);
|
|
213
249
|
}
|
|
250
|
+
|
|
251
|
+
/** No-op kept for call-site compatibility; liveness is handled by resolveRegisters. */
|
|
252
|
+
resetTemps() {}
|
|
214
253
|
addUpvalue(name, isLocal, index) {
|
|
215
254
|
const existing = this.upvalues.findIndex(u => u.name === name);
|
|
216
255
|
if (existing !== -1) return existing;
|
|
@@ -225,11 +264,20 @@ class FnContext {
|
|
|
225
264
|
}
|
|
226
265
|
// ── Compiler ──────────────────────────────────────────────────────────────────
|
|
227
266
|
export class Compiler {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
267
|
+
log(...messages) {
|
|
268
|
+
if (this.options.verbose) {
|
|
269
|
+
console.log(...messages);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
_cloneRegisterOperand(operand) {
|
|
273
|
+
if (!operand || typeof operand !== "object") return operand;
|
|
274
|
+
if (operand.type !== "register") return operand;
|
|
275
|
+
return JSON.parse(JSON.stringify(operand));
|
|
276
|
+
}
|
|
232
277
|
emit(bc, instr, node) {
|
|
278
|
+
for (let i = 1; i < instr.length; i++) {
|
|
279
|
+
instr[i] = this._cloneRegisterOperand(instr[i]);
|
|
280
|
+
}
|
|
233
281
|
bc.push(instr);
|
|
234
282
|
instr[SOURCE_NODE_SYM] = node;
|
|
235
283
|
}
|
|
@@ -249,10 +297,6 @@ export class Compiler {
|
|
|
249
297
|
this.MICRO_OPS = {};
|
|
250
298
|
this.SPECIALIZED_OPS = {};
|
|
251
299
|
this.ALIASED_OPS = {};
|
|
252
|
-
this._internals = {
|
|
253
|
-
globally: new Map(),
|
|
254
|
-
opcodes: new Map()
|
|
255
|
-
};
|
|
256
300
|
this.OP = {
|
|
257
301
|
...OP_ORIGINAL
|
|
258
302
|
};
|
|
@@ -280,7 +324,7 @@ export class Compiler {
|
|
|
280
324
|
if (ctx.scope._locals.has(name)) {
|
|
281
325
|
return {
|
|
282
326
|
kind: "local",
|
|
283
|
-
|
|
327
|
+
reg: ctx.scope._locals.get(name)
|
|
284
328
|
};
|
|
285
329
|
}
|
|
286
330
|
if (!ctx.parentCtx) return {
|
|
@@ -291,7 +335,7 @@ export class Compiler {
|
|
|
291
335
|
kind: "global"
|
|
292
336
|
};
|
|
293
337
|
const isLocal = parentResult.kind === "local";
|
|
294
|
-
const index = isLocal ? parentResult.
|
|
338
|
+
const index = isLocal ? parentResult.reg : parentResult.index;
|
|
295
339
|
const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
|
|
296
340
|
return {
|
|
297
341
|
kind: "upvalue",
|
|
@@ -300,31 +344,30 @@ export class Compiler {
|
|
|
300
344
|
}
|
|
301
345
|
|
|
302
346
|
// ── Variable hoisting ──────────────────────────────────────────────────────
|
|
303
|
-
// Pre-scan a statement list and reserve
|
|
304
|
-
// function declaration, for-in iterator, and try-catch binding.
|
|
305
|
-
// Must be called before
|
|
306
|
-
|
|
307
|
-
_hoistVars(stmts, scope) {
|
|
347
|
+
// Pre-scan a statement list and reserve virtual registers for every var
|
|
348
|
+
// declaration, function declaration, for-in iterator, and try-catch binding.
|
|
349
|
+
// Must be called before any emit so that locals are allocated before temps.
|
|
350
|
+
_hoistVars(stmts, scope, ctx) {
|
|
308
351
|
for (const stmt of stmts) {
|
|
309
352
|
switch (stmt.type) {
|
|
310
353
|
case "VariableDeclaration":
|
|
311
354
|
for (const decl of stmt.declarations) {
|
|
312
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
355
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
313
356
|
}
|
|
314
357
|
break;
|
|
315
358
|
case "FunctionDeclaration":
|
|
316
|
-
if (stmt.id) scope.define(stmt.id.name);
|
|
359
|
+
if (stmt.id) scope.define(stmt.id.name, ctx);
|
|
317
360
|
break;
|
|
318
361
|
case "BlockStatement":
|
|
319
|
-
this._hoistVars(stmt.body, scope);
|
|
362
|
+
this._hoistVars(stmt.body, scope, ctx);
|
|
320
363
|
break;
|
|
321
364
|
case "IfStatement":
|
|
322
365
|
{
|
|
323
366
|
const cons = stmt.consequent.type === "BlockStatement" ? stmt.consequent.body : [stmt.consequent];
|
|
324
|
-
this._hoistVars(cons, scope);
|
|
367
|
+
this._hoistVars(cons, scope, ctx);
|
|
325
368
|
if (stmt.alternate) {
|
|
326
369
|
const alt = stmt.alternate.type === "BlockStatement" ? stmt.alternate.body : [stmt.alternate];
|
|
327
|
-
this._hoistVars(alt, scope);
|
|
370
|
+
this._hoistVars(alt, scope, ctx);
|
|
328
371
|
}
|
|
329
372
|
break;
|
|
330
373
|
}
|
|
@@ -332,65 +375,73 @@ export class Compiler {
|
|
|
332
375
|
case "DoWhileStatement":
|
|
333
376
|
{
|
|
334
377
|
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
335
|
-
this._hoistVars(body, scope);
|
|
378
|
+
this._hoistVars(body, scope, ctx);
|
|
336
379
|
break;
|
|
337
380
|
}
|
|
338
381
|
case "ForStatement":
|
|
339
382
|
{
|
|
340
383
|
if (stmt.init?.type === "VariableDeclaration") {
|
|
341
384
|
for (const decl of stmt.init.declarations) {
|
|
342
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
385
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
343
386
|
}
|
|
344
387
|
}
|
|
345
388
|
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
346
|
-
this._hoistVars(body, scope);
|
|
389
|
+
this._hoistVars(body, scope, ctx);
|
|
347
390
|
break;
|
|
348
391
|
}
|
|
349
392
|
case "ForInStatement":
|
|
350
393
|
{
|
|
351
|
-
// Reserve a hidden register for the iterator object.
|
|
352
|
-
stmt._iterSlot =
|
|
394
|
+
// Reserve a hidden virtual register for the iterator object.
|
|
395
|
+
stmt._iterSlot = ctx._newReg();
|
|
353
396
|
if (stmt.left.type === "VariableDeclaration") {
|
|
354
397
|
for (const decl of stmt.left.declarations) {
|
|
355
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
398
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
356
399
|
}
|
|
357
400
|
}
|
|
358
401
|
const body = stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
359
|
-
this._hoistVars(body, scope);
|
|
402
|
+
this._hoistVars(body, scope, ctx);
|
|
360
403
|
break;
|
|
361
404
|
}
|
|
362
405
|
case "SwitchStatement":
|
|
363
|
-
for (const c of stmt.cases) this._hoistVars(c.consequent, scope);
|
|
406
|
+
for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
|
|
364
407
|
break;
|
|
365
408
|
case "TryStatement":
|
|
366
|
-
this._hoistVars(stmt.block.body, scope);
|
|
409
|
+
this._hoistVars(stmt.block.body, scope, ctx);
|
|
367
410
|
if (stmt.handler) {
|
|
368
411
|
if (stmt.handler.param?.type === "Identifier") {
|
|
369
412
|
// Catch parameter IS the exception register.
|
|
370
|
-
scope.define(stmt.handler.param.name);
|
|
413
|
+
scope.define(stmt.handler.param.name, ctx);
|
|
371
414
|
} else {
|
|
372
|
-
// No catch binding – reserve a dummy
|
|
373
|
-
stmt._exceptionSlot =
|
|
415
|
+
// No catch binding – reserve a dummy virtual register for the exception value.
|
|
416
|
+
stmt._exceptionSlot = ctx._newReg();
|
|
374
417
|
}
|
|
375
|
-
this._hoistVars(stmt.handler.body.body, scope);
|
|
418
|
+
this._hoistVars(stmt.handler.body.body, scope, ctx);
|
|
376
419
|
}
|
|
377
420
|
break;
|
|
378
421
|
case "LabeledStatement":
|
|
379
|
-
this._hoistVars([stmt.body], scope);
|
|
422
|
+
this._hoistVars([stmt.body], scope, ctx);
|
|
380
423
|
break;
|
|
381
424
|
}
|
|
382
425
|
}
|
|
383
426
|
}
|
|
427
|
+
profileData = {
|
|
428
|
+
transforms: {}
|
|
429
|
+
};
|
|
384
430
|
|
|
385
431
|
// ── Entry point ───────────────────────────────────────────────────────────
|
|
386
432
|
compile(source) {
|
|
433
|
+
let startedAt = now();
|
|
387
434
|
const ast = parse(source, {
|
|
388
|
-
sourceType: "script"
|
|
435
|
+
sourceType: "script",
|
|
436
|
+
allowReturnOutsideFunction: true
|
|
389
437
|
});
|
|
438
|
+
this.profileData.parseTime = now() - startedAt;
|
|
390
439
|
return this.compileAST(ast);
|
|
391
440
|
}
|
|
392
441
|
compileAST(ast) {
|
|
442
|
+
let startedAt = now();
|
|
393
443
|
this._compileMain(ast.program.body);
|
|
444
|
+
this.profileData.compileTime = now() - startedAt;
|
|
394
445
|
return this.bytecode;
|
|
395
446
|
}
|
|
396
447
|
|
|
@@ -402,28 +453,31 @@ export class Compiler {
|
|
|
402
453
|
const entryLabel = this._makeLabel(`fn_${fnIdx}`);
|
|
403
454
|
var desc = {};
|
|
404
455
|
this.fnDescriptors.push(desc);
|
|
405
|
-
const ctx = new FnContext(this, this._currentCtx);
|
|
456
|
+
const ctx = new FnContext(this, this._currentCtx, fnIdx);
|
|
406
457
|
const savedCtx = this._currentCtx;
|
|
407
458
|
this._currentCtx = ctx;
|
|
408
459
|
const savedLoopStack = this._loopStack;
|
|
409
460
|
this._loopStack = [];
|
|
410
461
|
|
|
411
|
-
// 1. Define parameters (occupy
|
|
462
|
+
// 1. Define parameters as virtual registers (occupy the first IDs in order).
|
|
463
|
+
let hasRest = false;
|
|
412
464
|
for (const param of node.params) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
465
|
+
if (param.type === "RestElement") {
|
|
466
|
+
ok(param.argument.type === "Identifier", "Rest element must be a simple identifier");
|
|
467
|
+
hasRest = true;
|
|
468
|
+
ctx.scope.define(param.argument.name, ctx);
|
|
469
|
+
} else {
|
|
470
|
+
let identifier = param.type === "AssignmentPattern" ? param.left : param;
|
|
471
|
+
ok(identifier.type === "Identifier", "Only simple identifiers allowed as parameters");
|
|
472
|
+
ctx.scope.define(identifier.name, ctx);
|
|
473
|
+
}
|
|
416
474
|
}
|
|
417
475
|
|
|
418
|
-
// 2. Reserve the `arguments`
|
|
419
|
-
ctx.scope.define("arguments");
|
|
476
|
+
// 2. Reserve the `arguments` virtual register (immediately after params).
|
|
477
|
+
ctx.scope.define("arguments", ctx);
|
|
420
478
|
|
|
421
|
-
// 3. Hoist all var declarations so
|
|
422
|
-
this._hoistVars(node.body.body, ctx.scope);
|
|
423
|
-
|
|
424
|
-
// 4. Temps now start above all locals.
|
|
425
|
-
ctx.regTop = ctx.scope.localCount;
|
|
426
|
-
ctx.maxRegTop = ctx.regTop;
|
|
479
|
+
// 3. Hoist all var declarations so locals are allocated before any temps.
|
|
480
|
+
this._hoistVars(node.body.body, ctx.scope, ctx);
|
|
427
481
|
|
|
428
482
|
// 5. Emit default-value guards.
|
|
429
483
|
for (const param of node.params) {
|
|
@@ -469,25 +523,32 @@ export class Compiler {
|
|
|
469
523
|
desc.bytecode = ctx.bc;
|
|
470
524
|
desc._fnIdx = fnIdx;
|
|
471
525
|
desc.paramCount = node.params.length;
|
|
472
|
-
desc.
|
|
526
|
+
desc.hasRest = hasRest;
|
|
527
|
+
// regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
|
|
473
528
|
desc.upvalues = ctx.upvalues.slice();
|
|
529
|
+
desc.ctx = ctx;
|
|
474
530
|
return desc;
|
|
475
531
|
}
|
|
476
532
|
|
|
477
533
|
// Emit MAKE_CLOSURE with all metadata as inline operands.
|
|
478
534
|
// Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
|
|
535
|
+
// regCount is emitted as a fnRegCount IR operand; resolveRegisters() fills it.
|
|
479
536
|
_emitMakeClosure(desc, node, bc) {
|
|
480
537
|
const ctx = this._currentCtx;
|
|
481
538
|
const dst = ctx.allocReg();
|
|
482
539
|
const uvOperands = [];
|
|
483
540
|
for (const uv of desc.upvalues) {
|
|
484
541
|
uvOperands.push(uv.isLocal ? 1 : 0);
|
|
485
|
-
uvOperands.push(uv.index);
|
|
542
|
+
uvOperands.push(uv.index); // RegisterOperand if isLocal, number if upvalue chain
|
|
486
543
|
}
|
|
487
544
|
this.emit(bc, [this.OP.MAKE_CLOSURE, dst, {
|
|
488
545
|
type: "label",
|
|
489
546
|
label: desc.entryLabel
|
|
490
|
-
}, desc.paramCount,
|
|
547
|
+
}, desc.paramCount, b.fnRegCountOperand(desc._fnIdx),
|
|
548
|
+
// resolved by resolveRegisters()
|
|
549
|
+
desc.upvalues.length, desc.hasRest ? 1 : 0,
|
|
550
|
+
// 1 = last param is a rest element
|
|
551
|
+
...uvOperands], node);
|
|
491
552
|
return dst;
|
|
492
553
|
}
|
|
493
554
|
|
|
@@ -501,7 +562,7 @@ export class Compiler {
|
|
|
501
562
|
async: false,
|
|
502
563
|
generator: false,
|
|
503
564
|
params: [],
|
|
504
|
-
id:
|
|
565
|
+
id: t.identifier("main"),
|
|
505
566
|
body: t.blockStatement([...body])
|
|
506
567
|
});
|
|
507
568
|
for (const descriptor of this.fnDescriptors) {
|
|
@@ -513,7 +574,8 @@ export class Compiler {
|
|
|
513
574
|
this.bytecode.push(instr);
|
|
514
575
|
}
|
|
515
576
|
}
|
|
516
|
-
|
|
577
|
+
|
|
578
|
+
// mainRegCount is set by resolveRegisters() after the pipeline runs.
|
|
517
579
|
this.mainFn = desc;
|
|
518
580
|
this._currentCtx = savedCtx;
|
|
519
581
|
}
|
|
@@ -592,14 +654,8 @@ export class Compiler {
|
|
|
592
654
|
this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
|
|
593
655
|
}
|
|
594
656
|
} else {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
// Actually: just emit LOAD_CONST directly into slot.
|
|
598
|
-
// Undo the allocReg – instead emit directly:
|
|
599
|
-
ctx.regTop--; // undo the allocReg above
|
|
600
|
-
const tmp = ctx.allocReg();
|
|
601
|
-
this.emit(bc, [this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)], node);
|
|
602
|
-
if (tmp !== slot) this.emit(bc, [this.OP.MOVE, slot, tmp], node);
|
|
657
|
+
// No initializer: var x; → load undefined directly into the local's register.
|
|
658
|
+
this.emit(bc, [this.OP.LOAD_CONST, slot, b.constantOperand(undefined)], node);
|
|
603
659
|
}
|
|
604
660
|
} else {
|
|
605
661
|
if (decl.init) {
|
|
@@ -617,14 +673,11 @@ export class Compiler {
|
|
|
617
673
|
case "IfStatement":
|
|
618
674
|
{
|
|
619
675
|
const elseOrEndLabel = this._makeLabel("if_else");
|
|
620
|
-
const savedTop = ctx.regTop;
|
|
621
676
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
622
677
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
623
678
|
type: "label",
|
|
624
679
|
label: elseOrEndLabel
|
|
625
680
|
}], node);
|
|
626
|
-
ctx.regTop = savedTop; // free test temps
|
|
627
|
-
|
|
628
681
|
const consequentBody = node.consequent.type === "BlockStatement" ? node.consequent.body : [node.consequent];
|
|
629
682
|
for (const stmt of consequentBody) {
|
|
630
683
|
this._compileStatement(stmt, scope, bc);
|
|
@@ -671,13 +724,11 @@ export class Compiler {
|
|
|
671
724
|
type: "defineLabel",
|
|
672
725
|
label: loopTopLabel
|
|
673
726
|
}], node);
|
|
674
|
-
const savedTop = ctx.regTop;
|
|
675
727
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
676
728
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
677
729
|
type: "label",
|
|
678
730
|
label: exitLabel
|
|
679
731
|
}], node);
|
|
680
|
-
ctx.regTop = savedTop;
|
|
681
732
|
const whileBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
682
733
|
for (const stmt of whileBody) {
|
|
683
734
|
this._compileStatement(stmt, scope, bc);
|
|
@@ -718,13 +769,11 @@ export class Compiler {
|
|
|
718
769
|
type: "defineLabel",
|
|
719
770
|
label: continueLabel
|
|
720
771
|
}], node);
|
|
721
|
-
const savedTop = ctx.regTop;
|
|
722
772
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
723
773
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
724
774
|
type: "label",
|
|
725
775
|
label: exitLabel
|
|
726
776
|
}], node);
|
|
727
|
-
ctx.regTop = savedTop;
|
|
728
777
|
this.emit(bc, [this.OP.JUMP, {
|
|
729
778
|
type: "label",
|
|
730
779
|
label: loopTopLabel
|
|
@@ -762,13 +811,11 @@ export class Compiler {
|
|
|
762
811
|
label: loopTopLabel
|
|
763
812
|
}], node);
|
|
764
813
|
if (node.test) {
|
|
765
|
-
const savedTop = ctx.regTop;
|
|
766
814
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
767
815
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
768
816
|
type: "label",
|
|
769
817
|
label: exitLabel
|
|
770
818
|
}], node);
|
|
771
|
-
ctx.regTop = savedTop;
|
|
772
819
|
}
|
|
773
820
|
const forBody = node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
774
821
|
for (const stmt of forBody) {
|
|
@@ -880,7 +927,6 @@ export class Compiler {
|
|
|
880
927
|
const cas = cases[i];
|
|
881
928
|
if (cas.test === null) continue;
|
|
882
929
|
const nextCheckLabel = this._makeLabel("sw_next");
|
|
883
|
-
const savedTop = ctx.regTop;
|
|
884
930
|
const caseValReg = this._compileExpr(cas.test, scope, bc);
|
|
885
931
|
const cmpReg = ctx.allocReg();
|
|
886
932
|
this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
|
|
@@ -888,7 +934,6 @@ export class Compiler {
|
|
|
888
934
|
type: "label",
|
|
889
935
|
label: nextCheckLabel
|
|
890
936
|
}], node);
|
|
891
|
-
ctx.regTop = savedTop;
|
|
892
937
|
this.emit(bc, [this.OP.JUMP, {
|
|
893
938
|
type: "label",
|
|
894
939
|
label: caseLabels[i]
|
|
@@ -992,7 +1037,7 @@ export class Compiler {
|
|
|
992
1037
|
} else if (node.left.type === "Identifier") {
|
|
993
1038
|
const res = this._resolve(node.left.name, this._currentCtx);
|
|
994
1039
|
if (res.kind === "local") {
|
|
995
|
-
if (keyReg !== res.
|
|
1040
|
+
if (keyReg !== res.reg) this.emit(bc, [this.OP.MOVE, res.reg, keyReg], node);
|
|
996
1041
|
} else if (res.kind === "upvalue") {
|
|
997
1042
|
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
|
|
998
1043
|
} else {
|
|
@@ -1076,12 +1121,38 @@ export class Compiler {
|
|
|
1076
1121
|
}
|
|
1077
1122
|
|
|
1078
1123
|
// ── Expressions ───────────────────────────────────────────────────────────
|
|
1079
|
-
// Returns the
|
|
1080
|
-
// For local variables: returns their
|
|
1081
|
-
// For all others: allocates a fresh
|
|
1124
|
+
// Returns the virtual RegisterOperand that holds the result.
|
|
1125
|
+
// For local variables: returns their RegisterOperand directly (no instruction emitted).
|
|
1126
|
+
// For all others: allocates a fresh virtual register, emits the instruction(s),
|
|
1082
1127
|
// and returns the allocated register.
|
|
1083
1128
|
_compileExpr(node, scope, bc) {
|
|
1084
1129
|
const ctx = this._currentCtx;
|
|
1130
|
+
|
|
1131
|
+
// Intrinsic for emitting raw bytecode, useful for emitting register address
|
|
1132
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "_VM_") {
|
|
1133
|
+
const argJSONStrng = node.arguments[0].value;
|
|
1134
|
+
console.log("Emitting raw bytecode from _VM_ call:", argJSONStrng);
|
|
1135
|
+
const arg = JSON.parse(argJSONStrng);
|
|
1136
|
+
console.log("Parsed bytecode:", arg);
|
|
1137
|
+
const dst = ctx.allocReg();
|
|
1138
|
+
let operand = arg[0];
|
|
1139
|
+
this.emit(bc, [this.OP.MOVE, dst, operand], node); // emit a breakpoint for easy inspection
|
|
1140
|
+
|
|
1141
|
+
return dst;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// _VM_JUMP_("labelName") — emits JUMP with a label operand.
|
|
1145
|
+
// Used by bytecode transforms (e.g. CFF) via Template to express jumps
|
|
1146
|
+
// to labels that exist in the parent compiler's bytecode stream.
|
|
1147
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "_VM_JUMP_") {
|
|
1148
|
+
const label = node.arguments[0].value;
|
|
1149
|
+
bc.push([this.OP.JUMP, {
|
|
1150
|
+
type: "label",
|
|
1151
|
+
label
|
|
1152
|
+
}]);
|
|
1153
|
+
// Return a dummy register — caller (ExpressionStatement) discards it.
|
|
1154
|
+
return ctx.allocReg();
|
|
1155
|
+
}
|
|
1085
1156
|
switch (node.type) {
|
|
1086
1157
|
case "NumericLiteral":
|
|
1087
1158
|
case "StringLiteral":
|
|
@@ -1100,7 +1171,7 @@ export class Compiler {
|
|
|
1100
1171
|
case "Identifier":
|
|
1101
1172
|
{
|
|
1102
1173
|
const res = this._resolve(node.name, this._currentCtx);
|
|
1103
|
-
if (res.kind === "local") return res.
|
|
1174
|
+
if (res.kind === "local") return res.reg; // register IS the local
|
|
1104
1175
|
if (res.kind === "upvalue") {
|
|
1105
1176
|
const dst = ctx.allocReg();
|
|
1106
1177
|
this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
|
|
@@ -1129,9 +1200,7 @@ export class Compiler {
|
|
|
1129
1200
|
{
|
|
1130
1201
|
const exprs = node.expressions;
|
|
1131
1202
|
for (let i = 0; i < exprs.length - 1; i++) {
|
|
1132
|
-
|
|
1133
|
-
this._compileExpr(exprs[i], scope, bc);
|
|
1134
|
-
ctx.regTop = savedTop; // discard intermediate result
|
|
1203
|
+
this._compileExpr(exprs[i], scope, bc); // result discarded; virtual reg is unused
|
|
1135
1204
|
}
|
|
1136
1205
|
return this._compileExpr(exprs[exprs.length - 1], scope, bc);
|
|
1137
1206
|
}
|
|
@@ -1140,17 +1209,13 @@ export class Compiler {
|
|
|
1140
1209
|
const n = node;
|
|
1141
1210
|
const elseLabel = this._makeLabel("ternary_else");
|
|
1142
1211
|
const endLabel = this._makeLabel("ternary_end");
|
|
1143
|
-
|
|
1144
|
-
// Compile test; free its temps after the jump is emitted.
|
|
1145
|
-
const baseTop = ctx.regTop;
|
|
1146
1212
|
const testReg = this._compileExpr(n.test, scope, bc);
|
|
1147
1213
|
this.emit(bc, [this.OP.JUMP_IF_FALSE, testReg, {
|
|
1148
1214
|
type: "label",
|
|
1149
1215
|
label: elseLabel
|
|
1150
1216
|
}], node);
|
|
1151
|
-
ctx.regTop = baseTop; // free test temps
|
|
1152
1217
|
|
|
1153
|
-
//
|
|
1218
|
+
// reg_result is a stable virtual register both branches write into.
|
|
1154
1219
|
const reg_result = ctx.allocReg();
|
|
1155
1220
|
|
|
1156
1221
|
// Consequent branch.
|
|
@@ -1161,22 +1226,18 @@ export class Compiler {
|
|
|
1161
1226
|
label: endLabel
|
|
1162
1227
|
}], node);
|
|
1163
1228
|
|
|
1164
|
-
// Alternate branch
|
|
1229
|
+
// Alternate branch — each allocReg() gets a unique virtual ID so no
|
|
1230
|
+
// slot collision is possible; no need to "re-occupy" reg_result.
|
|
1165
1231
|
this.emit(bc, [null, {
|
|
1166
1232
|
type: "defineLabel",
|
|
1167
1233
|
label: elseLabel
|
|
1168
1234
|
}], node);
|
|
1169
|
-
ctx.regTop = baseTop;
|
|
1170
|
-
ctx.allocReg(); // re-occupy reg_result slot
|
|
1171
1235
|
const altReg = this._compileExpr(n.alternate, scope, bc);
|
|
1172
1236
|
if (altReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
|
|
1173
1237
|
this.emit(bc, [null, {
|
|
1174
1238
|
type: "defineLabel",
|
|
1175
1239
|
label: endLabel
|
|
1176
1240
|
}], node);
|
|
1177
|
-
|
|
1178
|
-
// Leave reg_result allocated above baseTop.
|
|
1179
|
-
ctx.regTop = baseTop + 1;
|
|
1180
1241
|
return reg_result;
|
|
1181
1242
|
}
|
|
1182
1243
|
case "LogicalExpression":
|
|
@@ -1185,9 +1246,7 @@ export class Compiler {
|
|
|
1185
1246
|
const endLabel = this._makeLabel("logical_end");
|
|
1186
1247
|
const isOr = n.operator === "||";
|
|
1187
1248
|
if (!isOr && n.operator !== "&&") throw new Error(`Unsupported logical operator: ${n.operator}`);
|
|
1188
|
-
const baseTop = ctx.regTop;
|
|
1189
1249
|
const lhsReg = this._compileExpr(n.left, scope, bc);
|
|
1190
|
-
ctx.regTop = baseTop;
|
|
1191
1250
|
const reg_result = ctx.allocReg();
|
|
1192
1251
|
if (lhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
|
|
1193
1252
|
|
|
@@ -1199,15 +1258,12 @@ export class Compiler {
|
|
|
1199
1258
|
}], node);
|
|
1200
1259
|
|
|
1201
1260
|
// Compile RHS into reg_result.
|
|
1202
|
-
ctx.regTop = baseTop;
|
|
1203
|
-
ctx.allocReg(); // re-occupy reg_result
|
|
1204
1261
|
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1205
1262
|
if (rhsReg !== reg_result) this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
|
|
1206
1263
|
this.emit(bc, [null, {
|
|
1207
1264
|
type: "defineLabel",
|
|
1208
1265
|
label: endLabel
|
|
1209
1266
|
}], node);
|
|
1210
|
-
ctx.regTop = baseTop + 1;
|
|
1211
1267
|
return reg_result;
|
|
1212
1268
|
}
|
|
1213
1269
|
case "TemplateLiteral":
|
|
@@ -1269,7 +1325,8 @@ export class Compiler {
|
|
|
1269
1325
|
|
|
1270
1326
|
// Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
|
|
1271
1327
|
const applyBump = curReg => {
|
|
1272
|
-
const postfixReg = n.prefix ?
|
|
1328
|
+
const postfixReg = n.prefix ? curReg // prefix: postfix copy unused; caller returns newReg instead
|
|
1329
|
+
: (() => {
|
|
1273
1330
|
const r = ctx.allocReg();
|
|
1274
1331
|
this.emit(bc, [this.OP.MOVE, r, curReg], node);
|
|
1275
1332
|
return r;
|
|
@@ -1301,7 +1358,7 @@ export class Compiler {
|
|
|
1301
1358
|
const res = this._resolve(name, this._currentCtx);
|
|
1302
1359
|
let curReg;
|
|
1303
1360
|
if (res.kind === "local") {
|
|
1304
|
-
curReg = res.
|
|
1361
|
+
curReg = res.reg;
|
|
1305
1362
|
} else if (res.kind === "upvalue") {
|
|
1306
1363
|
curReg = ctx.allocReg();
|
|
1307
1364
|
this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
|
|
@@ -1311,7 +1368,7 @@ export class Compiler {
|
|
|
1311
1368
|
}
|
|
1312
1369
|
const [postfixReg, newReg] = applyBump(curReg);
|
|
1313
1370
|
if (res.kind === "local") {
|
|
1314
|
-
this.emit(bc, [this.OP.MOVE, res.
|
|
1371
|
+
this.emit(bc, [this.OP.MOVE, res.reg, newReg], node);
|
|
1315
1372
|
} else if (res.kind === "upvalue") {
|
|
1316
1373
|
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, newReg], node);
|
|
1317
1374
|
} else {
|
|
@@ -1369,7 +1426,7 @@ export class Compiler {
|
|
|
1369
1426
|
// Load current value of the variable.
|
|
1370
1427
|
let curReg;
|
|
1371
1428
|
if (res.kind === "local") {
|
|
1372
|
-
curReg = res.
|
|
1429
|
+
curReg = res.reg;
|
|
1373
1430
|
} else if (res.kind === "upvalue") {
|
|
1374
1431
|
curReg = ctx.allocReg();
|
|
1375
1432
|
this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
|
|
@@ -1386,8 +1443,8 @@ export class Compiler {
|
|
|
1386
1443
|
|
|
1387
1444
|
// Store result and return it.
|
|
1388
1445
|
if (res.kind === "local") {
|
|
1389
|
-
if (rhsReg !== res.
|
|
1390
|
-
return res.
|
|
1446
|
+
if (rhsReg !== res.reg) this.emit(bc, [this.OP.MOVE, res.reg, rhsReg], node);
|
|
1447
|
+
return res.reg;
|
|
1391
1448
|
} else if (res.kind === "upvalue") {
|
|
1392
1449
|
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
|
|
1393
1450
|
return rhsReg;
|
|
@@ -1608,6 +1665,7 @@ class Serializer {
|
|
|
1608
1665
|
const v = constants[idx];
|
|
1609
1666
|
if (!key) return v;
|
|
1610
1667
|
if (typeof v === "number") return v ^ key;
|
|
1668
|
+
if (typeof v !== "string") return v;
|
|
1611
1669
|
// String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
|
|
1612
1670
|
const bytes = Buffer.from(v, "base64");
|
|
1613
1671
|
let out = "";
|
|
@@ -1617,16 +1675,25 @@ class Serializer {
|
|
|
1617
1675
|
}
|
|
1618
1676
|
return out;
|
|
1619
1677
|
}
|
|
1620
|
-
|
|
1678
|
+
_generateComment(instr) {
|
|
1621
1679
|
const op = instr[0];
|
|
1622
1680
|
const operands = instr.slice(1);
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
|
|
1627
|
-
ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
|
|
1681
|
+
if (op === null && operands[0]?.type === "defineLabel") {
|
|
1682
|
+
const label = operands[0].label;
|
|
1683
|
+
return `${label}:`;
|
|
1628
1684
|
}
|
|
1629
|
-
|
|
1685
|
+
const constants = this.compiler.constants;
|
|
1686
|
+
const emittedOperands = operands.filter(operand => operand?.placeholder !== true);
|
|
1687
|
+
const resolvedOperands = emittedOperands.map(o => o?.resolvedValue ?? o);
|
|
1688
|
+
const displayOperands = operands.map((o, i) => {
|
|
1689
|
+
const resolvedValue = resolvedOperands[i];
|
|
1690
|
+
const label = o?.label;
|
|
1691
|
+
let displayOperand = resolvedValue;
|
|
1692
|
+
if (label) {
|
|
1693
|
+
return label;
|
|
1694
|
+
}
|
|
1695
|
+
return displayOperand;
|
|
1696
|
+
});
|
|
1630
1697
|
let name = this.OP_NAME[op];
|
|
1631
1698
|
if (!name || name.includes("{")) {
|
|
1632
1699
|
name = `OP_${op}`;
|
|
@@ -1637,73 +1704,79 @@ class Serializer {
|
|
|
1637
1704
|
}
|
|
1638
1705
|
const sourceNode = instr[SOURCE_NODE_SYM];
|
|
1639
1706
|
const sourceLocation = sourceNode?.loc ? [formatLoc(sourceNode.loc.start), formatLoc(sourceNode.loc.end)].filter(Boolean).join("-") : "";
|
|
1640
|
-
if (
|
|
1707
|
+
if (displayOperands.length > 0) {
|
|
1641
1708
|
// Operand[0] is always `dst` for instruction types that produce a value.
|
|
1642
|
-
const dst =
|
|
1709
|
+
const dst = displayOperands[0];
|
|
1643
1710
|
switch (op) {
|
|
1644
1711
|
case this.OP.LOAD_CONST:
|
|
1645
1712
|
{
|
|
1646
1713
|
// resolvedOperands: [dst, constIdx, concealKey]
|
|
1647
|
-
const val = this._decryptConst(constants,
|
|
1714
|
+
const val = this._decryptConst(constants, displayOperands[1], displayOperands[2]);
|
|
1648
1715
|
comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
|
|
1649
1716
|
break;
|
|
1650
1717
|
}
|
|
1718
|
+
case this.OP.LOAD_INT:
|
|
1719
|
+
{
|
|
1720
|
+
// resolvedOperands: [dst, intValue]
|
|
1721
|
+
comment += ` reg[${dst}] = ${displayOperands[1]}`;
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1651
1724
|
case this.OP.LOAD_GLOBAL:
|
|
1652
1725
|
// resolvedOperands: [dst, constIdx, concealKey]
|
|
1653
|
-
comment += ` reg[${dst}] = ${this._decryptConst(constants,
|
|
1726
|
+
comment += ` reg[${dst}] = ${this._decryptConst(constants, displayOperands[1], displayOperands[2])}`;
|
|
1654
1727
|
break;
|
|
1655
1728
|
case this.OP.STORE_GLOBAL:
|
|
1656
1729
|
// resolvedOperands: [constIdx, concealKey, srcReg]
|
|
1657
|
-
comment += ` ${this._decryptConst(constants,
|
|
1730
|
+
comment += ` ${this._decryptConst(constants, displayOperands[0], displayOperands[1])} = reg[${displayOperands[2]}]`;
|
|
1658
1731
|
break;
|
|
1659
1732
|
case this.OP.LOAD_UPVALUE:
|
|
1660
|
-
comment += ` reg[${dst}] = upvalue[${
|
|
1733
|
+
comment += ` reg[${dst}] = upvalue[${displayOperands[1]}]`;
|
|
1661
1734
|
break;
|
|
1662
1735
|
case this.OP.STORE_UPVALUE:
|
|
1663
|
-
comment += ` upvalue[${
|
|
1736
|
+
comment += ` upvalue[${displayOperands[0]}] = reg[${displayOperands[1]}]`;
|
|
1664
1737
|
break;
|
|
1665
1738
|
case this.OP.MOVE:
|
|
1666
|
-
comment += ` reg[${dst}] = reg[${
|
|
1739
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}]`;
|
|
1667
1740
|
break;
|
|
1668
1741
|
case this.OP.MAKE_CLOSURE:
|
|
1669
|
-
comment += ` reg[${dst}] PC=${
|
|
1742
|
+
comment += ` reg[${dst}] PC=${displayOperands[1]} (params=${displayOperands[2]} regs=${displayOperands[3]} upvalues=${displayOperands[4]})`;
|
|
1670
1743
|
break;
|
|
1671
1744
|
case this.OP.CALL:
|
|
1672
|
-
comment += ` reg[${dst}] = reg[${
|
|
1745
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}](${displayOperands.slice(3).map(v => `reg[${v}]`).join(", ")})`;
|
|
1673
1746
|
break;
|
|
1674
1747
|
case this.OP.CALL_METHOD:
|
|
1675
|
-
comment += ` reg[${dst}] = reg[${
|
|
1748
|
+
comment += ` reg[${dst}] = reg[${displayOperands[2]}](recv=reg[${displayOperands[1]}], ${displayOperands[3]} args)`;
|
|
1676
1749
|
break;
|
|
1677
1750
|
case this.OP.NEW:
|
|
1678
|
-
comment += ` reg[${dst}] = new reg[${
|
|
1751
|
+
comment += ` reg[${dst}] = new reg[${displayOperands[1]}](${displayOperands[2]} args)`;
|
|
1679
1752
|
break;
|
|
1680
1753
|
case this.OP.RETURN:
|
|
1681
|
-
comment += ` reg[${
|
|
1754
|
+
comment += ` reg[${displayOperands[0]}]`;
|
|
1682
1755
|
break;
|
|
1683
1756
|
case this.OP.BUILD_ARRAY:
|
|
1684
|
-
comment += ` reg[${dst}] = [${
|
|
1757
|
+
comment += ` reg[${dst}] = [${displayOperands[2]} elems]`;
|
|
1685
1758
|
break;
|
|
1686
1759
|
case this.OP.BUILD_OBJECT:
|
|
1687
|
-
comment += ` reg[${dst}] = {${
|
|
1760
|
+
comment += ` reg[${dst}] = {${displayOperands[1]} pairs}`;
|
|
1688
1761
|
break;
|
|
1689
1762
|
case this.OP.GET_PROP:
|
|
1690
|
-
comment += ` reg[${dst}] = reg[${
|
|
1763
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}][reg[${displayOperands[2]}]]`;
|
|
1691
1764
|
break;
|
|
1692
1765
|
case this.OP.SET_PROP:
|
|
1693
|
-
comment += ` reg[${
|
|
1766
|
+
comment += ` reg[${displayOperands[0]}][reg[${displayOperands[1]}]] = reg[${displayOperands[2]}]`;
|
|
1767
|
+
break;
|
|
1768
|
+
case this.OP.JUMP_REG:
|
|
1769
|
+
comment += ` PC = reg[${displayOperands[0]}]`;
|
|
1694
1770
|
break;
|
|
1695
1771
|
default:
|
|
1696
|
-
comment +=
|
|
1772
|
+
comment += displayOperands.length === 1 ? ` ${displayOperands[0]}` : ` [${displayOperands.join(", ")}]`;
|
|
1697
1773
|
}
|
|
1698
1774
|
}
|
|
1699
1775
|
comment = comment.padEnd(50) + sourceLocation;
|
|
1700
1776
|
const values = [op, ...resolvedOperands];
|
|
1701
1777
|
const instrText = `[${values.join(", ")}]`;
|
|
1702
1778
|
const text = `${(instrText + ",").padEnd(20)} ${comment}`;
|
|
1703
|
-
return
|
|
1704
|
-
text,
|
|
1705
|
-
values
|
|
1706
|
-
};
|
|
1779
|
+
return text;
|
|
1707
1780
|
}
|
|
1708
1781
|
_serializeConstants(constants) {
|
|
1709
1782
|
const lines = ["var CONSTANTS = ["];
|
|
@@ -1716,14 +1789,23 @@ class Serializer {
|
|
|
1716
1789
|
_serializeBytecode(bytecode, compiler) {
|
|
1717
1790
|
const serialized = [];
|
|
1718
1791
|
for (const instr of bytecode) {
|
|
1719
|
-
|
|
1792
|
+
const op = instr[0];
|
|
1793
|
+
const operands = instr.slice(1);
|
|
1794
|
+
if (instr[0] === null) continue; // null opcodes are not emitted
|
|
1795
|
+
|
|
1796
|
+
const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
|
|
1720
1797
|
const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
|
|
1721
1798
|
if (specializedOpInfo) {
|
|
1722
|
-
const operands = instr.slice(1);
|
|
1723
|
-
const resolvedValues = operands.map(o => o?.resolvedValue ?? o);
|
|
1724
1799
|
const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
|
|
1725
1800
|
compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValues.join("_")}`;
|
|
1726
1801
|
}
|
|
1802
|
+
|
|
1803
|
+
// Validate no opcode or operand exceeds u16 limit
|
|
1804
|
+
for (const o of resolvedValues) {
|
|
1805
|
+
ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
|
|
1806
|
+
ok(o >= 0 && o <= 0xffffffff, `Operand overflow (max 0xFFFFFFFF u32): ${o}`);
|
|
1807
|
+
}
|
|
1808
|
+
ok(op >= 0 && op <= 0xffffffff, `Opcode overflow (max 0xFFFFFFFF u32): ${op}`);
|
|
1727
1809
|
serialized.push(instr);
|
|
1728
1810
|
}
|
|
1729
1811
|
return {
|
|
@@ -1731,10 +1813,12 @@ class Serializer {
|
|
|
1731
1813
|
};
|
|
1732
1814
|
}
|
|
1733
1815
|
_encodeBytecode(flat) {
|
|
1734
|
-
const buf = new Uint8Array(flat.length *
|
|
1816
|
+
const buf = new Uint8Array(flat.length * 4);
|
|
1735
1817
|
flat.forEach((w, i) => {
|
|
1736
|
-
buf[i *
|
|
1737
|
-
buf[i *
|
|
1818
|
+
buf[i * 4] = w & 0xff;
|
|
1819
|
+
buf[i * 4 + 1] = w >>> 8 & 0xff;
|
|
1820
|
+
buf[i * 4 + 2] = w >>> 16 & 0xff;
|
|
1821
|
+
buf[i * 4 + 3] = w >>> 24 & 0xff;
|
|
1738
1822
|
});
|
|
1739
1823
|
return Buffer.from(buf).toString("base64");
|
|
1740
1824
|
}
|
|
@@ -1767,58 +1851,121 @@ class Serializer {
|
|
|
1767
1851
|
}
|
|
1768
1852
|
}
|
|
1769
1853
|
export async function compileAndSerialize(sourceCode, options) {
|
|
1854
|
+
let obfuscationStartedAt = now();
|
|
1770
1855
|
const compiler = new Compiler(options);
|
|
1771
1856
|
let bytecode = compiler.compile(sourceCode);
|
|
1772
1857
|
const passes = [];
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1858
|
+
if (options.stringConcealing) {
|
|
1859
|
+
passes.push({
|
|
1860
|
+
pass: stringConcealing,
|
|
1861
|
+
name: "stringConcealing"
|
|
1862
|
+
});
|
|
1776
1863
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1864
|
+
|
|
1865
|
+
// CFF and Dispatcher both run before resolveRegisters and resolveLabels
|
|
1866
|
+
if (options.controlFlowFlattening) {
|
|
1867
|
+
passes.push({
|
|
1868
|
+
pass: controlFlowFlattening,
|
|
1869
|
+
name: "controlFlowFlattening"
|
|
1870
|
+
});
|
|
1779
1871
|
}
|
|
1780
|
-
if (options.
|
|
1781
|
-
passes.push(
|
|
1872
|
+
if (options.dispatcher) {
|
|
1873
|
+
passes.push({
|
|
1874
|
+
pass: dispatcher,
|
|
1875
|
+
name: "dispatcher"
|
|
1876
|
+
});
|
|
1782
1877
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1878
|
+
passes.push({
|
|
1879
|
+
pass: concealConstants,
|
|
1880
|
+
name: "concealConstants"
|
|
1881
|
+
});
|
|
1882
|
+
if (options.specializedOpcodes) {
|
|
1883
|
+
passes.push({
|
|
1884
|
+
pass: specializedOpcodes,
|
|
1885
|
+
name: "specializedOpcodes"
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
if (options.macroOpcodes) {
|
|
1889
|
+
passes.push({
|
|
1890
|
+
pass: macroOpcodes,
|
|
1891
|
+
name: "macroOpcodes"
|
|
1892
|
+
});
|
|
1785
1893
|
}
|
|
1786
1894
|
if (options.aliasedOpcodes) {
|
|
1787
|
-
passes.push(
|
|
1895
|
+
passes.push({
|
|
1896
|
+
pass: aliasedOpcodes,
|
|
1897
|
+
name: "aliasedOpcodes"
|
|
1898
|
+
});
|
|
1788
1899
|
}
|
|
1789
|
-
|
|
1900
|
+
const timings = {};
|
|
1901
|
+
function runAndTime(pass, name) {
|
|
1902
|
+
const startedAt = now();
|
|
1903
|
+
compiler.log(`Running bytecode pass ${name}...`);
|
|
1790
1904
|
const passResult = pass(bytecode, compiler);
|
|
1791
1905
|
bytecode = passResult.bytecode;
|
|
1906
|
+
const endedAt = now();
|
|
1907
|
+
const elapsedMs = endedAt - startedAt;
|
|
1908
|
+
timings[name] = elapsedMs;
|
|
1909
|
+
compiler.profileData.transforms[name] = {
|
|
1910
|
+
transformTime: elapsedMs,
|
|
1911
|
+
bytecodeSize: bytecode.length
|
|
1912
|
+
};
|
|
1913
|
+
compiler.log(`Bytecode pass ${name} completed in ${Math.floor(elapsedMs)}ms`);
|
|
1914
|
+
return passResult;
|
|
1915
|
+
}
|
|
1916
|
+
for (const pass of passes) {
|
|
1917
|
+
runAndTime(pass.pass, pass.name);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Resolve virtual registers to concrete slot indices and set regCount per fn.
|
|
1921
|
+
// Must run BEFORE selfModifying: that pass moves body instructions to the end
|
|
1922
|
+
// of the bytecode while leaving RETURN in place, splitting a function's code
|
|
1923
|
+
// into two non-contiguous regions. Linear-scan liveness then sees incorrect
|
|
1924
|
+
// firstUse/lastUse for registers that span the gap, causing slot collisions.
|
|
1925
|
+
const regsResult = runAndTime(resolveRegisters, "resolveRegisters");
|
|
1926
|
+
bytecode = regsResult.bytecode;
|
|
1927
|
+
|
|
1928
|
+
// selfModifying runs after register resolution so concrete slot indices are
|
|
1929
|
+
// already in place; only label operands remain unresolved at this stage.
|
|
1930
|
+
if (options.selfModifying) {
|
|
1931
|
+
const smResult = runAndTime(selfModifying, "selfModifying");
|
|
1932
|
+
bytecode = smResult.bytecode;
|
|
1792
1933
|
}
|
|
1793
1934
|
|
|
1794
1935
|
// Resolve label references to flat bytecode indices.
|
|
1795
|
-
const labelsResult = resolveLabels
|
|
1936
|
+
const labelsResult = runAndTime(resolveLabels, "resolveLabels");
|
|
1796
1937
|
bytecode = labelsResult.bytecode;
|
|
1797
1938
|
|
|
1798
1939
|
// Set mainStartPc from the first function descriptor (or 0 for top-level start).
|
|
1799
1940
|
compiler.mainStartPc = compiler.mainFn.startPc;
|
|
1800
1941
|
|
|
1801
1942
|
// Resolve constant references to pool indices (+ conceal key operand).
|
|
1802
|
-
const constResult = resolveConstants
|
|
1943
|
+
const constResult = runAndTime(resolveConstants, "resolveConstants");
|
|
1803
1944
|
bytecode = constResult.bytecode;
|
|
1804
1945
|
compiler.constants = constResult.constants;
|
|
1805
1946
|
|
|
1806
1947
|
// Build and obfuscate the runtime.
|
|
1807
1948
|
const runtimeSource = compiler.serializer.serialize(bytecode, constResult.constants, compiler);
|
|
1808
1949
|
|
|
1809
|
-
//
|
|
1950
|
+
// for (const key of Object.keys(timings)) {
|
|
1951
|
+
// console.log(` ${key}: ${timings[key]}ms`);
|
|
1952
|
+
// }
|
|
1953
|
+
|
|
1954
|
+
// This part was purposefully pulled out Serializer as OP_NAME's get resolved during buildRuntime
|
|
1810
1955
|
// So for the most useful comments, it's ran absolutely last
|
|
1811
1956
|
// Tests also rely on correct comments so it's required
|
|
1812
1957
|
const generateBytecodeComment = () => {
|
|
1813
1958
|
var lines = [];
|
|
1814
1959
|
for (const instr of bytecode) {
|
|
1815
|
-
const
|
|
1816
|
-
lines.push("// " +
|
|
1960
|
+
const comment = compiler.serializer._generateComment(instr);
|
|
1961
|
+
lines.push("// " + comment);
|
|
1817
1962
|
}
|
|
1818
1963
|
return lines.join("\n");
|
|
1819
1964
|
};
|
|
1820
|
-
const code = await
|
|
1965
|
+
const code = await buildRuntime(runtimeSource, bytecode, options, compiler, generateBytecodeComment);
|
|
1966
|
+
compiler.profileData.obfuscationTime = now() - obfuscationStartedAt;
|
|
1821
1967
|
return {
|
|
1822
|
-
code
|
|
1968
|
+
code,
|
|
1969
|
+
profileData: compiler.profileData
|
|
1823
1970
|
};
|
|
1824
1971
|
}
|