js-confuser-vm 0.1.0 → 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.
Files changed (58) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +75 -94
  3. package/bench.ts +146 -0
  4. package/disassemble.ts +12 -0
  5. package/dist/build-runtime.js +41 -15
  6. package/dist/compiler.js +134 -60
  7. package/dist/disassembler.js +317 -0
  8. package/dist/index.js +7 -2
  9. package/dist/runtime.js +68 -46
  10. package/dist/template.js +116 -0
  11. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  12. package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
  13. package/dist/transforms/bytecode/dispatcher.js +13 -109
  14. package/dist/transforms/bytecode/macroOpcodes.js +2 -2
  15. package/dist/transforms/bytecode/resolveConstants.js +100 -0
  16. package/dist/transforms/bytecode/resolveRegisters.js +4 -0
  17. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  18. package/dist/transforms/bytecode/specializedOpcodes.js +18 -10
  19. package/dist/transforms/bytecode/stringConcealing.js +110 -0
  20. package/dist/transforms/runtime/classObfuscation.js +43 -0
  21. package/dist/transforms/runtime/handlerTable.js +91 -0
  22. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  23. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  24. package/dist/types.js +1 -1
  25. package/dist/utils/ast-utils.js +14 -0
  26. package/dist/utils/op-utils.js +0 -2
  27. package/dist/utils/pass-utils.js +100 -0
  28. package/dist/utils/profile-utils.js +3 -0
  29. package/index.ts +22 -17
  30. package/jest.config.js +14 -2
  31. package/output.disassembled.js +41 -0
  32. package/package.json +2 -1
  33. package/src/build-runtime.ts +113 -78
  34. package/src/compiler.ts +2703 -2593
  35. package/src/disassembler.ts +329 -0
  36. package/src/index.ts +12 -2
  37. package/src/options.ts +7 -1
  38. package/src/runtime.ts +84 -51
  39. package/src/template.ts +125 -1
  40. package/src/transforms/bytecode/aliasedOpcodes.ts +4 -1
  41. package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
  42. package/src/transforms/bytecode/dispatcher.ts +19 -125
  43. package/src/transforms/bytecode/macroOpcodes.ts +2 -2
  44. package/src/transforms/bytecode/resolveRegisters.ts +5 -0
  45. package/src/transforms/bytecode/specializedOpcodes.ts +22 -11
  46. package/src/transforms/bytecode/stringConcealing.ts +130 -0
  47. package/src/transforms/runtime/classObfuscation.ts +59 -0
  48. package/src/transforms/runtime/specializedOpcodes.ts +14 -9
  49. package/src/types.ts +42 -1
  50. package/src/utils/ast-utils.ts +19 -0
  51. package/src/utils/op-utils.ts +0 -2
  52. package/src/utils/pass-utils.ts +126 -0
  53. package/src/utils/profile-utils.ts +3 -0
  54. package/tsconfig.json +1 -1
  55. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  56. package/src/transforms/runtime/internalVariables.ts +0 -270
  57. package/src/transforms/runtime/microOpcodes.ts +0 -93
  58. /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Simple bytecode disassembler for debugging.
