js-confuser-vm 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/compiler.ts CHANGED
@@ -1,4 +1,4 @@
1
- import parser from "@babel/parser";
1
+ import { parse } from "@babel/parser";
2
2
  import traverseImport from "@babel/traverse";
3
3
  import { generate } from "@babel/generator";
4
4
 
@@ -6,33 +6,53 @@ import { readFileSync } from "fs";
6
6
  import { join } from "path";
7
7
  import { stripTypeScriptTypes } from "module";
8
8
  import JSON5 from "json5";
9
+ import * as t from "@babel/types";
10
+ import { ok } from "assert";
11
+ import { obfuscateRuntime } from "./runtimeObf.ts";
12
+ import { DEFAULT_OPTIONS, type Options } from "./options.ts";
13
+ import { resolveLabels } from "./transforms/resolveLabels.ts";
14
+ import { resolveConstants } from "./transforms/resolveContants.ts";
15
+ import { selfModifying } from "./transforms/selfModifying.ts";
16
+ import * as b from "./types.ts";
17
+
18
+ const traverse = (traverseImport.default ||
19
+ traverseImport) as typeof traverseImport.default;
20
+
21
+ const readVMRuntimeFile = () => {
22
+ let code;
23
+ try {
24
+ code = readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8");
25
+ } catch (e) {
26
+ code = readFileSync(join(import.meta.dirname, "./runtime.js"), "utf-8");
27
+ }
9
28
 
10
- const traverse = traverseImport.default;
29
+ return stripTypeScriptTypes?.(code) || code;
30
+ };
11
31
 
12
- const SHUFFLE_OPCODES = false;
13
- const PACK = true;
32
+ const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
33
+ export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE"); // Attach source node location to pseudo bytecode instructions
14
34
 
15
- // ── Opcodes ──────────────────────────────────────────────────────
16
- const OP_ORIGINAL = {
35
+ // Opcodes
36
+ export const OP_ORIGINAL = {
17
37
  LOAD_CONST: 0,
18
38
  LOAD_LOCAL: 1,
19
39
  STORE_LOCAL: 2,
20
40
  LOAD_GLOBAL: 3,
21
41
  STORE_GLOBAL: 4,
22
42
  GET_PROP: 5,
23
- ADD: 6,
24
- SUB: 7,
25
- MUL: 8,
26
- DIV: 9,
43
+ ADD: 6, // a + b (both are popped)
44
+ SUB: 7, // a - b
45
+ MUL: 8, // a * b
46
+ DIV: 9, // a / b
27
47
  MAKE_CLOSURE: 10,
28
48
  CALL: 11,
29
49
  CALL_METHOD: 12,
30
50
  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
51
+ POP: 14, // discard top of stack
52
+ LT: 15, // pop b, pop a -> push (a < b)
53
+ GT: 16, // pop b, pop a -> push (a > b)
54
+ EQ: 17, // pop b, pop a -> push (a === b)
55
+ JUMP: 18, // unconditional - operand = absolute bytecode index
36
56
  JUMP_IF_FALSE: 19, // pop value; jump if falsy
37
57
  LTE: 20, // a <= b
38
58
  GTE: 21, // a >= b
@@ -40,19 +60,19 @@ const OP_ORIGINAL = {
40
60
  LOAD_UPVALUE: 23, // push frame.closure.upvalues[operand].read()
41
61
  STORE_UPVALUE: 24, // frame.closure.upvalues[operand].write(pop())
42
62
 
43
- // ── Unary ──────────────────────────
63
+ // Unary
44
64
  UNARY_NEG: 25, // -x
45
65
  UNARY_POS: 26, // +x
46
66
  UNARY_NOT: 27, // !x
47
67
  UNARY_BITNOT: 28, // ~x
48
68
  TYPEOF: 29, // typeof x
49
- VOID: 30, // void x always undefined
69
+ VOID: 30, // void x -> always undefined
50
70
 
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])
71
+ TYPEOF_SAFE: 31, // operand = name constIdx - typeof guard for undeclared globals
72
+ BUILD_ARRAY: 32, // operand = element count - pops N values -> pushes array
73
+ BUILD_OBJECT: 33, // operand = pair count - pops N*2 (key,val) -> pushes object
74
+ SET_PROP: 34, // pop val, pop key, peek obj -> obj[key] = val (obj stays on stack)
75
+ GET_PROP_COMPUTED: 35, // pop key, peek obj -> push obj[key] (computed: nums[i])
56
76
 
57
77
  MOD: 36, // a % b
58
78
  BAND: 37, // a & b
@@ -62,91 +82,50 @@ const OP_ORIGINAL = {
62
82
  SHR: 41, // a >> b
63
83
  USHR: 42, // a >>> b
64
84
 
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
85
+ JUMP_IF_FALSE_OR_POP: 43, // && - if top falsy: jump (keep), else: pop, eval RHS
86
+ JUMP_IF_TRUE_OR_POP: 44, // || - if top truthy: jump (keep), else: pop, eval RHS
67
87
 
68
88
  DELETE_PROP: 45,
69
89
  IN: 46, // a in b
70
90
  INSTANCEOF: 47, // a instanceof b
71
91
 
72
- // ── NEW ────────────────────────────────────────────
92
+ // NEW
73
93
  LOAD_THIS: 48, // push frame.thisVal
74
- NEW: 49, // operand = argCount construct a new object
94
+ NEW: 49, // operand = argCount - construct a new object
75
95
  DUP: 50, // duplicate top of stack
76
96
  THROW: 51, // pop value, throw it
77
97
  LOOSE_EQ: 52, // a == b (abstract equality)
78
98
  LOOSE_NEQ: 53, // a != b (abstract inequality)
79
99
 
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
100
+ FOR_IN_SETUP: 54, // pop obj -> build enumerable-key iterator -> push {keys,i}
101
+ FOR_IN_NEXT: 55, // operand=exit_pc; pop iter; if done->jump; else push next key
82
102
 
83
- // ── Self-modifying bytecode ────────────────────────────────
103
+ // Self-modifying bytecode
84
104
  PATCH: 56, // pop destPc; constants[operand]=word[]; write words into bytecode[destPc..]
85
- };
86
105
 
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
- }
106
+ // Try-Catch
107
+ TRY_SETUP: 57, // operand = catch_pc; push exception handler onto frame._handlerStack
108
+ TRY_END: 58, // pop exception handler (normal exit from try body)
102
109
 
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
- // Constant Pool
116
- // Primitives (string/number/bool) are interned (deduped).
117
- // Object entries (fn descriptors) are always appended — no dedup.
118
- // ─────────────────────────────────────────────────────────────────
119
- class ConstantPool {
120
- items: any[];
121
- _index: Map<string, number>;
122
-
123
- constructor() {
124
- this.items = []; // ordered pool entries
125
- this._index = new Map(); // primitive dedup map
126
- }
110
+ // Getter / Setter (ES5 object literal accessor syntax)
111
+ DEFINE_GETTER: 59, // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {get: fn})
112
+ DEFINE_SETTER: 60, // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {set: fn})
127
113
 
128
- intern(val) {
129
- // Only intern primitives — objects must use addObject()
130
- const key = `${typeof val}:${val}`;
131
- if (this._index.has(key)) return this._index.get(key);
132
- const idx = this.items.length;
133
- this.items.push(val);
134
- this._index.set(key, idx);
135
- return idx;
136
- }
114
+ DEBUGGER: 61, // for dev/testing -- emits a "debugger" statement with a comment of the original source location
137
115
 
138
- addObject(obj) {
139
- const idx = this.items.length;
140
- this.items.push(obj);
141
- return idx;
142
- }
143
- }
116
+ // Push the raw integer operand directly onto the stack (no constant pool lookup).
117
+ // Identical pipeline to JUMP ops: {type:"label"} pseudo-operands resolve to a
118
+ // raw PC number that becomes the operand, which is pushed as-is at runtime.
119
+ LOAD_INT: 62,
120
+
121
+ // Reserved / unused opcode slot (formerly the inline DATA header word).
122
+ // Kept to avoid renumbering; should never appear in compiled output.
123
+ DATA: 63,
124
+ };
144
125
 
145
- // ─────────────────────────────────────────────────────────────────
146
126
  // Scope
147
127
  // Each function call gets its own Scope. Locals are resolved to
