js-confuser-vm 0.0.1 → 0.0.2

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/src/compiler.ts CHANGED
@@ -6,33 +6,45 @@ import { readFileSync } from "fs";
6
6
  import { join } from "path";
7
7
  import { stripTypeScriptTypes } from "module";
8
8
  import JSON5 from "json5";
9
+ import { choice, getRandomInt } from "./random.ts";
10
+ import * as t from "@babel/types";
11
+ import { ok } from "assert";
12
+ import { obfuscateRuntime } from "./runtimeObf.ts";
13
+ import type { Options } from "./options.ts";
9
14
 
10
15
  const traverse = traverseImport.default;
11
16
 
12
- const SHUFFLE_OPCODES = false;
13
- const PACK = true;
17
+ const readVMRuntimeFile = () => {
18
+ try {
19
+ return readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8");
20
+ } catch (e) {
21
+ return readFileSync(join(import.meta.dirname, "./runtime.js"), "utf-8");
22
+ }
23
+ };
24
+
25
+ const VM_RUNTIME = stripTypeScriptTypes(readVMRuntimeFile().split("@START")[1]);
14
26
 
15
- // ── Opcodes ──────────────────────────────────────────────────────
16
- const OP_ORIGINAL = {
27
+ // Opcodes
28
+ export const OP_ORIGINAL = {
17
29
  LOAD_CONST: 0,
18
30
  LOAD_LOCAL: 1,
19
31
  STORE_LOCAL: 2,
20
32
  LOAD_GLOBAL: 3,
21
33
  STORE_GLOBAL: 4,
22
34
  GET_PROP: 5,
23
- ADD: 6,
24
- SUB: 7,
25
- MUL: 8,
26
- DIV: 9,
35
+ ADD: 6, // a + b (both are popped)
36
+ SUB: 7, // a - b
37
+ MUL: 8, // a * b
38
+ DIV: 9, // a / b
27
39
  MAKE_CLOSURE: 10,
28
40
  CALL: 11,
29
41
  CALL_METHOD: 12,
30
42
  RETURN: 13,
31
- POP: 14,
32
- LT: 15, // pop b, pop a push (a < b)
33
- GT: 16,
34
- EQ: 17,
35
- JUMP: 18, // unconditional operand = absolute bytecode index
43
+ POP: 14, // discard top of stack
44
+ LT: 15, // pop b, pop a -> push (a < b)
45
+ GT: 16, // pop b, pop a -> push (a > b)
46
+ EQ: 17, // pop b, pop a -> push (a === b)
47
+ JUMP: 18, // unconditional - operand = absolute bytecode index
36
48
  JUMP_IF_FALSE: 19, // pop value; jump if falsy
37
49
  LTE: 20, // a <= b
38
50
  GTE: 21, // a >= b
@@ -40,19 +52,19 @@ const OP_ORIGINAL = {
40
52
  LOAD_UPVALUE: 23, // push frame.closure.upvalues[operand].read()
41
53
  STORE_UPVALUE: 24, // frame.closure.upvalues[operand].write(pop())
42
54
 
43
- // ── Unary ──────────────────────────
55
+ // Unary
44
56
  UNARY_NEG: 25, // -x
45
57
  UNARY_POS: 26, // +x
46
58
  UNARY_NOT: 27, // !x
47
59
  UNARY_BITNOT: 28, // ~x
48
60
  TYPEOF: 29, // typeof x
49
- VOID: 30, // void x always undefined
61
+ VOID: 30, // void x -> always undefined
50
62
 
51
- TYPEOF_SAFE: 31, // operand = name constIdx typeof guard for undeclared globals
52
- BUILD_ARRAY: 32, // operand = element count pops N values pushes array
53
- BUILD_OBJECT: 33, // operand = pair count pops N*2 (key,val) pushes object
54
- SET_PROP: 34, // pop val, pop key, peek obj obj[key] = val (obj stays on stack)
55
- GET_PROP_COMPUTED: 35, // pop key, peek obj push obj[key] (computed: nums[i])
63
+ TYPEOF_SAFE: 31, // operand = name constIdx - typeof guard for undeclared globals
64
+ BUILD_ARRAY: 32, // operand = element count - pops N values -> pushes array
65
+ BUILD_OBJECT: 33, // operand = pair count - pops N*2 (key,val) -> pushes object
66
+ SET_PROP: 34, // pop val, pop key, peek obj -> obj[key] = val (obj stays on stack)
67
+ GET_PROP_COMPUTED: 35, // pop key, peek obj -> push obj[key] (computed: nums[i])
56
68
 
57
69
  MOD: 36, // a % b
58
70
  BAND: 37, // a & b
@@ -62,60 +74,31 @@ const OP_ORIGINAL = {
62
74
  SHR: 41, // a >> b
63
75
  USHR: 42, // a >>> b
64
76
 
65
- JUMP_IF_FALSE_OR_POP: 43, // && if top falsy: jump (keep), else: pop, eval RHS
66
- JUMP_IF_TRUE_OR_POP: 44, // || if top truthy: jump (keep), else: pop, eval RHS
77
+ JUMP_IF_FALSE_OR_POP: 43, // && - if top falsy: jump (keep), else: pop, eval RHS
78
+ JUMP_IF_TRUE_OR_POP: 44, // || - if top truthy: jump (keep), else: pop, eval RHS
67
79
 
68
80
  DELETE_PROP: 45,
69
81
  IN: 46, // a in b
70
82
  INSTANCEOF: 47, // a instanceof b
71
83
 
72
- // ── NEW ────────────────────────────────────────────
84
+ // NEW
73
85
  LOAD_THIS: 48, // push frame.thisVal
74
- NEW: 49, // operand = argCount construct a new object
86
+ NEW: 49, // operand = argCount - construct a new object
75
87
  DUP: 50, // duplicate top of stack
76
88
  THROW: 51, // pop value, throw it
77
89
  LOOSE_EQ: 52, // a == b (abstract equality)
78
90
  LOOSE_NEQ: 53, // a != b (abstract inequality)
79
91
 
80
- FOR_IN_SETUP: 54, // pop obj build enumerable-key iterator push {keys,i}
81
- FOR_IN_NEXT: 55, // operand=exit_pc; pop iter; if donejump; else push next key
92
+ FOR_IN_SETUP: 54, // pop obj -> build enumerable-key iterator -> push {keys,i}
93
+ FOR_IN_NEXT: 55, // operand=exit_pc; pop iter; if done->jump; else push next key
82
94
 
83
- // ── Self-modifying bytecode ────────────────────────────────
95
+ // Self-modifying bytecode
84
96
  PATCH: 56, // pop destPc; constants[operand]=word[]; write words into bytecode[destPc..]
85
97
  };
86
98
 
87
- export let OP: Partial<typeof OP_ORIGINAL> = {};
88
- // Construct randomized opcode mapping
89
- if (SHUFFLE_OPCODES) {
90
- let used = new Set();
91
- for (const key in OP_ORIGINAL) {
92
- let val;
93
- do {
94
- val = Math.floor(Math.random() * 256);
95
- } while (used.has(val));
96
- used.add(val);
97
- OP[key] = val;
98
- }
99
- } else {
100
- OP = OP_ORIGINAL;
101
- }
102
-
103
- // Reverse map for comment generation
104
- const OP_NAME = Object.fromEntries(Object.entries(OP).map(([k, v]) => [v, k]));
105
-
106
- const JUMP_OPS = new Set([
107
- OP.JUMP,
108
- OP.JUMP_IF_FALSE,
109
- OP.JUMP_IF_TRUE_OR_POP,
110
- OP.JUMP_IF_FALSE_OR_POP,
111
- OP.FOR_IN_NEXT,
112
- ]);
113
-
114
- // ─────────────────────────────────────────────────────────────────
115
99
  // Constant Pool
116
100
  // Primitives (string/number/bool) are interned (deduped).
117
- // Object entries (fn descriptors) are always appended no dedup.
118
- // ─────────────────────────────────────────────────────────────────
101
+ // Object entries (fn descriptors) are always appended - no dedup.
119
102
  class ConstantPool {
120
103
  items: any[];
121
104
  _index: Map<string, number>;
@@ -126,7 +109,7 @@ class ConstantPool {
126
109
  }
127
110
 
128
111
  intern(val) {
129
- // Only intern primitives objects must use addObject()
112
+ // Only intern primitives -- objects must use addObject()
130
113
  const key = `${typeof val}:${val}`;
131
114
  if (this._index.has(key)) return this._index.get(key);
132
115
  const idx = this.items.length;
@@ -142,11 +125,9 @@ class ConstantPool {
142
125
  }
143
126
  }
144
127
 
145
- // ─────────────────────────────────────────────────────────────────
146
128
  // Scope
147
129
  // Each function call gets its own Scope. Locals are resolved to
148
- // numeric slots at compile time zero name lookups at runtime.
149
- // ─────────────────────────────────────────────────────────────────
130
+ // numeric slots at compile time -- zero name lookups at runtime.
150
131
  class Scope {
151
132
  parent: Scope | null;
152
133
  _locals: Map<string, number>;
@@ -154,7 +135,7 @@ class Scope {
154
135
 
155
136
  constructor(parent = null) {
156
137
  this.parent = parent;
157
- this._locals = new Map(); // name slot index
138
+ this._locals = new Map(); // name -> slot index
158
139
  this._next = 0;
159
140
  }
160
141
 
@@ -165,7 +146,7 @@ class Scope {
165
146
  return this._locals.get(name);
166
147
  }
167
148
 
168
- // Walk up scope chain. If we fall off the top global.
149
+ // Walk up scope chain. If we fall off the top -> global.
169
150
  resolve(name) {
170
151
  if (this._locals.has(name)) {
171
152
  return { kind: "local", slot: this._locals.get(name) };
@@ -179,11 +160,9 @@ class Scope {
179
160
  }
180
161
  }
181
162
 
182
- // ─────────────────────────────────────────────────────────────────
183
163
  // FnContext
184
164
  // Compiler-side state for the function currently being compiled.
185
- // Distinct from runtime Frame this is compile-time only.
186
- // ─────────────────────────────────────────────────────────────────
165
+ // Distinct from runtime Frame -- this is compile-time only.
187
166
  class FnContext {
188
167
  upvalues: { name: string; isLocal: number; index: number }[];
189
168
  parentCtx: FnContext | null;
@@ -201,8 +180,8 @@ class FnContext {
201
180
  }
202
181
 
203
182
  // Find or register a captured variable as an upvalue.
204
- // isLocal=true captured directly from parent's locals[index]
205
- // isLocal=false relayed from parent's own upvalue list[index]
183
+ // isLocal=true -> captured directly from parent's locals[index]
184
+ // isLocal=false -> relayed from parent's own upvalue list[index]
206
185
  addUpvalue(name, isLocal, index) {
207
186
  const existing = this.upvalues.findIndex((u) => u.name === name);
208
187
  if (existing !== -1) return existing;
@@ -212,9 +191,7 @@ class FnContext {
212
191
  }
213
192
  }
214
193
 
215
- // ─────────────────────────────────────────────────────────────────
216
194
  // Compiler
217
- // ─────────────────────────────────────────────────────────────────
218
195
  class Compiler {
219
196
  constants: ConstantPool;
220
197
  fnDescriptors: any[];
@@ -234,6 +211,10 @@ class Compiler {
234
211
  options: Options;
235
212
  serializer: Serializer;
236
213
 
214
+ OP: Partial<typeof OP_ORIGINAL>;
215
+ OP_NAME: Record<number, string>;
216
+ JUMP_OPS: Set<number>;
217
+
237
218
  constructor(options: Options) {
238
219
  this.options = options;
239
220
  this.constants = new ConstantPool();
@@ -246,11 +227,40 @@ class Compiler {
246
227
  this._forInCount = 0; // counter for synthetic for-in iterator global names
247
228
 
248
229
  this.serializer = new Serializer(this);
230
+
231
+ this.OP = {};
232
+ // Construct randomized opcode mapping
233
+ if (this.options.randomizeOpcodes) {
234
+ let usedNumbers = new Set<number>();
235
+ for (const key in OP_ORIGINAL) {
236
+ let val;
237
+ do {
238
+ val = Math.floor(Math.random() * 256);
239
+ } while (usedNumbers.has(val));
240
+ usedNumbers.add(val);
241
+ this.OP[key] = val;
242
+ }
243
+ } else {
244
+ this.OP = OP_ORIGINAL;
245
+ }
246
+
247
+ // Reverse map for comment generation
248
+ this.OP_NAME = Object.fromEntries(
249
+ Object.entries(this.OP).map(([k, v]) => [v, k]),
250
+ );
251
+
252
+ this.JUMP_OPS = new Set([
253
+ this.OP.JUMP,
254
+ this.OP.JUMP_IF_FALSE,
255
+ this.OP.JUMP_IF_TRUE_OR_POP,
256
+ this.OP.JUMP_IF_FALSE_OR_POP,
257
+ this.OP.FOR_IN_NEXT,
258
+ ]);
249
259
  }
250
260
 
251
- // ── Variable resolution ──────────────────────────────────────
261
+ // Variable resolution
252
262
  // Walks up the FnContext chain. Crossing a context boundary means
253
- // we're capturing from an outer function register an upvalue.
263
+ // we're capturing from an outer function - register an upvalue.
254
264
  _resolve(name, ctx) {
255
265
  if (!ctx) return { kind: "global" };
256
266
 
@@ -259,14 +269,14 @@ class Compiler {
259
269
  return { kind: "local", slot: ctx.scope._locals.get(name) };
260
270
  }
261
271
 
262
- // 2. No parent context must be global
272
+ // 2. No parent context -> must be global
263
273
  if (!ctx.parentCtx) return { kind: "global" };
264
274
 
265
- // 3. Ask parent recurse up the chain
275
+ // 3. Ask parent -- recurse up the chain
266
276
  const parentResult = this._resolve(name, ctx.parentCtx);
267
277
  if (parentResult.kind === "global") return { kind: "global" };
268
278
 
269
- // 4. Parent has it (as local or upvalue) register an upvalue here.
279
+ // 4. Parent has it (as local or upvalue) -- register an upvalue here.
270
280
  // isLocal=true means "take it straight from parent's locals[index]"
271
281
  // isLocal=false means "relay parent's upvalue[index]" (multi-level capture)
272
282
  const isLocal = parentResult.kind === "local";
@@ -275,16 +285,15 @@ class Compiler {
275
285
  return { kind: "upvalue", index: uvIdx };
276
286
  }
277
287
 
278
- // ── Entry point ──────────────────────────────────────────────
279
-
280
- compile(source) {
288
+ // Entry point
289
+ compile(source: string) {
281
290
  const ast = parser.parse(source, { sourceType: "script" });
282
291
 
283
292
  return this.compileAST(ast);
284
293
  }
285
294
 
286
- compileAST(ast) {
287
- // Pass 1 compile every FunctionDeclaration into a descriptor.
295
+ compileAST(ast: t.File) {
296
+ // Pass 1 - compile every FunctionDeclaration into a descriptor.
288
297
  // Traverse finds them regardless of nesting depth.
289
298
  traverse(ast, {
290
299
  FunctionDeclaration: (path) => {
@@ -296,7 +305,7 @@ class Compiler {
296
305
  },
297
306
  });
298
307
 
299
- // Pass 2 compile top-level statements into BYTECODE.
308
+ // Pass 2 -- compile top-level statements into BYTECODE.
300
309
  this._compileMain(ast.program.body);
301
310
 
302
311
  return {
@@ -305,9 +314,9 @@ class Compiler {
305
314
  };
306
315
  }
307
316
 
308
- // ── Function Declaration ──────────────────────────────────────
317
+ // Function Declaration
309
318
 
310
- _compileFunctionDecl(node) {
319
+ _compileFunctionDecl(node: t.FunctionDeclaration | t.FunctionExpression) {
311
320
  // Create a context whose parent is whatever we're currently compiling.
312
321
  // This is what lets _resolve cross function boundaries correctly.
313
322
  const ctx = new FnContext(this, this._currentCtx);
@@ -316,18 +325,20 @@ class Compiler {
316
325
 
317
326
  // Params occupy the first N local slots (args are copied in on CALL)
318
327
  for (const param of node.params) {
319
- if (param.type === "AssignmentPattern") {
320
- ctx.scope.define(param.left.name);
321
- } else {
322
- ctx.scope.define(param.name);
323
- }
328
+ let identifier = param.type === "AssignmentPattern" ? param.left : param;
329
+ ok(
330
+ identifier.type === "Identifier",
331
+ "Only simple identifiers allowed as parameters",
332
+ );
333
+
334
+ ctx.scope.define(identifier.name);
324
335
  }
325
336
 
326
337
  // Reserve the next slot for the implicit `arguments` object.
327
338
  // Slot index will always equal paramCount (params are 0..paramCount-1).
328
339
  ctx.scope.define("arguments");
329
340
 
330
- // ── Pass 2: emit default-value guards at top of fn body ─────
341
+ // Pass 2: emit default-value guards at top of fn body
331
342
  // Mirrors what JS engines do: if the caller passed undefined (or
332
343
  // nothing), evaluate the default expression and overwrite the slot.
333
344
  // Default expressions are full expressions, so f(x = a + b) and
@@ -335,17 +346,17 @@ class Compiler {
335
346
  for (const param of node.params) {
336
347
  if (param.type !== "AssignmentPattern") continue;
337
348
 
338
- const slot = ctx.scope._locals.get(param.left.name);
349
+ const slot = ctx.scope._locals.get((param.left as t.Identifier).name);
339
350
 
340
351
  // if (param === undefined) param = <default expr>
341
- ctx.bc.push([OP.LOAD_LOCAL, slot]);
342
- ctx.bc.push([OP.LOAD_CONST, this.constants.intern(undefined)]);
343
- ctx.bc.push([OP.EQ]);
344
- ctx.bc.push([OP.JUMP_IF_FALSE, 0]);
352
+ ctx.bc.push([this.OP.LOAD_LOCAL, slot]);
353
+ ctx.bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
354
+ ctx.bc.push([this.OP.EQ]);
355
+ ctx.bc.push([this.OP.JUMP_IF_FALSE, 0]);
345
356
  const skipIdx = ctx.bc.length - 1;
346
357
 
347
358
  this._compileExpr(param.right, ctx.scope, ctx.bc); // eval default
348
- ctx.bc.push([OP.STORE_LOCAL, slot]);
359
+ ctx.bc.push([this.OP.STORE_LOCAL, slot]);
349
360
 
350
361
  ctx.bc[skipIdx][1] = ctx.bc.length; // patch skip jump
351
362
  }
@@ -355,13 +366,13 @@ class Compiler {
355
366
  }
356
367
 
357
368
  // If we fall off the end of the function, implicitly return undefined.
358
- ctx.bc.push([OP.LOAD_CONST, this.constants.intern(undefined)]);
359
- ctx.bc.push([OP.RETURN]);
369
+ ctx.bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
370
+ ctx.bc.push([this.OP.RETURN]);
360
371
 
361
372
  this._currentCtx = savedCtx; // restore before touching fnDescriptors
362
373
 
363
374
  var fnIdx = this.fnDescriptors.length;
364
- node._fnIdx = fnIdx; // for error messages
375
+ (node as any)._fnIdx = fnIdx; // for error messages
365
376
 
366
377
  const desc = {
367
378
  name: node.id?.name || "<anonymous>",
@@ -382,35 +393,36 @@ class Compiler {
382
393
  return desc;
383
394
  }
384
395
 
385
- // ── Main (top-level) ─────────────────────────────────────────
386
-
387
- _compileMain(body) {
396
+ // Main (top-level)
397
+ _compileMain(body: t.Statement[]) {
388
398
  this.mainStartPc = 0; // ← record main's entry point
389
399
  const bc = this.bytecode;
390
400
 
391
- // Hoist all FunctionDeclarations: MAKE_CLOSURE STORE_GLOBAL
392
- // (mirrors JS hoisting functions are available before other code)
401
+ // Hoist all FunctionDeclarations: MAKE_CLOSURE -> STORE_GLOBAL
402
+ // (mirrors JS hoisting -- functions are available before other code)
393
403
  for (const node of body) {
394
404
  if (node.type !== "FunctionDeclaration") continue;
395
- const desc = this.fnDescriptors.find((d) => d._fnIdx === node._fnIdx);
405
+ const desc = this.fnDescriptors.find(
406
+ (d) => d._fnIdx === (node as any)._fnIdx,
407
+ );
396
408
  const nameIdx = this.constants.intern(node.id.name);
397
- bc.push([OP.MAKE_CLOSURE, desc._constIdx]);
398
- bc.push([OP.STORE_GLOBAL, nameIdx]);
409
+ bc.push([this.OP.MAKE_CLOSURE, desc._constIdx]);
410
+ bc.push([this.OP.STORE_GLOBAL, nameIdx]);
399
411
  }
400
412
 
401
413
  // Compile everything else in order
402
414
  for (const node of body) {
403
415
  if (node.type === "FunctionDeclaration") continue;
404
- this._compileStatement(node, null, bc); // null scope global context
416
+ this._compileStatement(node, null, bc); // null scope -> global context
405
417
  }
406
418
 
407
- bc.push([OP.RETURN]); // end program
419
+ bc.push([this.OP.RETURN]); // end program
408
420
 
409
421
  // Now that main is compiled, we can append all the function bodies at the end of the bytecode.
410
422
  for (const descriptor of this.fnDescriptors) {
411
423
  descriptor.startPc = this.bytecode.length;
412
424
 
413
- descriptor.bytecode.push([OP.RETURN]); // ensure every function ends with RETURN
425
+ descriptor.bytecode.push([this.OP.RETURN]); // ensure every function ends with RETURN
414
426
 
415
427
  if (this.options.selfModifying) {
416
428
  // Preamble is 2 instructions: LOAD_CONST(destPc) + PATCH(bodyConst)
@@ -430,12 +442,15 @@ class Compiler {
430
442
 
431
443
  // Emit preamble: push destination PC, then PATCH.
432
444
  const destPcConstIdx = this.constants.intern(bodyPc);
433
- this.bytecode.push([OP.LOAD_CONST, destPcConstIdx]);
434
- this.bytecode.push([OP.PATCH, bodyConstIdx]);
445
+ this.bytecode.push([this.OP.LOAD_CONST, destPcConstIdx]);
446
+ this.bytecode.push([this.OP.PATCH, bodyConstIdx]);
435
447
 
436
- // Garbage fill same length as real body, never executed (PATCH fires first).
448
+ // Garbage fill -- same length as real body, never executed (PATCH fires first).
437
449
  for (let i = 0; i < realBodyInstrs.length; i++) {
438
- this.bytecode.push([OP.LOAD_CONST, 0]);
450
+ this.bytecode.push([
451
+ choice(Object.values(this.OP)),
452
+ getRandomInt(0, 255),
453
+ ]);
439
454
  }
440
455
  } else {
441
456
  for (const instr of descriptor.bytecode) {
@@ -456,16 +471,20 @@ class Compiler {
456
471
  }
457
472
 
458
473
  _offsetJump(instr, offset) {
459
- if (JUMP_OPS.has(instr[0]) && instr[1] !== undefined) {
474
+ if (this.JUMP_OPS.has(instr[0]) && instr[1] !== undefined) {
460
475
  return [instr[0], instr[1] + offset];
461
476
  }
462
477
  return instr;
463
478
  }
464
479
 
465
- // ── Statements ───────────────────────────────────────────────
466
-
467
- _compileStatement(node, scope, bc) {
480
+ // Statements
481
+ _compileStatement(node: t.Statement, scope, bc) {
468
482
  switch (node.type) {
483
+ case "EmptyStatement": {
484
+ // nothing to emit -- bare semicolon is a no-op
485
+ break;
486
+ }
487
+
469
488
  case "BlockStatement": {
470
489
  for (const stmt of node.body) {
471
490
  this._compileStatement(stmt, scope, bc);
@@ -474,23 +493,23 @@ class Compiler {
474
493
  }
475
494
 
476
495
  case "FunctionDeclaration": {
477
- // Nested function compile it into a descriptor, then emit
496
+ // Nested function -- compile it into a descriptor, then emit
478
497
  // MAKE_CLOSURE so it's captured as a live closure at runtime.
479
498
  // (_compileFunctionDecl pushes/pops _currentCtx internally)
480
499
  const desc = this._compileFunctionDecl(node);
481
- bc.push([OP.MAKE_CLOSURE, desc._constIdx]);
500
+ bc.push([this.OP.MAKE_CLOSURE, desc._constIdx]);
482
501
  if (scope) {
483
502
  const slot = scope.define(node.id.name);
484
- bc.push([OP.STORE_LOCAL, slot]);
503
+ bc.push([this.OP.STORE_LOCAL, slot]);
485
504
  } else {
486
- bc.push([OP.STORE_GLOBAL, this.constants.intern(node.id.name)]);
505
+ bc.push([this.OP.STORE_GLOBAL, this.constants.intern(node.id.name)]);
487
506
  }
488
507
  break;
489
508
  }
490
509
 
491
510
  case "ThrowStatement": {
492
511
  this._compileExpr(node.argument, scope, bc);
493
- bc.push([OP.THROW]);
512
+ bc.push([this.OP.THROW]);
494
513
  break;
495
514
  }
496
515
 
@@ -498,15 +517,15 @@ class Compiler {
498
517
  if (node.argument) {
499
518
  this._compileExpr(node.argument, scope, bc);
500
519
  } else {
501
- bc.push([OP.LOAD_CONST, this.constants.intern(undefined)]);
520
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
502
521
  }
503
- bc.push([OP.RETURN]);
522
+ bc.push([this.OP.RETURN]);
504
523
  break;
505
524
  }
506
525
 
507
526
  case "ExpressionStatement": {
508
527
  this._compileExpr(node.expression, scope, bc);
509
- bc.push([OP.POP]); // discard return value of statement-level expressions
528
+ bc.push([this.OP.POP]); // discard return value of statement-level expressions
510
529
  break;
511
530
  }
512
531
 
@@ -516,24 +535,33 @@ class Compiler {
516
535
  if (decl.init) {
517
536
  this._compileExpr(decl.init, scope, bc);
518
537
  } else {
519
- bc.push([OP.LOAD_CONST, this.constants.intern(undefined)]);
538
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
520
539
  }
540
+
541
+ ok(
542
+ decl.id.type === "Identifier",
543
+ "Only simple identifiers can be declared",
544
+ );
545
+
521
546
  // Store: local slot if inside a function, global name otherwise
522
547
  if (scope) {
523
548
  const slot = scope.define(decl.id.name);
524
- bc.push([OP.STORE_LOCAL, slot]);
549
+ bc.push([this.OP.STORE_LOCAL, slot]);
525
550
  } else {
526
- bc.push([OP.STORE_GLOBAL, this.constants.intern(decl.id.name)]);
551
+ bc.push([
552
+ this.OP.STORE_GLOBAL,
553
+ this.constants.intern(decl.id.name),
554
+ ]);
527
555
  }
528
556
  }
529
557
  break;
530
558
  }
531
559
 
532
560
  case "IfStatement": {
533
- // 1. Compile the test expression leaves a value on the stack
561
+ // 1. Compile the test expression -> leaves a value on the stack
534
562
  this._compileExpr(node.test, scope, bc);
535
563
  // 2. Emit JUMP_IF_FALSE with placeholder target
536
- bc.push([OP.JUMP_IF_FALSE, 0]);
564
+ bc.push([this.OP.JUMP_IF_FALSE, 0]);
537
565
  const jumpIfFalseIdx = bc.length - 1;
538
566
  // 3. Compile the consequent block (the "then" branch)
539
567
  // Consequent may be a BlockStatement or a bare statement (no braces)
@@ -546,7 +574,7 @@ class Compiler {
546
574
  }
547
575
  if (node.alternate) {
548
576
  // 4a. Consequent needs to jump OVER the else block when done
549
- bc.push([OP.JUMP, 0]);
577
+ bc.push([this.OP.JUMP, 0]);
550
578
  const jumpOverElseIdx = bc.length - 1;
551
579
  // Patch JUMP_IF_FALSE to land here (start of else)
552
580
  bc[jumpIfFalseIdx][1] = bc.length;
@@ -554,14 +582,14 @@ class Compiler {
554
582
  const altBody =
555
583
  node.alternate.type === "BlockStatement"
556
584
  ? node.alternate.body
557
- : [node.alternate]; // handles `else if` it's just a nested IfStatement
585
+ : [node.alternate]; // handles `else if` -- it's just a nested IfStatement
558
586
  for (const stmt of altBody) {
559
587
  this._compileStatement(stmt, scope, bc);
560
588
  }
561
589
  // Patch the JUMP to land after the else block
562
590
  bc[jumpOverElseIdx][1] = bc.length;
563
591
  } else {
564
- // 4b. No else patch JUMP_IF_FALSE to land right after the then block
592
+ // 4b. No else -- patch JUMP_IF_FALSE to land right after the then block
565
593
  bc[jumpIfFalseIdx][1] = bc.length;
566
594
  }
567
595
  break;
@@ -580,16 +608,18 @@ class Compiler {
580
608
 
581
609
  const loopTop = bc.length;
582
610
  this._compileExpr(node.test, scope, bc);
583
- bc.push([OP.JUMP_IF_FALSE, 0]);
611
+ bc.push([this.OP.JUMP_IF_FALSE, 0]);
584
612
  const exitJumpIdx = bc.length - 1;
585
613
 
586
- for (const stmt of node.body.body) {
614
+ const whileBody =
615
+ node.body.type === "BlockStatement" ? node.body.body : [node.body];
616
+ for (const stmt of whileBody) {
587
617
  this._compileStatement(stmt, scope, bc);
588
618
  }
589
619
 
590
- // continue re-evaluate the test
620
+ // continue -> re-evaluate the test
591
621
  for (const idx of loopCtxW.continueJumps) bc[idx][1] = loopTop;
592
- bc.push([OP.JUMP, loopTop]);
622
+ bc.push([this.OP.JUMP, loopTop]);
593
623
 
594
624
  const exitTargetW = bc.length;
595
625
  bc[exitJumpIdx][1] = exitTargetW;
@@ -612,19 +642,21 @@ class Compiler {
612
642
 
613
643
  const loopTopDW = bc.length;
614
644
 
615
- for (const stmt of node.body.body) {
645
+ const doWhileBody =
646
+ node.body.type === "BlockStatement" ? node.body.body : [node.body];
647
+ for (const stmt of doWhileBody) {
616
648
  this._compileStatement(stmt, scope, bc);
617
649
  }
618
650
 
619
- // continue skip rest of body, fall through to test
651
+ // continue -> skip rest of body, fall through to test
620
652
  const continueTargetDW = bc.length;
621
653
  for (const idx of loopCtxDW.continueJumps)
622
654
  bc[idx][1] = continueTargetDW;
623
655
 
624
656
  this._compileExpr(node.test, scope, bc);
625
- bc.push([OP.JUMP_IF_FALSE, 0]);
657
+ bc.push([this.OP.JUMP_IF_FALSE, 0]);
626
658
  const exitJumpIdxDW = bc.length - 1;
627
- bc.push([OP.JUMP, loopTopDW]);
659
+ bc.push([this.OP.JUMP, loopTopDW]);
628
660
 
629
661
  const exitTargetDW = bc.length;
630
662
  bc[exitJumpIdxDW][1] = exitTargetDW;
@@ -650,34 +682,36 @@ class Compiler {
650
682
  this._compileStatement(node.init, scope, bc);
651
683
  } else {
652
684
  this._compileExpr(node.init, scope, bc);
653
- bc.push([OP.POP]);
685
+ bc.push([this.OP.POP]);
654
686
  }
655
687
  }
656
688
 
657
689
  const loopTopF = bc.length;
658
690
  if (node.test) {
659
691
  this._compileExpr(node.test, scope, bc);
660
- bc.push([OP.JUMP_IF_FALSE, 0]);
692
+ bc.push([this.OP.JUMP_IF_FALSE, 0]);
661
693
  }
662
694
  const exitJumpIdxF = node.test ? bc.length - 1 : null;
663
695
 
664
- for (const stmt of node.body.body) {
696
+ const forBody =
697
+ node.body.type === "BlockStatement" ? node.body.body : [node.body];
698
+ for (const stmt of forBody) {
665
699
  this._compileStatement(stmt, scope, bc);
666
700
  }
667
701
 
668
- // continue run update (if any) then back to test
702
+ // continue -> run update (if any) then back to test
669
703
  if (node.update) {
670
704
  const continueTargetF = bc.length;
671
705
  for (const idx of loopCtxF.continueJumps)
672
706
  bc[idx][1] = continueTargetF;
673
707
  this._compileExpr(node.update, scope, bc);
674
- bc.push([OP.POP]);
708
+ bc.push([this.OP.POP]);
675
709
  } else {
676
- // No update continue goes straight to the test
710
+ // No update -- continue goes straight to the test
677
711
  for (const idx of loopCtxF.continueJumps) bc[idx][1] = loopTopF;
678
712
  }
679
713
 
680
- bc.push([OP.JUMP, loopTopF]);
714
+ bc.push([this.OP.JUMP, loopTopF]);
681
715
 
682
716
  const exitTargetF = bc.length;
683
717
  if (exitJumpIdxF !== null) bc[exitJumpIdxF][1] = exitTargetF;
@@ -688,7 +722,7 @@ class Compiler {
688
722
  }
689
723
 
690
724
  case "BreakStatement": {
691
- bc.push([OP.JUMP, 0]);
725
+ bc.push([this.OP.JUMP, 0]);
692
726
  const _bJumpIdx = bc.length - 1;
693
727
  if (node.label) {
694
728
  const _bLabelName = node.label.name;
@@ -713,7 +747,7 @@ class Compiler {
713
747
  }
714
748
 
715
749
  case "ContinueStatement": {
716
- bc.push([OP.JUMP, 0]);
750
+ bc.push([this.OP.JUMP, 0]);
717
751
  const _cJumpIdx = bc.length - 1;
718
752
  if (node.label) {
719
753
  const _cLabelName = node.label.name;
@@ -771,14 +805,14 @@ class Compiler {
771
805
  if (cas.test === null) continue; // Skip default in dispatch
772
806
 
773
807
  // Check this case: DUP; LOAD_CONST; EQ; JUMP_IF_FALSE
774
- bc.push([OP.DUP]);
808
+ bc.push([this.OP.DUP]);
775
809
  this._compileExpr(cas.test, scope, bc);
776
- bc.push([OP.EQ]);
777
- bc.push([OP.JUMP_IF_FALSE, 0]); // Jump to next check (patched later)
810
+ bc.push([this.OP.EQ]);
811
+ bc.push([this.OP.JUMP_IF_FALSE, 0]); // Jump to next check (patched later)
778
812
  const skipIdx = bc.length - 1;
779
813
 
780
814
  // If matched, jump to this case's body
781
- bc.push([OP.JUMP, 0]); // Jump to body (patched later)
815
+ bc.push([this.OP.JUMP, 0]); // Jump to body (patched later)
782
816
  bodyJumps.push({ cas, jumpIdx: bc.length - 1 });
783
817
 
784
818
  // Patch the JUMP_IF_FALSE to the next check
@@ -786,7 +820,7 @@ class Compiler {
786
820
  }
787
821
 
788
822
  // No match found: jump to default (or exit if no default)
789
- bc.push([OP.JUMP, 0]);
823
+ bc.push([this.OP.JUMP, 0]);
790
824
  const noMatchJumpIdx = bc.length - 1;
791
825
 
792
826
  // Body section: compile all case bodies in source order
@@ -812,7 +846,7 @@ class Compiler {
812
846
  }
813
847
 
814
848
  // Exit: pop the discriminant and patch break jumps
815
- bc.push([OP.POP]);
849
+ bc.push([this.OP.POP]);
816
850
  for (const idx of switchCtx.breakJumps) {
817
851
  bc[idx][1] = bc.length - 1; // Point to the POP instruction
818
852
  }
@@ -837,7 +871,7 @@ class Compiler {
837
871
  this._compileStatement(_lBody, scope, bc);
838
872
  this._pendingLabel = null; // safety clear if handler didn't consume it
839
873
  } else {
840
- // Non-loop labeled statement (e.g. labeled block) only break is valid
874
+ // Non-loop labeled statement (e.g. labeled block) -- only break is valid
841
875
  this._loopStack.push({
842
876
  type: "block",
843
877
  label: _lName,
@@ -856,10 +890,10 @@ class Compiler {
856
890
  const _fiLabel = this._pendingLabel;
857
891
  this._pendingLabel = null;
858
892
 
859
- // Evaluate the object expression on stack
893
+ // Evaluate the object expression -> on stack
860
894
  this._compileExpr(node.right, scope, bc);
861
895
  // FOR_IN_SETUP: pops obj, pushes iterator {keys, i}
862
- bc.push([OP.FOR_IN_SETUP]);
896
+ bc.push([this.OP.FOR_IN_SETUP]);
863
897
 
864
898
  // Store iterator in a hidden slot so break/continue need no cleanup
865
899
  let emitLoadIter: () => void;
@@ -867,15 +901,15 @@ class Compiler {
867
901
  if (scope) {
868
902
  // Reserve a hidden local slot (no name mapping needed)
869
903
  const iterSlot = scope._next++;
870
- emitLoadIter = () => bc.push([OP.LOAD_LOCAL, iterSlot]);
871
- emitStoreIter = () => bc.push([OP.STORE_LOCAL, iterSlot]);
904
+ emitLoadIter = () => bc.push([this.OP.LOAD_LOCAL, iterSlot]);
905
+ emitStoreIter = () => bc.push([this.OP.STORE_LOCAL, iterSlot]);
872
906
  } else {
873
- // Top level use a synthetic global that won't collide with user code
907
+ // Top level -- use a synthetic global that won't collide with user code
874
908
  const iterNameIdx = this.constants.intern(
875
909
  "__fi" + this._forInCount++,
876
910
  );
877
- emitLoadIter = () => bc.push([OP.LOAD_GLOBAL, iterNameIdx]);
878
- emitStoreIter = () => bc.push([OP.STORE_GLOBAL, iterNameIdx]);
911
+ emitLoadIter = () => bc.push([this.OP.LOAD_GLOBAL, iterNameIdx]);
912
+ emitStoreIter = () => bc.push([this.OP.STORE_GLOBAL, iterNameIdx]);
879
913
  }
880
914
  emitStoreIter();
881
915
 
@@ -891,31 +925,39 @@ class Compiler {
891
925
 
892
926
  // Load iterator, attempt to get next key
893
927
  emitLoadIter();
894
- bc.push([OP.FOR_IN_NEXT, 0]); // exit target patched below
928
+ bc.push([this.OP.FOR_IN_NEXT, 0]); // exit target patched below
895
929
  const forInNextPatch = bc.length - 1;
896
930
 
897
931
  // Assign the key (now on top of stack) to the loop variable
898
932
  if (node.left.type === "VariableDeclaration") {
899
- const name = node.left.declarations[0].id.name;
933
+ const identifier = node.left.declarations[0].id;
934
+ ok(
935
+ identifier.type === "Identifier",
936
+ "Only simple identifiers can be declared in for-in loops",
937
+ );
938
+ const name = identifier.name;
900
939
  if (scope) {
901
940
  const slot = scope.define(name);
902
- bc.push([OP.STORE_LOCAL, slot]);
941
+ bc.push([this.OP.STORE_LOCAL, slot]);
903
942
  } else {
904
- bc.push([OP.STORE_GLOBAL, this.constants.intern(name)]);
943
+ bc.push([this.OP.STORE_GLOBAL, this.constants.intern(name)]);
905
944
  }
906
945
  } else if (node.left.type === "Identifier") {
907
946
  const res = this._resolve(node.left.name, this._currentCtx);
908
947
  if (res.kind === "local") {
909
- bc.push([OP.STORE_LOCAL, res.slot]);
948
+ bc.push([this.OP.STORE_LOCAL, res.slot]);
910
949
  } else if (res.kind === "upvalue") {
911
- bc.push([OP.STORE_UPVALUE, res.index]);
950
+ bc.push([this.OP.STORE_UPVALUE, res.index]);
912
951
  } else {
913
- bc.push([OP.STORE_GLOBAL, this.constants.intern(node.left.name)]);
952
+ bc.push([
953
+ this.OP.STORE_GLOBAL,
954
+ this.constants.intern(node.left.name),
955
+ ]);
914
956
  }
915
957
  } else {
916
958
  const src = generate(node.left).code;
917
959
  throw new Error(
918
- `Unsupported for-in left-hand side: ${node.left.type}\n ${src}`,
960
+ `Unsupported for-in left-hand side: ${node.left.type}\n -> ${src}`,
919
961
  );
920
962
  }
921
963
 
@@ -926,9 +968,9 @@ class Compiler {
926
968
  this._compileStatement(stmt, scope, bc);
927
969
  }
928
970
 
929
- // continue re-load iterator and check next key
971
+ // continue -> re-load iterator and check next key
930
972
  for (const idx of loopCtxFI.continueJumps) bc[idx][1] = loopTopFI;
931
- bc.push([OP.JUMP, loopTopFI]);
973
+ bc.push([this.OP.JUMP, loopTopFI]);
932
974
 
933
975
  const exitTargetFI = bc.length;
934
976
  bc[forInNextPatch][1] = exitTargetFI;
@@ -942,65 +984,64 @@ class Compiler {
942
984
  // Use @babel/generator to reproduce the source of unsupported nodes
943
985
  // so we can emit a clear error with context.
944
986
  const src = generate(node).code;
945
- throw new Error(`Unsupported statement: ${node.type}\n ${src}`);
987
+ throw new Error(`Unsupported statement: ${node.type}\n -> ${src}`);
946
988
  }
947
989
  }
948
990
  }
949
991
 
950
- // ── Expressions ──────────────────────────────────────────────
951
-
992
+ // Expressions
952
993
  _compileExpr(node, scope, bc) {
953
994
  switch (node.type) {
954
995
  case "NumericLiteral":
955
996
  case "StringLiteral": {
956
- bc.push([OP.LOAD_CONST, this.constants.intern(node.value)]);
997
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(node.value)]);
957
998
  break;
958
999
  }
959
1000
 
960
1001
  case "BooleanLiteral": {
961
- bc.push([OP.LOAD_CONST, this.constants.intern(node.value)]);
1002
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(node.value)]);
962
1003
  break;
963
1004
  }
964
1005
 
965
1006
  case "NullLiteral": {
966
- bc.push([OP.LOAD_CONST, this.constants.intern(null)]);
1007
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(null)]);
967
1008
  break;
968
1009
  }
969
1010
 
970
1011
  case "Identifier": {
971
- // scope=null means we're at the top-level always global
1012
+ // scope=null means we're at the top-level -> always global
972
1013
  const res = this._resolve(node.name, this._currentCtx);
973
1014
  if (res.kind === "local") {
974
- bc.push([OP.LOAD_LOCAL, res.slot]);
1015
+ bc.push([this.OP.LOAD_LOCAL, res.slot]);
975
1016
  } else if (res.kind === "upvalue") {
976
- bc.push([OP.LOAD_UPVALUE, res.index]);
1017
+ bc.push([this.OP.LOAD_UPVALUE, res.index]);
977
1018
  } else {
978
- bc.push([OP.LOAD_GLOBAL, this.constants.intern(node.name)]);
1019
+ bc.push([this.OP.LOAD_GLOBAL, this.constants.intern(node.name)]);
979
1020
  }
980
1021
  break;
981
1022
  }
982
1023
 
983
1024
  case "ThisExpression": {
984
- bc.push([OP.LOAD_THIS]);
1025
+ bc.push([this.OP.LOAD_THIS]);
985
1026
  break;
986
1027
  }
987
1028
 
988
1029
  case "NewExpression": {
989
- // Push callee, then args identical layout to CALL but uses NEW opcode
1030
+ // Push callee, then args -- identical layout to CALL but uses NEW opcode
990
1031
  this._compileExpr(node.callee, scope, bc);
991
1032
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
992
- bc.push([OP.NEW, node.arguments.length]);
1033
+ bc.push([this.OP.NEW, node.arguments.length]);
993
1034
  break;
994
1035
  }
995
1036
 
996
1037
  case "SequenceExpression": {
997
- // (a, b, c) eval a POP, eval b POP, eval c leave on stack
1038
+ // (a, b, c) -> eval a -> POP, eval b -> POP, eval c -> leave on stack
998
1039
  // Matches CPython's BINARY_OP / POP_TOP pattern for comma expressions.
999
1040
  for (let i = 0; i < node.expressions.length - 1; i++) {
1000
1041
  this._compileExpr(node.expressions[i], scope, bc);
1001
- bc.push([OP.POP]); // discard intermediate result
1042
+ bc.push([this.OP.POP]); // discard intermediate result
1002
1043
  }
1003
- // Last expression its value is the result of the whole sequence
1044
+ // Last expression -- its value is the result of the whole sequence
1004
1045
  this._compileExpr(
1005
1046
  node.expressions[node.expressions.length - 1],
1006
1047
  scope,
@@ -1014,39 +1055,39 @@ class Compiler {
1014
1055
  // Identical to IfStatement codegen, just lives in expression context.
1015
1056
  this._compileExpr(node.test, scope, bc);
1016
1057
 
1017
- bc.push([OP.JUMP_IF_FALSE, 0]);
1058
+ bc.push([this.OP.JUMP_IF_FALSE, 0]);
1018
1059
  const jumpToElse = bc.length - 1;
1019
1060
 
1020
1061
  this._compileExpr(node.consequent, scope, bc);
1021
1062
 
1022
- bc.push([OP.JUMP, 0]);
1063
+ bc.push([this.OP.JUMP, 0]);
1023
1064
  const jumpToEnd = bc.length - 1;
1024
1065
 
1025
- bc[jumpToElse][1] = bc.length; // patch: false alternate
1066
+ bc[jumpToElse][1] = bc.length; // patch: false -> alternate
1026
1067
  this._compileExpr(node.alternate, scope, bc);
1027
1068
 
1028
- bc[jumpToEnd][1] = bc.length; // patch: after consequent end
1069
+ bc[jumpToEnd][1] = bc.length; // patch: after consequent -> end
1029
1070
  break;
1030
1071
  }
1031
1072
 
1032
1073
  case "LogicalExpression": {
1033
1074
  // Pattern (CPython-style):
1034
1075
  // eval LHS
1035
- // JUMP_IF_*_OR_POP target (past RHS)
1076
+ // JUMP_IF_*_OR_POP -> target (past RHS)
1036
1077
  // eval RHS ← only reached if LHS didn't short-circuit
1037
1078
  // [target lands here, stack top is the result either way]
1038
1079
 
1039
1080
  this._compileExpr(node.left, scope, bc);
1040
1081
 
1041
1082
  if (node.operator === "||") {
1042
- // Short-circuit if LHS is TRUTHY keep it, skip RHS
1043
- bc.push([OP.JUMP_IF_TRUE_OR_POP, 0]);
1083
+ // Short-circuit if LHS is TRUTHY -- keep it, skip RHS
1084
+ bc.push([this.OP.JUMP_IF_TRUE_OR_POP, 0]);
1044
1085
  const jumpIdx = bc.length - 1;
1045
1086
  this._compileExpr(node.right, scope, bc);
1046
1087
  bc[jumpIdx][1] = bc.length; // patch target to after RHS
1047
1088
  } else if (node.operator === "&&") {
1048
- // Short-circuit if LHS is FALSY keep it, skip RHS
1049
- bc.push([OP.JUMP_IF_FALSE_OR_POP, 0]);
1089
+ // Short-circuit if LHS is FALSY -- keep it, skip RHS
1090
+ bc.push([this.OP.JUMP_IF_FALSE_OR_POP, 0]);
1050
1091
  const jumpIdx = bc.length - 1;
1051
1092
  this._compileExpr(node.right, scope, bc);
1052
1093
  bc[jumpIdx][1] = bc.length; // patch target to after RHS
@@ -1060,30 +1101,30 @@ class Compiler {
1060
1101
  this._compileExpr(node.left, scope, bc);
1061
1102
  this._compileExpr(node.right, scope, bc);
1062
1103
  const arithOp = {
1063
- "+": OP.ADD,
1064
- "-": OP.SUB,
1065
- "*": OP.MUL,
1066
- "/": OP.DIV,
1067
- "%": OP.MOD,
1068
- "&": OP.BAND,
1069
- "|": OP.BOR,
1070
- "^": OP.BXOR,
1071
- "<<": OP.SHL,
1072
- ">>": OP.SHR,
1073
- ">>>": OP.USHR,
1104
+ "+": this.OP.ADD,
1105
+ "-": this.OP.SUB,
1106
+ "*": this.OP.MUL,
1107
+ "/": this.OP.DIV,
1108
+ "%": this.OP.MOD,
1109
+ "&": this.OP.BAND,
1110
+ "|": this.OP.BOR,
1111
+ "^": this.OP.BXOR,
1112
+ "<<": this.OP.SHL,
1113
+ ">>": this.OP.SHR,
1114
+ ">>>": this.OP.USHR,
1074
1115
  }[node.operator];
1075
1116
 
1076
1117
  const cmpOp = {
1077
- "<": OP.LT,
1078
- ">": OP.GT,
1079
- "===": OP.EQ,
1080
- "==": OP.LOOSE_EQ,
1081
- "<=": OP.LTE,
1082
- ">=": OP.GTE,
1083
- "!==": OP.NEQ,
1084
- "!=": OP.LOOSE_NEQ,
1085
- in: OP.IN, // ← add
1086
- instanceof: OP.INSTANCEOF, // ← add
1118
+ "<": this.OP.LT,
1119
+ ">": this.OP.GT,
1120
+ "===": this.OP.EQ,
1121
+ "==": this.OP.LOOSE_EQ,
1122
+ "<=": this.OP.LTE,
1123
+ ">=": this.OP.GTE,
1124
+ "!==": this.OP.NEQ,
1125
+ "!=": this.OP.LOOSE_NEQ,
1126
+ in: this.OP.IN, // ← add
1127
+ instanceof: this.OP.INSTANCEOF, // ← add
1087
1128
  }[node.operator];
1088
1129
  const resolvedOp = arithOp ?? cmpOp;
1089
1130
  if (resolvedOp === undefined)
@@ -1095,34 +1136,34 @@ class Compiler {
1095
1136
 
1096
1137
  case "UpdateExpression": {
1097
1138
  const res = this._resolve(node.argument.name, this._currentCtx);
1098
- const bumpOp = node.operator === "++" ? OP.ADD : OP.SUB;
1139
+ const bumpOp = node.operator === "++" ? this.OP.ADD : this.OP.SUB;
1099
1140
  const one = this.constants.intern(1);
1100
1141
 
1101
1142
  // Helper closures: emit load / store for whichever resolution kind we have
1102
1143
  const emitLoad = () => {
1103
- if (res.kind === "local") bc.push([OP.LOAD_LOCAL, res.slot]);
1144
+ if (res.kind === "local") bc.push([this.OP.LOAD_LOCAL, res.slot]);
1104
1145
  else if (res.kind === "upvalue")
1105
- bc.push([OP.LOAD_UPVALUE, res.index]);
1146
+ bc.push([this.OP.LOAD_UPVALUE, res.index]);
1106
1147
  else
1107
1148
  bc.push([
1108
- OP.LOAD_GLOBAL,
1149
+ this.OP.LOAD_GLOBAL,
1109
1150
  this.constants.intern(node.argument.name),
1110
1151
  ]);
1111
1152
  };
1112
1153
  const emitStore = () => {
1113
- if (res.kind === "local") bc.push([OP.STORE_LOCAL, res.slot]);
1154
+ if (res.kind === "local") bc.push([this.OP.STORE_LOCAL, res.slot]);
1114
1155
  else if (res.kind === "upvalue")
1115
- bc.push([OP.STORE_UPVALUE, res.index]);
1156
+ bc.push([this.OP.STORE_UPVALUE, res.index]);
1116
1157
  else
1117
1158
  bc.push([
1118
- OP.STORE_GLOBAL,
1159
+ this.OP.STORE_GLOBAL,
1119
1160
  this.constants.intern(node.argument.name),
1120
1161
  ]);
1121
1162
  };
1122
1163
 
1123
1164
  emitLoad();
1124
- if (!node.prefix) bc.push([OP.DUP]); // post: save old value before mutating
1125
- bc.push([OP.LOAD_CONST, one]);
1165
+ if (!node.prefix) bc.push([this.OP.DUP]); // post: save old value before mutating
1166
+ bc.push([this.OP.LOAD_CONST, one]);
1126
1167
  bc.push([bumpOp]);
1127
1168
  emitStore();
1128
1169
  if (node.prefix) emitLoad(); // pre: reload new value as result
@@ -1132,17 +1173,17 @@ class Compiler {
1132
1173
 
1133
1174
  case "AssignmentExpression": {
1134
1175
  const compoundOp = {
1135
- "+=": OP.ADD,
1136
- "-=": OP.SUB,
1137
- "*=": OP.MUL,
1138
- "/=": OP.DIV,
1139
- "%=": OP.MOD,
1140
- "&=": OP.BAND,
1141
- "|=": OP.BOR,
1142
- "^=": OP.BXOR,
1143
- "<<=": OP.SHL,
1144
- ">>=": OP.SHR,
1145
- ">>>=": OP.USHR,
1176
+ "+=": this.OP.ADD,
1177
+ "-=": this.OP.SUB,
1178
+ "*=": this.OP.MUL,
1179
+ "/=": this.OP.DIV,
1180
+ "%=": this.OP.MOD,
1181
+ "&=": this.OP.BAND,
1182
+ "|=": this.OP.BOR,
1183
+ "^=": this.OP.BXOR,
1184
+ "<<=": this.OP.SHL,
1185
+ ">>=": this.OP.SHR,
1186
+ ">>>=": this.OP.USHR,
1146
1187
  }[node.operator];
1147
1188
 
1148
1189
  const isCompound = compoundOp !== undefined;
@@ -1151,7 +1192,7 @@ class Compiler {
1151
1192
  throw new Error(`Unsupported assignment operator: ${node.operator}`);
1152
1193
  }
1153
1194
 
1154
- // ── Member assignment: obj.x = val or arr[i] = val ──────
1195
+ // Member assignment: obj.x = val or arr[i] = val
1155
1196
  if (node.left.type === "MemberExpression") {
1156
1197
  this._compileExpr(node.left.object, scope, bc); // push obj
1157
1198
 
@@ -1159,7 +1200,7 @@ class Compiler {
1159
1200
  this._compileExpr(node.left.property, scope, bc); // push key (runtime)
1160
1201
  } else {
1161
1202
  bc.push([
1162
- OP.LOAD_CONST,
1203
+ this.OP.LOAD_CONST,
1163
1204
  this.constants.intern(node.left.property.name),
1164
1205
  ]);
1165
1206
  }
@@ -1167,7 +1208,7 @@ class Compiler {
1167
1208
  if (isCompound) {
1168
1209
  // Duplicate obj+key on the stack so we can read before we write.
1169
1210
  // Stack before DUP2: [..., obj, key]
1170
- // We need: [..., obj, key, obj, key] GET_PROP_COMPUTED [..., obj, key, currentVal]
1211
+ // We need: [..., obj, key, obj, key] -> GET_PROP_COMPUTED -> [..., obj, key, currentVal]
1171
1212
  // Cheapest approach without a DUP opcode: re-compile the member read.
1172
1213
  // (emits obj + key again; a future peephole pass could DUP instead)
1173
1214
  this._compileExpr(node.left.object, scope, bc);
@@ -1175,52 +1216,55 @@ class Compiler {
1175
1216
  this._compileExpr(node.left.property, scope, bc);
1176
1217
  } else {
1177
1218
  bc.push([
1178
- OP.LOAD_CONST,
1219
+ this.OP.LOAD_CONST,
1179
1220
  this.constants.intern(node.left.property.name),
1180
1221
  ]);
1181
1222
  }
1182
- bc.push([OP.GET_PROP_COMPUTED]); // [..., obj, key, currentVal]
1223
+ bc.push([this.OP.GET_PROP_COMPUTED]); // [..., obj, key, currentVal]
1183
1224
  this._compileExpr(node.right, scope, bc); // [..., obj, key, currentVal, rhs]
1184
1225
  bc.push([compoundOp]); // [..., obj, key, newVal]
1185
1226
  } else {
1186
1227
  this._compileExpr(node.right, scope, bc); // [..., obj, key, val]
1187
1228
  }
1188
1229
 
1189
- bc.push([OP.SET_PROP]); // obj[key] = val, leaves val on stack
1230
+ bc.push([this.OP.SET_PROP]); // obj[key] = val, leaves val on stack
1190
1231
  break;
1191
1232
  }
1192
1233
 
1193
- // ── Plain identifier assignment ────────────────────────────
1234
+ // Plain identifier assignment
1194
1235
  const res = this._resolve(node.left.name, this._currentCtx);
1195
1236
 
1196
1237
  if (isCompound) {
1197
1238
  // Load the current value of the target first
1198
1239
  if (res.kind === "local") {
1199
- bc.push([OP.LOAD_LOCAL, res.slot]);
1240
+ bc.push([this.OP.LOAD_LOCAL, res.slot]);
1200
1241
  } else if (res.kind === "upvalue") {
1201
- bc.push([OP.LOAD_UPVALUE, res.index]);
1242
+ bc.push([this.OP.LOAD_UPVALUE, res.index]);
1202
1243
  } else {
1203
- bc.push([OP.LOAD_GLOBAL, this.constants.intern(node.left.name)]);
1244
+ bc.push([
1245
+ this.OP.LOAD_GLOBAL,
1246
+ this.constants.intern(node.left.name),
1247
+ ]);
1204
1248
  }
1205
1249
  }
1206
1250
 
1207
1251
  this._compileExpr(node.right, scope, bc); // push RHS
1208
1252
 
1209
1253
  if (isCompound) {
1210
- bc.push([compoundOp]); // apply binary op leaves newVal on stack
1254
+ bc.push([compoundOp]); // apply binary op -> leaves newVal on stack
1211
1255
  }
1212
1256
 
1213
1257
  // Store & leave value on stack (assignment is an expression)
1214
1258
  if (res.kind === "local") {
1215
- bc.push([OP.STORE_LOCAL, res.slot]);
1216
- bc.push([OP.LOAD_LOCAL, res.slot]);
1259
+ bc.push([this.OP.STORE_LOCAL, res.slot]);
1260
+ bc.push([this.OP.LOAD_LOCAL, res.slot]);
1217
1261
  } else if (res.kind === "upvalue") {
1218
- bc.push([OP.STORE_UPVALUE, res.index]);
1219
- bc.push([OP.LOAD_UPVALUE, res.index]);
1262
+ bc.push([this.OP.STORE_UPVALUE, res.index]);
1263
+ bc.push([this.OP.LOAD_UPVALUE, res.index]);
1220
1264
  } else {
1221
1265
  const nameIdx = this.constants.intern(node.left.name);
1222
- bc.push([OP.STORE_GLOBAL, nameIdx]);
1223
- bc.push([OP.LOAD_GLOBAL, nameIdx]);
1266
+ bc.push([this.OP.STORE_GLOBAL, nameIdx]);
1267
+ bc.push([this.OP.LOAD_GLOBAL, nameIdx]);
1224
1268
  }
1225
1269
  break;
1226
1270
  }
@@ -1232,15 +1276,15 @@ class Compiler {
1232
1276
  this._compileExpr(node.callee.object, scope, bc);
1233
1277
  const prop = node.callee.property.name;
1234
1278
  const propIdx = this.constants.intern(prop);
1235
- bc.push([OP.LOAD_CONST, propIdx]);
1236
- bc.push([OP.GET_PROP]);
1279
+ bc.push([this.OP.LOAD_CONST, propIdx]);
1280
+ bc.push([this.OP.GET_PROP]);
1237
1281
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1238
- bc.push([OP.CALL_METHOD, node.arguments.length]);
1282
+ bc.push([this.OP.CALL_METHOD, node.arguments.length]);
1239
1283
  } else {
1240
1284
  // ── Plain call: add(5, 10)
1241
1285
  this._compileExpr(node.callee, scope, bc);
1242
1286
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1243
- bc.push([OP.CALL, node.arguments.length]);
1287
+ bc.push([this.OP.CALL, node.arguments.length]);
1244
1288
  }
1245
1289
  break;
1246
1290
  }
@@ -1252,113 +1296,133 @@ class Compiler {
1252
1296
  if (node.operator === "typeof" && node.argument.type === "Identifier") {
1253
1297
  const res = this._resolve(node.argument.name, this._currentCtx);
1254
1298
  if (res.kind === "global") {
1255
- // Potentially undeclared let VM guard it
1256
- bc.push([OP.LOAD_CONST, this.constants.intern(node.argument.name)]);
1257
- bc.push([OP.TYPEOF_SAFE]);
1299
+ // Potentially undeclared -- let VM guard it
1300
+ bc.push([
1301
+ this.OP.LOAD_CONST,
1302
+ this.constants.intern(node.argument.name),
1303
+ ]);
1304
+ bc.push([this.OP.TYPEOF_SAFE]);
1258
1305
  break;
1259
1306
  }
1260
- // Known local or upvalue safe to load first, then typeof
1307
+ // Known local or upvalue -- safe to load first, then typeof
1308
+ }
1309
+
1310
+ // Special case: delete -- argument must NOT be pre-evaluated.
1311
+ // The generic path below compiles the argument first, which would leave
1312
+ // a stale value on the stack before the delete result, corrupting it.
1313
+ if (node.operator === "delete") {
1314
+ const arg = node.argument;
1315
+ if (arg.type === "MemberExpression") {
1316
+ this._compileExpr(arg.object, scope, bc);
1317
+ if (arg.computed) {
1318
+ this._compileExpr(arg.property, scope, bc);
1319
+ } else {
1320
+ bc.push([
1321
+ this.OP.LOAD_CONST,
1322
+ this.constants.intern(arg.property.name),
1323
+ ]);
1324
+ }
1325
+ bc.push([this.OP.DELETE_PROP]);
1326
+ } else {
1327
+ // delete x, delete 0, etc. -- always true in non-strict, just push true
1328
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(true)]);
1329
+ }
1330
+ break;
1261
1331
  }
1332
+
1262
1333
  // All other unary ops: compile argument first, then apply operator
1263
1334
  this._compileExpr(node.argument, scope, bc);
1264
1335
  switch (node.operator) {
1265
1336
  case "-":
1266
- bc.push([OP.UNARY_NEG]);
1337
+ bc.push([this.OP.UNARY_NEG]);
1267
1338
  break;
1268
1339
  case "+":
1269
- bc.push([OP.UNARY_POS]);
1340
+ bc.push([this.OP.UNARY_POS]);
1270
1341
  break;
1271
1342
  case "!":
1272
- bc.push([OP.UNARY_NOT]);
1343
+ bc.push([this.OP.UNARY_NOT]);
1273
1344
  break;
1274
1345
  case "~":
1275
- bc.push([OP.UNARY_BITNOT]);
1346
+ bc.push([this.OP.UNARY_BITNOT]);
1276
1347
  break;
1277
1348
  case "typeof":
1278
- bc.push([OP.TYPEOF]);
1349
+ bc.push([this.OP.TYPEOF]);
1279
1350
  break;
1280
1351
  case "void":
1281
- bc.push([OP.VOID]);
1352
+ bc.push([this.OP.VOID]);
1282
1353
  break;
1283
1354
 
1284
- case "delete": {
1285
- const arg = node.argument;
1286
- if (arg.type === "MemberExpression") {
1287
- this._compileExpr(arg.object, scope, bc);
1288
- if (arg.computed) {
1289
- this._compileExpr(arg.property, scope, bc);
1290
- } else {
1291
- bc.push([
1292
- OP.LOAD_CONST,
1293
- this.constants.intern(arg.property.name),
1294
- ]);
1295
- }
1296
- bc.push([OP.DELETE_PROP]);
1297
- } else {
1298
- // delete x, delete 0, etc. — always true in non-strict, just push true
1299
- bc.push([OP.LOAD_CONST, this.constants.intern(true)]);
1300
- }
1301
- break;
1302
- }
1303
-
1304
1355
  default:
1305
1356
  throw new Error(`Unsupported unary operator: ${node.operator}`);
1306
1357
  }
1307
1358
  break;
1308
1359
  }
1309
1360
 
1361
+ case "RegExpLiteral": {
1362
+ // Emit: new RegExp(pattern, flags)
1363
+ // Fresh object per evaluation -- correct for stateful g/y flags.
1364
+ bc.push([this.OP.LOAD_GLOBAL, this.constants.intern("RegExp")]);
1365
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(node.pattern)]);
1366
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(node.flags)]);
1367
+ bc.push([this.OP.NEW, 2]);
1368
+ break;
1369
+ }
1370
+
1310
1371
  case "FunctionExpression": {
1311
1372
  // Compile into a descriptor exactly like a declaration,
1312
- // but leave the resulting closure ON THE STACK no store.
1373
+ // but leave the resulting closure ON THE STACK -- no store.
1313
1374
  // The surrounding expression (assignment, call arg, return) consumes it.
1314
1375
  const desc = this._compileFunctionDecl(node);
1315
- bc.push([OP.MAKE_CLOSURE, desc._constIdx]);
1376
+ bc.push([this.OP.MAKE_CLOSURE, desc._constIdx]);
1316
1377
  break;
1317
1378
  }
1318
1379
 
1319
1380
  case "MemberExpression": {
1320
1381
  this._compileExpr(node.object, scope, bc);
1321
1382
  if (node.computed) {
1322
- // nums[i] key is runtime value
1383
+ // nums[i] -- key is runtime value
1323
1384
  this._compileExpr(node.property, scope, bc);
1324
1385
  } else {
1325
- // point.x push key as string, same opcode handles both
1326
- bc.push([OP.LOAD_CONST, this.constants.intern(node.property.name)]);
1386
+ // point.x -- push key as string, same opcode handles both
1387
+ bc.push([
1388
+ this.OP.LOAD_CONST,
1389
+ this.constants.intern(node.property.name),
1390
+ ]);
1327
1391
  }
1328
1392
 
1329
- // GET_PROP_COMPUTED pops the object correct for value access.
1393
+ // GET_PROP_COMPUTED pops the object -- correct for value access.
1330
1394
  // GET_PROP (peek) is only used in CallExpression's method call path
1331
1395
  // where the receiver must survive on the stack for CALL_METHOD.
1332
- bc.push([OP.GET_PROP_COMPUTED]);
1396
+ bc.push([this.OP.GET_PROP_COMPUTED]);
1333
1397
  break;
1334
1398
  }
1335
1399
 
1336
1400
  case "ArrayExpression": {
1337
- // Compile each element leftright, then BUILD_ARRAY collapses them.
1401
+ // Compile each element left->right, then BUILD_ARRAY collapses them.
1338
1402
  // Sparse arrays (holes) get explicit undefined per slot.
1339
1403
  for (const el of node.elements) {
1340
1404
  if (el === null) {
1341
1405
  // hole: e.g. [1,,3]
1342
- bc.push([OP.LOAD_CONST, this.constants.intern(undefined)]);
1406
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(undefined)]);
1343
1407
  } else {
1344
1408
  this._compileExpr(el, scope, bc);
1345
1409
  }
1346
1410
  }
1347
- bc.push([OP.BUILD_ARRAY, node.elements.length]);
1411
+ bc.push([this.OP.BUILD_ARRAY, node.elements.length]);
1348
1412
  break;
1349
1413
  }
1350
1414
  case "ObjectExpression": {
1351
1415
  // For each property: push key (always as string), push value.
1352
- // BUILD_OBJECT pops pairs rightleft and assembles the object.
1416
+ // BUILD_OBJECT pops pairs right->left and assembles the object.
1353
1417
  for (const prop of node.properties) {
1354
1418
  if (prop.type === "SpreadElement") {
1355
1419
  throw new Error("Object spread not supported");
1356
1420
  }
1357
- // Key identifier shorthand (`{x:1}`) or string/number literal
1421
+ // Key -- identifier shorthand (`{x:1}`) or string/number literal
1358
1422
  const key = prop.key;
1359
1423
  let keyStr;
1360
1424
  if (key.type === "Identifier") {
1361
- keyStr = key.name; // {x: 1} key is "x"
1425
+ keyStr = key.name; // {x: 1} -> key is "x"
1362
1426
  } else if (
1363
1427
  key.type === "StringLiteral" ||
1364
1428
  key.type === "NumericLiteral"
@@ -1367,26 +1431,24 @@ class Compiler {
1367
1431
  } else {
1368
1432
  throw new Error(`Unsupported object key type: ${key.type}`);
1369
1433
  }
1370
- bc.push([OP.LOAD_CONST, this.constants.intern(keyStr)]);
1371
- // Value any expression, including FunctionExpression
1434
+ bc.push([this.OP.LOAD_CONST, this.constants.intern(keyStr)]);
1435
+ // Value -- any expression, including FunctionExpression
1372
1436
  this._compileExpr(prop.value, scope, bc);
1373
1437
  }
1374
- bc.push([OP.BUILD_OBJECT, node.properties.length]);
1438
+ bc.push([this.OP.BUILD_OBJECT, node.properties.length]);
1375
1439
  break;
1376
1440
  }
1377
1441
 
1378
1442
  default: {
1379
1443
  const src = generate(node).code;
1380
- throw new Error(`Unsupported expression: ${node.type}\n ${src}`);
1444
+ throw new Error(`Unsupported expression: ${node.type}\n -> ${src}`);
1381
1445
  }
1382
1446
  }
1383
1447
  }
1384
1448
  }
1385
1449
 
1386
- // ─────────────────────────────────────────────────────────────────
1387
1450
  // Serializer
1388
1451
  // Turns the compiled output into a commented JS source string.
1389
- // ─────────────────────────────────────────────────────────────────
1390
1452
  class Serializer {
1391
1453
  compiler: Compiler;
1392
1454
 
@@ -1394,6 +1456,22 @@ class Serializer {
1394
1456
  this.compiler = compiler;
1395
1457
  }
1396
1458
 
1459
+ get options() {
1460
+ return this.compiler.options;
1461
+ }
1462
+
1463
+ get OP() {
1464
+ return this.compiler.OP;
1465
+ }
1466
+
1467
+ get OP_NAME() {
1468
+ return this.compiler.OP_NAME;
1469
+ }
1470
+
1471
+ get JUMP_OPS() {
1472
+ return this.compiler.JUMP_OPS;
1473
+ }
1474
+
1397
1475
  get constants() {
1398
1476
  return this.compiler.constants.items;
1399
1477
  }
@@ -1407,57 +1485,57 @@ class Serializer {
1407
1485
  if (val === null) return "null";
1408
1486
  if (val === undefined) return "undefined";
1409
1487
  if (typeof val === "object" && val._fnIdx !== undefined) {
1410
- return `FN[${val._fnIdx}]`; // fn descriptor reference by FN index
1488
+ return `FN[${val._fnIdx}]`; // fn descriptor -> reference by FN index
1411
1489
  }
1412
1490
  return JSON.stringify(val); // number / string / bool
1413
1491
  }
1414
1492
 
1415
- // One instruction "[op, operand] // MNEMONIC description"
1493
+ // One instruction -> "[op, operand] // MNEMONIC description"
1416
1494
  _serializeInstr(instr) {
1417
1495
  const constants = this.constants;
1418
1496
 
1419
1497
  const [op, operand] = instr;
1420
- const name = OP_NAME[op] || `OP_${op}`;
1498
+ const name = this.OP_NAME[op] || `OP_${op}`;
1421
1499
  let comment = name;
1422
1500
 
1423
1501
  // Annotate operand with its meaning
1424
1502
  if (operand !== undefined) {
1425
1503
  switch (op) {
1426
- case OP.LOAD_CONST:
1427
- case OP.MAKE_CLOSURE: {
1504
+ case this.OP.LOAD_CONST:
1505
+ case this.OP.MAKE_CLOSURE: {
1428
1506
  const val = constants[operand];
1429
1507
  if (val && typeof val === "object" && val.name) {
1430
- comment += ` FN[${val._fnIdx}] fn:${val.name}`;
1508
+ comment += ` FN[${val._fnIdx}] -> fn:${val.name}`;
1431
1509
  } else {
1432
1510
  comment += ` ${JSON.stringify(val)}`;
1433
1511
  }
1434
1512
  break;
1435
1513
  }
1436
- case OP.LOAD_LOCAL:
1437
- case OP.STORE_LOCAL:
1514
+ case this.OP.LOAD_LOCAL:
1515
+ case this.OP.STORE_LOCAL:
1438
1516
  comment += ` slot[${operand}]`;
1439
1517
  break;
1440
- case OP.LOAD_UPVALUE:
1441
- case OP.STORE_UPVALUE:
1518
+ case this.OP.LOAD_UPVALUE:
1519
+ case this.OP.STORE_UPVALUE:
1442
1520
  comment += ` upvalue[${operand}]`;
1443
1521
  break;
1444
- case OP.LOAD_GLOBAL:
1445
- case OP.STORE_GLOBAL:
1522
+ case this.OP.LOAD_GLOBAL:
1523
+ case this.OP.STORE_GLOBAL:
1446
1524
  comment += ` "${constants[operand]}"`;
1447
1525
  break;
1448
- case OP.CALL:
1449
- case OP.CALL_METHOD:
1526
+ case this.OP.CALL:
1527
+ case this.OP.CALL_METHOD:
1450
1528
  comment += ` (${operand} args)`;
1451
1529
  break;
1452
1530
 
1453
- case OP.BUILD_ARRAY:
1531
+ case this.OP.BUILD_ARRAY:
1454
1532
  comment += ` (${operand} elements)`;
1455
1533
  break;
1456
- case OP.BUILD_OBJECT:
1534
+ case this.OP.BUILD_OBJECT:
1457
1535
  comment += ` (${operand} pairs)`;
1458
1536
  break;
1459
1537
 
1460
- case OP.NEW:
1538
+ case this.OP.NEW:
1461
1539
  comment += ` (${operand} args)`;
1462
1540
  break;
1463
1541
 
@@ -1469,7 +1547,7 @@ class Serializer {
1469
1547
  // Pack a [op, operand?] instruction pair into a single 32-bit word.
1470
1548
  // Shared between the Serializer and the obfuscation path in _compileMain.
1471
1549
 
1472
- if (!PACK) {
1550
+ if (!this.options.encodeBytecode) {
1473
1551
  const instrText =
1474
1552
  operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
1475
1553
 
@@ -1499,7 +1577,7 @@ class Serializer {
1499
1577
  // Serialize one fn descriptor into its FN[n] block
1500
1578
  _serializeFn(desc) {
1501
1579
  const lines = [
1502
- ` { // FN[${desc._fnIdx}] ${desc.name}`,
1580
+ ` { // FN[${desc._fnIdx}] -- ${desc.name}`,
1503
1581
  ` paramCount: ${desc.paramCount},`,
1504
1582
  ` localCount: ${desc.localCount},`,
1505
1583
  ` upvalueDescriptors: ${JSON5.stringify(desc.upvalueDescriptors)},`,
@@ -1520,18 +1598,18 @@ class Serializer {
1520
1598
  }
1521
1599
 
1522
1600
  _serializeBytecode(bytecode) {
1523
- if (!PACK) {
1601
+ if (!this.options.encodeBytecode) {
1524
1602
  return bytecode.map((instr) => this._serializeInstr(instr).value);
1525
1603
  }
1526
1604
 
1527
1605
  let words = [];
1528
1606
 
1529
- // ── BYTECODE
1607
+ // BYTECODE
1530
1608
  for (const instr of bytecode) {
1531
1609
  words.push(this._serializeInstr(instr).value);
1532
1610
  }
1533
1611
 
1534
- // Convert packed words raw 4-byte little-endian binary base64
1612
+ // Convert packed words -> raw 4-byte little-endian binary -> base64
1535
1613
  const buf = new Uint8Array(words.length * 4);
1536
1614
  words.forEach((w, i) => {
1537
1615
  buf[i * 4] = w & 0xff;
@@ -1558,7 +1636,7 @@ class Serializer {
1558
1636
  // ── CONSTANTS
1559
1637
  sections.push(this._serializeConstants());
1560
1638
 
1561
- if (PACK) {
1639
+ if (this.options.encodeBytecode) {
1562
1640
  sections.push(`var BYTECODE = "${this._serializeBytecode(bytecode)}";`);
1563
1641
  } else {
1564
1642
  sections.push(
@@ -1566,41 +1644,23 @@ class Serializer {
1566
1644
  );
1567
1645
  }
1568
1646
 
1569
- // ── MAIN_START_PC
1647
+ // MAIN_START_PC
1570
1648
  sections.push(`var MAIN_START_PC = ${mainStartPc};`);
1649
+ sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
1650
+ sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
1651
+ // Opcodes
1652
+ sections.push(`var OP = ${JSON5.stringify(this.OP)};`);
1571
1653
 
1572
- sections.push(`var PACK = ${PACK};`);
1573
-
1574
- // ── VM runtime
1654
+ // VM runtime
1575
1655
  sections.push(VM_RUNTIME);
1576
1656
 
1577
1657
  return sections.join("\n\n");
1578
1658
  }
1579
1659
  }
1580
1660
 
1581
- // ─────────────────────────────────────────────────────────────────
1582
- // VM Runtime (emitted verbatim into the output file)
1583
- // ─────────────────────────────────────────────────────────────────
1584
- const VM_RUNTIME = `
1585
- // ── Opcodes ──────────────────────────────────────────────────────
1586
- var OP = ${JSON5.stringify(OP)};
1587
- ${stripTypeScriptTypes(
1588
- readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8").split(
1589
- "@START",
1590
- )[1],
1591
- )}
1592
- `;
1593
-
1594
- interface Options {
1595
- sourceMap?: boolean;
1596
- selfModifying?: boolean;
1597
- }
1598
-
1599
- export function compileAndSerialize(
1661
+ export async function compileAndSerialize(
1600
1662
  sourceCode: string,
1601
- options: Options = {
1602
- selfModifying: true,
1603
- },
1663
+ options: Options,
1604
1664
  ) {
1605
1665
  const compiler = new Compiler(options);
1606
1666
  const result = compiler.compile(sourceCode);
@@ -1609,7 +1669,7 @@ export function compileAndSerialize(
1609
1669
  result.mainStartPc,
1610
1670
  );
1611
1671
 
1612
- const finalOutput = output;
1672
+ const finalOutput = await obfuscateRuntime(output, options);
1613
1673
 
1614
1674
  return {
1615
1675
  code: finalOutput,