js-confuser-vm 0.0.2 → 0.0.4

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