148
- // numeric slots at compile time zero name lookups at runtime.
149
- // ─────────────────────────────────────────────────────────────────
128
+ // numeric slots at compile time -- zero name lookups at runtime.
150
129
  class Scope {
151
130
  parent: Scope | null;
152
131
  _locals: Map<string, number>;
@@ -154,7 +133,7 @@ class Scope {
154
133
 
155
134
  constructor(parent = null) {
156
135
  this.parent = parent;
157
- this._locals = new Map(); // name slot index
136
+ this._locals = new Map(); // name -> slot index
158
137
  this._next = 0;
159
138
  }
160
139
 
@@ -165,7 +144,7 @@ class Scope {
165
144
  return this._locals.get(name);
166
145
  }
167
146
 
168
- // Walk up scope chain. If we fall off the top global.
147
+ // Walk up scope chain. If we fall off the top -> global.
169
148
  resolve(name) {
170
149
  if (this._locals.has(name)) {
171
150
  return { kind: "local", slot: this._locals.get(name) };
@@ -179,17 +158,15 @@ class Scope {
179
158
  }
180
159
  }
181
160
 
182
- // ─────────────────────────────────────────────────────────────────
183
161
  // FnContext
184
162
  // Compiler-side state for the function currently being compiled.
185
- // Distinct from runtime Frame this is compile-time only.
186
- // ─────────────────────────────────────────────────────────────────
163
+ // Distinct from runtime Frame -- this is compile-time only.
187
164
  class FnContext {
188
165
  upvalues: { name: string; isLocal: number; index: number }[];
189
166
  parentCtx: FnContext | null;
190
167
  scope: Scope;
191
168
  compiler: Compiler;
192
- bc: any[];
169
+ bc: b.Instruction[];
193
170
 
194
171
  constructor(compiler, parentCtx = null) {
195
172
  this.compiler = compiler;
@@ -201,8 +178,8 @@ class FnContext {
201
178
  }
202
179
 
203
180
  // 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]
181
+ // isLocal=true -> captured directly from parent's locals[index]
182
+ // isLocal=false -> relayed from parent's own upvalue list[index]
206
183
  addUpvalue(name, isLocal, index) {
207
184
  const existing = this.upvalues.findIndex((u) => u.name === name);
208
185
  if (existing !== -1) return existing;
@@ -212,45 +189,95 @@ class FnContext {
212
189
  }
213
190
  }
214
191
 
215
- // ─────────────────────────────────────────────────────────────────
216
192
  // Compiler
217
- // ─────────────────────────────────────────────────────────────────
218
- class Compiler {
219
- constants: ConstantPool;
193
+ export class Compiler {
220
194
  fnDescriptors: any[];
221
- bytecode: any[];
195
+ bytecode: b.Bytecode;
222
196
  mainStartPc: number;
223
197
 
224
198
  _currentCtx: FnContext | null;
225
199
  _pendingLabel: string | null;
226
200
  _forInCount: number;
201
+ _labelCount: number;
227
202
  _loopStack: {
228
203
  type: "loop" | "switch" | "block";
229
204
  label: string | null;
230
- breakJumps: number[];
231
- continueJumps: number[];
205
+ // Label that break statements targeting this entry should jump to.
206
+ breakLabel: string;
207
+ // Label that continue statements targeting this entry should jump to.
208
+ continueLabel: string;
232
209
  }[];
233
210
 
234
211
  options: Options;
235
212
  serializer: Serializer;
236
213
 
237
- constructor(options: Options) {
214
+ OP: Partial<typeof OP_ORIGINAL>;
215
+ OP_NAME: Record<number, string>;
216
+ JUMP_OPS: Set<number>;
217
+
218
+ emit(bc: b.Bytecode, instr: b.Instruction, node: t.Node) {
219
+ bc.push(instr);
220
+
221
+ instr[SOURCE_NODE_SYM] = node;
222
+ }
223
+
224
+ // DO NOT USE THIS KEY UNLESS YOU ARE "RESOLVE CONSTANTS"
225
+ // CONSTANTS DURING COMPILATION MUST BE USED BY REFERENCE WITH b.constantOperand("myConstantHere")
226
+ constants: any[];
227
+
228
+ constructor(options: Options = DEFAULT_OPTIONS) {
238
229
  this.options = options;
239
- this.constants = new ConstantPool();
240
230
  this.fnDescriptors = []; // populated in pass 1
241
231
  this.bytecode = [];
242
232
  this.mainStartPc = 0;
243
233
  this._currentCtx = null; // FnContext of the function being compiled, null at top-level
244
- this._loopStack = []; // { breakJumps: number[], continueJumps: number[] } per active loop
234
+ this._loopStack = []; // per active loop/switch/block/try
245
235
  this._pendingLabel = null;
246
236
  this._forInCount = 0; // counter for synthetic for-in iterator global names
237
+ this._labelCount = 0; // monotonically increasing counter for unique label names
247
238
 
248
239
  this.serializer = new Serializer(this);
240
+
241
+ this.OP = {};
242
+ // Construct randomized opcode mapping
243
+ if (this.options.randomizeOpcodes) {
244
+ let usedNumbers = new Set<number>();
245
+ for (const key in OP_ORIGINAL) {
246
+ let val;
247
+ do {
248
+ val = Math.floor(Math.random() * 256);
249
+ } while (usedNumbers.has(val));
250
+ usedNumbers.add(val);
251
+ this.OP[key] = val;
252
+ }
253
+ } else {
254
+ this.OP = OP_ORIGINAL;
255
+ }
256
+
257
+ // Reverse map for comment generation
258
+ this.OP_NAME = Object.fromEntries(
259
+ Object.entries(this.OP).map(([k, v]) => [v, k]),
260
+ );
261
+
262
+ this.JUMP_OPS = new Set([
263
+ this.OP.JUMP,
264
+ this.OP.JUMP_IF_FALSE,
265
+ this.OP.JUMP_IF_TRUE_OR_POP,
266
+ this.OP.JUMP_IF_FALSE_OR_POP,
267
+ this.OP.FOR_IN_NEXT,
268
+ this.OP.TRY_SETUP, // catch_pc operand needs offset adjustment like jump targets
269
+ ]);
249
270
  }
250
271
 
251
- // ── Variable resolution ──────────────────────────────────────
272
+ // Generate a globally unique label string with an optional hint for readability.
273
+ _makeLabel(hint = ""): string {
274
+ var id = this._labelCount++;
275
+ return `${hint || "L"}_${id}`;
276
+ }
277
+
278
+ // Variable resolution
252
279
  // Walks up the FnContext chain. Crossing a context boundary means
253
- // we're capturing from an outer function register an upvalue.
280
+ // we're capturing from an outer function - register an upvalue.
254
281
  _resolve(name, ctx) {
255
282
  if (!ctx) return { kind: "global" };
256
283
 
@@ -259,14 +286,14 @@ class Compiler {
259
286
  return { kind: "local", slot: ctx.scope._locals.get(name) };
260
287
  }
261
288
 
262
- // 2. No parent context must be global
289
+ // 2. No parent context -> must be global
263
290
  if (!ctx.parentCtx) return { kind: "global" };
264
291
 
265
- // 3. Ask parent recurse up the chain
292
+ // 3. Ask parent -- recurse up the chain
266
293
  const parentResult = this._resolve(name, ctx.parentCtx);
267
294
  if (parentResult.kind === "global") return { kind: "global" };
268
295
 
269
- // 4. Parent has it (as local or upvalue) register an upvalue here.
296
+ // 4. Parent has it (as local or upvalue) -- register an upvalue here.
270
297
  // isLocal=true means "take it straight from parent's locals[index]"
271
298
  // isLocal=false means "relay parent's upvalue[index]" (multi-level capture)
272
299
  const isLocal = parentResult.kind === "local";
@@ -275,16 +302,15 @@ class Compiler {
275
302
  return { kind: "upvalue", index: uvIdx };
276
303
  }
277
304
 
278
- // ── Entry point ──────────────────────────────────────────────
279
-
280
- compile(source) {
281
- const ast = parser.parse(source, { sourceType: "script" });
305
+ // Entry point
306
+ compile(source: string) {
307
+ const ast = parse(source, { sourceType: "script" });
282
308
 
283
309
  return this.compileAST(ast);
284
310
  }
285
311
 
286
- compileAST(ast) {
287
- // Pass 1 compile every FunctionDeclaration into a descriptor.
312
+ compileAST(ast: t.File) {
313
+ // Pass 1 - compile every FunctionDeclaration into a descriptor.
288
314
  // Traverse finds them regardless of nesting depth.
289
315
  traverse(ast, {
290
316
  FunctionDeclaration: (path) => {
@@ -296,58 +322,81 @@ class Compiler {
296
322
  },
297
323
  });
298
324
 
299
- // Pass 2 compile top-level statements into BYTECODE.
325
+ // Pass 2 -- compile top-level statements into BYTECODE.
300
326
  this._compileMain(ast.program.body);
301
327
 
302
- return {
303
- bytecode: this.bytecode,
304
- mainStartPc: this.mainStartPc,
305
- };
328
+ return this.bytecode;
306
329
  }
307
330
 
308
- // ── Function Declaration ──────────────────────────────────────
331
+ // Function Declaration
332
+
333
+ _compileFunctionDecl(node: t.FunctionDeclaration | t.FunctionExpression) {
334
+ // Reserve a slot in fnDescriptors NOW, before compiling the body, so that
335
+ // any nested _compileFunctionDecl calls see the correct .length and get a
336
+ // distinct _fnIdx. The placeholder object is mutated in-place below once
337
+ // the body and header are ready.
338
+ var fnIdx = this.fnDescriptors.length;
339
+ const entryLabel = this._makeLabel(`fn_${fnIdx}`);
340
+ var desc: any = {}; // placeholder — filled in after compilation
341
+ this.fnDescriptors.push(desc);
309
342
 
310
- _compileFunctionDecl(node) {
311
343
  // Create a context whose parent is whatever we're currently compiling.
312
344
  // This is what lets _resolve cross function boundaries correctly.
313
345
  const ctx = new FnContext(this, this._currentCtx);
314
346
  const savedCtx = this._currentCtx;
315
347
  this._currentCtx = ctx;
316
348
 
349
+ // Isolate the loop stack so that try/loop entries from the outer scope
350
+ // don't cause spurious TRY_END / extra jumps inside this function body.
351
+ const savedLoopStack = this._loopStack;
352
+ this._loopStack = [];
353
+
317
354
  // Params occupy the first N local slots (args are copied in on CALL)
318
355
  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
- }
356
+ let identifier = param.type === "AssignmentPattern" ? param.left : param;
357
+ ok(
358
+ identifier.type === "Identifier",
359
+ "Only simple identifiers allowed as parameters",
360
+ );
361
+
362
+ ctx.scope.define(identifier.name);
324
363
  }
325
364
 
326
365
  // Reserve the next slot for the implicit `arguments` object.
327
366
  // Slot index will always equal paramCount (params are 0..paramCount-1).
328
367
  ctx.scope.define("arguments");
329
368
 
330
- // ── Pass 2: emit default-value guards at top of fn body ─────
369
+ // Pass 2: emit default-value guards at top of fn body
331
370
  // Mirrors what JS engines do: if the caller passed undefined (or
332
371
  // nothing), evaluate the default expression and overwrite the slot.
333
- // Default expressions are full expressions, so f(x = a + b) and
334
- // f(x = foo()) both work correctly.
335
372
  for (const param of node.params) {
336
373
  if (param.type !== "AssignmentPattern") continue;
337
374
 
338
- const slot = ctx.scope._locals.get(param.left.name);
375
+ const slot = ctx.scope._locals.get((param.left as t.Identifier).name);
376
+ const skipLabel = this._makeLabel("param_skip");
339
377
 
340
378
  // 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]);
345
- const skipIdx = ctx.bc.length - 1;
379
+ this.emit(ctx.bc, [this.OP.LOAD_LOCAL, slot], param);
380
+ this.emit(
381
+ ctx.bc,
382
+ [this.OP.LOAD_CONST, b.constantOperand(undefined)],
383
+ param,
384
+ );
385
+ this.emit(ctx.bc, [this.OP.EQ], param);
386
+ this.emit(
387
+ ctx.bc,
388
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: skipLabel }],
389
+ param,
390
+ );
346
391
 
347
392
  this._compileExpr(param.right, ctx.scope, ctx.bc); // eval default
348
- ctx.bc.push([OP.STORE_LOCAL, slot]);
393
+ this.emit(ctx.bc, [this.OP.STORE_LOCAL, slot], param);
349
394
 
350
- ctx.bc[skipIdx][1] = ctx.bc.length; // patch skip jump
395
+ this.emit(
396
+ ctx.bc,
397
+ [null, { type: "defineLabel", label: skipLabel }],
398
+ param,
399
+ );
351
400
  }
352
401
 
353
402
  for (const stmt of node.body.body) {
@@ -355,92 +404,85 @@ class Compiler {
355
404
  }
356
405
 
357
406
  // 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]);
407
+ this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
408
+ this.emit(ctx.bc, [this.OP.RETURN], node);
360
409
 
361
410
  this._currentCtx = savedCtx; // restore before touching fnDescriptors
411
+ this._loopStack = savedLoopStack;
412
+
413
+ (node as any)._fnIdx = fnIdx;
414
+
415
+ // Fill the placeholder that was reserved at the top of this function.
416
+ // Metadata (paramCount, localCount, upvalues) is stored on desc and emitted
417
+ // as LOAD_INT instructions onto the value stack at each MAKE_CLOSURE call
418
+ // site — the runtime reads them from the stack, not from DATA words.
419
+ desc.name = node.id?.name || "<anonymous>";
420
+ desc.entryLabel = entryLabel;
421
+ desc.bytecode = ctx.bc as b.Bytecode;
422
+ desc._fnIdx = fnIdx;
423
+ desc.paramCount = node.params.length;
424
+ desc.localCount = ctx.scope.localCount;
425
+ desc.upvalues = ctx.upvalues.slice();
362
426
 
363
- var fnIdx = this.fnDescriptors.length;
364
- node._fnIdx = fnIdx; // for error messages
365
-
366
- const desc = {
367
- name: node.id?.name || "<anonymous>",
368
- paramCount: node.params.length,
369
- localCount: ctx.scope.localCount,
370
- upvalueDescriptors: ctx.upvalues.map((u) => ({
371
- isLocal: u.isLocal,
372
- _index: u.index,
373
- })),
374
- bytecode: ctx.bc,
375
- // Indices assigned after pushing into the pool
376
- _fnIdx: this.fnDescriptors.length,
377
- _constIdx: null,
378
- };
379
-
380
- this.fnDescriptors.push(desc);
381
- desc._constIdx = this.constants.addObject(desc); // object entry, no dedup
382
427
  return desc;
383
428
  }
384
429
 
385
- // ── Main (top-level) ─────────────────────────────────────────
430
+ // Emit LOAD_INT instructions that push closure metadata onto the value stack
431
+ // immediately before a MAKE_CLOSURE instruction. The runtime pops these
432
+ // values in MAKE_CLOSURE instead of reading DATA words from bytecode.
433
+ //
434
+ // Stack layout when MAKE_CLOSURE executes (top is rightmost):
435
+ // [isLocal_0, idx_0, ..., isLocal_N-1, idx_N-1, uvCount, localCount, paramCount]
436
+ _emitClosureMetadata(desc: any, node: t.Node, bc: b.Bytecode) {
437
+ // Push each upvalue descriptor in order; runtime pops them in reverse.
438
+ for (const uv of desc.upvalues) {
439
+ this.emit(bc, [this.OP.LOAD_INT, uv.isLocal ? 1 : 0], node);
440
+ this.emit(bc, [this.OP.LOAD_INT, uv.index], node);
441
+ }
442
+ this.emit(bc, [this.OP.LOAD_INT, desc.upvalues.length], node);
443
+ this.emit(bc, [this.OP.LOAD_INT, desc.localCount], node);
444
+ this.emit(bc, [this.OP.LOAD_INT, desc.paramCount], node);
445
+ }
386
446
 
387
- _compileMain(body) {
388
- this.mainStartPc = 0; // ← record main's entry point
447
+ // Main (top-level)
448
+ _compileMain(body: t.Statement[]) {
389
449
  const bc = this.bytecode;
390
450
 
391
- // Hoist all FunctionDeclarations: MAKE_CLOSURE STORE_GLOBAL
392
- // (mirrors JS hoisting functions are available before other code)
451
+ // Hoist all FunctionDeclarations: MAKE_CLOSURE -> STORE_GLOBAL
452
+ // (mirrors JS hoisting -- functions are available before other code)
393
453
  for (const node of body) {
394
454
  if (node.type !== "FunctionDeclaration") continue;
395
- const desc = this.fnDescriptors.find((d) => d._fnIdx === node._fnIdx);
396
- const nameIdx = this.constants.intern(node.id.name);
397
- bc.push([OP.MAKE_CLOSURE, desc._constIdx]);
398
- bc.push([OP.STORE_GLOBAL, nameIdx]);
455
+ const desc = this.fnDescriptors.find(
456
+ (d) => d._fnIdx === (node as any)._fnIdx,
457
+ );
458
+ const nameRef = b.constantOperand(node.id.name);
459
+ this._emitClosureMetadata(desc, node, bc);
460
+ this.emit(
461
+ bc,
462
+ [this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
463
+ node,
464
+ );
465
+ this.emit(bc, [this.OP.STORE_GLOBAL, nameRef], node);
399
466
  }
400
467
 
401
468
  // Compile everything else in order
402
469
  for (const node of body) {
403
470
  if (node.type === "FunctionDeclaration") continue;
404
- this._compileStatement(node, null, bc); // null scope global context
471
+ this._compileStatement(node, null, bc); // null scope -> global context
405
472
  }
406
473
 
407
- bc.push([OP.RETURN]); // end program
474
+ this.emit(bc, [this.OP.RETURN], null); // end program
408
475
 
409
- // Now that main is compiled, we can append all the function bodies at the end of the bytecode.
476
+ // Append all function bodies. Each function's entryLabel (already generated
477
+ // in _compileFunctionDecl) points directly to the first body instruction;
478
+ // metadata is pushed onto the stack at each call site, not stored inline.
410
479
  for (const descriptor of this.fnDescriptors) {
411
- descriptor.startPc = this.bytecode.length;
412
-
413
- descriptor.bytecode.push([OP.RETURN]); // ensure every function ends with RETURN
414
-
415
- if (this.options.selfModifying) {
416
- // Preamble is 2 instructions: LOAD_CONST(destPc) + PATCH(bodyConst)
417
- // Real body starts immediately after the preamble.
418
- const bodyPc = descriptor.startPc + 2;
419
-
420
- // Build real body with jump targets resolved from bodyPc as the base.
421
- const realBodyInstrs = descriptor.bytecode.map((instr) =>
422
- this._offsetJump(instr, bodyPc),
423
- );
424
-
425
- // Pack each instruction into a 32-bit word and store as a constant.
426
- // The PATCH handler will write these words directly into this.bytecode.
427
- const realBodyWords =
428
- this.serializer._serializeBytecode(realBodyInstrs);
429
- const bodyConstIdx = this.constants.addObject(realBodyWords);
430
-
431
- // Emit preamble: push destination PC, then PATCH.
432
- const destPcConstIdx = this.constants.intern(bodyPc);
433
- this.bytecode.push([OP.LOAD_CONST, destPcConstIdx]);
434
- this.bytecode.push([OP.PATCH, bodyConstIdx]);
435
-
436
- // Garbage fill — same length as real body, never executed (PATCH fires first).
437
- for (let i = 0; i < realBodyInstrs.length; i++) {
438
- this.bytecode.push([OP.LOAD_CONST, 0]);
439
- }
440
- } else {
441
- for (const instr of descriptor.bytecode) {
442
- this.bytecode.push(this._offsetJump(instr, descriptor.startPc));
443
- }
480
+ this.bytecode.push([
481
+ null,
482
+ { type: "defineLabel", label: descriptor.entryLabel },
483
+ ]);
484
+ for (const instr of descriptor.bytecode) {
485
+ this.bytecode.push(instr);
444
486
  }
445
487
  }
446
488
 
@@ -449,23 +491,24 @@ class Compiler {
449
491
  `Program too large: ${this.bytecode.length} instructions, max 16,777,215`,
450
492
  );
451
493
 
452
- if (this.constants.items.length > 0xffffff)
453
- throw new Error(
454
- `Constant pool too large: ${this.constants.items.length} entries, max 16,777,215`,
455
- );
494
+ // if (this.constants.items.length > 0xffffff)
495
+ // throw new Error(
496
+ // `Constant pool too large: ${this.constants.items.length} entries, max 16,777,215`,
497
+ // );
456
498
  }
457
499
 
458
- _offsetJump(instr, offset) {
459
- if (JUMP_OPS.has(instr[0]) && instr[1] !== undefined) {
460
- return [instr[0], instr[1] + offset];
461
- }
462
- return instr;
463
- }
500
+ // Statements
501
+ _compileStatement(node: t.Statement, scope: Scope, bc: b.Bytecode) {
502
+ switch (node.type) {
503
+ case "EmptyStatement": {
504
+ // nothing to emit -- bare semicolon is a no-op
505
+ break;
506
+ }
464
507
 
465
- // ── Statements ───────────────────────────────────────────────
508
+ case "DebuggerStatement":
509
+ this.emit(bc, [this.OP.DEBUGGER], node);
510
+ break;
466
511
 
467
- _compileStatement(node, scope, bc) {
468
- switch (node.type) {
469
512
  case "BlockStatement": {
470
513
  for (const stmt of node.body) {
471
514
  this._compileStatement(stmt, scope, bc);
@@ -474,23 +517,32 @@ class Compiler {
474
517
  }
475
518
 
476
519
  case "FunctionDeclaration": {
477
- // Nested function compile it into a descriptor, then emit
520
+ // Nested function -- compile it into a descriptor, then emit
478
521
  // MAKE_CLOSURE so it's captured as a live closure at runtime.
479
522
  // (_compileFunctionDecl pushes/pops _currentCtx internally)
480
523
  const desc = this._compileFunctionDecl(node);
481
- bc.push([OP.MAKE_CLOSURE, desc._constIdx]);
524
+ this._emitClosureMetadata(desc, node, bc);
525
+ this.emit(
526
+ bc,
527
+ [this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
528
+ node,
529
+ );
482
530
  if (scope) {
483
531
  const slot = scope.define(node.id.name);
484
- bc.push([OP.STORE_LOCAL, slot]);
532
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
485
533
  } else {
486
- bc.push([OP.STORE_GLOBAL, this.constants.intern(node.id.name)]);
534
+ this.emit(
535
+ bc,
536
+ [this.OP.STORE_GLOBAL, b.constantOperand(node.id.name)],
537
+ node,
538
+ );
487
539
  }
488
540
  break;
489
541
  }
490
542
 
491
543
  case "ThrowStatement": {
492
544
  this._compileExpr(node.argument, scope, bc);
493
- bc.push([OP.THROW]);
545
+ this.emit(bc, [this.OP.THROW], node);
494
546
  break;
495
547
  }
496
548
 
@@ -498,15 +550,27 @@ class Compiler {
498
550
  if (node.argument) {
499
551
  this._compileExpr(node.argument, scope, bc);
500
552
  } else {
501
- bc.push([OP.LOAD_CONST, this.constants.intern(undefined)]);
553
+ this.emit(
554
+ bc,
555
+ [this.OP.LOAD_CONST, b.constantOperand(undefined)],
556
+ node,
557
+ );
502
558
  }
503
- bc.push([OP.RETURN]);
559
+ // Disarm any open try handlers before leaving the function.
560
+ // TRY_END only touches frame._handlerStack, not the value stack,
561
+ // so the return value sitting on top is safe.
562
+ for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
563
+ if ((this._loopStack[_ri].type as any) === "try") {
564
+ this.emit(bc, [this.OP.TRY_END], node);
565
+ }
566
+ }
567
+ this.emit(bc, [this.OP.RETURN], node);
504
568
  break;
505
569
  }
506
570
 
507
571
  case "ExpressionStatement": {
508
572
  this._compileExpr(node.expression, scope, bc);
509
- bc.push([OP.POP]); // discard return value of statement-level expressions
573
+ this.emit(bc, [this.OP.POP], node); // discard return value of statement-level expressions
510
574
  break;
511
575
  }
512
576
 
@@ -516,27 +580,44 @@ class Compiler {
516
580
  if (decl.init) {
517
581
  this._compileExpr(decl.init, scope, bc);
518
582
  } else {
519
- bc.push([OP.LOAD_CONST, this.constants.intern(undefined)]);
583
+ this.emit(
584
+ bc,
585
+ [this.OP.LOAD_CONST, b.constantOperand(undefined)],
586
+ node,
587
+ );
520
588
  }
589
+
590
+ ok(
591
+ decl.id.type === "Identifier",
592
+ "Only simple identifiers can be declared",
593
+ );
594
+
521
595
  // Store: local slot if inside a function, global name otherwise
522
596
  if (scope) {
523
597
  const slot = scope.define(decl.id.name);
524
- bc.push([OP.STORE_LOCAL, slot]);
598
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
525
599
  } else {
526
- bc.push([OP.STORE_GLOBAL, this.constants.intern(decl.id.name)]);
600
+ this.emit(
601
+ bc,
602
+ [this.OP.STORE_GLOBAL, b.constantOperand(decl.id.name)],
603
+ node,
604
+ );
527
605
  }
528
606
  }
529
607
  break;
530
608
  }
531
609
 
532
610
  case "IfStatement": {
533
- // 1. Compile the test expression → leaves a value on the stack
611
+ const elseOrEndLabel = this._makeLabel("if_else");
612
+ // 1. Compile the test expression -> leaves a value on the stack
534
613
  this._compileExpr(node.test, scope, bc);
535
- // 2. Emit JUMP_IF_FALSE with placeholder target
536
- bc.push([OP.JUMP_IF_FALSE, 0]);
537
- const jumpIfFalseIdx = bc.length - 1;
614
+ // 2. Emit JUMP_IF_FALSE to the else branch (or end if no else)
615
+ this.emit(
616
+ bc,
617
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: elseOrEndLabel }],
618
+ node,
619
+ );
538
620
  // 3. Compile the consequent block (the "then" branch)
539
- // Consequent may be a BlockStatement or a bare statement (no braces)
540
621
  const consequentBody =
541
622
  node.consequent.type === "BlockStatement"
542
623
  ? node.consequent.body
@@ -546,23 +627,35 @@ class Compiler {
546
627
  }
547
628
  if (node.alternate) {
548
629
  // 4a. Consequent needs to jump OVER the else block when done
549
- bc.push([OP.JUMP, 0]);
550
- const jumpOverElseIdx = bc.length - 1;
551
- // Patch JUMP_IF_FALSE to land here (start of else)
552
- bc[jumpIfFalseIdx][1] = bc.length;
630
+ const endLabel = this._makeLabel("if_end");
631
+ this.emit(
632
+ bc,
633
+ [this.OP.JUMP, { type: "label", label: endLabel }],
634
+ node,
635
+ );
636
+ // Mark start of else
637
+ this.emit(
638
+ bc,
639
+ [null, { type: "defineLabel", label: elseOrEndLabel }],
640
+ node,
641
+ );
553
642
  // 5. Compile the alternate (else) block
554
643
  const altBody =
555
644
  node.alternate.type === "BlockStatement"
556
645
  ? node.alternate.body
557
- : [node.alternate]; // handles `else if` it's just a nested IfStatement
646
+ : [node.alternate]; // handles `else if` -- it's just a nested IfStatement
558
647
  for (const stmt of altBody) {
559
648
  this._compileStatement(stmt, scope, bc);
560
649
  }
561
- // Patch the JUMP to land after the else block
562
- bc[jumpOverElseIdx][1] = bc.length;
650
+ // Mark end (consequent's jump lands here)
651
+ this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
563
652
  } else {
564
- // 4b. No else patch JUMP_IF_FALSE to land right after the then block
565
- bc[jumpIfFalseIdx][1] = bc.length;
653
+ // 4b. No else -- label lands right after the then block
654
+ this.emit(
655
+ bc,
656
+ [null, { type: "defineLabel", label: elseOrEndLabel }],
657
+ node,
658
+ );
566
659
  }
567
660
  break;
568
661
  }
@@ -570,30 +663,41 @@ class Compiler {
570
663
  case "WhileStatement": {
571
664
  const _wLabel = this._pendingLabel;
572
665
  this._pendingLabel = null;
666
+
667
+ const loopTopLabel = this._makeLabel("while_top");
668
+ const exitLabel = this._makeLabel("while_exit");
669
+
573
670
  this._loopStack.push({
574
671
  type: "loop",
575
672
  label: _wLabel,
576
- breakJumps: [],
577
- continueJumps: [],
673
+ breakLabel: exitLabel,
674
+ continueLabel: loopTopLabel, // continue re-evaluates the test
578
675
  });
579
- const loopCtxW = this._loopStack[this._loopStack.length - 1];
580
676
 
581
- const loopTop = bc.length;
677
+ this.emit(
678
+ bc,
679
+ [null, { type: "defineLabel", label: loopTopLabel }],
680
+ node,
681
+ );
582
682
  this._compileExpr(node.test, scope, bc);
583
- bc.push([OP.JUMP_IF_FALSE, 0]);
584
- const exitJumpIdx = bc.length - 1;
683
+ this.emit(
684
+ bc,
685
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
686
+ node,
687
+ );
585
688
 
586
- for (const stmt of node.body.body) {
689
+ const whileBody =
690
+ node.body.type === "BlockStatement" ? node.body.body : [node.body];
691
+ for (const stmt of whileBody) {
587
692
  this._compileStatement(stmt, scope, bc);
588
693
  }
589
694
 
590
- // continue → re-evaluate the test
591
- for (const idx of loopCtxW.continueJumps) bc[idx][1] = loopTop;
592
- bc.push([OP.JUMP, loopTop]);
593
-
594
- const exitTargetW = bc.length;
595
- bc[exitJumpIdx][1] = exitTargetW;
596
- for (const idx of loopCtxW.breakJumps) bc[idx][1] = exitTargetW;
695
+ this.emit(
696
+ bc,
697
+ [this.OP.JUMP, { type: "label", label: loopTopLabel }],
698
+ node,
699
+ );
700
+ this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
597
701
 
598
702
  this._loopStack.pop();
599
703
  break;
@@ -602,33 +706,49 @@ class Compiler {
602
706
  case "DoWhileStatement": {
603
707
  const _dwLabel = this._pendingLabel;
604
708
  this._pendingLabel = null;
709
+
710
+ const loopTopLabel = this._makeLabel("dowhile_top");
711
+ const continueLabel = this._makeLabel("dowhile_cont");
712
+ const exitLabel = this._makeLabel("dowhile_exit");
713
+
605
714
  this._loopStack.push({
606
715
  type: "loop",
607
716
  label: _dwLabel,
608
- breakJumps: [],
609
- continueJumps: [],
717
+ breakLabel: exitLabel,
718
+ continueLabel: continueLabel, // continue falls to the test
610
719
  });
611
- const loopCtxDW = this._loopStack[this._loopStack.length - 1];
612
720
 
613
- const loopTopDW = bc.length;
721
+ this.emit(
722
+ bc,
723
+ [null, { type: "defineLabel", label: loopTopLabel }],
724
+ node,
725
+ );
614
726
 
615
- for (const stmt of node.body.body) {
727
+ const doWhileBody =
728
+ node.body.type === "BlockStatement" ? node.body.body : [node.body];
729
+ for (const stmt of doWhileBody) {
616
730
  this._compileStatement(stmt, scope, bc);
617
731
  }
618
732
 
619
- // continue skip rest of body, fall through to test
620
- const continueTargetDW = bc.length;
621
- for (const idx of loopCtxDW.continueJumps)
622
- bc[idx][1] = continueTargetDW;
623
-
733
+ // continue -> skip rest of body, fall through to test
734
+ this.emit(
735
+ bc,
736
+ [null, { type: "defineLabel", label: continueLabel }],
737
+ node,
738
+ );
624
739
  this._compileExpr(node.test, scope, bc);
625
- bc.push([OP.JUMP_IF_FALSE, 0]);
626
- const exitJumpIdxDW = bc.length - 1;
627
- bc.push([OP.JUMP, loopTopDW]);
740
+ this.emit(
741
+ bc,
742
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
743
+ node,
744
+ );
745
+ this.emit(
746
+ bc,
747
+ [this.OP.JUMP, { type: "label", label: loopTopLabel }],
748
+ node,
749
+ );
628
750
 
629
- const exitTargetDW = bc.length;
630
- bc[exitJumpIdxDW][1] = exitTargetDW;
631
- for (const idx of loopCtxDW.breakJumps) bc[idx][1] = exitTargetDW;
751
+ this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
632
752
 
633
753
  this._loopStack.pop();
634
754
  break;
@@ -637,126 +757,172 @@ class Compiler {
637
757
  case "ForStatement": {
638
758
  const _fLabel = this._pendingLabel;
639
759
  this._pendingLabel = null;
760
+
761
+ const loopTopLabel = this._makeLabel("for_top");
762
+ const exitLabel = this._makeLabel("for_exit");
763
+ // continue jumps to the update clause if present, else straight to test
764
+ const updateLabel = node.update
765
+ ? this._makeLabel("for_update")
766
+ : loopTopLabel;
767
+
640
768
  this._loopStack.push({
641
769
  type: "loop",
642
770
  label: _fLabel,
643
- breakJumps: [],
644
- continueJumps: [],
771
+ breakLabel: exitLabel,
772
+ continueLabel: updateLabel,
645
773
  });
646
- const loopCtxF = this._loopStack[this._loopStack.length - 1];
647
774
 
648
775
  if (node.init) {
649
776
  if (node.init.type === "VariableDeclaration") {
650
777
  this._compileStatement(node.init, scope, bc);
651
778
  } else {
652
779
  this._compileExpr(node.init, scope, bc);
653
- bc.push([OP.POP]);
780
+ this.emit(bc, [this.OP.POP], node);
654
781
  }
655
782
  }
656
783
 
657
- const loopTopF = bc.length;
784
+ this.emit(
785
+ bc,
786
+ [null, { type: "defineLabel", label: loopTopLabel }],
787
+ node,
788
+ );
658
789
  if (node.test) {
659
790
  this._compileExpr(node.test, scope, bc);
660
- bc.push([OP.JUMP_IF_FALSE, 0]);
791
+ this.emit(
792
+ bc,
793
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
794
+ node,
795
+ );
661
796
  }
662
- const exitJumpIdxF = node.test ? bc.length - 1 : null;
663
797
 
664
- for (const stmt of node.body.body) {
798
+ const forBody =
799
+ node.body.type === "BlockStatement" ? node.body.body : [node.body];
800
+ for (const stmt of forBody) {
665
801
  this._compileStatement(stmt, scope, bc);
666
802
  }
667
803
 
668
- // continue run update (if any) then back to test
804
+ // continue -> run update (if any) then back to test
669
805
  if (node.update) {
670
- const continueTargetF = bc.length;
671
- for (const idx of loopCtxF.continueJumps)
672
- bc[idx][1] = continueTargetF;
806
+ this.emit(
807
+ bc,
808
+ [null, { type: "defineLabel", label: updateLabel }],
809
+ node,
810
+ );
673
811
  this._compileExpr(node.update, scope, bc);
674
- bc.push([OP.POP]);
675
- } else {
676
- // No update — continue goes straight to the test
677
- for (const idx of loopCtxF.continueJumps) bc[idx][1] = loopTopF;
812
+ this.emit(bc, [this.OP.POP], node);
678
813
  }
679
814
 
680
- bc.push([OP.JUMP, loopTopF]);
681
-
682
- const exitTargetF = bc.length;
683
- if (exitJumpIdxF !== null) bc[exitJumpIdxF][1] = exitTargetF;
684
- for (const idx of loopCtxF.breakJumps) bc[idx][1] = exitTargetF;
815
+ this.emit(
816
+ bc,
817
+ [this.OP.JUMP, { type: "label", label: loopTopLabel }],
818
+ node,
819
+ );
820
+ this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
685
821
 
686
822
  this._loopStack.pop();
687
823
  break;
688
824
  }
689
825
 
690
826
  case "BreakStatement": {
691
- bc.push([OP.JUMP, 0]);
692
- const _bJumpIdx = bc.length - 1;
827
+ // Find the jump target in the loop stack.
828
+ let _bTargetIdx = -1;
693
829
  if (node.label) {
694
830
  const _bLabelName = node.label.name;
695
- let _bFound = -1;
696
831
  for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
697
832
  if (this._loopStack[_bi].label === _bLabelName) {
698
- _bFound = _bi;
833
+ _bTargetIdx = _bi;
699
834
  break;
700
835
  }
701
836
  }
702
- if (_bFound === -1)
703
- throw new Error(`Label '${_bLabelName}' not found`);
704
- this._loopStack[_bFound].breakJumps.push(_bJumpIdx);
837
+ if (_bTargetIdx === -1)
838
+ throw new Error(`Label '${node.label.name}' not found`);
705
839
  } else {
706
- if (this._loopStack.length === 0)
707
- throw new Error("break outside loop");
708
- this._loopStack[this._loopStack.length - 1].breakJumps.push(
709
- _bJumpIdx,
710
- );
840
+ // Find innermost loop/switch/block (skip "try" entries)
841
+ for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
842
+ if ((this._loopStack[_bi].type as any) !== "try") {
843
+ _bTargetIdx = _bi;
844
+ break;
845
+ }
846
+ }
847
+ if (_bTargetIdx === -1) throw new Error("break outside loop");
848
+ }
849
+ // Emit TRY_END for every open try block between here and the target.
850
+ for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
851
+ if ((this._loopStack[_bi].type as any) === "try") {
852
+ this.emit(bc, [this.OP.TRY_END], node);
853
+ }
711
854
  }
855
+ this.emit(
856
+ bc,
857
+ [
858
+ this.OP.JUMP,
859
+ { type: "label", label: this._loopStack[_bTargetIdx].breakLabel },
860
+ ],
861
+ node,
862
+ );
712
863
  break;
713
864
  }
714
865
 
715
866
  case "ContinueStatement": {
716
- bc.push([OP.JUMP, 0]);
717
- const _cJumpIdx = bc.length - 1;
867
+ // Find the target loop in the loop stack.
868
+ let _cTargetIdx = -1;
718
869
  if (node.label) {
719
870
  const _cLabelName = node.label.name;
720
- let _cFound = -1;
721
871
  for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
722
872
  if (
723
873
  this._loopStack[_ci].label === _cLabelName &&
724
874
  this._loopStack[_ci].type === "loop"
725
875
  ) {
726
- _cFound = _ci;
876
+ _cTargetIdx = _ci;
727
877
  break;
728
878
  }
729
879
  }
730
- if (_cFound === -1)
731
- throw new Error(`Label '${_cLabelName}' not found for continue`);
732
- this._loopStack[_cFound].continueJumps.push(_cJumpIdx);
880
+ if (_cTargetIdx === -1)
881
+ throw new Error(
882
+ `Label '${node.label.name}' not found for continue`,
883
+ );
733
884
  } else {
734
- if (this._loopStack.length === 0)
735
- throw new Error("continue outside loop");
736
- // Find the innermost loop (skip switch and block contexts)
737
- let loopIdx = -1;
738
- for (let i = this._loopStack.length - 1; i >= 0; i--) {
739
- if (this._loopStack[i].type === "loop") {
740
- loopIdx = i;
885
+ // Find the innermost loop (skip switch, block, and try contexts)
886
+ for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
887
+ if (this._loopStack[_ci].type === "loop") {
888
+ _cTargetIdx = _ci;
741
889
  break;
742
890
  }
743
891
  }
744
- if (loopIdx === -1) throw new Error("continue outside loop");
745
- this._loopStack[loopIdx].continueJumps.push(_cJumpIdx);
892
+ if (_cTargetIdx === -1) throw new Error("continue outside loop");
746
893
  }
894
+ // Emit TRY_END for every open try block between here and the target loop.
895
+ for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
896
+ if ((this._loopStack[_ci].type as any) === "try") {
897
+ this.emit(bc, [this.OP.TRY_END], node);
898
+ }
899
+ }
900
+ this.emit(
901
+ bc,
902
+ [
903
+ this.OP.JUMP,
904
+ {
905
+ type: "label",
906
+ label: this._loopStack[_cTargetIdx].continueLabel,
907
+ },
908
+ ],
909
+ node,
910
+ );
747
911
  break;
748
912
  }
749
913
 
750
914
  case "SwitchStatement": {
751
915
  const _swLabel = this._pendingLabel;
752
916
  this._pendingLabel = null;
917
+
918
+ const switchBreakLabel = this._makeLabel("sw_break");
919
+
753
920
  this._loopStack.push({
754
921
  type: "switch",
755
922
  label: _swLabel,
756
- breakJumps: [],
757
- continueJumps: [],
923
+ breakLabel: switchBreakLabel,
924
+ continueLabel: switchBreakLabel, // not used for switch
758
925
  });
759
- const switchCtx = this._loopStack[this._loopStack.length - 1];
760
926
 
761
927
  // Compile the discriminant and leave it on the stack
762
928
  this._compileExpr(node.discriminant, scope, bc);
@@ -764,58 +930,70 @@ class Compiler {
764
930
  const cases = node.cases;
765
931
  const defaultIdx = cases.findIndex((c) => c.test === null);
766
932
 
767
- // Dispatch section: emit case checks
768
- const bodyJumps = []; // { cas, jumpIdx }
933
+ // Pre-allocate a label for each case body so dispatch can reference them
934
+ const caseLabels = cases.map((_, i) => this._makeLabel(`sw_case_${i}`));
769
935
 
770
- for (const cas of cases) {
771
- if (cas.test === null) continue; // Skip default in dispatch
936
+ // Dispatch section: for each non-default case, check and jump to its body
937
+ for (let i = 0; i < cases.length; i++) {
938
+ const cas = cases[i];
939
+ if (cas.test === null) continue; // skip default in dispatch
772
940
 
773
- // Check this case: DUP; LOAD_CONST; EQ; JUMP_IF_FALSE
774
- bc.push([OP.DUP]);
941
+ const nextCheckLabel = this._makeLabel("sw_next");
942
+ this.emit(bc, [this.OP.DUP], node);
775
943
  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)
778
- const skipIdx = bc.length - 1;
779
-
780
- // If matched, jump to this case's body
781
- bc.push([OP.JUMP, 0]); // Jump to body (patched later)
782
- bodyJumps.push({ cas, jumpIdx: bc.length - 1 });
783
-
784
- // Patch the JUMP_IF_FALSE to the next check
785
- bc[skipIdx][1] = bc.length;
944
+ this.emit(bc, [this.OP.EQ], node);
945
+ // If not matched, fall through to the next check
946
+ this.emit(
947
+ bc,
948
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: nextCheckLabel }],
949
+ node,
950
+ );
951
+ // If matched, jump directly to this case's body
952
+ this.emit(
953
+ bc,
954
+ [this.OP.JUMP, { type: "label", label: caseLabels[i] }],
955
+ node,
956
+ );
957
+ this.emit(
958
+ bc,
959
+ [null, { type: "defineLabel", label: nextCheckLabel }],
960
+ node,
961
+ );
786
962
  }
787
963
 
788
- // No match found: jump to default (or exit if no default)
789
- bc.push([OP.JUMP, 0]);
790
- const noMatchJumpIdx = bc.length - 1;
964
+ // No case matched: jump to default body or exit (which pops discriminant)
965
+ this.emit(
966
+ bc,
967
+ [
968
+ this.OP.JUMP,
969
+ {
970
+ type: "label",
971
+ label:
972
+ defaultIdx !== -1 ? caseLabels[defaultIdx] : switchBreakLabel,
973
+ },
974
+ ],
975
+ node,
976
+ );
791
977
 
792
- // Body section: compile all case bodies in source order
793
- const bodyStart = new Map();
794
- for (const cas of cases) {
795
- bodyStart.set(cas, bc.length);
796
- for (const stmt of cas.consequent) {
978
+ // Body section: compile all case bodies in source order (fallthrough intact)
979
+ for (let i = 0; i < cases.length; i++) {
980
+ this.emit(
981
+ bc,
982
+ [null, { type: "defineLabel", label: caseLabels[i] }],
983
+ node,
984
+ );
985
+ for (const stmt of cases[i].consequent) {
797
986
  this._compileStatement(stmt, scope, bc);
798
987
  }
799
988
  }
800
989
 
801
- // Patch the no-match jump to default or exit
802
- const exitTarget = bc.length;
803
- if (defaultIdx !== -1) {
804
- bc[noMatchJumpIdx][1] = bodyStart.get(cases[defaultIdx]);
805
- } else {
806
- bc[noMatchJumpIdx][1] = exitTarget;
807
- }
808
-
809
- // Patch all body jumps
810
- for (const { cas, jumpIdx } of bodyJumps) {
811
- bc[jumpIdx][1] = bodyStart.get(cas);
812
- }
813
-
814
- // Exit: pop the discriminant and patch break jumps
815
- bc.push([OP.POP]);
816
- for (const idx of switchCtx.breakJumps) {
817
- bc[idx][1] = bc.length - 1; // Point to the POP instruction
818
- }
990
+ // break label lands here; pop the discriminant and continue after switch
991
+ this.emit(
992
+ bc,
993
+ [null, { type: "defineLabel", label: switchBreakLabel }],
994
+ node,
995
+ );
996
+ this.emit(bc, [this.OP.POP], node);
819
997
 
820
998
  this._loopStack.pop();
821
999
  break;
@@ -837,17 +1015,21 @@ class Compiler {
837
1015
  this._compileStatement(_lBody, scope, bc);
838
1016
  this._pendingLabel = null; // safety clear if handler didn't consume it
839
1017
  } else {
840
- // Non-loop labeled statement (e.g. labeled block) only break is valid
1018
+ // Non-loop labeled statement (e.g. labeled block) -- only break is valid
1019
+ const blockBreakLabel = this._makeLabel("block_break");
841
1020
  this._loopStack.push({
842
1021
  type: "block",
843
1022
  label: _lName,
844
- breakJumps: [],
845
- continueJumps: [],
1023
+ breakLabel: blockBreakLabel,
1024
+ continueLabel: blockBreakLabel, // unused
846
1025
  });
847
1026
  this._compileStatement(_lBody, scope, bc);
848
- const _lEntry = this._loopStack.pop()!;
849
- const _lExit = bc.length;
850
- for (const _lIdx of _lEntry.breakJumps) bc[_lIdx][1] = _lExit;
1027
+ this._loopStack.pop();
1028
+ this.emit(
1029
+ bc,
1030
+ [null, { type: "defineLabel", label: blockBreakLabel }],
1031
+ node,
1032
+ );
851
1033
  }
852
1034
  break;
853
1035
  }
@@ -856,10 +1038,10 @@ class Compiler {
856
1038
  const _fiLabel = this._pendingLabel;
857
1039
  this._pendingLabel = null;
858
1040
 
859
- // Evaluate the object expression on stack
1041
+ // Evaluate the object expression -> on stack
860
1042
  this._compileExpr(node.right, scope, bc);
861
1043
  // FOR_IN_SETUP: pops obj, pushes iterator {keys, i}
862
- bc.push([OP.FOR_IN_SETUP]);
1044
+ this.emit(bc, [this.OP.FOR_IN_SETUP], node);
863
1045
 
864
1046
  // Store iterator in a hidden slot so break/continue need no cleanup
865
1047
  let emitLoadIter: () => void;
@@ -867,55 +1049,79 @@ class Compiler {
867
1049
  if (scope) {
868
1050
  // Reserve a hidden local slot (no name mapping needed)
869
1051
  const iterSlot = scope._next++;
870
- emitLoadIter = () => bc.push([OP.LOAD_LOCAL, iterSlot]);
871
- emitStoreIter = () => bc.push([OP.STORE_LOCAL, iterSlot]);
1052
+ emitLoadIter = () =>
1053
+ this.emit(bc, [this.OP.LOAD_LOCAL, iterSlot], node);
1054
+ emitStoreIter = () =>
1055
+ this.emit(bc, [this.OP.STORE_LOCAL, iterSlot], node);
872
1056
  } else {
873
- // Top level use a synthetic global that won't collide with user code
874
- const iterNameIdx = this.constants.intern(
875
- "__fi" + this._forInCount++,
876
- );
877
- emitLoadIter = () => bc.push([OP.LOAD_GLOBAL, iterNameIdx]);
878
- emitStoreIter = () => bc.push([OP.STORE_GLOBAL, iterNameIdx]);
1057
+ // Top level -- use a synthetic global that won't collide with user code
1058
+ const iterNameIdx = b.constantOperand("__fi" + this._forInCount++);
1059
+ emitLoadIter = () =>
1060
+ this.emit(bc, [this.OP.LOAD_GLOBAL, iterNameIdx], node);
1061
+ emitStoreIter = () =>
1062
+ this.emit(bc, [this.OP.STORE_GLOBAL, iterNameIdx], node);
879
1063
  }
880
1064
  emitStoreIter();
881
1065
 
1066
+ const loopTopLabel = this._makeLabel("forin_top");
1067
+ const exitLabel = this._makeLabel("forin_exit");
1068
+
882
1069
  this._loopStack.push({
883
1070
  type: "loop",
884
1071
  label: _fiLabel,
885
- breakJumps: [],
886
- continueJumps: [],
1072
+ breakLabel: exitLabel,
1073
+ continueLabel: loopTopLabel, // continue re-checks the iterator
887
1074
  });
888
- const loopCtxFI = this._loopStack[this._loopStack.length - 1];
889
1075
 
890
- const loopTopFI = bc.length;
1076
+ this.emit(
1077
+ bc,
1078
+ [null, { type: "defineLabel", label: loopTopLabel }],
1079
+ node,
1080
+ );
891
1081
 
892
1082
  // Load iterator, attempt to get next key
893
1083
  emitLoadIter();
894
- bc.push([OP.FOR_IN_NEXT, 0]); // exit target patched below
895
- const forInNextPatch = bc.length - 1;
1084
+ this.emit(
1085
+ bc,
1086
+ [this.OP.FOR_IN_NEXT, { type: "label", label: exitLabel }],
1087
+ node,
1088
+ );
896
1089
 
897
1090
  // Assign the key (now on top of stack) to the loop variable
898
1091
  if (node.left.type === "VariableDeclaration") {
899
- const name = node.left.declarations[0].id.name;
1092
+ const identifier = node.left.declarations[0].id;
1093
+ ok(
1094
+ identifier.type === "Identifier",
1095
+ "Only simple identifiers can be declared in for-in loops",
1096
+ );
1097
+ const name = identifier.name;
900
1098
  if (scope) {
901
1099
  const slot = scope.define(name);
902
- bc.push([OP.STORE_LOCAL, slot]);
1100
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
903
1101
  } else {
904
- bc.push([OP.STORE_GLOBAL, this.constants.intern(name)]);
1102
+ this.emit(
1103
+ bc,
1104
+ [this.OP.STORE_GLOBAL, b.constantOperand(name)],
1105
+ node,
1106
+ );
905
1107
  }
906
1108
  } else if (node.left.type === "Identifier") {
907
1109
  const res = this._resolve(node.left.name, this._currentCtx);
908
1110
  if (res.kind === "local") {
909
- bc.push([OP.STORE_LOCAL, res.slot]);
1111
+ this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
910
1112
  } else if (res.kind === "upvalue") {
911
- bc.push([OP.STORE_UPVALUE, res.index]);
1113
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
912
1114
  } else {
913
- bc.push([OP.STORE_GLOBAL, this.constants.intern(node.left.name)]);
1115
+ this.emit(
1116
+ bc,
1117
+ [this.OP.STORE_GLOBAL, b.constantOperand(node.left.name)],
1118
+ node,
1119
+ );
914
1120
  }
915
1121
  } else {
916
1122
  const src = generate(node.left).code;
917
1123
  throw new Error(
918
- `Unsupported for-in left-hand side: ${node.left.type}\n ${src}`,
1124
+ `Unsupported for-in left-hand side: ${node.left.type}\n -> ${src}`,
919
1125
  );
920
1126
  }
921
1127
 
@@ -926,15 +1132,101 @@ class Compiler {
926
1132
  this._compileStatement(stmt, scope, bc);
927
1133
  }
928
1134
 
929
- // continue → re-load iterator and check next key
930
- for (const idx of loopCtxFI.continueJumps) bc[idx][1] = loopTopFI;
931
- bc.push([OP.JUMP, loopTopFI]);
1135
+ this.emit(
1136
+ bc,
1137
+ [this.OP.JUMP, { type: "label", label: loopTopLabel }],
1138
+ node,
1139
+ );
1140
+ this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
1141
+
1142
+ this._loopStack.pop();
1143
+ break;
1144
+ }
1145
+
1146
+ case "TryStatement": {
1147
+ if (node.finalizer) {
1148
+ throw new Error(
1149
+ "try..finally is not supported. Use a helper function instead",
1150
+ );
1151
+ }
1152
+ if (!node.handler) {
1153
+ // try without catch requires finally — not supported
1154
+ throw new Error(
1155
+ "try without catch is not supported (requires finally).",
1156
+ );
1157
+ }
1158
+
1159
+ const catchLabel = this._makeLabel("catch");
1160
+ const afterCatchLabel = this._makeLabel("after_catch");
1161
+
1162
+ // Emit TRY_SETUP with the catch block's label as the handler PC.
1163
+ // At runtime: saves stack depth + frame stack depth, pushes handler.
1164
+ this.emit(
1165
+ bc,
1166
+ [this.OP.TRY_SETUP, { type: "label", label: catchLabel }],
1167
+ node,
1168
+ );
1169
+
1170
+ // Track the open try block so that break/continue/return inside the
1171
+ // try body can emit the matching TRY_END before their jump.
1172
+ this._loopStack.push({
1173
+ type: "try" as any,
1174
+ label: null,
1175
+ breakLabel: "", // unused
1176
+ continueLabel: "", // unused
1177
+ });
932
1178
 
933
- const exitTargetFI = bc.length;
934
- bc[forInNextPatch][1] = exitTargetFI;
935
- for (const idx of loopCtxFI.breakJumps) bc[idx][1] = exitTargetFI;
1179
+ // Compile try body
1180
+ for (const stmt of node.block.body) {
1181
+ this._compileStatement(stmt, scope, bc);
1182
+ }
936
1183
 
1184
+ // Done compiling the try body — pop the tracking entry.
937
1185
  this._loopStack.pop();
1186
+
1187
+ // Normal exit: disarm the exception handler.
1188
+ this.emit(bc, [this.OP.TRY_END], node);
1189
+
1190
+ // Jump over the catch block on normal path.
1191
+ this.emit(
1192
+ bc,
1193
+ [this.OP.JUMP, { type: "label", label: afterCatchLabel }],
1194
+ node,
1195
+ );
1196
+
1197
+ // Catch block: exception is on top of the stack (pushed by the VM).
1198
+ this.emit(bc, [null, { type: "defineLabel", label: catchLabel }], node);
1199
+
1200
+ const handler = node.handler;
1201
+ if (handler.param) {
1202
+ // Bind the exception value to the catch variable.
1203
+ const name = (handler.param as t.Identifier).name;
1204
+ if (scope) {
1205
+ const slot = scope.define(name);
1206
+ this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
1207
+ } else {
1208
+ this.emit(
1209
+ bc,
1210
+ [this.OP.STORE_GLOBAL, b.constantOperand(name)],
1211
+ node,
1212
+ );
1213
+ }
1214
+ } else {
1215
+ // Optional catch binding (catch without a variable — ES2019+)
1216
+ this.emit(bc, [this.OP.POP], node);
1217
+ }
1218
+
1219
+ // Compile catch body
1220
+ for (const stmt of handler.body.body) {
1221
+ this._compileStatement(stmt, scope, bc);
1222
+ }
1223
+
1224
+ // Normal-path jump lands here (after the catch block).
1225
+ this.emit(
1226
+ bc,
1227
+ [null, { type: "defineLabel", label: afterCatchLabel }],
1228
+ node,
1229
+ );
938
1230
  break;
939
1231
  }
940
1232
 
@@ -942,65 +1234,75 @@ class Compiler {
942
1234
  // Use @babel/generator to reproduce the source of unsupported nodes
943
1235
  // so we can emit a clear error with context.
944
1236
  const src = generate(node).code;
945
- throw new Error(`Unsupported statement: ${node.type}\n ${src}`);
1237
+ throw new Error(`Unsupported statement: ${node.type}\n -> ${src}`);
946
1238
  }
947
1239
  }
948
1240
  }
949
1241
 
950
- // ── Expressions ──────────────────────────────────────────────
951
-
1242
+ // Expressions
952
1243
  _compileExpr(node, scope, bc) {
953
1244
  switch (node.type) {
954
1245
  case "NumericLiteral":
955
1246
  case "StringLiteral": {
956
- bc.push([OP.LOAD_CONST, this.constants.intern(node.value)]);
1247
+ this.emit(
1248
+ bc,
1249
+ [this.OP.LOAD_CONST, b.constantOperand(node.value)],
1250
+ node,
1251
+ );
957
1252
  break;
958
1253
  }
959
1254
 
960
1255
  case "BooleanLiteral": {
961
- bc.push([OP.LOAD_CONST, this.constants.intern(node.value)]);
1256
+ this.emit(
1257
+ bc,
1258
+ [this.OP.LOAD_CONST, b.constantOperand(node.value)],
1259
+ node,
1260
+ );
962
1261
  break;
963
1262
  }
964
1263
 
965
1264
  case "NullLiteral": {
966
- bc.push([OP.LOAD_CONST, this.constants.intern(null)]);
1265
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(null)], node);
967
1266
  break;
968
1267
  }
969
1268
 
970
1269
  case "Identifier": {
971
- // scope=null means we're at the top-level always global
1270
+ // scope=null means we're at the top-level -> always global
972
1271
  const res = this._resolve(node.name, this._currentCtx);
973
1272
  if (res.kind === "local") {
974
- bc.push([OP.LOAD_LOCAL, res.slot]);
1273
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
975
1274
  } else if (res.kind === "upvalue") {
976
- bc.push([OP.LOAD_UPVALUE, res.index]);
1275
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
977
1276
  } else {
978
- bc.push([OP.LOAD_GLOBAL, this.constants.intern(node.name)]);
1277
+ this.emit(
1278
+ bc,
1279
+ [this.OP.LOAD_GLOBAL, b.constantOperand(node.name)],
1280
+ node,
1281
+ );
979
1282
  }
980
1283
  break;
981
1284
  }
982
1285
 
983
1286
  case "ThisExpression": {
984
- bc.push([OP.LOAD_THIS]);
1287
+ this.emit(bc, [this.OP.LOAD_THIS], node);
985
1288
  break;
986
1289
  }
987
1290
 
988
1291
  case "NewExpression": {
989
- // Push callee, then args identical layout to CALL but uses NEW opcode
1292
+ // Push callee, then args -- identical layout to CALL but uses NEW opcode
990
1293
  this._compileExpr(node.callee, scope, bc);
991
1294
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
992
- bc.push([OP.NEW, node.arguments.length]);
1295
+ this.emit(bc, [this.OP.NEW, node.arguments.length], node);
993
1296
  break;
994
1297
  }
995
1298
 
996
1299
  case "SequenceExpression": {
997
- // (a, b, c) eval a POP, eval b POP, eval c leave on stack
998
- // Matches CPython's BINARY_OP / POP_TOP pattern for comma expressions.
1300
+ // (a, b, c) -> eval a -> POP, eval b -> POP, eval c -> leave on stack
999
1301
  for (let i = 0; i < node.expressions.length - 1; i++) {
1000
1302
  this._compileExpr(node.expressions[i], scope, bc);
1001
- bc.push([OP.POP]); // discard intermediate result
1303
+ this.emit(bc, [this.OP.POP], node); // discard intermediate result
1002
1304
  }
1003
- // Last expression its value is the result of the whole sequence
1305
+ // Last expression -- its value is the result of the whole sequence
1004
1306
  this._compileExpr(
1005
1307
  node.expressions[node.expressions.length - 1],
1006
1308
  scope,
@@ -1011,45 +1313,55 @@ class Compiler {
1011
1313
 
1012
1314
  case "ConditionalExpression": {
1013
1315
  // test ? consequent : alternate
1014
- // Identical to IfStatement codegen, just lives in expression context.
1015
- this._compileExpr(node.test, scope, bc);
1316
+ const elseLabel = this._makeLabel("ternary_else");
1317
+ const endLabel = this._makeLabel("ternary_end");
1016
1318
 
1017
- bc.push([OP.JUMP_IF_FALSE, 0]);
1018
- const jumpToElse = bc.length - 1;
1319
+ this._compileExpr(node.test, scope, bc);
1320
+ this.emit(
1321
+ bc,
1322
+ [this.OP.JUMP_IF_FALSE, { type: "label", label: elseLabel }],
1323
+ node,
1324
+ );
1019
1325
 
1020
1326
  this._compileExpr(node.consequent, scope, bc);
1327
+ this.emit(bc, [this.OP.JUMP, { type: "label", label: endLabel }], node);
1021
1328
 
1022
- bc.push([OP.JUMP, 0]);
1023
- const jumpToEnd = bc.length - 1;
1024
-
1025
- bc[jumpToElse][1] = bc.length; // patch: false → alternate
1329
+ this.emit(bc, [null, { type: "defineLabel", label: elseLabel }], node);
1026
1330
  this._compileExpr(node.alternate, scope, bc);
1027
1331
 
1028
- bc[jumpToEnd][1] = bc.length; // patch: after consequent → end
1332
+ this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1029
1333
  break;
1030
1334
  }
1031
1335
 
1032
1336
  case "LogicalExpression": {
1033
1337
  // Pattern (CPython-style):
1034
1338
  // eval LHS
1035
- // JUMP_IF_*_OR_POP target (past RHS)
1339
+ // JUMP_IF_*_OR_POP -> target (past RHS)
1036
1340
  // eval RHS ← only reached if LHS didn't short-circuit
1037
1341
  // [target lands here, stack top is the result either way]
1038
1342
 
1039
1343
  this._compileExpr(node.left, scope, bc);
1040
1344
 
1041
1345
  if (node.operator === "||") {
1042
- // Short-circuit if LHS is TRUTHY keep it, skip RHS
1043
- bc.push([OP.JUMP_IF_TRUE_OR_POP, 0]);
1044
- const jumpIdx = bc.length - 1;
1346
+ // Short-circuit if LHS is TRUTHY -- keep it, skip RHS
1347
+ const endLabel = this._makeLabel("or_end");
1348
+ this.emit(
1349
+ bc,
1350
+ [this.OP.JUMP_IF_TRUE_OR_POP, { type: "label", label: endLabel }],
1351
+ node,
1352
+ );
1045
1353
  this._compileExpr(node.right, scope, bc);
1046
- bc[jumpIdx][1] = bc.length; // patch target to after RHS
1354
+ this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1047
1355
  } 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]);
1050
- const jumpIdx = bc.length - 1;
1356
+ // Short-circuit if LHS is FALSY -- keep it, skip RHS
1357
+ const endLabel = this._makeLabel("and_end");
1358
+ this.emit(
1359
+ bc,
1360
+ [this.OP.JUMP_IF_FALSE_OR_POP, { type: "label", label: endLabel }],
1361
+ node,
1362
+ );
1051
1363
  this._compileExpr(node.right, scope, bc);
1052
- bc[jumpIdx][1] = bc.length; // patch target to after RHS
1364
+ this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
1053
1365
  } else {
1054
1366
  throw new Error(`Unsupported logical operator: ${node.operator}`);
1055
1367
  }
@@ -1060,70 +1372,74 @@ class Compiler {
1060
1372
  this._compileExpr(node.left, scope, bc);
1061
1373
  this._compileExpr(node.right, scope, bc);
1062
1374
  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,
1375
+ "+": this.OP.ADD,
1376
+ "-": this.OP.SUB,
1377
+ "*": this.OP.MUL,
1378
+ "/": this.OP.DIV,
1379
+ "%": this.OP.MOD,
1380
+ "&": this.OP.BAND,
1381
+ "|": this.OP.BOR,
1382
+ "^": this.OP.BXOR,
1383
+ "<<": this.OP.SHL,
1384
+ ">>": this.OP.SHR,
1385
+ ">>>": this.OP.USHR,
1074
1386
  }[node.operator];
1075
1387
 
1076
1388
  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
1389
+ "<": this.OP.LT,
1390
+ ">": this.OP.GT,
1391
+ "===": this.OP.EQ,
1392
+ "==": this.OP.LOOSE_EQ,
1393
+ "<=": this.OP.LTE,
1394
+ ">=": this.OP.GTE,
1395
+ "!==": this.OP.NEQ,
1396
+ "!=": this.OP.LOOSE_NEQ,
1397
+ in: this.OP.IN, // ← add
1398
+ instanceof: this.OP.INSTANCEOF, // ← add
1087
1399
  }[node.operator];
1088
1400
  const resolvedOp = arithOp ?? cmpOp;
1089
1401
  if (resolvedOp === undefined)
1090
1402
  throw new Error(`Unsupported operator: ${node.operator}`);
1091
- bc.push([resolvedOp]);
1403
+ this.emit(bc, [resolvedOp], node);
1092
1404
 
1093
1405
  break;
1094
1406
  }
1095
1407
 
1096
1408
  case "UpdateExpression": {
1097
1409
  const res = this._resolve(node.argument.name, this._currentCtx);
1098
- const bumpOp = node.operator === "++" ? OP.ADD : OP.SUB;
1099
- const one = this.constants.intern(1);
1410
+ const bumpOp = node.operator === "++" ? this.OP.ADD : this.OP.SUB;
1411
+ const one = b.constantOperand(1);
1100
1412
 
1101
1413
  // Helper closures: emit load / store for whichever resolution kind we have
1102
1414
  const emitLoad = () => {
1103
- if (res.kind === "local") bc.push([OP.LOAD_LOCAL, res.slot]);
1415
+ if (res.kind === "local")
1416
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1104
1417
  else if (res.kind === "upvalue")
1105
- bc.push([OP.LOAD_UPVALUE, res.index]);
1418
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1106
1419
  else
1107
- bc.push([
1108
- OP.LOAD_GLOBAL,
1109
- this.constants.intern(node.argument.name),
1110
- ]);
1420
+ this.emit(
1421
+ bc,
1422
+ [this.OP.LOAD_GLOBAL, b.constantOperand(node.argument.name)],
1423
+ node,
1424
+ );
1111
1425
  };
1112
1426
  const emitStore = () => {
1113
- if (res.kind === "local") bc.push([OP.STORE_LOCAL, res.slot]);
1427
+ if (res.kind === "local")
1428
+ this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
1114
1429
  else if (res.kind === "upvalue")
1115
- bc.push([OP.STORE_UPVALUE, res.index]);
1430
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
1116
1431
  else
1117
- bc.push([
1118
- OP.STORE_GLOBAL,
1119
- this.constants.intern(node.argument.name),
1120
- ]);
1432
+ this.emit(
1433
+ bc,
1434
+ [this.OP.STORE_GLOBAL, b.constantOperand(node.argument.name)],
1435
+ node,
1436
+ );
1121
1437
  };
1122
1438
 
1123
1439
  emitLoad();
1124
- if (!node.prefix) bc.push([OP.DUP]); // post: save old value before mutating
1125
- bc.push([OP.LOAD_CONST, one]);
1126
- bc.push([bumpOp]);
1440
+ if (!node.prefix) this.emit(bc, [this.OP.DUP], node); // post: save old value before mutating
1441
+ this.emit(bc, [this.OP.LOAD_CONST, one], node);
1442
+ this.emit(bc, [bumpOp], node);
1127
1443
  emitStore();
1128
1444
  if (node.prefix) emitLoad(); // pre: reload new value as result
1129
1445
 
@@ -1132,17 +1448,17 @@ class Compiler {
1132
1448
 
1133
1449
  case "AssignmentExpression": {
1134
1450
  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,
1451
+ "+=": this.OP.ADD,
1452
+ "-=": this.OP.SUB,
1453
+ "*=": this.OP.MUL,
1454
+ "/=": this.OP.DIV,
1455
+ "%=": this.OP.MOD,
1456
+ "&=": this.OP.BAND,
1457
+ "|=": this.OP.BOR,
1458
+ "^=": this.OP.BXOR,
1459
+ "<<=": this.OP.SHL,
1460
+ ">>=": this.OP.SHR,
1461
+ ">>>=": this.OP.USHR,
1146
1462
  }[node.operator];
1147
1463
 
1148
1464
  const isCompound = compoundOp !== undefined;
@@ -1151,76 +1467,85 @@ class Compiler {
1151
1467
  throw new Error(`Unsupported assignment operator: ${node.operator}`);
1152
1468
  }
1153
1469
 
1154
- // ── Member assignment: obj.x = val or arr[i] = val ──────
1470
+ // Member assignment: obj.x = val or arr[i] = val
1155
1471
  if (node.left.type === "MemberExpression") {
1156
1472
  this._compileExpr(node.left.object, scope, bc); // push obj
1157
1473
 
1158
1474
  if (node.left.computed) {
1159
1475
  this._compileExpr(node.left.property, scope, bc); // push key (runtime)
1160
1476
  } else {
1161
- bc.push([
1162
- OP.LOAD_CONST,
1163
- this.constants.intern(node.left.property.name),
1164
- ]);
1477
+ this.emit(
1478
+ bc,
1479
+ [this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)],
1480
+ node,
1481
+ );
1165
1482
  }
1166
1483
 
1167
1484
  if (isCompound) {
1168
1485
  // Duplicate obj+key on the stack so we can read before we write.
1169
1486
  // Stack before DUP2: [..., obj, key]
1170
- // We need: [..., obj, key, obj, key] GET_PROP_COMPUTED [..., obj, key, currentVal]
1487
+ // We need: [..., obj, key, obj, key] -> GET_PROP_COMPUTED -> [..., obj, key, currentVal]
1171
1488
  // Cheapest approach without a DUP opcode: re-compile the member read.
1172
1489
  // (emits obj + key again; a future peephole pass could DUP instead)
1173
1490
  this._compileExpr(node.left.object, scope, bc);
1174
1491
  if (node.left.computed) {
1175
1492
  this._compileExpr(node.left.property, scope, bc);
1176
1493
  } else {
1177
- bc.push([
1178
- OP.LOAD_CONST,
1179
- this.constants.intern(node.left.property.name),
1180
- ]);
1494
+ this.emit(
1495
+ bc,
1496
+ [
1497
+ this.OP.LOAD_CONST,
1498
+ b.constantOperand(node.left.property.name),
1499
+ ],
1500
+ node,
1501
+ );
1181
1502
  }
1182
- bc.push([OP.GET_PROP_COMPUTED]); // [..., obj, key, currentVal]
1503
+ this.emit(bc, [this.OP.GET_PROP_COMPUTED], node); // [..., obj, key, currentVal]
1183
1504
  this._compileExpr(node.right, scope, bc); // [..., obj, key, currentVal, rhs]
1184
- bc.push([compoundOp]); // [..., obj, key, newVal]
1505
+ this.emit(bc, [compoundOp], node); // [..., obj, key, newVal]
1185
1506
  } else {
1186
1507
  this._compileExpr(node.right, scope, bc); // [..., obj, key, val]
1187
1508
  }
1188
1509
 
1189
- bc.push([OP.SET_PROP]); // obj[key] = val, leaves val on stack
1510
+ this.emit(bc, [this.OP.SET_PROP], node); // obj[key] = val, leaves val on stack
1190
1511
  break;
1191
1512
  }
1192
1513
 
1193
- // ── Plain identifier assignment ────────────────────────────
1514
+ // Plain identifier assignment
1194
1515
  const res = this._resolve(node.left.name, this._currentCtx);
1195
1516
 
1196
1517
  if (isCompound) {
1197
1518
  // Load the current value of the target first
1198
1519
  if (res.kind === "local") {
1199
- bc.push([OP.LOAD_LOCAL, res.slot]);
1520
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1200
1521
  } else if (res.kind === "upvalue") {
1201
- bc.push([OP.LOAD_UPVALUE, res.index]);
1522
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1202
1523
  } else {
1203
- bc.push([OP.LOAD_GLOBAL, this.constants.intern(node.left.name)]);
1524
+ this.emit(
1525
+ bc,
1526
+ [this.OP.LOAD_GLOBAL, b.constantOperand(node.left.name)],
1527
+ node,
1528
+ );
1204
1529
  }
1205
1530
  }
1206
1531
 
1207
1532
  this._compileExpr(node.right, scope, bc); // push RHS
1208
1533
 
1209
1534
  if (isCompound) {
1210
- bc.push([compoundOp]); // apply binary op leaves newVal on stack
1535
+ this.emit(bc, [compoundOp], node); // apply binary op -> leaves newVal on stack
1211
1536
  }
1212
1537
 
1213
1538
  // Store & leave value on stack (assignment is an expression)
1214
1539
  if (res.kind === "local") {
1215
- bc.push([OP.STORE_LOCAL, res.slot]);
1216
- bc.push([OP.LOAD_LOCAL, res.slot]);
1540
+ this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
1541
+ this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
1217
1542
  } else if (res.kind === "upvalue") {
1218
- bc.push([OP.STORE_UPVALUE, res.index]);
1219
- bc.push([OP.LOAD_UPVALUE, res.index]);
1543
+ this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
1544
+ this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
1220
1545
  } else {
1221
- const nameIdx = this.constants.intern(node.left.name);
1222
- bc.push([OP.STORE_GLOBAL, nameIdx]);
1223
- bc.push([OP.LOAD_GLOBAL, nameIdx]);
1546
+ const nameIdx = b.constantOperand(node.left.name);
1547
+ this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx], node);
1548
+ this.emit(bc, [this.OP.LOAD_GLOBAL, nameIdx], node);
1224
1549
  }
1225
1550
  break;
1226
1551
  }
@@ -1231,16 +1556,16 @@ class Compiler {
1231
1556
  // Push receiver first (GET_PROP leaves it; CALL_METHOD pops it as `this`)
1232
1557
  this._compileExpr(node.callee.object, scope, bc);
1233
1558
  const prop = node.callee.property.name;
1234
- const propIdx = this.constants.intern(prop);
1235
- bc.push([OP.LOAD_CONST, propIdx]);
1236
- bc.push([OP.GET_PROP]);
1559
+ const propIdx = b.constantOperand(prop);
1560
+ this.emit(bc, [this.OP.LOAD_CONST, propIdx], node);
1561
+ this.emit(bc, [this.OP.GET_PROP], node);
1237
1562
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1238
- bc.push([OP.CALL_METHOD, node.arguments.length]);
1563
+ this.emit(bc, [this.OP.CALL_METHOD, node.arguments.length], node);
1239
1564
  } else {
1240
1565
  // ── Plain call: add(5, 10)
1241
1566
  this._compileExpr(node.callee, scope, bc);
1242
1567
  for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
1243
- bc.push([OP.CALL, node.arguments.length]);
1568
+ this.emit(bc, [this.OP.CALL, node.arguments.length], node);
1244
1569
  }
1245
1570
  break;
1246
1571
  }
@@ -1252,141 +1577,244 @@ class Compiler {
1252
1577
  if (node.operator === "typeof" && node.argument.type === "Identifier") {
1253
1578
  const res = this._resolve(node.argument.name, this._currentCtx);
1254
1579
  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]);
1580
+ // Potentially undeclared -- let VM guard it
1581
+ this.emit(
1582
+ bc,
1583
+ [this.OP.LOAD_CONST, b.constantOperand(node.argument.name)],
1584
+ node,
1585
+ );
1586
+ this.emit(bc, [this.OP.TYPEOF_SAFE], node);
1258
1587
  break;
1259
1588
  }
1260
- // Known local or upvalue safe to load first, then typeof
1589
+ // Known local or upvalue -- safe to load first, then typeof
1590
+ }
1591
+
1592
+ // Special case: delete -- argument must NOT be pre-evaluated.
1593
+ if (node.operator === "delete") {
1594
+ const arg = node.argument;
1595
+ if (arg.type === "MemberExpression") {
1596
+ this._compileExpr(arg.object, scope, bc);
1597
+ if (arg.computed) {
1598
+ this._compileExpr(arg.property, scope, bc);
1599
+ } else {
1600
+ this.emit(
1601
+ bc,
1602
+ [this.OP.LOAD_CONST, b.constantOperand(arg.property.name)],
1603
+ node,
1604
+ );
1605
+ }
1606
+ this.emit(bc, [this.OP.DELETE_PROP], node);
1607
+ } else {
1608
+ // delete x, delete 0, etc. -- always true in non-strict, just push true
1609
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(true)], node);
1610
+ }
1611
+ break;
1261
1612
  }
1613
+
1262
1614
  // All other unary ops: compile argument first, then apply operator
1263
1615
  this._compileExpr(node.argument, scope, bc);
1264
1616
  switch (node.operator) {
1265
1617
  case "-":
1266
- bc.push([OP.UNARY_NEG]);
1618
+ this.emit(bc, [this.OP.UNARY_NEG], node);
1267
1619
  break;
1268
1620
  case "+":
1269
- bc.push([OP.UNARY_POS]);
1621
+ this.emit(bc, [this.OP.UNARY_POS], node);
1270
1622
  break;
1271
1623
  case "!":
1272
- bc.push([OP.UNARY_NOT]);
1624
+ this.emit(bc, [this.OP.UNARY_NOT], node);
1273
1625
  break;
1274
1626
  case "~":
1275
- bc.push([OP.UNARY_BITNOT]);
1627
+ this.emit(bc, [this.OP.UNARY_BITNOT], node);
1276
1628
  break;
1277
1629
  case "typeof":
1278
- bc.push([OP.TYPEOF]);
1630
+ this.emit(bc, [this.OP.TYPEOF], node);
1279
1631
  break;
1280
1632
  case "void":
1281
- bc.push([OP.VOID]);
1633
+ this.emit(bc, [this.OP.VOID], node);
1282
1634
  break;
1283
1635
 
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
1636
  default:
1305
1637
  throw new Error(`Unsupported unary operator: ${node.operator}`);
1306
1638
  }
1307
1639
  break;
1308
1640
  }
1309
1641
 
1642
+ case "RegExpLiteral": {
1643
+ // Emit: new RegExp(pattern, flags)
1644
+ // Fresh object per evaluation -- correct for stateful g/y flags.
1645
+ this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand("RegExp")], node);
1646
+ this.emit(
1647
+ bc,
1648
+ [this.OP.LOAD_CONST, b.constantOperand(node.pattern)],
1649
+ node,
1650
+ );
1651
+ this.emit(
1652
+ bc,
1653
+ [this.OP.LOAD_CONST, b.constantOperand(node.flags)],
1654
+ node,
1655
+ );
1656
+ this.emit(bc, [this.OP.NEW, 2], node);
1657
+ break;
1658
+ }
1659
+
1310
1660
  case "FunctionExpression": {
1311
1661
  // Compile into a descriptor exactly like a declaration,
1312
- // but leave the resulting closure ON THE STACK no store.
1662
+ // but leave the resulting closure ON THE STACK -- no store.
1313
1663
  // The surrounding expression (assignment, call arg, return) consumes it.
1314
1664
  const desc = this._compileFunctionDecl(node);
1315
- bc.push([OP.MAKE_CLOSURE, desc._constIdx]);
1665
+ this._emitClosureMetadata(desc, node, bc);
1666
+ this.emit(
1667
+ bc,
1668
+ [this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
1669
+ node,
1670
+ );
1316
1671
  break;
1317
1672
  }
1318
1673
 
1319
1674
  case "MemberExpression": {
1320
1675
  this._compileExpr(node.object, scope, bc);
1321
1676
  if (node.computed) {
1322
- // nums[i] key is runtime value
1677
+ // nums[i] -- key is runtime value
1323
1678
  this._compileExpr(node.property, scope, bc);
1324
1679
  } else {
1325
- // point.x push key as string, same opcode handles both
1326
- bc.push([OP.LOAD_CONST, this.constants.intern(node.property.name)]);
1680
+ // point.x -- push key as string, same opcode handles both
1681
+ this.emit(
1682
+ bc,
1683
+ [this.OP.LOAD_CONST, b.constantOperand(node.property.name)],
1684
+ node,
1685
+ );
1327
1686
  }
1328
1687
 
1329
- // GET_PROP_COMPUTED pops the object correct for value access.
1688
+ // GET_PROP_COMPUTED pops the object -- correct for value access.
1330
1689
  // GET_PROP (peek) is only used in CallExpression's method call path
1331
1690
  // where the receiver must survive on the stack for CALL_METHOD.
1332
- bc.push([OP.GET_PROP_COMPUTED]);
1691
+ this.emit(bc, [this.OP.GET_PROP_COMPUTED], node);
1333
1692
  break;
1334
1693
  }
1335
1694
 
1336
1695
  case "ArrayExpression": {
1337
- // Compile each element leftright, then BUILD_ARRAY collapses them.
1696
+ // Compile each element left->right, then BUILD_ARRAY collapses them.
1338
1697
  // Sparse arrays (holes) get explicit undefined per slot.
1339
1698
  for (const el of node.elements) {
1340
1699
  if (el === null) {
1341
1700
  // hole: e.g. [1,,3]
1342
- bc.push([OP.LOAD_CONST, this.constants.intern(undefined)]);
1701
+ this.emit(
1702
+ bc,
1703
+ [this.OP.LOAD_CONST, b.constantOperand(undefined)],
1704
+ node,
1705
+ );
1343
1706
  } else {
1344
1707
  this._compileExpr(el, scope, bc);
1345
1708
  }
1346
1709
  }
1347
- bc.push([OP.BUILD_ARRAY, node.elements.length]);
1710
+ this.emit(bc, [this.OP.BUILD_ARRAY, node.elements.length], node);
1348
1711
  break;
1349
1712
  }
1350
1713
  case "ObjectExpression": {
1351
- // For each property: push key (always as string), push value.
1352
- // BUILD_OBJECT pops pairs right→left and assembles the object.
1714
+ // Separate regular data properties from ES5 accessor methods (get/set).
1715
+ const regularProps: t.ObjectProperty[] = [];
1716
+ const accessorProps: t.ObjectMethod[] = [];
1717
+
1353
1718
  for (const prop of node.properties) {
1354
1719
  if (prop.type === "SpreadElement") {
1355
1720
  throw new Error("Object spread not supported");
1356
1721
  }
1357
- // Key identifier shorthand (`{x:1}`) or string/number literal
1722
+ if (prop.type === "ObjectMethod") {
1723
+ if (prop.kind === "get" || prop.kind === "set") {
1724
+ if (prop.computed) {
1725
+ throw new Error(
1726
+ "Computed getter/setter keys are not supported",
1727
+ );
1728
+ }
1729
+ accessorProps.push(prop);
1730
+ } else {
1731
+ throw new Error(`Shorthand method syntax is not supported`);
1732
+ }
1733
+ } else {
1734
+ regularProps.push(prop as t.ObjectProperty);
1735
+ }
1736
+ }
1737
+
1738
+ // Build the base object from data properties.
1739
+ for (const prop of regularProps) {
1358
1740
  const key = prop.key;
1359
- let keyStr;
1741
+ let keyStr: string;
1360
1742
  if (key.type === "Identifier") {
1361
- keyStr = key.name; // {x: 1} → key is "x"
1743
+ keyStr = key.name;
1362
1744
  } else if (
1363
1745
  key.type === "StringLiteral" ||
1364
1746
  key.type === "NumericLiteral"
1365
1747
  ) {
1366
- keyStr = String(key.value); // {"x": 1} or {0: 1}
1748
+ keyStr = String(key.value);
1367
1749
  } else {
1368
1750
  throw new Error(`Unsupported object key type: ${key.type}`);
1369
1751
  }
1370
- bc.push([OP.LOAD_CONST, this.constants.intern(keyStr)]);
1371
- // Value — any expression, including FunctionExpression
1752
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
1372
1753
  this._compileExpr(prop.value, scope, bc);
1373
1754
  }
1374
- bc.push([OP.BUILD_OBJECT, node.properties.length]);
1755
+ this.emit(bc, [this.OP.BUILD_OBJECT, regularProps.length], node);
1756
+
1757
+ // Define each accessor on the object that is now on top of the stack.
1758
+ // Stack after BUILD_OBJECT: [..., obj]
1759
+ // For each accessor: DUP obj, push key, compile fn, DEFINE_GETTER/DEFINE_SETTER
1760
+ // DEFINE_GETTER/DEFINE_SETTER pops fn+key+obj, leaving the original obj.
1761
+ for (const prop of accessorProps) {
1762
+ const key = prop.key;
1763
+ let keyStr: string;
1764
+ if (key.type === "Identifier") {
1765
+ keyStr = key.name;
1766
+ } else if (
1767
+ key.type === "StringLiteral" ||
1768
+ key.type === "NumericLiteral"
1769
+ ) {
1770
+ keyStr = String(key.value);
1771
+ } else {
1772
+ throw new Error(`Unsupported object key type: ${key.type}`);
1773
+ }
1774
+
1775
+ this.emit(bc, [this.OP.DUP], node); // dup so the original obj stays after the define
1776
+ this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
1777
+
1778
+ // Compile the accessor body as an anonymous function descriptor.
1779
+ const desc = this._compileFunctionDecl(prop as any);
1780
+ this._emitClosureMetadata(desc, prop as any, bc);
1781
+ this.emit(
1782
+ bc,
1783
+ [
1784
+ this.OP.MAKE_CLOSURE,
1785
+ {
1786
+ type: "label",
1787
+ label: desc.entryLabel,
1788
+ },
1789
+ ],
1790
+ node,
1791
+ );
1792
+
1793
+ this.emit(
1794
+ bc,
1795
+ [
1796
+ prop.kind === "get"
1797
+ ? this.OP.DEFINE_GETTER
1798
+ : this.OP.DEFINE_SETTER,
1799
+ ],
1800
+ node,
1801
+ );
1802
+ }
1803
+
1375
1804
  break;
1376
1805
  }
1377
1806
 
1378
1807
  default: {
1379
- const src = generate(node).code;
1380
- throw new Error(`Unsupported expression: ${node.type}\n → ${src}`);
1808
+ throw new Error(`Unsupported expression: ${node.type}`);
1381
1809
  }
1382
1810
  }
1383
1811
  }
1384
1812
  }
1385
1813
 
1386
- // ─────────────────────────────────────────────────────────────────
1387
1814
  // Serializer
1388
1815
  // Turns the compiled output into a commented JS source string.
1389
- // ─────────────────────────────────────────────────────────────────
1816
+ // Expects fully-resolved bytecode (all label refs and constant refs already
1817
+ // converted to plain integers by resolveLabels + resolveConstants passes).
1390
1818
  class Serializer {
1391
1819
  compiler: Compiler;
1392
1820
 
@@ -1394,70 +1822,95 @@ class Serializer {
1394
1822
  this.compiler = compiler;
1395
1823
  }
1396
1824
 
1397
- get constants() {
1398
- return this.compiler.constants.items;
1825
+ get options() {
1826
+ return this.compiler.options;
1827
+ }
1828
+
1829
+ get OP() {
1830
+ return this.compiler.OP;
1831
+ }
1832
+
1833
+ get OP_NAME() {
1834
+ return this.compiler.OP_NAME;
1399
1835
  }
1400
1836
 
1401
- get fnDescriptors() {
1402
- return this.compiler.fnDescriptors;
1837
+ get JUMP_OPS() {
1838
+ return this.compiler.JUMP_OPS;
1403
1839
  }
1404
1840
 
1405
1841
  // Produce a JS literal for a constant pool entry
1406
1842
  _serializeConst(val) {
1407
1843
  if (val === null) return "null";
1408
1844
  if (val === undefined) return "undefined";
1409
- if (typeof val === "object" && val._fnIdx !== undefined) {
1410
- return `FN[${val._fnIdx}]`; // fn descriptor → reference by FN index
1411
- }
1412
1845
  return JSON.stringify(val); // number / string / bool
1413
1846
  }
1414
1847
 
1415
- // One instruction "[op, operand] // MNEMONIC description"
1416
- _serializeInstr(instr) {
1417
- const constants = this.constants;
1848
+ // One instruction -> "[op, operand] // MNEMONIC description"
1849
+ // Expects a fully-resolved instruction: operand is a plain number or undefined.
1850
+ _serializeInstr(instr: b.Instruction, constants: any[]) {
1851
+ const [op, rawOperand] = instr;
1852
+
1853
+ ok(
1854
+ rawOperand === undefined || typeof rawOperand === "number",
1855
+ "Unresolved operand: " + JSON.stringify(rawOperand),
1856
+ );
1857
+ const operand = rawOperand as number | undefined;
1418
1858
 
1419
- const [op, operand] = instr;
1420
- const name = OP_NAME[op] || `OP_${op}`;
1859
+ const name = this.OP_NAME[op] || `OP_${op}`;
1421
1860
  let comment = name;
1422
1861
 
1862
+ const sourceNode = instr[SOURCE_NODE_SYM];
1863
+ const sourceLocation = sourceNode
1864
+ ? sourceNode.loc.start?.line +
1865
+ ":" +
1866
+ sourceNode.loc.start?.column +
1867
+ "-" +
1868
+ (sourceNode.loc.end?.line + ":" + sourceNode.loc.end?.column)
1869
+ : "";
1870
+
1423
1871
  // Annotate operand with its meaning
1424
1872
  if (operand !== undefined) {
1425
1873
  switch (op) {
1426
- case OP.LOAD_CONST:
1427
- case OP.MAKE_CLOSURE: {
1874
+ case this.OP.LOAD_CONST: {
1428
1875
  const val = constants[operand];
1429
- if (val && typeof val === "object" && val.name) {
1430
- comment += ` FN[${val._fnIdx}] → fn:${val.name}`;
1431
- } else {
1432
- comment += ` ${JSON.stringify(val)}`;
1433
- }
1876
+ comment += ` ${this._serializeConst(val)}`;
1877
+ break;
1878
+ }
1879
+ case this.OP.MAKE_CLOSURE: {
1880
+ // operand is the absolute PC of the function body's first instruction
1881
+ comment += ` PC ${operand}`;
1434
1882
  break;
1435
1883
  }
1436
- case OP.LOAD_LOCAL:
1437
- case OP.STORE_LOCAL:
1884
+ case this.OP.DATA: {
1885
+ // Inline function header word — value is a raw integer
1886
+ comment += ` ${operand}`;
1887
+ break;
1888
+ }
1889
+ case this.OP.LOAD_LOCAL:
1890
+ case this.OP.STORE_LOCAL:
1438
1891
  comment += ` slot[${operand}]`;
1439
1892
  break;
1440
- case OP.LOAD_UPVALUE:
1441
- case OP.STORE_UPVALUE:
1893
+ case this.OP.LOAD_UPVALUE:
1894
+ case this.OP.STORE_UPVALUE:
1442
1895
  comment += ` upvalue[${operand}]`;
1443
1896
  break;
1444
- case OP.LOAD_GLOBAL:
1445
- case OP.STORE_GLOBAL:
1897
+ case this.OP.LOAD_GLOBAL:
1898
+ case this.OP.STORE_GLOBAL:
1446
1899
  comment += ` "${constants[operand]}"`;
1447
1900
  break;
1448
- case OP.CALL:
1449
- case OP.CALL_METHOD:
1901
+ case this.OP.CALL:
1902
+ case this.OP.CALL_METHOD:
1450
1903
  comment += ` (${operand} args)`;
1451
1904
  break;
1452
1905
 
1453
- case OP.BUILD_ARRAY:
1906
+ case this.OP.BUILD_ARRAY:
1454
1907
  comment += ` (${operand} elements)`;
1455
1908
  break;
1456
- case OP.BUILD_OBJECT:
1909
+ case this.OP.BUILD_OBJECT:
1457
1910
  comment += ` (${operand} pairs)`;
1458
1911
  break;
1459
1912
 
1460
- case OP.NEW:
1913
+ case this.OP.NEW:
1461
1914
  comment += ` (${operand} args)`;
1462
1915
  break;
1463
1916
 
@@ -1466,15 +1919,17 @@ class Serializer {
1466
1919
  }
1467
1920
  }
1468
1921
 
1922
+ comment = comment.padEnd(40) + sourceLocation;
1923
+
1469
1924
  // Pack a [op, operand?] instruction pair into a single 32-bit word.
1470
1925
  // Shared between the Serializer and the obfuscation path in _compileMain.
1471
1926
 
1472
- if (!PACK) {
1473
- const instrText =
1474
- operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
1927
+ const instrText = operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
1928
+ const text = `${(instrText + ",").padEnd(12)} ${comment}`;
1475
1929
 
1930
+ if (!this.options.encodeBytecode) {
1476
1931
  return {
1477
- text: ` ${instrText.padEnd(12)}, // ${comment}`,
1932
+ text: text,
1478
1933
  value: operand !== undefined ? [op, operand] : [op],
1479
1934
  };
1480
1935
  }
@@ -1491,47 +1946,38 @@ class Serializer {
1491
1946
  }
1492
1947
 
1493
1948
  return {
1494
- text: "",
1949
+ text: text,
1495
1950
  value: packInstr(instr),
1496
1951
  };
1497
1952
  }
1498
1953
 
1499
- // Serialize one fn descriptor into its FN[n] block
1500
- _serializeFn(desc) {
1501
- const lines = [
1502
- ` { // FN[${desc._fnIdx}] — ${desc.name}`,
1503
- ` paramCount: ${desc.paramCount},`,
1504
- ` localCount: ${desc.localCount},`,
1505
- ` upvalueDescriptors: ${JSON5.stringify(desc.upvalueDescriptors)},`,
1506
- ` startPc: ${desc.startPc},`,
1507
- ` },`,
1508
- ];
1509
- return lines.join("\n");
1510
- }
1511
-
1512
- // Serialize the CONSTANTS array, showing FN[n] references
1513
- _serializeConstants() {
1954
+ // Serialize the CONSTANTS array
1955
+ _serializeConstants(constants: any[]) {
1514
1956
  const lines = ["var CONSTANTS = ["];
1515
- this.constants.forEach((val, idx) => {
1957
+ constants.forEach((val, idx) => {
1516
1958
  lines.push(` /* ${idx} */ ${this._serializeConst(val)},`);
1517
1959
  });
1518
1960
  lines.push("];");
1519
1961
  return lines.join("\n");
1520
1962
  }
1521
1963
 
1522
- _serializeBytecode(bytecode) {
1523
- if (!PACK) {
1524
- return bytecode.map((instr) => this._serializeInstr(instr).value);
1525
- }
1964
+ // Filter out any remaining null-opcode pseudo-instructions.
1965
+ // (defineLabel pseudo-ops are already stripped by resolveLabels.)
1966
+ _serializeBytecode(bytecode: b.Bytecode): { bytecode: b.Bytecode } {
1967
+ return {
1968
+ bytecode: bytecode.filter((instr) => instr[0] !== null),
1969
+ };
1970
+ }
1526
1971
 
1972
+ __serializeBytecode(bytecode: b.Bytecode, constants: any[]) {
1527
1973
  let words = [];
1528
1974
 
1529
- // ── BYTECODE
1975
+ // BYTECODE
1530
1976
  for (const instr of bytecode) {
1531
- words.push(this._serializeInstr(instr).value);
1977
+ words.push(this._serializeInstr(instr, constants).value);
1532
1978
  }
1533
1979
 
1534
- // Convert packed words raw 4-byte little-endian binary base64
1980
+ // Convert packed words -> raw 4-byte little-endian binary -> base64
1535
1981
  const buf = new Uint8Array(words.length * 4);
1536
1982
  words.forEach((w, i) => {
1537
1983
  buf[i * 4] = w & 0xff;
@@ -1539,77 +1985,80 @@ class Serializer {
1539
1985
  buf[i * 4 + 2] = (w >>> 16) & 0xff;
1540
1986
  buf[i * 4 + 3] = (w >>> 24) & 0xff;
1541
1987
  });
1542
- const b64 = Buffer.from(buf).toString("base64");
1543
-
1544
- return b64;
1988
+ return Buffer.from(buf).toString("base64");
1545
1989
  }
1546
1990
 
1547
- serialize(bytecode, mainStartPc) {
1548
- const sections = [];
1991
+ serialize(bytecode: b.Bytecode, constants: any[], compiler: Compiler) {
1992
+ const mainStartPc = compiler.mainStartPc;
1993
+ let sections = [];
1994
+
1995
+ var textForm = [];
1996
+ var initBody = [];
1549
1997
 
1550
- // ── FN array
1551
- const fnLines = ["var FN = ["];
1552
- for (const desc of this.fnDescriptors) {
1553
- fnLines.push(this._serializeFn(desc));
1998
+ var bytecodeResult = this._serializeBytecode(bytecode);
1999
+
2000
+ for (const instr of bytecodeResult.bytecode) {
2001
+ const serialized = this._serializeInstr(instr, constants);
2002
+ textForm.push(serialized.text);
1554
2003
  }
1555
- fnLines.push("];");
1556
- sections.push(fnLines.join("\n"));
1557
2004
 
1558
- // ── CONSTANTS
1559
- sections.push(this._serializeConstants());
2005
+ initBody.push(textForm.map((line) => `// ${line}`).join("\n"));
1560
2006
 
1561
- if (PACK) {
1562
- sections.push(`var BYTECODE = "${this._serializeBytecode(bytecode)}";`);
2007
+ if (this.options.encodeBytecode) {
2008
+ sections.push(
2009
+ `var BYTECODE = "${this.__serializeBytecode(bytecodeResult.bytecode, constants)}";`,
2010
+ );
1563
2011
  } else {
1564
2012
  sections.push(
1565
- `var BYTECODE = [\n ${bytecode.map((instr) => this._serializeInstr(instr).text).join(",\n ")}\n];`,
2013
+ `var BYTECODE = [${bytecodeResult.bytecode.map((v) => "[" + v[0] + ", " + v[1] + "]").join(",")}]`,
1566
2014
  );
1567
2015
  }
1568
2016
 
1569
- // ── MAIN_START_PC
2017
+ // MAIN_START_PC
1570
2018
  sections.push(`var MAIN_START_PC = ${mainStartPc};`);
2019
+ sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
2020
+ sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
2021
+ // Opcodes
2022
+ sections.push(`var OP = ${JSON5.stringify(this.OP)};`);
2023
+
2024
+ // Constants must be defined before the bytecode
2025
+ initBody.push(this._serializeConstants(constants));
1571
2026
 
1572
- sections.push(`var PACK = ${PACK};`);
2027
+ sections = [...initBody, ...sections];
1573
2028
 
1574
- // ── VM runtime
2029
+ // VM runtime
1575
2030
  sections.push(VM_RUNTIME);
1576
2031
 
1577
2032
  return sections.join("\n\n");
1578
2033
  }
1579
2034
  }
1580
2035
 
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(
2036
+ export async function compileAndSerialize(
1600
2037
  sourceCode: string,
1601
- options: Options = {
1602
- selfModifying: true,
1603
- },
2038
+ options: Options,
1604
2039
  ) {
1605
2040
  const compiler = new Compiler(options);
1606
- const result = compiler.compile(sourceCode);
2041
+ let bytecode = compiler.compile(sourceCode);
2042
+
2043
+ // User transform passes (operate on unresolved IR with label/constant refs)
2044
+ const passes = [...(options.selfModifying ? [selfModifying] : [])];
2045
+ for (const pass of passes) {
2046
+ const passResult = pass(bytecode, compiler);
2047
+ bytecode = passResult.bytecode;
2048
+ }
2049
+
2050
+ // Assembler phases: resolve IR operands to plain integers before printing
2051
+ const { bytecode: labelResolved } = resolveLabels(bytecode, compiler);
2052
+ const { bytecode: finalBytecode, constants } =
2053
+ resolveConstants(labelResolved);
2054
+
1607
2055
  const output = compiler.serializer.serialize(
1608
- result.bytecode,
1609
- result.mainStartPc,
2056
+ finalBytecode,
2057
+ constants,
2058
+ compiler,
1610
2059
  );
1611
2060
 
1612
- const finalOutput = output;
2061
+ const finalOutput = await obfuscateRuntime(output, options);
1613
2062
 
1614
2063
  return {
1615
2064
  code: finalOutput,