3
+ *
4
+ * Takes the bytecode debug comment block (as generated by the compiler)
5
+ * and produces a flat pseudo-code listing with registers, gotos, and labels.
6
+ */
7
+
8
+ // Regex to match a single instruction line from the debug comment block.
9
+ // Groups: raw array, opcode name, annotation (rest of line after opcode name)
10
+ const INSTR_RE = /^\s*\/\/\s*\[([^\]]+)\],\s+(\w+)\s+(.*?)$/;
11
+
12
+ // Label line: // label_name:
13
+ const LABEL_RE = /^\s*\/\/\s*(\w+):$/;
14
+ function parseBlock(commentBlock) {
15
+ const lines = [];
16
+ for (const raw of commentBlock.split("\n")) {
17
+ const trimmed = raw.trim();
18
+ if (!trimmed || !trimmed.startsWith("//")) continue;
19
+ const instrMatch = trimmed.match(INSTR_RE);
20
+ if (instrMatch) {
21
+ const nums = instrMatch[1].split(",").map(s => parseInt(s.trim(), 10));
22
+ lines.push({
23
+ kind: "instr",
24
+ instr: {
25
+ raw: nums,
26
+ opName: instrMatch[2],
27
+ annotation: instrMatch[3].trim()
28
+ }
29
+ });
30
+ continue;
31
+ }
32
+ const labelMatch = trimmed.match(LABEL_RE);
33
+ if (labelMatch) {
34
+ lines.push({
35
+ kind: "label",
36
+ label: labelMatch[1]
37
+ });
38
+ }
39
+ }
40
+ return lines;
41
+ }
42
+
43
+ // Extract a label name from an annotation string like "[4, while_exit_8]" or "while_top_7"
44
+ // Also handles "PC=fn_1_1" for closures
45
+ function extractLabel(annotation) {
46
+ // Strip trailing source location info like "3:4-7:5"
47
+ const stripped = annotation.replace(/\s+\d+:\d+-\d+:\d+\s*$/, "").trim();
48
+ const bracketMatch = stripped.match(/\[\d+,\s*(\w+)\]/);
49
+ if (bracketMatch) return bracketMatch[1];
50
+ const pcMatch = stripped.match(/\bPC=(\w+)/);
51
+ if (pcMatch) return pcMatch[1];
52
+ const gotoMatch = stripped.match(/\bgoto\s+(\w+)/);
53
+ if (gotoMatch) return gotoMatch[1];
54
+
55
+ // Bare label at end: last whitespace-separated token
56
+ const lastToken = stripped.split(/\s+/).pop();
57
+ if (lastToken && /^[a-zA-Z_]\w*$/.test(lastToken)) return lastToken;
58
+ return null;
59
+ }
60
+ const ARITH_SYMBOLS = {
61
+ ADD: "+",
62
+ SUB: "-",
63
+ MUL: "*",
64
+ DIV: "/",
65
+ MOD: "%",
66
+ BAND: "&",
67
+ BOR: "|",
68
+ BXOR: "^",
69
+ SHL: "<<",
70
+ SHR: ">>",
71
+ USHR: ">>>"
72
+ };
73
+ const CMP_SYMBOLS = {
74
+ LT: "<",
75
+ GT: ">",
76
+ LTE: "<=",
77
+ GTE: ">=",
78
+ EQ: "===",
79
+ NEQ: "!==",
80
+ LOOSE_EQ: "==",
81
+ LOOSE_NEQ: "!=",
82
+ IN: "in",
83
+ INSTANCEOF: "instanceof"
84
+ };
85
+ const UNARY_SYMBOLS = {
86
+ UNARY_NEG: "-",
87
+ UNARY_POS: "+",
88
+ UNARY_NOT: "!",
89
+ UNARY_BITNOT: "~"
90
+ };
91
+
92
+ // Extract value from annotation like 'reg[1] = "Hello"' or 'reg[1] = 42'
93
+ function extractConstValue(annotation) {
94
+ const m = annotation.match(/=\s*(.+?)(?:\s+\d+:\d+-\d+:\d+)?$/);
95
+ return m ? m[1].trim() : null;
96
+ }
97
+ function disassembleInstr(instr) {
98
+ const {
99
+ raw,
100
+ opName,
101
+ annotation
102
+ } = instr;
103
+ const r = i => `r${raw[i]}`;
104
+ switch (opName) {
105
+ case "LOAD_CONST":
106
+ {
107
+ const val = extractConstValue(annotation);
108
+ return `${r(1)} = ${val ?? `const[${raw[2]}]`}`;
109
+ }
110
+ case "LOAD_INT":
111
+ return `${r(1)} = ${raw[2]}`;
112
+ case "LOAD_GLOBAL":
113
+ {
114
+ const val = extractConstValue(annotation);
115
+ return `${r(1)} = ${val ?? `global[${raw[2]}]`}`;
116
+ }
117
+ case "LOAD_UPVALUE":
118
+ return `${r(1)} = upvalue[${raw[2]}]`;
119
+ case "LOAD_THIS":
120
+ return `${r(1)} = this`;
121
+ case "MOVE":
122
+ return `${r(1)} = ${r(2)}`;
123
+ case "STORE_GLOBAL":
124
+ {
125
+ // annotation: globals[name] = reg[src]
126
+ const val = extractConstValue(annotation);
127
+ return `global[${val ?? raw[1]}] = ${r(2)}`;
128
+ }
129
+ case "STORE_UPVALUE":
130
+ return `upvalue[${raw[1]}] = ${r(2)}`;
131
+ case "GET_PROP":
132
+ return `${r(1)} = ${r(2)}[${r(3)}]`;
133
+ case "SET_PROP":
134
+ return `${r(1)}[${r(2)}] = ${r(3)}`;
135
+ case "DELETE_PROP":
136
+ return `${r(1)} = delete ${r(2)}[${r(3)}]`;
137
+
138
+ // Arithmetic / bitwise
139
+ case "ADD":
140
+ case "SUB":
141
+ case "MUL":
142
+ case "DIV":
143
+ case "MOD":
144
+ case "BAND":
145
+ case "BOR":
146
+ case "BXOR":
147
+ case "SHL":
148
+ case "SHR":
149
+ case "USHR":
150
+ return `${r(1)} = ${r(2)} ${ARITH_SYMBOLS[opName]} ${r(3)}`;
151
+
152
+ // Comparison
153
+ case "LT":
154
+ case "GT":
155
+ case "LTE":
156
+ case "GTE":
157
+ case "EQ":
158
+ case "NEQ":
159
+ case "LOOSE_EQ":
160
+ case "LOOSE_NEQ":
161
+ case "IN":
162
+ case "INSTANCEOF":
163
+ return `${r(1)} = ${r(2)} ${CMP_SYMBOLS[opName]} ${r(3)}`;
164
+
165
+ // Unary
166
+ case "UNARY_NEG":
167
+ case "UNARY_POS":
168
+ case "UNARY_NOT":
169
+ case "UNARY_BITNOT":
170
+ return `${r(1)} = ${UNARY_SYMBOLS[opName]}${r(2)}`;
171
+ case "TYPEOF":
172
+ return `${r(1)} = typeof ${r(2)}`;
173
+ case "VOID":
174
+ return `${r(1)} = void ${r(2)}`;
175
+ case "TYPEOF_SAFE":
176
+ {
177
+ const val = extractConstValue(annotation);
178
+ return `${r(1)} = typeof ${val ?? `safe[${raw[2]}]`}`;
179
+ }
180
+
181
+ // Control flow
182
+ case "JUMP":
183
+ {
184
+ const label = extractLabel(annotation);
185
+ return `goto: ${label ?? `pc:${raw[1]}`}`;
186
+ }
187
+ case "JUMP_IF_FALSE":
188
+ {
189
+ const label = extractLabel(annotation);
190
+ return `if (!${r(1)}) goto: ${label ?? `pc:${raw[2]}`}`;
191
+ }
192
+ case "JUMP_IF_TRUE":
193
+ {
194
+ const label = extractLabel(annotation);
195
+ return `if (${r(1)}) goto: ${label ?? `pc:${raw[2]}`}`;
196
+ }
197
+ case "JUMP_REG":
198
+ return `goto: *${r(1)}`;
199
+
200
+ // Calls
201
+ case "CALL":
202
+ {
203
+ const dst = r(1);
204
+ const callee = r(2);
205
+ const argc = raw[3];
206
+ const args = raw.slice(4, 4 + argc).map((_, i) => `r${raw[4 + i]}`);
207
+ return `${dst} = ${callee}(${args.join(", ")})`;
208
+ }
209
+ case "CALL_METHOD":
210
+ {
211
+ const dst = r(1);
212
+ const recv = r(2);
213
+ const callee = r(3);
214
+ const argc = raw[4];
215
+ const args = raw.slice(5, 5 + argc).map((_, i) => `r${raw[5 + i]}`);
216
+ return `${dst} = ${callee}.call(${recv}, ${args.join(", ")})`;
217
+ }
218
+ case "NEW":
219
+ {
220
+ const dst = r(1);
221
+ const callee = r(2);
222
+ const argc = raw[3];
223
+ const args = raw.slice(4, 4 + argc).map((_, i) => `r${raw[4 + i]}`);
224
+ return `${dst} = new ${callee}(${args.join(", ")})`;
225
+ }
226
+ case "RETURN":
227
+ return `return ${r(1)}`;
228
+ case "THROW":
229
+ return `throw ${r(1)}`;
230
+
231
+ // Closures
232
+ case "MAKE_CLOSURE":
233
+ {
234
+ const dst = r(1);
235
+ const startPc = raw[2];
236
+ const paramCount = raw[3];
237
+ const regCount = raw[4];
238
+ const uvCount = raw[5];
239
+ const label = extractLabel(annotation);
240
+ const uvParts = [];
241
+ for (let i = 0; i < uvCount; i++) {
242
+ const isLocal = raw[6 + i * 2];
243
+ const idx = raw[6 + i * 2 + 1];
244
+ uvParts.push(isLocal ? `local[${idx}]` : `uv[${idx}]`);
245
+ }
246
+ const uvStr = uvCount > 0 ? `, upvalues=[${uvParts.join(", ")}]` : "";
247
+ return `${dst} = MakeClosure(${label ?? `pc:${startPc}`}, params=${paramCount}, regs=${regCount}${uvStr})`;
248
+ }
249
+
250
+ // Collections
251
+ case "BUILD_ARRAY":
252
+ {
253
+ const dst = r(1);
254
+ const count = raw[2];
255
+ const elems = raw.slice(3, 3 + count).map((_, i) => `r${raw[3 + i]}`);
256
+ return `${dst} = [${elems.join(", ")}]`;
257
+ }
258
+ case "BUILD_OBJECT":
259
+ {
260
+ const dst = r(1);
261
+ const pairCount = raw[2];
262
+ const pairs = [];
263
+ for (let i = 0; i < pairCount; i++) {
264
+ pairs.push(`[r${raw[3 + i * 2]}]: r${raw[4 + i * 2]}`);
265
+ }
266
+ return `${dst} = {${pairs.join(", ")}}`;
267
+ }
268
+
269
+ // Property definitions
270
+ case "DEFINE_GETTER":
271
+ return `Object.defineGetter(${r(1)}, ${r(2)}, ${r(3)})`;
272
+ case "DEFINE_SETTER":
273
+ return `Object.defineSetter(${r(1)}, ${r(2)}, ${r(3)})`;
274
+
275
+ // For-in
276
+ case "FOR_IN_SETUP":
277
+ return `${r(1)} = ForInSetup(${r(2)})`;
278
+ case "FOR_IN_NEXT":
279
+ {
280
+ const label = extractLabel(annotation);
281
+ return `${r(1)} = ForInNext(${r(2)}) else goto ${label ?? `pc:${raw[3]}`}`;
282
+ }
283
+
284
+ // Exception handling
285
+ case "TRY_SETUP":
286
+ {
287
+ return `try { catch -> pc:${raw[1]}, exReg=${r(2)}`;
288
+ }
289
+ case "TRY_END":
290
+ return `} // end try`;
291
+
292
+ // Self-modifying
293
+ case "PATCH":
294
+ return `patch(dest=pc:${raw[1]}, src=pc:${raw[2]}..${raw[3]})`;
295
+ case "DEBUGGER":
296
+ return `debugger`;
297
+ default:
298
+ return `??? ${opName} [${raw.join(", ")}]`;
299
+ }
300
+ }
301
+ export function disassembleCommentBlock(commentBlock) {
302
+ const lines = parseBlock(commentBlock);
303
+ let bodies = [];
304
+ let currentBody = [];
305
+ for (const line of lines) {
306
+ if (line.kind === "label") {
307
+ let newBody = [];
308
+ newBody.push(`// ${line.label}:`);
309
+ bodies.push(newBody);
310
+ currentBody = newBody;
311
+ } else if (line.instr) {
312
+ currentBody.push(` ${disassembleInstr(line.instr)}`);
313
+ }
314
+ }
315
+ const out = bodies.flatMap(body => body);
316
+ return out.join("\n");
317
+ }
package/dist/index.js CHANGED
@@ -1,10 +1,15 @@
1
1
  import { compileAndSerialize } from "./compiler.js";
