js-confuser-vm 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -28
- package/LICENSE +21 -21
- package/README.MD +370 -196
- package/babel-plugin-inline-runtime.cjs +34 -34
- package/babel.config.json +23 -23
- package/dist/build-runtime.js +53 -0
- package/dist/compiler.js +107 -117
- package/dist/runtime.js +78 -84
- package/dist/transforms/bytecode/macroOpcodes.js +152 -0
- package/dist/transforms/{resolveContants.js → bytecode/resolveContants.js} +16 -6
- package/dist/transforms/bytecode/resolveLabels.js +80 -0
- package/dist/transforms/{selfModifying.js → bytecode/selfModifying.js} +33 -33
- package/dist/transforms/bytecode/specializedOpcodes.js +103 -0
- package/dist/transforms/runtime/macroOpcodes.js +88 -0
- package/dist/transforms/runtime/minify.js +1 -0
- package/dist/transforms/runtime/shuffleOpcodes.js +20 -0
- package/dist/transforms/runtime/specializedOpcodes.js +102 -0
- package/dist/transforms/utils/op-utils.js +25 -0
- package/dist/{random.js → transforms/utils/random-utils.js} +3 -3
- package/dist/types.js +4 -2
- package/index.ts +34 -22
- package/jest-strip-types.js +10 -10
- package/jest.config.js +35 -28
- package/package.json +49 -48
- package/src/build-runtime.ts +57 -0
- package/src/compiler.ts +2069 -2066
- package/src/index.ts +14 -14
- package/src/minify.ts +21 -21
- package/src/options.ts +14 -12
- package/src/runtime.ts +771 -779
- package/src/transforms/bytecode/macroOpcodes.ts +177 -0
- package/src/transforms/bytecode/resolveContants.ts +62 -0
- package/src/transforms/bytecode/resolveLabels.ts +107 -0
- package/src/transforms/{selfModifying.ts → bytecode/selfModifying.ts} +37 -40
- package/src/transforms/bytecode/specializedOpcodes.ts +118 -0
- package/src/transforms/runtime/macroOpcodes.ts +111 -0
- package/src/transforms/runtime/minify.ts +1 -0
- package/src/transforms/runtime/shuffleOpcodes.ts +24 -0
- package/src/transforms/runtime/specializedOpcodes.ts +146 -0
- package/src/transforms/utils/op-utils.ts +26 -0
- package/src/{random.ts → transforms/utils/random-utils.ts} +31 -31
- package/src/types.ts +33 -24
- package/src/utilts.ts +3 -3
- package/tsconfig.json +12 -12
- package/dist/runtimeObf.js +0 -56
- package/dist/transforms/controlFlowFlattening.js +0 -22
- package/dist/transforms/resolveLabels.js +0 -59
- package/src/runtimeObf.ts +0 -62
- package/src/transforms/controlFlowFlattening.ts +0 -30
- package/src/transforms/resolveContants.ts +0 -42
- package/src/transforms/resolveLabels.ts +0 -83
package/dist/compiler.js
CHANGED
|
@@ -2,17 +2,21 @@ import { parse } from "@babel/parser";
|
|
|
2
2
|
import traverseImport from "@babel/traverse";
|
|
3
3
|
import { generate } from "@babel/generator";
|
|
4
4
|
import { stripTypeScriptTypes } from "module";
|
|
5
|
-
import
|
|
5
|
+
import * as t from "@babel/types";
|
|
6
6
|
import { ok } from "assert";
|
|
7
|
-
import { obfuscateRuntime } from "./
|
|
7
|
+
import { obfuscateRuntime } from "./build-runtime.js";
|
|
8
8
|
import { DEFAULT_OPTIONS } from "./options.js";
|
|
9
|
-
import { resolveLabels } from "./transforms/resolveLabels.js";
|
|
10
|
-
import { resolveConstants } from "./transforms/resolveContants.js";
|
|
11
|
-
import { selfModifying } from "./transforms/selfModifying.js";
|
|
9
|
+
import { resolveLabels } from "./transforms/bytecode/resolveLabels.js";
|
|
10
|
+
import { resolveConstants } from "./transforms/bytecode/resolveContants.js";
|
|
11
|
+
import { selfModifying } from "./transforms/bytecode/selfModifying.js";
|
|
12
|
+
import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.js";
|
|
12
13
|
import * as b from "./types.js";
|
|
14
|
+
import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.js";
|
|
15
|
+
import { getRandomInt } from "./transforms/utils/random-utils.js";
|
|
16
|
+
import { U16_MAX } from "./transforms/utils/op-utils.js";
|
|
13
17
|
const traverse = traverseImport.default || traverseImport;
|
|
14
|
-
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\nconst BYTECODE = [];\nconst MAIN_START_PC = 0;\nconst CONSTANTS = [];\nconst ENCODE_BYTECODE = false;\nconst TIMING_CHECKS = false;\n// The text above is not included in the compiled output - for type intellisense only\n// @START\n\nfunction decodeBytecode(s) {\n if (!ENCODE_BYTECODE) return s;\n\n var b =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(s, \"base64\")\n : Uint8Array.from(atob(s), function (c) {\n return c.charCodeAt(0);\n });\n var r = new Int32Array(b.length / 4);\n for (var i = 0; i < r.length; i++)\n r[i] =\n b[i * 4] |\n (b[i * 4 + 1] << 8) |\n (b[i * 4 + 2] << 16) |\n (b[i * 4 + 3] << 24);\n return r;\n}\n\n// Closure symbol\n// Used to tag shell functions so the VM can fast-path back to the\n// inner Closure instead of going through a sub-VM on internal calls.\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\n\n// Upvalue\n// While the outer frame is alive: reads/writes go to frame.locals[slot].\n// After the outer frame returns (closed): reads/writes hit this.value.\nfunction Upvalue(frame, slot) {\n this._frame = frame;\n this._slot = slot;\n this._closed = false;\n this._value = undefined;\n}\nUpvalue.prototype._read = function () {\n return this._closed ? this._value : this._frame.locals[this._slot];\n};\nUpvalue.prototype._write = function (v) {\n if (this._closed) this._value = v;\n else this._frame.locals[this._slot] = v;\n};\nUpvalue.prototype._close = function () {\n this._value = this._frame.locals[this._slot];\n this._closed = true;\n};\n\n// Closure & Frame\nfunction Closure(fn) {\n this.fn = fn;\n this.upvalues = [];\n this.prototype = {}; // <- default prototype object for \\`new\\`\n}\n\nfunction Frame(closure, returnPc, parent, thisVal ) {\n this.closure = closure;\n this.locals = new Array(closure.fn.localCount).fill(undefined);\n this._pc = closure.fn.startPc; // <- initialize from fn descriptor\n this._returnPc = returnPc; // pc to resume in parent frame after RETURN\n this._parent = parent;\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\n this._newObj = null; // <- set by NEW so RETURN can see it\n this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP\n}\n\n// VM\nfunction VM(bytecode, mainStartPc, constants, globals) {\n this.bytecode = bytecode;\n this.constants = constants;\n this.globals = globals;\n this._stack = [];\n this._frameStack = [];\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\n\n var mainFn = {\n paramCount: 0,\n localCount: 0,\n startPc: mainStartPc, // <- where main begins\n };\n this._currentFrame = new Frame(new Closure(mainFn), null, null);\n}\n\nVM.prototype._push = function (v) {\n this._stack.push(v);\n};\nVM.prototype._pop = function () {\n return this._stack.pop();\n};\nVM.prototype.peek = function () {\n return this._stack[this._stack.length - 1];\n};\n\n// Read one instruction word from this.bytecode at `pc`, unwrapping the\n// encoding so callers always get a plain { op, operand } pair regardless\n// of whether ENCODE_BYTECODE is active.\nVM.prototype.readWord = function (pc) {\n var word = this.bytecode[pc];\n if (ENCODE_BYTECODE) {\n return { op: word & 0xff, operand: word >>> 8 };\n } else {\n return { op: word[0], operand: word[1] };\n }\n};\n\nVM.prototype.captureUpvalue = function (frame, slot) {\n // Reuse existing open upvalue for this frame+slot if one exists.\n // This is what makes two closures share the same mutable cell.\n for (var i = 0; i < this._openUpvalues.length; i++) {\n var uv = this._openUpvalues[i];\n if (uv._frame === frame && uv._slot === slot) return uv;\n }\n var uv = new Upvalue(frame, slot);\n this._openUpvalues.push(uv);\n return uv;\n};\n\nVM.prototype._closeUpvaluesFor = function (frame) {\n // Called on RETURN - close every upvalue that was pointing into this frame.\n // After this, closures that captured from the frame read from upvalue.value.\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\n if (uv._frame === frame) {\n uv._close();\n return false;\n }\n return true;\n });\n};\n\nVM.prototype.run = function () {\n var now = () => {\n return performance.now();\n };\n\n var t = now();\n\n while (true) {\n var frame = this._currentFrame;\n var bc = this.bytecode;\n if (frame._pc >= bc.length) break;\n\n var op, operand;\n var word = this.readWord(frame._pc++);\n\n op = word.op;\n operand = word.operand;\n\n // console.log(frame._pc - 1, op, operand);\n\n // Debugging protection\n if (TIMING_CHECKS) {\n var t2 = now();\n var isTamper = t2 - t > 1000;\n t = t2;\n if (isTamper) {\n op = OP.POP;\n }\n }\n\n try {\n /* @SWITCH */\n switch (op) {\n case OP.LOAD_CONST:\n this._push(this.constants[operand]);\n break;\n\n case OP.LOAD_INT:\n this._push(operand);\n break;\n\n case OP.LOAD_LOCAL:\n this._push(frame.locals[operand]);\n break;\n\n case OP.STORE_LOCAL:\n frame.locals[operand] = this._pop();\n break;\n\n case OP.LOAD_GLOBAL:\n this._push(this.globals[this.constants[operand]]);\n break;\n\n case OP.STORE_GLOBAL:\n this.globals[this.constants[operand]] = this._pop();\n break;\n\n case OP.GET_PROP: {\n // Stack: [..., obj, key] -> [..., obj, obj[key]]\n // obj is PEEKED (not popped) - CALL_METHOD needs it as receiver\n var key = this._pop();\n var obj = this.peek();\n this._push(obj[key]);\n break;\n }\n\n case OP.ADD: {\n var b = this._pop();\n this._push(this._pop() + b);\n break;\n }\n case OP.SUB: {\n var b = this._pop();\n this._push(this._pop() - b);\n break;\n }\n case OP.MUL: {\n var b = this._pop();\n this._push(this._pop() * b);\n break;\n }\n case OP.DIV: {\n var b = this._pop();\n this._push(this._pop() / b);\n break;\n }\n case OP.MOD: {\n var b = this._pop();\n this._push(this._pop() % b);\n break;\n }\n case OP.BAND: {\n var b = this._pop();\n this._push(this._pop() & b);\n break;\n }\n case OP.BOR: {\n var b = this._pop();\n this._push(this._pop() | b);\n break;\n }\n case OP.BXOR: {\n var b = this._pop();\n this._push(this._pop() ^ b);\n break;\n }\n case OP.SHL: {\n var b = this._pop();\n this._push(this._pop() << b);\n break;\n }\n case OP.SHR: {\n var b = this._pop();\n this._push(this._pop() >> b);\n break;\n }\n case OP.USHR: {\n var b = this._pop();\n this._push(this._pop() >>> b);\n break;\n }\n\n case OP.LT: {\n var b = this._pop();\n this._push(this._pop() < b);\n break;\n }\n case OP.GT: {\n var b = this._pop();\n this._push(this._pop() > b);\n break;\n }\n case OP.EQ: {\n var b = this._pop();\n this._push(this._pop() === b);\n break;\n }\n\n case OP.LTE: {\n var b = this._pop();\n this._push(this._pop() <= b);\n break;\n }\n case OP.GTE: {\n var b = this._pop();\n this._push(this._pop() >= b);\n break;\n }\n case OP.NEQ: {\n var b = this._pop();\n this._push(this._pop() !== b);\n break;\n }\n case OP.LOOSE_EQ: {\n var b = this._pop();\n this._push(this._pop() == b);\n break;\n }\n case OP.LOOSE_NEQ: {\n var b = this._pop();\n this._push(this._pop() != b);\n break;\n }\n\n case OP.IN: {\n var b = this._pop();\n this._push(this._pop() in b);\n break;\n }\n\n case OP.INSTANCEOF: {\n var ctor = this._pop();\n var obj = this._pop();\n if (typeof ctor === \"function\") {\n // Native constructor (e.g. Array, Date) - native instanceof is fine\n this._push(obj instanceof ctor);\n } else {\n // VM Closure - ctor.prototype was set by MAKE_CLOSURE / user assignment.\n // Walk obj's prototype chain looking for identity with ctor.prototype.\n var proto = ctor.prototype; // the .prototype property on the Closure\n var target = Object.getPrototypeOf(obj);\n var result = false;\n while (target !== null) {\n if (target === proto) {\n result = true;\n break;\n }\n target = Object.getPrototypeOf(target);\n }\n this._push(result);\n }\n break;\n }\n\n case OP.UNARY_NEG:\n this._push(-this._pop());\n break;\n case OP.UNARY_POS:\n this._push(this._pop());\n break;\n case OP.UNARY_NOT:\n this._push(!this._pop());\n break;\n case OP.UNARY_BITNOT:\n this._push(~this._pop());\n break;\n case OP.TYPEOF:\n this._push(typeof this._pop());\n break;\n case OP.VOID:\n this._pop();\n this._push(undefined);\n break;\n\n case OP.TYPEOF_SAFE: {\n // operand is a const index holding the variable name string.\n // Mimics JS semantics: typeof undeclaredVar === \"undefined\" (no throw).\n var name = this._pop(); // LOAD_CONST pushed the name - consume it\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\n ? this.globals[name]\n : undefined;\n this._push(typeof val);\n break;\n }\n\n case OP.JUMP:\n frame._pc = operand;\n break;\n\n case OP.JUMP_IF_FALSE:\n if (!this._pop()) frame._pc = operand;\n break;\n\n case OP.JUMP_IF_TRUE_OR_POP:\n // || semantics: if truthy, we're done - leave value, jump over RHS.\n // If falsy, discard it and fall through to evaluate RHS.\n if (this.peek()) {\n frame._pc = operand;\n } else {\n this._pop();\n }\n break;\n\n case OP.JUMP_IF_FALSE_OR_POP:\n // && semantics: if falsy, we're done - leave value, jump over RHS.\n // If truthy, discard it and fall through to evaluate RHS.\n if (!this.peek()) {\n frame._pc = operand;\n } else {\n this._pop();\n }\n break;\n\n case OP.MAKE_CLOSURE: {\n // operand = startPc: absolute index of the function body's first instruction.\n // Metadata is read from the value stack (pushed by _emitClosureMetadata).\n // Stack layout when we arrive here (top is rightmost):\n // [isLocal_0, idx_0, ..., isLocal_N-1, idx_N-1, uvCount, localCount, paramCount]\n var startPc = operand;\n var paramCount = this._pop();\n var localCount = this._pop();\n var uvCount = this._pop();\n\n // Upvalues were pushed in order 0..N-1 so we pop them in reverse.\n var uvDescs = new Array(uvCount);\n for (var i = uvCount - 1; i >= 0; i--) {\n var uvIndex = this._pop();\n var isLocalRaw = this._pop();\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\n }\n\n var fn = {\n paramCount: paramCount,\n localCount: localCount,\n startPc: startPc,\n upvalueDescriptors: uvDescs,\n };\n\n var closure = new Closure(fn);\n for (var i = 0; i < uvDescs.length; i++) {\n var uvd = uvDescs[i];\n if (uvd.isLocal) {\n // Capture directly from current frame's local slot\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\n } else {\n // Relay - take upvalue from the enclosing closure's list\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\n }\n }\n // Wrap in a native callable shell so host code (array methods,\n // test assertions, setTimeout, etc.) can invoke VM closures.\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\n var self = this;\n var shell = (function (c) {\n return function () {\n var args = Array.prototype.slice.call(arguments);\n var sub = new VM(self.bytecode, 0, self.constants, self.globals);\n // Sloppy-mode: null/undefined thisArg \u2192 global object\n var f = new Frame(\n c,\n null,\n null,\n this == null ? self.globals : this,\n );\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\n f.locals[c.fn.paramCount] = args;\n sub._currentFrame = f;\n return sub.run();\n };\n })(closure);\n shell[CLOSURE_SYM] = closure;\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\n this._push(shell);\n break;\n }\n\n case OP.DATA:\n // Should never appear in compiled output (reserved opcode slot).\n throw new Error(\"DATA opcode executed at pc \" + (frame._pc - 1));\n\n case OP.LOAD_UPVALUE:\n this._push(frame.closure.upvalues[operand]._read());\n break;\n\n case OP.STORE_UPVALUE:\n frame.closure.upvalues[operand]._write(this._pop());\n break;\n\n case OP.BUILD_ARRAY: {\n // Pop \\`operand\\` values off the stack in reverse, assemble array.\n var elems = this._stack.splice(this._stack.length - operand);\n this._push(elems);\n break;\n }\n\n case OP.BUILD_OBJECT: {\n // Stack has: key0, val0, key1, val1 ... keyN, valN (pushed left->right)\n // Pop all pairs and build the object.\n var pairs = this._stack.splice(this._stack.length - operand * 2);\n var o = {};\n for (var i = 0; i < pairs.length; i += 2) {\n o[pairs[i]] = pairs[i + 1]; // key at even index, val at odd\n }\n this._push(o);\n break;\n }\n case OP.SET_PROP: {\n // Stack: [..., obj, key, val]\n // Leaves val on stack - assignment is an expression in JS.\n var val = this._pop();\n var key = this._pop();\n var obj = this._pop();\n // Reflect.set performs [[Set]] without throwing on failure,\n // correctly simulating sloppy-mode assignment from a strict-mode host\n // (output.js is an ES module). This also properly invokes inherited\n // or prototype-chain setter functions.\n Reflect.set(obj, key, val);\n this._push(val); // assignment expression evaluates to the assigned value\n break;\n }\n case OP.GET_PROP_COMPUTED: {\n // Stack: [..., obj, key] - key is a runtime value (nums[i])\n // Mirrors GET_PROP but pops the key that was pushed dynamically.\n var key = this._pop();\n var obj = this._pop();\n this._push(obj[key]);\n break;\n }\n case OP.DELETE_PROP: {\n var key = this._pop();\n var obj = this._pop();\n this._push(delete obj[key]);\n break;\n }\n\n case OP.CALL: {\n var args = this._stack.splice(this._stack.length - operand);\n var callee = this._pop();\n if (callee && callee[CLOSURE_SYM]) {\n // VM closure - run directly in this VM, no sub-VM overhead\n var c = callee[CLOSURE_SYM];\n // Sloppy-mode: plain function call \u2192 global object as this\n var f = new Frame(c, frame._pc, frame, this.globals);\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\n f.locals[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Native function\n this._push(callee.apply(null, args));\n }\n break;\n }\n\n case OP.CALL_METHOD: {\n var args = this._stack.splice(this._stack.length - operand);\n var callee = this._pop();\n var receiver = this._pop(); // left on stack by GET_PROP\n if (callee && callee[CLOSURE_SYM]) {\n // VM closure - run directly in this VM with receiver as this\n var c = callee[CLOSURE_SYM];\n var f = new Frame(c, frame._pc, frame, receiver);\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\n f.locals[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Native method\n this._push(callee.apply(receiver, args));\n }\n break;\n }\n\n case OP.LOAD_THIS:\n this._push(frame.thisVal);\n break;\n\n case OP.NEW: {\n var args = this._stack.splice(this._stack.length - operand);\n var callee = this._pop();\n if (callee && callee[CLOSURE_SYM]) {\n // VM closure constructor - prototype is unified via shell.prototype = closure.prototype\n var c = callee[CLOSURE_SYM];\n var newObj = Object.create(c.prototype || null);\n var f = new Frame(c, frame._pc, frame, newObj);\n f._newObj = newObj;\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\n f.locals[c.fn.paramCount] = args;\n this._frameStack.push(this._currentFrame);\n this._currentFrame = f;\n } else {\n // Native constructor (e.g. new Error(), new Date()).\n // Reflect.construct is required - Object.create+apply does NOT set\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\n this._push(Reflect.construct(callee, args));\n }\n break;\n }\n\n case OP.RETURN: {\n var retVal = this._pop();\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\n if (this._frameStack.length === 0) return retVal;\n\n // new-call rule: primitive return -> discard, use the constructed object instead\n if (frame._newObj !== null) {\n if (typeof retVal !== \"object\" || retVal === null)\n retVal = frame._newObj;\n }\n\n this._currentFrame = this._frameStack.pop();\n this._push(retVal);\n break;\n }\n\n case OP.POP:\n this._pop();\n break;\n\n case OP.DUP:\n this._push(this.peek());\n break;\n\n case OP.THROW:\n throw this._pop();\n\n case OP.FOR_IN_SETUP: {\n // Pop the object; build an ordered list of all enumerable own+inherited\n // string keys by walking the prototype chain manually.\n // Uses getOwnPropertyNames (includes non-enumerable) + descriptor check,\n // so we never rely on Object.keys() and we handle inheritance correctly.\n var obj = this._pop();\n var keys = [];\n if (obj !== null && obj !== undefined) {\n var seen = Object.create(null);\n var cur = Object(obj); // box primitives\n while (cur !== null) {\n var ownNames = Object.getOwnPropertyNames(cur);\n for (var i = 0; i < ownNames.length; i++) {\n var k = ownNames[i];\n if (!(k in seen)) {\n seen[k] = true;\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\n if (propDesc && propDesc.enumerable) {\n keys.push(k);\n }\n }\n }\n cur = Object.getPrototypeOf(cur);\n }\n }\n this._push({ _keys: keys, i: 0 });\n break;\n }\n\n case OP.FOR_IN_NEXT: {\n // operand = jump target for the done case.\n // Pop the iterator; if exhausted jump to exit, otherwise push next key.\n var iter = this._pop();\n if (iter.i >= iter._keys.length) {\n frame._pc = operand;\n } else {\n this._push(iter._keys[iter.i++]);\n }\n break;\n }\n\n case OP.PATCH: {\n // Writes at operand the bytecode[arg1:arg2]\n var destPc = operand;\n var instructions = this.bytecode.slice(this._pop(), this._pop());\n\n for (var i = 0; i < instructions.length; i++) {\n this.bytecode[destPc + i] = instructions[i];\n }\n\n break;\n }\n\n case OP.TRY_SETUP: {\n // Push an exception handler record onto the current frame.\n // Saves: catch PC (operand), current stack depth, current frame-stack depth.\n // If an exception is thrown before TRY_END fires, the VM jumps here.\n frame._handlerStack.push({\n handlerPc: operand,\n stackDepth: this._stack.length,\n frameStackDepth: this._frameStack.length,\n });\n break;\n }\n\n case OP.TRY_END: {\n // Normal exit from a try block \u2014 disarm the exception handler.\n frame._handlerStack.pop();\n break;\n }\n\n case OP.DEFINE_GETTER: {\n // Stack: [..., obj, key, getterFn]\n // Pops all three; defines an enumerable, configurable getter on obj.\n // If a setter was already defined for this key, it is preserved.\n var getterFn = this._pop();\n var key = this._pop();\n var obj = this._pop();\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var getDesc = {\n get: getterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.set === \"function\") {\n getDesc.set = existingDesc.set;\n }\n Object.defineProperty(obj, key, getDesc);\n break;\n }\n\n case OP.DEFINE_SETTER: {\n // Stack: [..., obj, key, setterFn]\n // Pops all three; defines an enumerable, configurable setter on obj.\n // If a getter was already defined for this key, it is preserved.\n var setterFn = this._pop();\n var key = this._pop();\n var obj = this._pop();\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\n var setDesc = {\n set: setterFn,\n configurable: true,\n enumerable: true,\n };\n if (existingDesc && typeof existingDesc.get === \"function\") {\n setDesc.get = existingDesc.get;\n }\n Object.defineProperty(obj, key, setDesc);\n break;\n }\n\n case OP.DEBUGGER: {\n debugger;\n break;\n }\n\n default:\n throw new Error(\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\n );\n }\n } catch (err) {\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\n // Walk from the current frame upward until we find a frame that has an open\n // exception handler (TRY_SETUP without a matching TRY_END).\n // For every frame we abandon along the way, close its captured upvalues.\n var handledFrame = null;\n var searchFrame = this._currentFrame;\n while (true) {\n if (searchFrame._handlerStack.length > 0) {\n handledFrame = searchFrame;\n break;\n }\n // No handler in this frame \u2014 abandon it and walk up.\n this._closeUpvaluesFor(searchFrame);\n if (this._frameStack.length === 0) break;\n searchFrame = this._frameStack.pop();\n this._currentFrame = searchFrame;\n }\n\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\n\n var h = handledFrame._handlerStack.pop();\n // Restore the VM value stack to the depth recorded at TRY_SETUP time,\n // then push the caught exception so the catch binding can store it.\n this._stack.length = h.stackDepth;\n this._push(err);\n // Discard any call-frames that were pushed inside the try body\n // (functions called from within the try block that are still live).\n this._frameStack.length = h.frameStackDepth;\n // Jump to the catch block.\n handledFrame._pc = h.handlerPc;\n this._currentFrame = handledFrame;\n }\n }\n};\n\n// Boot\nvar globals = {}; // global object for globals\n\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\n// with a local `window` variable (e.g. the test harness fake window).\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\n globals[k] = globalThis[k];\n}\n// If a window object is in scope (browser or test harness), capture it\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\nif (typeof window !== \"undefined\") {\n globals[\"window\"] = window;\n}\n\n// Transfer common primitives\nglobals.undefined = undefined;\nglobals.Infinity = Infinity;\nglobals.NaN = NaN;\n\nvar vm = new VM(decodeBytecode(BYTECODE), MAIN_START_PC, CONSTANTS, globals);\nvm.run();\n";
|
|
15
|
-
const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
|
|
18
|
+
const readVMRuntimeFile = () => "import { OP_ORIGINAL as OP } from \"./compiler.ts\";\r\nconst BYTECODE = [];\r\nconst MAIN_START_PC = 0;\r\nconst CONSTANTS = [];\r\nconst ENCODE_BYTECODE = false;\r\nconst TIMING_CHECKS = false;\r\n// The text above is not included in the compiled output - for type intellisense only\r\n// @START\r\n\r\nfunction decodeBytecode(s) {\r\n if (!ENCODE_BYTECODE) return s;\r\n\r\n var b =\r\n typeof Buffer !== \"undefined\"\r\n ? Buffer.from(s, \"base64\")\r\n : Uint8Array.from(atob(s), function (c) {\r\n return c.charCodeAt(0);\r\n });\r\n // Each slot is a u16 stored as 2 little-endian bytes.\r\n var r = new Uint16Array(b.length / 2);\r\n for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | (b[i * 2 + 1] << 8);\r\n return r;\r\n}\r\n\r\n// Closure symbol\r\n// Used to tag shell functions so the VM can fast-path back to the\r\n// inner Closure instead of going through a sub-VM on internal calls.\r\nvar CLOSURE_SYM = Symbol(); // Nameless for obfuscation\r\n\r\n// Upvalue\r\n// While the outer frame is alive: reads/writes go to frame.locals[slot].\r\n// After the outer frame returns (closed): reads/writes hit this.value.\r\nfunction Upvalue(frame, slot) {\r\n this._frame = frame;\r\n this._slot = slot;\r\n this._closed = false;\r\n this._value = undefined;\r\n}\r\nUpvalue.prototype._read = function () {\r\n return this._closed ? this._value : this._frame.locals[this._slot];\r\n};\r\nUpvalue.prototype._write = function (v) {\r\n if (this._closed) this._value = v;\r\n else this._frame.locals[this._slot] = v;\r\n};\r\nUpvalue.prototype._close = function () {\r\n this._value = this._frame.locals[this._slot];\r\n this._closed = true;\r\n};\r\n\r\n// Closure & Frame\r\nfunction Closure(fn) {\r\n this.fn = fn;\r\n this.upvalues = [];\r\n this.prototype = {}; // <- default prototype object for \\`new\\`\r\n}\r\n\r\nfunction Frame(closure, returnPc, parent, thisVal ) {\r\n this.closure = closure;\r\n this.locals = new Array(closure.fn.localCount).fill(undefined);\r\n this._pc = closure.fn.startPc; // <- initialize from fn descriptor\r\n this._returnPc = returnPc; // pc to resume in parent frame after RETURN\r\n this._parent = parent;\r\n this.thisVal = thisVal !== undefined ? thisVal : undefined;\r\n this._newObj = null; // <- set by NEW so RETURN can see it\r\n this._handlerStack = []; // <- exception handlers pushed by TRY_SETUP\r\n}\r\n\r\n// VM\r\nfunction VM(bytecode, mainStartPc, constants, globals) {\r\n this.bytecode = bytecode;\r\n this.constants = constants;\r\n this.globals = globals;\r\n this._stack = [];\r\n this._frameStack = [];\r\n this._openUpvalues = []; // all currently open Upvalue objects across all frames\r\n\r\n var mainFn = {\r\n paramCount: 0,\r\n localCount: 0,\r\n startPc: mainStartPc, // <- where main begins\r\n };\r\n this._currentFrame = new Frame(new Closure(mainFn), null, null);\r\n}\r\n\r\nVM.prototype._push = function (v) {\r\n this._stack.push(v);\r\n};\r\nVM.prototype._pop = function () {\r\n return this._stack.pop();\r\n};\r\nVM.prototype.peek = function () {\r\n return this._stack[this._stack.length - 1];\r\n};\r\n\r\n// Consume the next slot from the flat bytecode stream and advance the PC.\r\n// Called by opcode handlers to read each of their operands in order.\r\nVM.prototype._operand = function () {\r\n return this.bytecode[this._currentFrame._pc++];\r\n};\r\n\r\nVM.prototype.captureUpvalue = function (frame, slot) {\r\n // Reuse existing open upvalue for this frame+slot if one exists.\r\n // This is what makes two closures share the same mutable cell.\r\n for (var i = 0; i < this._openUpvalues.length; i++) {\r\n var uv = this._openUpvalues[i];\r\n if (uv._frame === frame && uv._slot === slot) return uv;\r\n }\r\n var uv = new Upvalue(frame, slot);\r\n this._openUpvalues.push(uv);\r\n return uv;\r\n};\r\n\r\nVM.prototype._closeUpvaluesFor = function (frame) {\r\n // Called on RETURN - close every upvalue that was pointing into this frame.\r\n // After this, closures that captured from the frame read from upvalue.value.\r\n this._openUpvalues = this._openUpvalues.filter(function (uv) {\r\n if (uv._frame === frame) {\r\n uv._close();\r\n return false;\r\n }\r\n return true;\r\n });\r\n};\r\n\r\nVM.prototype.run = function () {\r\n var now = () => {\r\n return performance.now();\r\n };\r\n\r\n var lastTime = now();\r\n\r\n while (true) {\r\n var frame = this._currentFrame;\r\n var bc = this.bytecode;\r\n if (frame._pc >= bc.length) break;\r\n\r\n var op = this.bytecode[frame._pc++];\r\n\r\n // console.log(frame._pc - 1, op);\r\n\r\n // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks\r\n if (TIMING_CHECKS) {\r\n var currentTime = now();\r\n var isTamper = currentTime - lastTime > 1000;\r\n lastTime = currentTime;\r\n if (isTamper) {\r\n // Poison the bytecode\r\n for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0;\r\n // Break the current state\r\n op = OP.POP;\r\n this._stack = [];\r\n }\r\n }\r\n\r\n try {\r\n /* @SWITCH */\r\n switch (op) {\r\n case OP.LOAD_CONST:\r\n this._push(this.constants[this._operand()]);\r\n break;\r\n\r\n case OP.LOAD_INT:\r\n this._push(this._operand());\r\n break;\r\n\r\n case OP.LOAD_LOCAL:\r\n this._push(frame.locals[this._operand()]);\r\n break;\r\n\r\n case OP.STORE_LOCAL:\r\n frame.locals[this._operand()] = this._pop();\r\n break;\r\n\r\n case OP.LOAD_GLOBAL:\r\n this._push(this.globals[this.constants[this._operand()]]);\r\n break;\r\n\r\n case OP.STORE_GLOBAL:\r\n this.globals[this.constants[this._operand()]] = this._pop();\r\n break;\r\n\r\n case OP.GET_PROP: {\r\n // Stack: [..., obj, key] -> [..., obj, obj[key]]\r\n // obj is PEEKED (not popped) - CALL_METHOD needs it as receiver\r\n var key = this._pop();\r\n var obj = this.peek();\r\n this._push(obj[key]);\r\n break;\r\n }\r\n\r\n case OP.ADD: {\r\n var b = this._pop();\r\n this._push(this._pop() + b);\r\n break;\r\n }\r\n case OP.SUB: {\r\n var b = this._pop();\r\n this._push(this._pop() - b);\r\n break;\r\n }\r\n case OP.MUL: {\r\n var b = this._pop();\r\n this._push(this._pop() * b);\r\n break;\r\n }\r\n case OP.DIV: {\r\n var b = this._pop();\r\n this._push(this._pop() / b);\r\n break;\r\n }\r\n case OP.MOD: {\r\n var b = this._pop();\r\n this._push(this._pop() % b);\r\n break;\r\n }\r\n case OP.BAND: {\r\n var b = this._pop();\r\n this._push(this._pop() & b);\r\n break;\r\n }\r\n case OP.BOR: {\r\n var b = this._pop();\r\n this._push(this._pop() | b);\r\n break;\r\n }\r\n case OP.BXOR: {\r\n var b = this._pop();\r\n this._push(this._pop() ^ b);\r\n break;\r\n }\r\n case OP.SHL: {\r\n var b = this._pop();\r\n this._push(this._pop() << b);\r\n break;\r\n }\r\n case OP.SHR: {\r\n var b = this._pop();\r\n this._push(this._pop() >> b);\r\n break;\r\n }\r\n case OP.USHR: {\r\n var b = this._pop();\r\n this._push(this._pop() >>> b);\r\n break;\r\n }\r\n\r\n case OP.LT: {\r\n var b = this._pop();\r\n this._push(this._pop() < b);\r\n break;\r\n }\r\n case OP.GT: {\r\n var b = this._pop();\r\n this._push(this._pop() > b);\r\n break;\r\n }\r\n case OP.EQ: {\r\n var b = this._pop();\r\n this._push(this._pop() === b);\r\n break;\r\n }\r\n\r\n case OP.LTE: {\r\n var b = this._pop();\r\n this._push(this._pop() <= b);\r\n break;\r\n }\r\n case OP.GTE: {\r\n var b = this._pop();\r\n this._push(this._pop() >= b);\r\n break;\r\n }\r\n case OP.NEQ: {\r\n var b = this._pop();\r\n this._push(this._pop() !== b);\r\n break;\r\n }\r\n case OP.LOOSE_EQ: {\r\n var b = this._pop();\r\n this._push(this._pop() == b);\r\n break;\r\n }\r\n case OP.LOOSE_NEQ: {\r\n var b = this._pop();\r\n this._push(this._pop() != b);\r\n break;\r\n }\r\n\r\n case OP.IN: {\r\n var b = this._pop();\r\n this._push(this._pop() in b);\r\n break;\r\n }\r\n\r\n case OP.INSTANCEOF: {\r\n var ctor = this._pop();\r\n var obj = this._pop();\r\n if (typeof ctor === \"function\") {\r\n // Native constructor (e.g. Array, Date) - native instanceof is fine\r\n this._push(obj instanceof ctor);\r\n } else {\r\n // VM Closure - ctor.prototype was set by MAKE_CLOSURE / user assignment.\r\n // Walk obj's prototype chain looking for identity with ctor.prototype.\r\n var proto = ctor.prototype; // the .prototype property on the Closure\r\n var target = Object.getPrototypeOf(obj);\r\n var result = false;\r\n while (target !== null) {\r\n if (target === proto) {\r\n result = true;\r\n break;\r\n }\r\n target = Object.getPrototypeOf(target);\r\n }\r\n this._push(result);\r\n }\r\n break;\r\n }\r\n\r\n case OP.UNARY_NEG:\r\n this._push(-this._pop());\r\n break;\r\n case OP.UNARY_POS:\r\n this._push(this._pop());\r\n break;\r\n case OP.UNARY_NOT:\r\n this._push(!this._pop());\r\n break;\r\n case OP.UNARY_BITNOT:\r\n this._push(~this._pop());\r\n break;\r\n case OP.TYPEOF:\r\n this._push(typeof this._pop());\r\n break;\r\n case OP.VOID:\r\n this._pop();\r\n this._push(undefined);\r\n break;\r\n\r\n case OP.TYPEOF_SAFE: {\r\n // operand is a const index holding the variable name string.\r\n // Mimics JS semantics: typeof undeclaredVar === \"undefined\" (no throw).\r\n var name = this._pop(); // LOAD_CONST pushed the name - consume it\r\n var val = Object.prototype.hasOwnProperty.call(this.globals, name)\r\n ? this.globals[name]\r\n : undefined;\r\n this._push(typeof val);\r\n break;\r\n }\r\n\r\n case OP.JUMP:\r\n frame._pc = this._operand();\r\n break;\r\n\r\n case OP.JUMP_IF_FALSE: {\r\n var target = this._operand();\r\n if (!this._pop()) frame._pc = target;\r\n break;\r\n }\r\n\r\n case OP.JUMP_IF_TRUE_OR_POP: {\r\n // || semantics: if truthy, we're done - leave value, jump over RHS.\r\n // If falsy, discard it and fall through to evaluate RHS.\r\n var target = this._operand();\r\n if (this.peek()) {\r\n frame._pc = target;\r\n } else {\r\n this._pop();\r\n }\r\n break;\r\n }\r\n\r\n case OP.JUMP_IF_FALSE_OR_POP: {\r\n // && semantics: if falsy, we're done - leave value, jump over RHS.\r\n // If truthy, discard it and fall through to evaluate RHS.\r\n var target = this._operand();\r\n if (!this.peek()) {\r\n frame._pc = target;\r\n } else {\r\n this._pop();\r\n }\r\n break;\r\n }\r\n\r\n case OP.MAKE_CLOSURE: {\r\n // Inline operands: startPc, paramCount, localCount, uvCount,\r\n // [isLocal_0, idx_0, isLocal_1, idx_1, ...]\r\n var startPc = this._operand();\r\n var paramCount = this._operand();\r\n var localCount = this._operand();\r\n var uvCount = this._operand();\r\n\r\n var uvDescs = new Array(uvCount);\r\n for (var i = 0; i < uvCount; i++) {\r\n var isLocalRaw = this._operand();\r\n var uvIndex = this._operand();\r\n uvDescs[i] = { isLocal: isLocalRaw, _index: uvIndex };\r\n }\r\n\r\n var fn = {\r\n paramCount: paramCount,\r\n localCount: localCount,\r\n startPc: startPc,\r\n upvalueDescriptors: uvDescs,\r\n };\r\n\r\n var closure = new Closure(fn);\r\n for (var i = 0; i < uvDescs.length; i++) {\r\n var uvd = uvDescs[i];\r\n if (uvd.isLocal) {\r\n // Capture directly from current frame's local slot\r\n closure.upvalues.push(this.captureUpvalue(frame, uvd._index));\r\n } else {\r\n // Relay - take upvalue from the enclosing closure's list\r\n closure.upvalues.push(frame.closure.upvalues[uvd._index]);\r\n }\r\n }\r\n // Wrap in a native callable shell so host code (array methods,\r\n // test assertions, setTimeout, etc.) can invoke VM closures.\r\n // CLOSURE_SYM lets VM-internal CALL/NEW bypass the sub-VM entirely.\r\n var self = this;\r\n var shell = (function (c) {\r\n return function () {\r\n var args = Array.prototype.slice.call(arguments);\r\n var sub = new VM(self.bytecode, 0, self.constants, self.globals);\r\n // Sloppy-mode: null/undefined thisArg \u2192 global object\r\n var f = new Frame(\r\n c,\r\n null,\r\n null,\r\n this == null ? self.globals : this,\r\n );\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n sub._currentFrame = f;\r\n return sub.run();\r\n };\r\n })(closure);\r\n shell[CLOSURE_SYM] = closure;\r\n shell.prototype = closure.prototype; // unified prototype for new/instanceof\r\n this._push(shell);\r\n break;\r\n }\r\n\r\n case OP.LOAD_UPVALUE:\r\n this._push(frame.closure.upvalues[this._operand()]._read());\r\n break;\r\n\r\n case OP.STORE_UPVALUE:\r\n frame.closure.upvalues[this._operand()]._write(this._pop());\r\n break;\r\n\r\n case OP.BUILD_ARRAY: {\r\n var elems = this._stack.splice(this._stack.length - this._operand());\r\n this._push(elems);\r\n break;\r\n }\r\n\r\n case OP.BUILD_OBJECT: {\r\n // Stack has: key0, val0, key1, val1 ... keyN, valN (pushed left->right)\r\n // Pop all pairs and build the object.\r\n var pairs = this._stack.splice(\r\n this._stack.length - this._operand() * 2,\r\n );\r\n var o = {};\r\n for (var i = 0; i < pairs.length; i += 2) {\r\n o[pairs[i]] = pairs[i + 1]; // key at even index, val at odd\r\n }\r\n this._push(o);\r\n break;\r\n }\r\n case OP.SET_PROP: {\r\n // Stack: [..., obj, key, val]\r\n // Leaves val on stack - assignment is an expression in JS.\r\n var val = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n // Reflect.set performs [[Set]] without throwing on failure,\r\n // correctly simulating sloppy-mode assignment from a strict-mode host\r\n // (output.js is an ES module). This also properly invokes inherited\r\n // or prototype-chain setter functions.\r\n Reflect.set(obj, key, val);\r\n this._push(val); // assignment expression evaluates to the assigned value\r\n break;\r\n }\r\n case OP.GET_PROP_COMPUTED: {\r\n // Stack: [..., obj, key] - key is a runtime value (nums[i])\r\n // Mirrors GET_PROP but pops the key that was pushed dynamically.\r\n var key = this._pop();\r\n var obj = this._pop();\r\n this._push(obj[key]);\r\n break;\r\n }\r\n case OP.DELETE_PROP: {\r\n var key = this._pop();\r\n var obj = this._pop();\r\n this._push(delete obj[key]);\r\n break;\r\n }\r\n\r\n case OP.CALL: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure - run directly in this VM, no sub-VM overhead\r\n var c = callee[CLOSURE_SYM];\r\n // Sloppy-mode: plain function call \u2192 global object as this\r\n var f = new Frame(c, frame._pc, frame, this.globals);\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native function\r\n this._push(callee.apply(null, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.CALL_METHOD: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n var receiver = this._pop(); // left on stack by GET_PROP\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure - run directly in this VM with receiver as this\r\n var c = callee[CLOSURE_SYM];\r\n var f = new Frame(c, frame._pc, frame, receiver);\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native method\r\n this._push(callee.apply(receiver, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.LOAD_THIS:\r\n this._push(frame.thisVal);\r\n break;\r\n\r\n case OP.NEW: {\r\n var args = this._stack.splice(this._stack.length - this._operand());\r\n var callee = this._pop();\r\n if (callee && callee[CLOSURE_SYM]) {\r\n // VM closure constructor - prototype is unified via shell.prototype = closure.prototype\r\n var c = callee[CLOSURE_SYM];\r\n var newObj = Object.create(c.prototype || null);\r\n var f = new Frame(c, frame._pc, frame, newObj);\r\n f._newObj = newObj;\r\n for (var i = 0; i < args.length; i++) f.locals[i] = args[i];\r\n f.locals[c.fn.paramCount] = args;\r\n this._frameStack.push(this._currentFrame);\r\n this._currentFrame = f;\r\n } else {\r\n // Native constructor (e.g. new Error(), new Date()).\r\n // Reflect.construct is required - Object.create+apply does NOT set\r\n // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins.\r\n this._push(Reflect.construct(callee, args));\r\n }\r\n break;\r\n }\r\n\r\n case OP.RETURN: {\r\n var retVal = this._pop();\r\n this._closeUpvaluesFor(frame); // must happen before frame is abandoned\r\n if (this._frameStack.length === 0) return retVal;\r\n\r\n // new-call rule: primitive return -> discard, use the constructed object instead\r\n if (frame._newObj !== null) {\r\n if (typeof retVal !== \"object\" || retVal === null)\r\n retVal = frame._newObj;\r\n }\r\n\r\n this._currentFrame = this._frameStack.pop();\r\n this._push(retVal);\r\n break;\r\n }\r\n\r\n case OP.POP:\r\n this._pop();\r\n break;\r\n\r\n case OP.DUP:\r\n this._push(this.peek());\r\n break;\r\n\r\n case OP.THROW:\r\n throw this._pop();\r\n\r\n case OP.FOR_IN_SETUP: {\r\n // Pop the object; build an ordered list of all enumerable own+inherited\r\n // string keys by walking the prototype chain manually.\r\n // Uses getOwnPropertyNames (includes non-enumerable) + descriptor check,\r\n // so we never rely on Object.keys() and we handle inheritance correctly.\r\n var obj = this._pop();\r\n var keys = [];\r\n if (obj !== null && obj !== undefined) {\r\n var seen = Object.create(null);\r\n var cur = Object(obj); // box primitives\r\n while (cur !== null) {\r\n var ownNames = Object.getOwnPropertyNames(cur);\r\n for (var i = 0; i < ownNames.length; i++) {\r\n var k = ownNames[i];\r\n if (!(k in seen)) {\r\n seen[k] = true;\r\n var propDesc = Object.getOwnPropertyDescriptor(cur, k);\r\n if (propDesc && propDesc.enumerable) {\r\n keys.push(k);\r\n }\r\n }\r\n }\r\n cur = Object.getPrototypeOf(cur);\r\n }\r\n }\r\n this._push({ _keys: keys, i: 0 });\r\n break;\r\n }\r\n\r\n case OP.FOR_IN_NEXT: {\r\n // Operand = jump target for the done case. Must be read before the\r\n // conditional so the PC stays correctly aligned either way.\r\n var target = this._operand();\r\n var iter = this._pop();\r\n if (iter.i >= iter._keys.length) {\r\n frame._pc = target;\r\n } else {\r\n this._push(iter._keys[iter.i++]);\r\n }\r\n break;\r\n }\r\n\r\n case OP.PATCH: {\r\n // Inline operands: destPc, sliceStart, sliceEnd\r\n // Copies bytecode[sliceStart..sliceEnd) flat u16 slots to destPc.\r\n var destPc = this._operand();\r\n var sliceStart = this._operand();\r\n var sliceEnd = this._operand();\r\n\r\n for (var pi = sliceStart; pi < sliceEnd; pi++) {\r\n this.bytecode[destPc + (pi - sliceStart)] = this.bytecode[pi];\r\n }\r\n break;\r\n }\r\n\r\n case OP.TRY_SETUP: {\r\n // Push an exception handler record onto the current frame.\r\n // Saves: catch PC (operand), current stack depth, current frame-stack depth.\r\n // If an exception is thrown before TRY_END fires, the VM jumps here.\r\n frame._handlerStack.push({\r\n handlerPc: this._operand(),\r\n stackDepth: this._stack.length,\r\n frameStackDepth: this._frameStack.length,\r\n });\r\n break;\r\n }\r\n\r\n case OP.TRY_END: {\r\n // Normal exit from a try block \u2014 disarm the exception handler.\r\n frame._handlerStack.pop();\r\n break;\r\n }\r\n\r\n case OP.DEFINE_GETTER: {\r\n // Stack: [..., obj, key, getterFn]\r\n // Pops all three; defines an enumerable, configurable getter on obj.\r\n // If a setter was already defined for this key, it is preserved.\r\n var getterFn = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\r\n var getDesc = {\r\n get: getterFn,\r\n configurable: true,\r\n enumerable: true,\r\n };\r\n if (existingDesc && typeof existingDesc.set === \"function\") {\r\n getDesc.set = existingDesc.set;\r\n }\r\n Object.defineProperty(obj, key, getDesc);\r\n break;\r\n }\r\n\r\n case OP.DEFINE_SETTER: {\r\n // Stack: [..., obj, key, setterFn]\r\n // Pops all three; defines an enumerable, configurable setter on obj.\r\n // If a getter was already defined for this key, it is preserved.\r\n var setterFn = this._pop();\r\n var key = this._pop();\r\n var obj = this._pop();\r\n var existingDesc = Object.getOwnPropertyDescriptor(obj, key);\r\n var setDesc = {\r\n set: setterFn,\r\n configurable: true,\r\n enumerable: true,\r\n };\r\n if (existingDesc && typeof existingDesc.get === \"function\") {\r\n setDesc.get = existingDesc.get;\r\n }\r\n Object.defineProperty(obj, key, setDesc);\r\n break;\r\n }\r\n\r\n case OP.DEBUGGER: {\r\n debugger;\r\n break;\r\n }\r\n\r\n default:\r\n throw new Error(\r\n \"Unknown opcode: \" + op + \" at pc \" + (frame._pc - 1),\r\n );\r\n }\r\n } catch (err) {\r\n // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).\r\n // Walk from the current frame upward until we find a frame that has an open\r\n // exception handler (TRY_SETUP without a matching TRY_END).\r\n // For every frame we abandon along the way, close its captured upvalues.\r\n var handledFrame = null;\r\n var searchFrame = this._currentFrame;\r\n while (true) {\r\n if (searchFrame._handlerStack.length > 0) {\r\n handledFrame = searchFrame;\r\n break;\r\n }\r\n // No handler in this frame \u2014 abandon it and walk up.\r\n this._closeUpvaluesFor(searchFrame);\r\n if (this._frameStack.length === 0) break;\r\n searchFrame = this._frameStack.pop();\r\n this._currentFrame = searchFrame;\r\n }\r\n\r\n if (!handledFrame) throw err; // no handler anywhere \u2014 propagate to host\r\n\r\n var h = handledFrame._handlerStack.pop();\r\n // Restore the VM value stack to the depth recorded at TRY_SETUP time,\r\n // then push the caught exception so the catch binding can store it.\r\n this._stack.length = h.stackDepth;\r\n this._push(err);\r\n // Discard any call-frames that were pushed inside the try body\r\n // (functions called from within the try block that are still live).\r\n this._frameStack.length = h.frameStackDepth;\r\n // Jump to the catch block.\r\n handledFrame._pc = h.handlerPc;\r\n this._currentFrame = handledFrame;\r\n }\r\n }\r\n};\r\n\r\n// Boot\r\nvar globals = {}; // global object for globals\r\n\r\n// Always pull built-ins from globalThis so eval() scoping can't shadow them\r\n// with a local `window` variable (e.g. the test harness fake window).\r\nfor (var k of Object.getOwnPropertyNames(globalThis)) {\r\n globals[k] = globalThis[k];\r\n}\r\n// If a window object is in scope (browser or test harness), capture it\r\n// explicitly so VM code can read/write window.TEST_OUTPUT etc.\r\nif (typeof window !== \"undefined\") {\r\n globals[\"window\"] = window;\r\n}\r\n\r\n// Transfer common primitives\r\nglobals.undefined = undefined;\r\nglobals.Infinity = Infinity;\r\nglobals.NaN = NaN;\r\n\r\nvar vm = new VM(decodeBytecode(BYTECODE), MAIN_START_PC, CONSTANTS, globals);\r\nvm.run();\r\n";
|
|
19
|
+
export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
|
|
16
20
|
export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE"); // Attach source node location to pseudo bytecode instructions
|
|
17
21
|
|
|
18
22
|
// Opcodes
|
|
@@ -145,15 +149,12 @@ export const OP_ORIGINAL = {
|
|
|
145
149
|
// pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {set: fn})
|
|
146
150
|
|
|
147
151
|
DEBUGGER: 61,
|
|
148
|
-
//
|
|
152
|
+
// emits a "debugger" statement
|
|
149
153
|
|
|
150
154
|
// Push the raw integer operand directly onto the stack (no constant pool lookup).
|
|
151
155
|
// Identical pipeline to JUMP ops: {type:"label"} pseudo-operands resolve to a
|
|
152
156
|
// raw PC number that becomes the operand, which is pushed as-is at runtime.
|
|
153
|
-
LOAD_INT: 62
|
|
154
|
-
// Reserved / unused opcode slot (formerly the inline DATA header word).
|
|
155
|
-
// Kept to avoid renumbering; should never appear in compiled output.
|
|
156
|
-
DATA: 63
|
|
157
|
+
LOAD_INT: 62
|
|
157
158
|
};
|
|
158
159
|
|
|
159
160
|
// Scope
|
|
@@ -240,20 +241,23 @@ export class Compiler {
|
|
|
240
241
|
this._labelCount = 0; // monotonically increasing counter for unique label names
|
|
241
242
|
|
|
242
243
|
this.serializer = new Serializer(this);
|
|
243
|
-
this.
|
|
244
|
+
this.MACRO_OPS = {};
|
|
245
|
+
this.SPECIALIZED_OPS = {};
|
|
246
|
+
this.OP = {
|
|
247
|
+
...OP_ORIGINAL
|
|
248
|
+
};
|
|
249
|
+
|
|
244
250
|
// Construct randomized opcode mapping
|
|
245
251
|
if (this.options.randomizeOpcodes) {
|
|
246
252
|
let usedNumbers = new Set();
|
|
247
|
-
for (const key in
|
|
253
|
+
for (const key in this.OP) {
|
|
248
254
|
let val;
|
|
249
255
|
do {
|
|
250
|
-
val =
|
|
256
|
+
val = getRandomInt(0, U16_MAX);
|
|
251
257
|
} while (usedNumbers.has(val));
|
|
252
258
|
usedNumbers.add(val);
|
|
253
259
|
this.OP[key] = val;
|
|
254
260
|
}
|
|
255
|
-
} else {
|
|
256
|
-
this.OP = OP_ORIGINAL;
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
// Reverse map for comment generation
|
|
@@ -402,8 +406,7 @@ export class Compiler {
|
|
|
402
406
|
|
|
403
407
|
// Fill the placeholder that was reserved at the top of this function.
|
|
404
408
|
// Metadata (paramCount, localCount, upvalues) is stored on desc and emitted
|
|
405
|
-
// as
|
|
406
|
-
// site — the runtime reads them from the stack, not from DATA words.
|
|
409
|
+
// as inline operands on the MAKE_CLOSURE instruction via _emitMakeClosure.
|
|
407
410
|
desc.name = node.id?.name || "<anonymous>";
|
|
408
411
|
desc.entryLabel = entryLabel;
|
|
409
412
|
desc.bytecode = ctx.bc;
|
|
@@ -414,21 +417,22 @@ export class Compiler {
|
|
|
414
417
|
return desc;
|
|
415
418
|
}
|
|
416
419
|
|
|
417
|
-
// Emit
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
+
// Emit a single MAKE_CLOSURE instruction with all closure metadata packed
|
|
421
|
+
// as inline operands. The runtime reads them via _operand() — no stack
|
|
422
|
+
// shuffling needed.
|
|
420
423
|
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
424
|
+
// Flat operand layout: startPc, paramCount, localCount, uvCount,
|
|
425
|
+
// [isLocal_0, idx_0, isLocal_1, idx_1, ...]
|
|
426
|
+
_emitMakeClosure(desc, node, bc) {
|
|
427
|
+
const uvOperands = [];
|
|
425
428
|
for (const uv of desc.upvalues) {
|
|
426
|
-
|
|
427
|
-
|
|
429
|
+
uvOperands.push(uv.isLocal ? 1 : 0);
|
|
430
|
+
uvOperands.push(uv.index);
|
|
428
431
|
}
|
|
429
|
-
this.emit(bc, [this.OP.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
+
this.emit(bc, [this.OP.MAKE_CLOSURE, {
|
|
433
|
+
type: "label",
|
|
434
|
+
label: desc.entryLabel
|
|
435
|
+
}, desc.paramCount, desc.localCount, desc.upvalues.length, ...uvOperands], node);
|
|
432
436
|
}
|
|
433
437
|
|
|
434
438
|
// Main (top-level)
|
|
@@ -441,11 +445,7 @@ export class Compiler {
|
|
|
441
445
|
if (node.type !== "FunctionDeclaration") continue;
|
|
442
446
|
const desc = this.fnDescriptors.find(d => d._fnIdx === node._fnIdx);
|
|
443
447
|
const nameRef = b.constantOperand(node.id.name);
|
|
444
|
-
this.
|
|
445
|
-
this.emit(bc, [this.OP.MAKE_CLOSURE, {
|
|
446
|
-
type: "label",
|
|
447
|
-
label: desc.entryLabel
|
|
448
|
-
}], node);
|
|
448
|
+
this._emitMakeClosure(desc, node, bc);
|
|
449
449
|
this.emit(bc, [this.OP.STORE_GLOBAL, nameRef], node);
|
|
450
450
|
}
|
|
451
451
|
|
|
@@ -468,12 +468,6 @@ export class Compiler {
|
|
|
468
468
|
this.bytecode.push(instr);
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
|
-
if (this.bytecode.length > 0xffffff) throw new Error(`Program too large: ${this.bytecode.length} instructions, max 16,777,215`);
|
|
472
|
-
|
|
473
|
-
// if (this.constants.items.length > 0xffffff)
|
|
474
|
-
// throw new Error(
|
|
475
|
-
// `Constant pool too large: ${this.constants.items.length} entries, max 16,777,215`,
|
|
476
|
-
// );
|
|
477
471
|
}
|
|
478
472
|
|
|
479
473
|
// Statements
|
|
@@ -500,11 +494,7 @@ export class Compiler {
|
|
|
500
494
|
// MAKE_CLOSURE so it's captured as a live closure at runtime.
|
|
501
495
|
// (_compileFunctionDecl pushes/pops _currentCtx internally)
|
|
502
496
|
const desc = this._compileFunctionDecl(node);
|
|
503
|
-
this.
|
|
504
|
-
this.emit(bc, [this.OP.MAKE_CLOSURE, {
|
|
505
|
-
type: "label",
|
|
506
|
-
label: desc.entryLabel
|
|
507
|
-
}], node);
|
|
497
|
+
this._emitMakeClosure(desc, node, bc);
|
|
508
498
|
if (scope) {
|
|
509
499
|
const slot = scope.define(node.id.name);
|
|
510
500
|
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
@@ -1447,11 +1437,7 @@ export class Compiler {
|
|
|
1447
1437
|
// but leave the resulting closure ON THE STACK -- no store.
|
|
1448
1438
|
// The surrounding expression (assignment, call arg, return) consumes it.
|
|
1449
1439
|
const desc = this._compileFunctionDecl(node);
|
|
1450
|
-
this.
|
|
1451
|
-
this.emit(bc, [this.OP.MAKE_CLOSURE, {
|
|
1452
|
-
type: "label",
|
|
1453
|
-
label: desc.entryLabel
|
|
1454
|
-
}], node);
|
|
1440
|
+
this._emitMakeClosure(desc, node, bc);
|
|
1455
1441
|
break;
|
|
1456
1442
|
}
|
|
1457
1443
|
case "MemberExpression":
|
|
@@ -1544,11 +1530,7 @@ export class Compiler {
|
|
|
1544
1530
|
|
|
1545
1531
|
// Compile the accessor body as an anonymous function descriptor.
|
|
1546
1532
|
const desc = this._compileFunctionDecl(prop);
|
|
1547
|
-
this.
|
|
1548
|
-
this.emit(bc, [this.OP.MAKE_CLOSURE, {
|
|
1549
|
-
type: "label",
|
|
1550
|
-
label: desc.entryLabel
|
|
1551
|
-
}], node);
|
|
1533
|
+
this._emitMakeClosure(desc, prop, bc);
|
|
1552
1534
|
this.emit(bc, [prop.kind === "get" ? this.OP.DEFINE_GETTER : this.OP.DEFINE_SETTER], node);
|
|
1553
1535
|
}
|
|
1554
1536
|
break;
|
|
@@ -1589,19 +1571,27 @@ class Serializer {
|
|
|
1589
1571
|
return JSON.stringify(val); // number / string / bool
|
|
1590
1572
|
}
|
|
1591
1573
|
|
|
1592
|
-
// One instruction -> "[op,
|
|
1593
|
-
// Expects a fully-resolved instruction:
|
|
1574
|
+
// One instruction -> "[op, op1, op2, ...] // MNEMONIC description"
|
|
1575
|
+
// Expects a fully-resolved instruction: all operands are plain numbers.
|
|
1576
|
+
// Returns { text, values } where values is the flat u16 slots for this
|
|
1577
|
+
// instruction (opcode first, then one entry per operand).
|
|
1594
1578
|
_serializeInstr(instr, constants) {
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
const
|
|
1579
|
+
const op = instr[0];
|
|
1580
|
+
const operands = instr.slice(1);
|
|
1581
|
+
const resolvedOperands = operands.filter(operand => operand?.placeholder !== true).map(o => o?.resolvedValue ?? o);
|
|
1582
|
+
for (const o of resolvedOperands) {
|
|
1583
|
+
ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
|
|
1584
|
+
ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
|
|
1585
|
+
}
|
|
1586
|
+
ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
|
|
1587
|
+
const operand = resolvedOperands[0]; // first operand for single-operand comment cases
|
|
1598
1588
|
const name = this.OP_NAME[op] || `OP_${op}`;
|
|
1599
1589
|
let comment = name;
|
|
1600
1590
|
const sourceNode = instr[SOURCE_NODE_SYM];
|
|
1601
1591
|
const sourceLocation = sourceNode ? sourceNode.loc.start?.line + ":" + sourceNode.loc.start?.column + "-" + (sourceNode.loc.end?.line + ":" + sourceNode.loc.end?.column) : "";
|
|
1602
1592
|
|
|
1603
|
-
// Annotate
|
|
1604
|
-
if (
|
|
1593
|
+
// Annotate with human-readable operand meaning
|
|
1594
|
+
if (resolvedOperands.length > 0) {
|
|
1605
1595
|
switch (op) {
|
|
1606
1596
|
case this.OP.LOAD_CONST:
|
|
1607
1597
|
{
|
|
@@ -1611,14 +1601,7 @@ class Serializer {
|
|
|
1611
1601
|
}
|
|
1612
1602
|
case this.OP.MAKE_CLOSURE:
|
|
1613
1603
|
{
|
|
1614
|
-
|
|
1615
|
-
comment += ` PC ${operand}`;
|
|
1616
|
-
break;
|
|
1617
|
-
}
|
|
1618
|
-
case this.OP.DATA:
|
|
1619
|
-
{
|
|
1620
|
-
// Inline function header word — value is a raw integer
|
|
1621
|
-
comment += ` ${operand}`;
|
|
1604
|
+
comment += ` PC ${operand} (params=${resolvedOperands[1]} locals=${resolvedOperands[2]} upvalues=${resolvedOperands[3]})`;
|
|
1622
1605
|
break;
|
|
1623
1606
|
}
|
|
1624
1607
|
case this.OP.LOAD_LOCAL:
|
|
@@ -1647,32 +1630,16 @@ class Serializer {
|
|
|
1647
1630
|
comment += ` (${operand} args)`;
|
|
1648
1631
|
break;
|
|
1649
1632
|
default:
|
|
1650
|
-
comment += ` ${operand}`;
|
|
1633
|
+
comment += resolvedOperands.length === 1 ? ` ${operand}` : ` [${resolvedOperands.join(", ")}]`;
|
|
1651
1634
|
}
|
|
1652
1635
|
}
|
|
1653
1636
|
comment = comment.padEnd(40) + sourceLocation;
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
// Shared between the Serializer and the obfuscation path in _compileMain.
|
|
1657
|
-
|
|
1658
|
-
const instrText = operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
|
|
1637
|
+
const values = [op, ...resolvedOperands];
|
|
1638
|
+
const instrText = `[${values.join(", ")}]`;
|
|
1659
1639
|
const text = `${(instrText + ",").padEnd(12)} ${comment}`;
|
|
1660
|
-
if (!this.options.encodeBytecode) {
|
|
1661
|
-
return {
|
|
1662
|
-
text: text,
|
|
1663
|
-
value: operand !== undefined ? [op, operand] : [op]
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
function packInstr(instr) {
|
|
1667
|
-
const [op, operand] = instr;
|
|
1668
|
-
if (operand !== undefined && !Number.isInteger(operand)) throw new Error(`Non-integer operand: ${operand}`);
|
|
1669
|
-
if (operand !== undefined && operand < 0) throw new Error(`Negative operand: ${operand}`);
|
|
1670
|
-
if (operand !== undefined && operand > 0xffffff) throw new Error(`Operand overflow (max 0xFFFFFF): ${operand}`);
|
|
1671
|
-
return operand !== undefined ? operand << 8 | op : op;
|
|
1672
|
-
}
|
|
1673
1640
|
return {
|
|
1674
|
-
text
|
|
1675
|
-
|
|
1641
|
+
text,
|
|
1642
|
+
values
|
|
1676
1643
|
};
|
|
1677
1644
|
}
|
|
1678
1645
|
|
|
@@ -1688,26 +1655,29 @@ class Serializer {
|
|
|
1688
1655
|
|
|
1689
1656
|
// Filter out any remaining null-opcode pseudo-instructions.
|
|
1690
1657
|
// (defineLabel pseudo-ops are already stripped by resolveLabels.)
|
|
1691
|
-
_serializeBytecode(bytecode) {
|
|
1658
|
+
_serializeBytecode(bytecode, compiler) {
|
|
1659
|
+
const serialized = [];
|
|
1660
|
+
for (const instr of bytecode) {
|
|
1661
|
+
if (instr[0] === null) continue;
|
|
1662
|
+
const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
|
|
1663
|
+
if (specializedOpInfo) {
|
|
1664
|
+
const resolvedValue = instr[1]?.resolvedValue ?? instr[1];
|
|
1665
|
+
const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
|
|
1666
|
+
compiler.OP_NAME[instr[0]] = `${originalName}_${resolvedValue}`;
|
|
1667
|
+
specializedOpInfo.resolvedOperand = instr[1];
|
|
1668
|
+
}
|
|
1669
|
+
serialized.push(instr);
|
|
1670
|
+
}
|
|
1692
1671
|
return {
|
|
1693
|
-
bytecode:
|
|
1672
|
+
bytecode: serialized
|
|
1694
1673
|
};
|
|
1695
1674
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// Convert packed words -> raw 4-byte little-endian binary -> base64
|
|
1705
|
-
const buf = new Uint8Array(words.length * 4);
|
|
1706
|
-
words.forEach((w, i) => {
|
|
1707
|
-
buf[i * 4] = w & 0xff;
|
|
1708
|
-
buf[i * 4 + 1] = w >>> 8 & 0xff;
|
|
1709
|
-
buf[i * 4 + 2] = w >>> 16 & 0xff;
|
|
1710
|
-
buf[i * 4 + 3] = w >>> 24 & 0xff;
|
|
1675
|
+
_encodeBytecode(flat) {
|
|
1676
|
+
// Encode as little-endian Uint16Array -> base64.
|
|
1677
|
+
const buf = new Uint8Array(flat.length * 2);
|
|
1678
|
+
flat.forEach((w, i) => {
|
|
1679
|
+
buf[i * 2] = w & 0xff;
|
|
1680
|
+
buf[i * 2 + 1] = w >>> 8 & 0xff;
|
|
1711
1681
|
});
|
|
1712
1682
|
return Buffer.from(buf).toString("base64");
|
|
1713
1683
|
}
|
|
@@ -1716,16 +1686,23 @@ class Serializer {
|
|
|
1716
1686
|
let sections = [];
|
|
1717
1687
|
var textForm = [];
|
|
1718
1688
|
var initBody = [];
|
|
1719
|
-
var bytecodeResult = this._serializeBytecode(bytecode);
|
|
1689
|
+
var bytecodeResult = this._serializeBytecode(bytecode, compiler);
|
|
1720
1690
|
for (const instr of bytecodeResult.bytecode) {
|
|
1721
1691
|
const serialized = this._serializeInstr(instr, constants);
|
|
1722
1692
|
textForm.push(serialized.text);
|
|
1723
1693
|
}
|
|
1724
1694
|
initBody.push(textForm.map(line => `// ${line}`).join("\n"));
|
|
1695
|
+
const flat = bytecodeResult.bytecode.flatMap(instr => {
|
|
1696
|
+
let filtered = instr.filter(x => x?.placeholder !== true);
|
|
1697
|
+
let resolved = filtered.map(x => x?.resolvedValue ?? x);
|
|
1698
|
+
return resolved;
|
|
1699
|
+
});
|
|
1725
1700
|
if (this.options.encodeBytecode) {
|
|
1726
|
-
sections.push(`var BYTECODE = "${this.
|
|
1701
|
+
sections.push(`var BYTECODE = "${this._encodeBytecode(flat)}";`);
|
|
1727
1702
|
} else {
|
|
1728
|
-
|
|
1703
|
+
// Flatten each [op, ...operands] instruction into individual u16 slots.
|
|
1704
|
+
|
|
1705
|
+
sections.push(`var BYTECODE = [${flat.join(",")}]`);
|
|
1729
1706
|
}
|
|
1730
1707
|
|
|
1731
1708
|
// MAIN_START_PC
|
|
@@ -1733,7 +1710,8 @@ class Serializer {
|
|
|
1733
1710
|
sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
|
|
1734
1711
|
sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
|
|
1735
1712
|
// Opcodes
|
|
1736
|
-
|
|
1713
|
+
const object = t.objectExpression(Object.entries(this.OP).map(([name, value]) => t.objectProperty(t.identifier(name), t.numericLiteral(value))));
|
|
1714
|
+
sections.push(`var OP = ${generate(object).code};`);
|
|
1737
1715
|
|
|
1738
1716
|
// Constants must be defined before the bytecode
|
|
1739
1717
|
initBody.push(this._serializeConstants(constants));
|
|
@@ -1749,7 +1727,19 @@ export async function compileAndSerialize(sourceCode, options) {
|
|
|
1749
1727
|
let bytecode = compiler.compile(sourceCode);
|
|
1750
1728
|
|
|
1751
1729
|
// User transform passes (operate on unresolved IR with label/constant refs)
|
|
1752
|
-
|
|
1730
|
+
// macroOpcodes must run after selfModifying (so PATCH-stub bodies are in place)
|
|
1731
|
+
const passes = [];
|
|
1732
|
+
|
|
1733
|
+
// Due to current implementation, specialized must run BEFORE macroOpcodes
|
|
1734
|
+
if (options.specializedOpcodes) {
|
|
1735
|
+
passes.push(specializedOpcodes);
|
|
1736
|
+
}
|
|
1737
|
+
if (options.macroOpcodes) {
|
|
1738
|
+
passes.push(macroOpcodes);
|
|
1739
|
+
}
|
|
1740
|
+
if (options.selfModifying) {
|
|
1741
|
+
passes.push(selfModifying);
|
|
1742
|
+
}
|
|
1753
1743
|
for (const pass of passes) {
|
|
1754
1744
|
const passResult = pass(bytecode, compiler);
|
|
1755
1745
|
bytecode = passResult.bytecode;
|
|
@@ -1759,12 +1749,12 @@ export async function compileAndSerialize(sourceCode, options) {
|
|
|
1759
1749
|
const {
|
|
1760
1750
|
bytecode: labelResolved
|
|
1761
1751
|
} = resolveLabels(bytecode, compiler);
|
|
1762
|
-
|
|
1752
|
+
let {
|
|
1763
1753
|
bytecode: finalBytecode,
|
|
1764
1754
|
constants
|
|
1765
1755
|
} = resolveConstants(labelResolved);
|
|
1766
1756
|
const output = compiler.serializer.serialize(finalBytecode, constants, compiler);
|
|
1767
|
-
const finalOutput = await obfuscateRuntime(output, options);
|
|
1757
|
+
const finalOutput = await obfuscateRuntime(output, finalBytecode, options, compiler);
|
|
1768
1758
|
return {
|
|
1769
1759
|
code: finalOutput
|
|
1770
1760
|
};
|