2
2
  import { DEFAULT_OPTIONS } from "./options.js";
3
+ import { disassembleCommentBlock } from "./disassembler.js";
3
4
  async function obfuscate(source, options = DEFAULT_OPTIONS) {
4
- const result = compileAndSerialize(source, options);
5
+ const result = await compileAndSerialize(source, options);
5
6
  return result;
6
7
  }
8
+ async function disassemble(bytecodeComments) {
9
+ return disassembleCommentBlock(bytecodeComments);
10
+ }
7
11
  export const JsConfuserVM = {
8
- obfuscate
12
+ obfuscate,
13
+ disassemble
9
14
  };
10
15
  export default JsConfuserVM;
package/dist/runtime.js CHANGED
@@ -8,14 +8,17 @@ const TIMING_CHECKS = false;
8
8
  // The text above is not included in the compiled output - for type intellisense only
9
9
  // @START
10
10
 
11
- function decodeBytecode(s) {
12
- if (!ENCODE_BYTECODE) return s;
13
- var b = typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) {
11
+ function base64ToBytes(s) {
12
+ return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) {
14
13
  return c.charCodeAt(0);
15
14
  });
16
- // Each slot is a u16 stored as 2 little-endian bytes.
17
- var r = new Uint16Array(b.length / 2);
18
- for (var i = 0; i < r.length; i++) r[i] = b[i * 2] | b[i * 2 + 1] << 8;
15
+ }
16
+ function decodeBytecode(s) {
17
+ if (!ENCODE_BYTECODE) return s;
18
+ var b = base64ToBytes(s);
19
+ // Each slot is a u32 stored as 4 little-endian bytes.
20
+ var r = new Uint32Array(b.length / 4);
21
+ for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0;
19
22
  return r;
20
23
  }
21
24
 
@@ -88,7 +91,6 @@ function VM(bytecode, mainStartPc, mainRegCount, constants, globals) {
88
91
  startPc: mainStartPc
89
92
  };
90
93
  this._currentFrame = new Frame(new Closure(mainFn), null, null, undefined, 0, 0);
91
- this._internals = {};
92
94
  }
93
95
 
94
96
  // Consume the next slot from the flat bytecode stream and advance the PC.
@@ -124,9 +126,7 @@ VM.prototype._constant = function (idxIn, keyIn) {
124
126
  if (!key) return v;
125
127
  if (typeof v === "number") return v ^ key;
126
128
  // String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).
127
- var b = typeof Buffer !== "undefined" ? Buffer.from(v, "base64") : Uint8Array.from(atob(v), function (c) {
128
- return c.charCodeAt(0);
129
- });
129
+ var b = base64ToBytes(v);
130
130
  var out = "";
131
131
  for (var i = 0; i < b.length / 2; i++) {
132
132
  var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE
@@ -187,6 +187,7 @@ VM.prototype.run = function () {
187
187
  try {
188
188
  var regs = this._regs;
189
189
  var base = frame._base;
190
+
190
191
  /* @SWITCH */
191
192
  switch (op) {
192
193
  case OP.LOAD_CONST:
@@ -231,8 +232,7 @@ VM.prototype.run = function () {
231
232
  }
232
233
  case OP.STORE_GLOBAL:
233
234
  {
234
- // nameIdx and key are consumed inline so the concealConstants runtime
235
- // transform can rewrite this._constant() consistently.
235
+ // globals[globalName] = regs[src]
236
236
  this.globals[this._constant()] = regs[base + this._operand()];
237
237
  break;
238
238
  }
@@ -257,13 +257,14 @@ VM.prototype.run = function () {
257
257
  var obj = regs[base + this._operand()];
258
258
  var key = regs[base + this._operand()];
259
259
  var val = regs[base + this._operand()];
260
- // Reflect.set performs [[Set]] without throwing on failure,
261
- // correctly simulating sloppy-mode assignment from a strict-mode host.
260
+ // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)
262
261
  Reflect.set(obj, key, val);
263
262
  break;
264
263
  }
265
264
  case OP.DELETE_PROP:
266
265
  {
266
+ // regs[dst] = delete regs[obj][regs[key]]
267
+ // The delete operator returns true if successful which is most cases
267
268
  var dst = this._operand();
268
269
  var obj = regs[base + this._operand()];
269
270
  var key = regs[base + this._operand()];
@@ -271,7 +272,7 @@ VM.prototype.run = function () {
271
272
  break;
272
273
  }
273
274
 
274
- // ── Arithmetic (dst, src1, src2) ────────────────────────────────────
275
+ // Arithmetic (dst, src1, src2)
275
276
  case OP.ADD:
276
277
  {
277
278
  var dst = this._operand();
@@ -350,7 +351,7 @@ VM.prototype.run = function () {
350
351
  break;
351
352
  }
352
353
 
353
- // ── Comparison (dst, src1, src2) ─────────────────────────────────────
354
+ // Comparison (dst, src1, src2)
354
355
  case OP.LT:
355
356
  {
356
357
  var dst = this._operand();
@@ -416,13 +417,14 @@ VM.prototype.run = function () {
416
417
  }
417
418
  case OP.INSTANCEOF:
418
419
  {
420
+ // regs[dst] = regs[obj] instanceof regs[ctor]
419
421
  var dst = this._operand();
420
422
  var obj = regs[base + this._operand()];
421
423
  var ctor = regs[base + this._operand()];
422
424
  if (typeof ctor === "function") {
423
425
  regs[base + dst] = obj instanceof ctor;
424
426
  } else {
425
- // VM Closure - walk prototype chain for identity with ctor.prototype.
427
+ // TODO: Why is this needed?
426
428
  var proto = ctor.prototype;
427
429
  var target = Object.getPrototypeOf(obj);
428
430
  var result = false;
@@ -438,7 +440,7 @@ VM.prototype.run = function () {
438
440
  break;
439
441
  }
440
442
 
441
- // ── Unary (dst, src) ─────────────────────────────────────────────────
443
+ // Unary (dst, src)
442
444
  case OP.UNARY_NEG:
443
445
  {
444
446
  var dst = this._operand();
@@ -472,13 +474,14 @@ VM.prototype.run = function () {
472
474
  case OP.VOID:
473
475
  {
474
476
  var dst = this._operand();
475
- this._operand(); // consume src — evaluated for side-effects by compiler
477
+ this._operand(); // consumes argument (intended)
476
478
  regs[base + dst] = undefined;
477
479
  break;
478
480
  }
479
481
  case OP.TYPEOF_SAFE:
480
482
  {
481
- // dst, nameConstIdx — safe typeof for potentially-undeclared globals.
483
+ // regs[dst] = typeof window[name]
484
+ // Never throws ReferenceError, instead returns undefined for undeclared variables
482
485
  var dst = this._operand();
483
486
  var name = this._constant();
484
487
  var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined;
@@ -486,7 +489,7 @@ VM.prototype.run = function () {
486
489
  break;
487
490
  }
488
491
 
489
- // ── Control flow ──────────────────────────────────────────────────────
492
+ // Control flow
490
493
  case OP.JUMP:
491
494
  frame._pc = this._operand();
492
495
  break;
@@ -506,7 +509,7 @@ VM.prototype.run = function () {
506
509
  break;
507
510
  }
508
511
 
509
- // ── Calls ─────────────────────────────────────────────────────────────
512
+ // Calls
510
513
  case OP.CALL:
511
514
  {
512
515
  // dst, calleeReg, argc, [argReg...]
@@ -521,8 +524,12 @@ VM.prototype.run = function () {
521
524
  this._ensureRegisterWindow(newBase, closure.fn.regCount);
522
525
  this._regsTop = newBase + closure.fn.regCount;
523
526
  var f = new Frame(closure, frame._pc, frame, this.globals, dst, newBase);
524
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
525
- this._regs[newBase + i] = args[i];
527
+ if (closure.fn.hasRest) {
528
+ var restSlot = closure.fn.paramCount - 1;
529
+ for (var i = 0; i < restSlot; i++) this._regs[newBase + i] = i < args.length ? args[i] : undefined;
530
+ this._regs[newBase + restSlot] = args.slice(restSlot);
531
+ } else {
532
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++) this._regs[newBase + i] = args[i];
526
533
  }
527
534
  if (closure.fn.paramCount < closure.fn.regCount) {
528
535
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -549,8 +556,12 @@ VM.prototype.run = function () {
549
556
  this._ensureRegisterWindow(newBase, closure.fn.regCount);
550
557
  this._regsTop = newBase + closure.fn.regCount;
551
558
  var f = new Frame(closure, frame._pc, frame, receiver, dst, newBase);
552
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
553
- this._regs[newBase + i] = args[i];
559
+ if (closure.fn.hasRest) {
560
+ var restSlot = closure.fn.paramCount - 1;
561
+ for (var i = 0; i < restSlot; i++) this._regs[newBase + i] = i < args.length ? args[i] : undefined;
562
+ this._regs[newBase + restSlot] = args.slice(restSlot);
563
+ } else {
564
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++) this._regs[newBase + i] = args[i];
554
565
  }
555
566
  if (closure.fn.paramCount < closure.fn.regCount) {
556
567
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -577,8 +588,12 @@ VM.prototype.run = function () {
577
588
  this._ensureRegisterWindow(newBase, closure.fn.regCount);
578
589
  this._regsTop = newBase + closure.fn.regCount;
579
590
  var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);
580
- for (var i = 0; i < args.length && i < closure.fn.regCount; i++) {
581
- this._regs[newBase + i] = args[i];
591
+ if (closure.fn.hasRest) {
592
+ var restSlot = closure.fn.paramCount - 1;
593
+ for (var i = 0; i < restSlot; i++) this._regs[newBase + i] = i < args.length ? args[i] : undefined;
594
+ this._regs[newBase + restSlot] = args.slice(restSlot);
595
+ } else {
596
+ for (var i = 0; i < args.length && i < closure.fn.regCount; i++) this._regs[newBase + i] = args[i];
582
597
  }
583
598
  if (closure.fn.paramCount < closure.fn.regCount) {
584
599
  this._regs[newBase + closure.fn.paramCount] = args;
@@ -597,10 +612,14 @@ VM.prototype.run = function () {
597
612
  {
598
613
  var retVal = regs[base + this._operand()];
599
614
  this._closeUpvaluesFor(frame); // must happen before frame is abandoned
615
+
616
+ // Zero out callee's register window to limit exposing runtime values
617
+ var hi = frame._base + frame.closure.fn.regCount;
618
+ for (var i = frame._base; i < hi; i++) this._regs[i] = undefined;
600
619
  this._regsTop = frame._base;
601
620
  if (this._frameStack.length === 0) return retVal; // main script returning
602
621
 
603
- // new-call rule: primitive return -> discard, use the constructed object instead
622
+ // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)
604
623
  if (frame._newObj !== null) {
605
624
  if (typeof retVal !== "object" || retVal === null) retVal = frame._newObj;
606
625
  }
@@ -612,15 +631,17 @@ VM.prototype.run = function () {
612
631
  case OP.THROW:
613
632
  throw regs[base + this._operand()];
614
633
 
615
- // ── Closures ──────────────────────────────────────────────────────────
634
+ // Closures
616
635
  case OP.MAKE_CLOSURE:
617
636
  {
618
- // dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]
637
+ // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]
619
638
  var dst = this._operand();
620
639
  var startPc = this._operand();
621
640
  var paramCount = this._operand();
622
641
  var regCount = this._operand();
623
642
  var uvCount = this._operand();
643
+ var hasRest = this._operand(); // 1 if last param is a rest element
644
+
624
645
  var uvDescs = new Array(uvCount);
625
646
  for (var i = 0; i < uvCount; i++) {
626
647
  var isLocalRaw = this._operand();
@@ -634,7 +655,8 @@ VM.prototype.run = function () {
634
655
  paramCount: paramCount,
635
656
  regCount: regCount,
636
657
  startPc: startPc,
637
- upvalueDescriptors: uvDescs
658
+ upvalueDescriptors: uvDescs,
659
+ hasRest: hasRest
638
660
  };
639
661
  var closure = new Closure(fn);
640
662
  for (var i = 0; i < uvDescs.length; i++) {
@@ -656,8 +678,12 @@ VM.prototype.run = function () {
656
678
  var sub = new VM(self.bytecode, 0, c.fn.regCount, self.constants, self.globals);
657
679
  var f = new Frame(c, null, null, this == null ? self.globals : this, 0, 0);
658
680
  sub._currentFrame = f;
659
- for (var i = 0; i < args.length && i < c.fn.regCount; i++) {
660
- sub._regs[i] = args[i];
681
+ if (c.fn.hasRest) {
682
+ var restSlot = c.fn.paramCount - 1;
683
+ for (var i = 0; i < restSlot; i++) sub._regs[i] = i < args.length ? args[i] : undefined;
684
+ sub._regs[restSlot] = args.slice(restSlot);
685
+ } else {
686
+ for (var i = 0; i < args.length && i < c.fn.regCount; i++) sub._regs[i] = args[i];
661
687
  }
662
688
  if (c.fn.paramCount < c.fn.regCount) {
663
689
  sub._regs[c.fn.paramCount] = args;
@@ -671,7 +697,7 @@ VM.prototype.run = function () {
671
697
  break;
672
698
  }
673
699
 
674
- // ── Collections ───────────────────────────────────────────────────────
700
+ // Collections
675
701
  case OP.BUILD_ARRAY:
676
702
  {
677
703
  // dst, count, [elemReg...]
@@ -697,7 +723,7 @@ VM.prototype.run = function () {
697
723
  break;
698
724
  }
699
725
 
700
- // ── Property definitions (getters / setters) ──────────────────────────
726
+ // Object methods (getters / setters)
701
727
  case OP.DEFINE_GETTER:
702
728
  {
703
729
  // obj, key, fn
@@ -799,7 +825,7 @@ VM.prototype.run = function () {
799
825
  break;
800
826
  }
801
827
 
802
- // ── Self-modifying bytecode ───────────────────────────────────────────
828
+ // Self-modifying bytecode
803
829
  case OP.PATCH:
804
830
  {
805
831
  // destPc, sliceStart, sliceEnd
@@ -813,10 +839,7 @@ VM.prototype.run = function () {
813
839
  }
814
840
  case OP.JUMP_REG:
815
841
  {
816
- // Indirect jump: target PC is read from a register rather than a
817
- // bytecode immediate. Used by the jumpDispatcher pass so that static
818
- // analysis cannot determine the jump destination without tracking the
819
- // register value (which contains an encoded PC resolved at runtime).
842
+ // Indirect jump: allows VM to jump based on runtime values.
820
843
  frame._pc = regs[base + this._operand()];
821
844
  break;
822
845
  }
@@ -829,9 +852,8 @@ VM.prototype.run = function () {
829
852
  throw new Error("Unknown opcode: " + op + " at pc " + (frame._pc - 1));
830
853
  }
831
854
  } catch (err) {
832
- // Exception handler unwinding (CPython-style frame walk, Lua-style upvalue close).
833
- // Walk from the current frame upward until we find a frame that has an open
834
- // exception handler (TRY_SETUP without a matching TRY_END).
855
+ // Exception handler unwinding
856
+ // Walk from the current frame upward until we find a frame that has an open exception handler (TRY_SETUP without a matching TRY_END).
835
857
  // For every frame we abandon along the way, close its captured upvalues.
836
858
  var handledFrame = null;
837
859
  var searchFrame = this._currentFrame;
@@ -847,7 +869,7 @@ VM.prototype.run = function () {
847
869
  searchFrame = this._frameStack.pop();
848
870
  this._currentFrame = searchFrame;
849
871
  }
850
- if (!handledFrame) throw err; // no handler anywhere propagate to host
872
+ if (!handledFrame) throw err; // if there's no handler, propagate back to host
851
873
 
852
874
  var h = handledFrame._handlerStack.pop();
853
875
  // Discard any call-frames that were pushed inside the try body.
@@ -862,7 +884,7 @@ VM.prototype.run = function () {
862
884
  }
863
885
  };
864
886
 
865
- // Boot
887
+ /* @BOOT */ // <- This comment can't be removed!
866
888
  var globals = {}; // global object for globals
867
889
 
868
890
  // Always pull built-ins from globalThis so eval() scoping can't shadow them