js-confuser-vm 0.0.2 → 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/CHANGELOG.md +28 -0
- package/README.MD +16 -10
- package/babel-plugin-inline-runtime.cjs +34 -0
- package/babel.config.json +2 -3
- package/dist/compiler.js +687 -421
- package/dist/index.js +2 -1
- package/dist/options.js +1 -1
- package/dist/runtime.js +598 -463
- package/dist/runtimeObf.js +26 -6
- package/dist/transforms/controlFlowFlattening.js +22 -0
- package/dist/transforms/resolveContants.js +33 -0
- package/dist/transforms/resolveLabels.js +59 -0
- package/dist/transforms/selfModifying.js +107 -0
- package/dist/types.js +13 -0
- package/index.ts +0 -6
- package/jest.config.js +10 -0
- package/package.json +1 -1
- package/src/compiler.ts +876 -487
- package/src/index.ts +2 -1
- package/src/options.ts +2 -0
- package/src/runtime.ts +589 -455
- package/src/runtimeObf.ts +22 -8
- package/src/transforms/controlFlowFlattening.ts +30 -0
- package/src/transforms/resolveContants.ts +42 -0
- package/src/transforms/resolveLabels.ts +83 -0
- package/src/transforms/selfModifying.ts +124 -0
- package/src/types.ts +24 -0
- package/dist/minify_empty_externs.js +0 -4
package/src/compiler.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
2
|
import traverseImport from "@babel/traverse";
|
|
3
3
|
import { generate } from "@babel/generator";
|
|
4
4
|
|
|
@@ -6,23 +6,31 @@ import { readFileSync } from "fs";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { stripTypeScriptTypes } from "module";
|
|
8
8
|
import JSON5 from "json5";
|
|
9
|
-
import { choice, getRandomInt } from "./random.ts";
|
|
10
9
|
import * as t from "@babel/types";
|
|
11
10
|
import { ok } from "assert";
|
|
12
11
|
import { obfuscateRuntime } from "./runtimeObf.ts";
|
|
13
|
-
import type
|
|
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";
|
|
14
17
|
|
|
15
|
-
const traverse = traverseImport.default
|
|
18
|
+
const traverse = (traverseImport.default ||
|
|
19
|
+
traverseImport) as typeof traverseImport.default;
|
|
16
20
|
|
|
17
21
|
const readVMRuntimeFile = () => {
|
|
22
|
+
let code;
|
|
18
23
|
try {
|
|
19
|
-
|
|
24
|
+
code = readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8");
|
|
20
25
|
} catch (e) {
|
|
21
|
-
|
|
26
|
+
code = readFileSync(join(import.meta.dirname, "./runtime.js"), "utf-8");
|
|
22
27
|
}
|
|
28
|
+
|
|
29
|
+
return stripTypeScriptTypes?.(code) || code;
|
|
23
30
|
};
|
|
24
31
|
|
|
25
|
-
const VM_RUNTIME =
|
|
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
|
|
26
34
|
|
|
27
35
|
// Opcodes
|
|
28
36
|
export const OP_ORIGINAL = {
|
|
@@ -94,36 +102,26 @@ export const OP_ORIGINAL = {
|
|
|
94
102
|
|
|
95
103
|
// Self-modifying bytecode
|
|
96
104
|
PATCH: 56, // pop destPc; constants[operand]=word[]; write words into bytecode[destPc..]
|
|
97
|
-
};
|
|
98
105
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
class ConstantPool {
|
|
103
|
-
items: any[];
|
|
104
|
-
_index: Map<string, number>;
|
|
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)
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
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})
|
|
110
113
|
|
|
111
|
-
|
|
112
|
-
// Only intern primitives -- objects must use addObject()
|
|
113
|
-
const key = `${typeof val}:${val}`;
|
|
114
|
-
if (this._index.has(key)) return this._index.get(key);
|
|
115
|
-
const idx = this.items.length;
|
|
116
|
-
this.items.push(val);
|
|
117
|
-
this._index.set(key, idx);
|
|
118
|
-
return idx;
|
|
119
|
-
}
|
|
114
|
+
DEBUGGER: 61, // for dev/testing -- emits a "debugger" statement with a comment of the original source location
|
|
120
115
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
};
|
|
127
125
|
|
|
128
126
|
// Scope
|
|
129
127
|
// Each function call gets its own Scope. Locals are resolved to
|
|
@@ -168,7 +166,7 @@ class FnContext {
|
|
|
168
166
|
parentCtx: FnContext | null;
|
|
169
167
|
scope: Scope;
|
|
170
168
|
compiler: Compiler;
|
|
171
|
-
bc:
|
|
169
|
+
bc: b.Instruction[];
|
|
172
170
|
|
|
173
171
|
constructor(compiler, parentCtx = null) {
|
|
174
172
|
this.compiler = compiler;
|
|
@@ -192,20 +190,22 @@ class FnContext {
|
|
|
192
190
|
}
|
|
193
191
|
|
|
194
192
|
// Compiler
|
|
195
|
-
class Compiler {
|
|
196
|
-
constants: ConstantPool;
|
|
193
|
+
export class Compiler {
|
|
197
194
|
fnDescriptors: any[];
|
|
198
|
-
bytecode:
|
|
195
|
+
bytecode: b.Bytecode;
|
|
199
196
|
mainStartPc: number;
|
|
200
197
|
|
|
201
198
|
_currentCtx: FnContext | null;
|
|
202
199
|
_pendingLabel: string | null;
|
|
203
200
|
_forInCount: number;
|
|
201
|
+
_labelCount: number;
|
|
204
202
|
_loopStack: {
|
|
205
203
|
type: "loop" | "switch" | "block";
|
|
206
204
|
label: string | null;
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
209
209
|
}[];
|
|
210
210
|
|
|
211
211
|
options: Options;
|
|
@@ -215,16 +215,26 @@ class Compiler {
|
|
|
215
215
|
OP_NAME: Record<number, string>;
|
|
216
216
|
JUMP_OPS: Set<number>;
|
|
217
217
|
|
|
218
|
-
|
|
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) {
|
|
219
229
|
this.options = options;
|
|
220
|
-
this.constants = new ConstantPool();
|
|
221
230
|
this.fnDescriptors = []; // populated in pass 1
|
|
222
231
|
this.bytecode = [];
|
|
223
232
|
this.mainStartPc = 0;
|
|
224
233
|
this._currentCtx = null; // FnContext of the function being compiled, null at top-level
|
|
225
|
-
this._loopStack = []; //
|
|
234
|
+
this._loopStack = []; // per active loop/switch/block/try
|
|
226
235
|
this._pendingLabel = null;
|
|
227
236
|
this._forInCount = 0; // counter for synthetic for-in iterator global names
|
|
237
|
+
this._labelCount = 0; // monotonically increasing counter for unique label names
|
|
228
238
|
|
|
229
239
|
this.serializer = new Serializer(this);
|
|
230
240
|
|
|
@@ -255,9 +265,16 @@ class Compiler {
|
|
|
255
265
|
this.OP.JUMP_IF_TRUE_OR_POP,
|
|
256
266
|
this.OP.JUMP_IF_FALSE_OR_POP,
|
|
257
267
|
this.OP.FOR_IN_NEXT,
|
|
268
|
+
this.OP.TRY_SETUP, // catch_pc operand needs offset adjustment like jump targets
|
|
258
269
|
]);
|
|
259
270
|
}
|
|
260
271
|
|
|
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
|
+
|
|
261
278
|
// Variable resolution
|
|
262
279
|
// Walks up the FnContext chain. Crossing a context boundary means
|
|
263
280
|
// we're capturing from an outer function - register an upvalue.
|
|
@@ -287,7 +304,7 @@ class Compiler {
|
|
|
287
304
|
|
|
288
305
|
// Entry point
|
|
289
306
|
compile(source: string) {
|
|
290
|
-
const ast =
|
|
307
|
+
const ast = parse(source, { sourceType: "script" });
|
|
291
308
|
|
|
292
309
|
return this.compileAST(ast);
|
|
293
310
|
}
|
|
@@ -308,21 +325,32 @@ class Compiler {
|
|
|
308
325
|
// Pass 2 -- compile top-level statements into BYTECODE.
|
|
309
326
|
this._compileMain(ast.program.body);
|
|
310
327
|
|
|
311
|
-
return
|
|
312
|
-
bytecode: this.bytecode,
|
|
313
|
-
mainStartPc: this.mainStartPc,
|
|
314
|
-
};
|
|
328
|
+
return this.bytecode;
|
|
315
329
|
}
|
|
316
330
|
|
|
317
331
|
// Function Declaration
|
|
318
332
|
|
|
319
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);
|
|
342
|
+
|
|
320
343
|
// Create a context whose parent is whatever we're currently compiling.
|
|
321
344
|
// This is what lets _resolve cross function boundaries correctly.
|
|
322
345
|
const ctx = new FnContext(this, this._currentCtx);
|
|
323
346
|
const savedCtx = this._currentCtx;
|
|
324
347
|
this._currentCtx = ctx;
|
|
325
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
|
+
|
|
326
354
|
// Params occupy the first N local slots (args are copied in on CALL)
|
|
327
355
|
for (const param of node.params) {
|
|
328
356
|
let identifier = param.type === "AssignmentPattern" ? param.left : param;
|
|
@@ -341,24 +369,34 @@ class Compiler {
|
|
|
341
369
|
// Pass 2: emit default-value guards at top of fn body
|
|
342
370
|
// Mirrors what JS engines do: if the caller passed undefined (or
|
|
343
371
|
// nothing), evaluate the default expression and overwrite the slot.
|
|
344
|
-
// Default expressions are full expressions, so f(x = a + b) and
|
|
345
|
-
// f(x = foo()) both work correctly.
|
|
346
372
|
for (const param of node.params) {
|
|
347
373
|
if (param.type !== "AssignmentPattern") continue;
|
|
348
374
|
|
|
349
375
|
const slot = ctx.scope._locals.get((param.left as t.Identifier).name);
|
|
376
|
+
const skipLabel = this._makeLabel("param_skip");
|
|
350
377
|
|
|
351
378
|
// if (param === undefined) param = <default expr>
|
|
352
|
-
ctx.bc
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
);
|
|
357
391
|
|
|
358
392
|
this._compileExpr(param.right, ctx.scope, ctx.bc); // eval default
|
|
359
|
-
ctx.bc
|
|
393
|
+
this.emit(ctx.bc, [this.OP.STORE_LOCAL, slot], param);
|
|
360
394
|
|
|
361
|
-
|
|
395
|
+
this.emit(
|
|
396
|
+
ctx.bc,
|
|
397
|
+
[null, { type: "defineLabel", label: skipLabel }],
|
|
398
|
+
param,
|
|
399
|
+
);
|
|
362
400
|
}
|
|
363
401
|
|
|
364
402
|
for (const stmt of node.body.body) {
|
|
@@ -366,36 +404,48 @@ class Compiler {
|
|
|
366
404
|
}
|
|
367
405
|
|
|
368
406
|
// If we fall off the end of the function, implicitly return undefined.
|
|
369
|
-
ctx.bc
|
|
370
|
-
ctx.bc
|
|
407
|
+
this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
|
|
408
|
+
this.emit(ctx.bc, [this.OP.RETURN], node);
|
|
371
409
|
|
|
372
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();
|
|
373
426
|
|
|
374
|
-
var fnIdx = this.fnDescriptors.length;
|
|
375
|
-
(node as any)._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
|
-
|
|
391
|
-
this.fnDescriptors.push(desc);
|
|
392
|
-
desc._constIdx = this.constants.addObject(desc); // object entry, no dedup
|
|
393
427
|
return desc;
|
|
394
428
|
}
|
|
395
429
|
|
|
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
|
+
}
|
|
446
|
+
|
|
396
447
|
// Main (top-level)
|
|
397
448
|
_compileMain(body: t.Statement[]) {
|
|
398
|
-
this.mainStartPc = 0; // ← record main's entry point
|
|
399
449
|
const bc = this.bytecode;
|
|
400
450
|
|
|
401
451
|
// Hoist all FunctionDeclarations: MAKE_CLOSURE -> STORE_GLOBAL
|
|
@@ -405,9 +455,14 @@ class Compiler {
|
|
|
405
455
|
const desc = this.fnDescriptors.find(
|
|
406
456
|
(d) => d._fnIdx === (node as any)._fnIdx,
|
|
407
457
|
);
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
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);
|
|
411
466
|
}
|
|
412
467
|
|
|
413
468
|
// Compile everything else in order
|
|
@@ -416,46 +471,18 @@ class Compiler {
|
|
|
416
471
|
this._compileStatement(node, null, bc); // null scope -> global context
|
|
417
472
|
}
|
|
418
473
|
|
|
419
|
-
|
|
474
|
+
this.emit(bc, [this.OP.RETURN], null); // end program
|
|
420
475
|
|
|
421
|
-
//
|
|
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.
|
|
422
479
|
for (const descriptor of this.fnDescriptors) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
// Real body starts immediately after the preamble.
|
|
430
|
-
const bodyPc = descriptor.startPc + 2;
|
|
431
|
-
|
|
432
|
-
// Build real body with jump targets resolved from bodyPc as the base.
|
|
433
|
-
const realBodyInstrs = descriptor.bytecode.map((instr) =>
|
|
434
|
-
this._offsetJump(instr, bodyPc),
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
// Pack each instruction into a 32-bit word and store as a constant.
|
|
438
|
-
// The PATCH handler will write these words directly into this.bytecode.
|
|
439
|
-
const realBodyWords =
|
|
440
|
-
this.serializer._serializeBytecode(realBodyInstrs);
|
|
441
|
-
const bodyConstIdx = this.constants.addObject(realBodyWords);
|
|
442
|
-
|
|
443
|
-
// Emit preamble: push destination PC, then PATCH.
|
|
444
|
-
const destPcConstIdx = this.constants.intern(bodyPc);
|
|
445
|
-
this.bytecode.push([this.OP.LOAD_CONST, destPcConstIdx]);
|
|
446
|
-
this.bytecode.push([this.OP.PATCH, bodyConstIdx]);
|
|
447
|
-
|
|
448
|
-
// Garbage fill -- same length as real body, never executed (PATCH fires first).
|
|
449
|
-
for (let i = 0; i < realBodyInstrs.length; i++) {
|
|
450
|
-
this.bytecode.push([
|
|
451
|
-
choice(Object.values(this.OP)),
|
|
452
|
-
getRandomInt(0, 255),
|
|
453
|
-
]);
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
for (const instr of descriptor.bytecode) {
|
|
457
|
-
this.bytecode.push(this._offsetJump(instr, descriptor.startPc));
|
|
458
|
-
}
|
|
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);
|
|
459
486
|
}
|
|
460
487
|
}
|
|
461
488
|
|
|
@@ -464,27 +491,24 @@ class Compiler {
|
|
|
464
491
|
`Program too large: ${this.bytecode.length} instructions, max 16,777,215`,
|
|
465
492
|
);
|
|
466
493
|
|
|
467
|
-
if (this.constants.items.length > 0xffffff)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
_offsetJump(instr, offset) {
|
|
474
|
-
if (this.JUMP_OPS.has(instr[0]) && instr[1] !== undefined) {
|
|
475
|
-
return [instr[0], instr[1] + offset];
|
|
476
|
-
}
|
|
477
|
-
return instr;
|
|
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
|
+
// );
|
|
478
498
|
}
|
|
479
499
|
|
|
480
500
|
// Statements
|
|
481
|
-
_compileStatement(node: t.Statement, scope, bc) {
|
|
501
|
+
_compileStatement(node: t.Statement, scope: Scope, bc: b.Bytecode) {
|
|
482
502
|
switch (node.type) {
|
|
483
503
|
case "EmptyStatement": {
|
|
484
504
|
// nothing to emit -- bare semicolon is a no-op
|
|
485
505
|
break;
|
|
486
506
|
}
|
|
487
507
|
|
|
508
|
+
case "DebuggerStatement":
|
|
509
|
+
this.emit(bc, [this.OP.DEBUGGER], node);
|
|
510
|
+
break;
|
|
511
|
+
|
|
488
512
|
case "BlockStatement": {
|
|
489
513
|
for (const stmt of node.body) {
|
|
490
514
|
this._compileStatement(stmt, scope, bc);
|
|
@@ -497,19 +521,28 @@ class Compiler {
|
|
|
497
521
|
// MAKE_CLOSURE so it's captured as a live closure at runtime.
|
|
498
522
|
// (_compileFunctionDecl pushes/pops _currentCtx internally)
|
|
499
523
|
const desc = this._compileFunctionDecl(node);
|
|
500
|
-
|
|
524
|
+
this._emitClosureMetadata(desc, node, bc);
|
|
525
|
+
this.emit(
|
|
526
|
+
bc,
|
|
527
|
+
[this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
|
|
528
|
+
node,
|
|
529
|
+
);
|
|
501
530
|
if (scope) {
|
|
502
531
|
const slot = scope.define(node.id.name);
|
|
503
|
-
|
|
532
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
504
533
|
} else {
|
|
505
|
-
|
|
534
|
+
this.emit(
|
|
535
|
+
bc,
|
|
536
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(node.id.name)],
|
|
537
|
+
node,
|
|
538
|
+
);
|
|
506
539
|
}
|
|
507
540
|
break;
|
|
508
541
|
}
|
|
509
542
|
|
|
510
543
|
case "ThrowStatement": {
|
|
511
544
|
this._compileExpr(node.argument, scope, bc);
|
|
512
|
-
|
|
545
|
+
this.emit(bc, [this.OP.THROW], node);
|
|
513
546
|
break;
|
|
514
547
|
}
|
|
515
548
|
|
|
@@ -517,15 +550,27 @@ class Compiler {
|
|
|
517
550
|
if (node.argument) {
|
|
518
551
|
this._compileExpr(node.argument, scope, bc);
|
|
519
552
|
} else {
|
|
520
|
-
|
|
553
|
+
this.emit(
|
|
554
|
+
bc,
|
|
555
|
+
[this.OP.LOAD_CONST, b.constantOperand(undefined)],
|
|
556
|
+
node,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
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
|
+
}
|
|
521
566
|
}
|
|
522
|
-
|
|
567
|
+
this.emit(bc, [this.OP.RETURN], node);
|
|
523
568
|
break;
|
|
524
569
|
}
|
|
525
570
|
|
|
526
571
|
case "ExpressionStatement": {
|
|
527
572
|
this._compileExpr(node.expression, scope, bc);
|
|
528
|
-
|
|
573
|
+
this.emit(bc, [this.OP.POP], node); // discard return value of statement-level expressions
|
|
529
574
|
break;
|
|
530
575
|
}
|
|
531
576
|
|
|
@@ -535,7 +580,11 @@ class Compiler {
|
|
|
535
580
|
if (decl.init) {
|
|
536
581
|
this._compileExpr(decl.init, scope, bc);
|
|
537
582
|
} else {
|
|
538
|
-
|
|
583
|
+
this.emit(
|
|
584
|
+
bc,
|
|
585
|
+
[this.OP.LOAD_CONST, b.constantOperand(undefined)],
|
|
586
|
+
node,
|
|
587
|
+
);
|
|
539
588
|
}
|
|
540
589
|
|
|
541
590
|
ok(
|
|
@@ -546,25 +595,29 @@ class Compiler {
|
|
|
546
595
|
// Store: local slot if inside a function, global name otherwise
|
|
547
596
|
if (scope) {
|
|
548
597
|
const slot = scope.define(decl.id.name);
|
|
549
|
-
|
|
598
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
550
599
|
} else {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
this.
|
|
554
|
-
|
|
600
|
+
this.emit(
|
|
601
|
+
bc,
|
|
602
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(decl.id.name)],
|
|
603
|
+
node,
|
|
604
|
+
);
|
|
555
605
|
}
|
|
556
606
|
}
|
|
557
607
|
break;
|
|
558
608
|
}
|
|
559
609
|
|
|
560
610
|
case "IfStatement": {
|
|
611
|
+
const elseOrEndLabel = this._makeLabel("if_else");
|
|
561
612
|
// 1. Compile the test expression -> leaves a value on the stack
|
|
562
613
|
this._compileExpr(node.test, scope, bc);
|
|
563
|
-
// 2. Emit JUMP_IF_FALSE
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
);
|
|
566
620
|
// 3. Compile the consequent block (the "then" branch)
|
|
567
|
-
// Consequent may be a BlockStatement or a bare statement (no braces)
|
|
568
621
|
const consequentBody =
|
|
569
622
|
node.consequent.type === "BlockStatement"
|
|
570
623
|
? node.consequent.body
|
|
@@ -574,10 +627,18 @@ class Compiler {
|
|
|
574
627
|
}
|
|
575
628
|
if (node.alternate) {
|
|
576
629
|
// 4a. Consequent needs to jump OVER the else block when done
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
+
);
|
|
581
642
|
// 5. Compile the alternate (else) block
|
|
582
643
|
const altBody =
|
|
583
644
|
node.alternate.type === "BlockStatement"
|
|
@@ -586,11 +647,15 @@ class Compiler {
|
|
|
586
647
|
for (const stmt of altBody) {
|
|
587
648
|
this._compileStatement(stmt, scope, bc);
|
|
588
649
|
}
|
|
589
|
-
//
|
|
590
|
-
bc[
|
|
650
|
+
// Mark end (consequent's jump lands here)
|
|
651
|
+
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
591
652
|
} else {
|
|
592
|
-
// 4b. No else --
|
|
593
|
-
|
|
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
|
+
);
|
|
594
659
|
}
|
|
595
660
|
break;
|
|
596
661
|
}
|
|
@@ -598,18 +663,28 @@ class Compiler {
|
|
|
598
663
|
case "WhileStatement": {
|
|
599
664
|
const _wLabel = this._pendingLabel;
|
|
600
665
|
this._pendingLabel = null;
|
|
666
|
+
|
|
667
|
+
const loopTopLabel = this._makeLabel("while_top");
|
|
668
|
+
const exitLabel = this._makeLabel("while_exit");
|
|
669
|
+
|
|
601
670
|
this._loopStack.push({
|
|
602
671
|
type: "loop",
|
|
603
672
|
label: _wLabel,
|
|
604
|
-
|
|
605
|
-
|
|
673
|
+
breakLabel: exitLabel,
|
|
674
|
+
continueLabel: loopTopLabel, // continue re-evaluates the test
|
|
606
675
|
});
|
|
607
|
-
const loopCtxW = this._loopStack[this._loopStack.length - 1];
|
|
608
676
|
|
|
609
|
-
|
|
677
|
+
this.emit(
|
|
678
|
+
bc,
|
|
679
|
+
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
680
|
+
node,
|
|
681
|
+
);
|
|
610
682
|
this._compileExpr(node.test, scope, bc);
|
|
611
|
-
|
|
612
|
-
|
|
683
|
+
this.emit(
|
|
684
|
+
bc,
|
|
685
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
|
|
686
|
+
node,
|
|
687
|
+
);
|
|
613
688
|
|
|
614
689
|
const whileBody =
|
|
615
690
|
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
@@ -617,13 +692,12 @@ class Compiler {
|
|
|
617
692
|
this._compileStatement(stmt, scope, bc);
|
|
618
693
|
}
|
|
619
694
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
bc[
|
|
626
|
-
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);
|
|
627
701
|
|
|
628
702
|
this._loopStack.pop();
|
|
629
703
|
break;
|
|
@@ -632,15 +706,23 @@ class Compiler {
|
|
|
632
706
|
case "DoWhileStatement": {
|
|
633
707
|
const _dwLabel = this._pendingLabel;
|
|
634
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
|
+
|
|
635
714
|
this._loopStack.push({
|
|
636
715
|
type: "loop",
|
|
637
716
|
label: _dwLabel,
|
|
638
|
-
|
|
639
|
-
|
|
717
|
+
breakLabel: exitLabel,
|
|
718
|
+
continueLabel: continueLabel, // continue falls to the test
|
|
640
719
|
});
|
|
641
|
-
const loopCtxDW = this._loopStack[this._loopStack.length - 1];
|
|
642
720
|
|
|
643
|
-
|
|
721
|
+
this.emit(
|
|
722
|
+
bc,
|
|
723
|
+
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
724
|
+
node,
|
|
725
|
+
);
|
|
644
726
|
|
|
645
727
|
const doWhileBody =
|
|
646
728
|
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
@@ -649,18 +731,24 @@ class Compiler {
|
|
|
649
731
|
}
|
|
650
732
|
|
|
651
733
|
// continue -> skip rest of body, fall through to test
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
734
|
+
this.emit(
|
|
735
|
+
bc,
|
|
736
|
+
[null, { type: "defineLabel", label: continueLabel }],
|
|
737
|
+
node,
|
|
738
|
+
);
|
|
656
739
|
this._compileExpr(node.test, scope, bc);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
+
);
|
|
660
750
|
|
|
661
|
-
|
|
662
|
-
bc[exitJumpIdxDW][1] = exitTargetDW;
|
|
663
|
-
for (const idx of loopCtxDW.breakJumps) bc[idx][1] = exitTargetDW;
|
|
751
|
+
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
664
752
|
|
|
665
753
|
this._loopStack.pop();
|
|
666
754
|
break;
|
|
@@ -669,29 +757,43 @@ class Compiler {
|
|
|
669
757
|
case "ForStatement": {
|
|
670
758
|
const _fLabel = this._pendingLabel;
|
|
671
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
|
+
|
|
672
768
|
this._loopStack.push({
|
|
673
769
|
type: "loop",
|
|
674
770
|
label: _fLabel,
|
|
675
|
-
|
|
676
|
-
|
|
771
|
+
breakLabel: exitLabel,
|
|
772
|
+
continueLabel: updateLabel,
|
|
677
773
|
});
|
|
678
|
-
const loopCtxF = this._loopStack[this._loopStack.length - 1];
|
|
679
774
|
|
|
680
775
|
if (node.init) {
|
|
681
776
|
if (node.init.type === "VariableDeclaration") {
|
|
682
777
|
this._compileStatement(node.init, scope, bc);
|
|
683
778
|
} else {
|
|
684
779
|
this._compileExpr(node.init, scope, bc);
|
|
685
|
-
|
|
780
|
+
this.emit(bc, [this.OP.POP], node);
|
|
686
781
|
}
|
|
687
782
|
}
|
|
688
783
|
|
|
689
|
-
|
|
784
|
+
this.emit(
|
|
785
|
+
bc,
|
|
786
|
+
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
787
|
+
node,
|
|
788
|
+
);
|
|
690
789
|
if (node.test) {
|
|
691
790
|
this._compileExpr(node.test, scope, bc);
|
|
692
|
-
|
|
791
|
+
this.emit(
|
|
792
|
+
bc,
|
|
793
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
|
|
794
|
+
node,
|
|
795
|
+
);
|
|
693
796
|
}
|
|
694
|
-
const exitJumpIdxF = node.test ? bc.length - 1 : null;
|
|
695
797
|
|
|
696
798
|
const forBody =
|
|
697
799
|
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
@@ -701,96 +803,126 @@ class Compiler {
|
|
|
701
803
|
|
|
702
804
|
// continue -> run update (if any) then back to test
|
|
703
805
|
if (node.update) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
806
|
+
this.emit(
|
|
807
|
+
bc,
|
|
808
|
+
[null, { type: "defineLabel", label: updateLabel }],
|
|
809
|
+
node,
|
|
810
|
+
);
|
|
707
811
|
this._compileExpr(node.update, scope, bc);
|
|
708
|
-
|
|
709
|
-
} else {
|
|
710
|
-
// No update -- continue goes straight to the test
|
|
711
|
-
for (const idx of loopCtxF.continueJumps) bc[idx][1] = loopTopF;
|
|
812
|
+
this.emit(bc, [this.OP.POP], node);
|
|
712
813
|
}
|
|
713
814
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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);
|
|
719
821
|
|
|
720
822
|
this._loopStack.pop();
|
|
721
823
|
break;
|
|
722
824
|
}
|
|
723
825
|
|
|
724
826
|
case "BreakStatement": {
|
|
725
|
-
|
|
726
|
-
|
|
827
|
+
// Find the jump target in the loop stack.
|
|
828
|
+
let _bTargetIdx = -1;
|
|
727
829
|
if (node.label) {
|
|
728
830
|
const _bLabelName = node.label.name;
|
|
729
|
-
let _bFound = -1;
|
|
730
831
|
for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
|
|
731
832
|
if (this._loopStack[_bi].label === _bLabelName) {
|
|
732
|
-
|
|
833
|
+
_bTargetIdx = _bi;
|
|
733
834
|
break;
|
|
734
835
|
}
|
|
735
836
|
}
|
|
736
|
-
if (
|
|
737
|
-
throw new Error(`Label '${
|
|
738
|
-
this._loopStack[_bFound].breakJumps.push(_bJumpIdx);
|
|
837
|
+
if (_bTargetIdx === -1)
|
|
838
|
+
throw new Error(`Label '${node.label.name}' not found`);
|
|
739
839
|
} else {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
+
}
|
|
745
854
|
}
|
|
855
|
+
this.emit(
|
|
856
|
+
bc,
|
|
857
|
+
[
|
|
858
|
+
this.OP.JUMP,
|
|
859
|
+
{ type: "label", label: this._loopStack[_bTargetIdx].breakLabel },
|
|
860
|
+
],
|
|
861
|
+
node,
|
|
862
|
+
);
|
|
746
863
|
break;
|
|
747
864
|
}
|
|
748
865
|
|
|
749
866
|
case "ContinueStatement": {
|
|
750
|
-
|
|
751
|
-
|
|
867
|
+
// Find the target loop in the loop stack.
|
|
868
|
+
let _cTargetIdx = -1;
|
|
752
869
|
if (node.label) {
|
|
753
870
|
const _cLabelName = node.label.name;
|
|
754
|
-
let _cFound = -1;
|
|
755
871
|
for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
|
|
756
872
|
if (
|
|
757
873
|
this._loopStack[_ci].label === _cLabelName &&
|
|
758
874
|
this._loopStack[_ci].type === "loop"
|
|
759
875
|
) {
|
|
760
|
-
|
|
876
|
+
_cTargetIdx = _ci;
|
|
761
877
|
break;
|
|
762
878
|
}
|
|
763
879
|
}
|
|
764
|
-
if (
|
|
765
|
-
throw new Error(
|
|
766
|
-
|
|
880
|
+
if (_cTargetIdx === -1)
|
|
881
|
+
throw new Error(
|
|
882
|
+
`Label '${node.label.name}' not found for continue`,
|
|
883
|
+
);
|
|
767
884
|
} else {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
for (let i = this._loopStack.length - 1; i >= 0; i--) {
|
|
773
|
-
if (this._loopStack[i].type === "loop") {
|
|
774
|
-
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;
|
|
775
889
|
break;
|
|
776
890
|
}
|
|
777
891
|
}
|
|
778
|
-
if (
|
|
779
|
-
this._loopStack[loopIdx].continueJumps.push(_cJumpIdx);
|
|
892
|
+
if (_cTargetIdx === -1) throw new Error("continue outside loop");
|
|
780
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
|
+
);
|
|
781
911
|
break;
|
|
782
912
|
}
|
|
783
913
|
|
|
784
914
|
case "SwitchStatement": {
|
|
785
915
|
const _swLabel = this._pendingLabel;
|
|
786
916
|
this._pendingLabel = null;
|
|
917
|
+
|
|
918
|
+
const switchBreakLabel = this._makeLabel("sw_break");
|
|
919
|
+
|
|
787
920
|
this._loopStack.push({
|
|
788
921
|
type: "switch",
|
|
789
922
|
label: _swLabel,
|
|
790
|
-
|
|
791
|
-
|
|
923
|
+
breakLabel: switchBreakLabel,
|
|
924
|
+
continueLabel: switchBreakLabel, // not used for switch
|
|
792
925
|
});
|
|
793
|
-
const switchCtx = this._loopStack[this._loopStack.length - 1];
|
|
794
926
|
|
|
795
927
|
// Compile the discriminant and leave it on the stack
|
|
796
928
|
this._compileExpr(node.discriminant, scope, bc);
|
|
@@ -798,58 +930,70 @@ class Compiler {
|
|
|
798
930
|
const cases = node.cases;
|
|
799
931
|
const defaultIdx = cases.findIndex((c) => c.test === null);
|
|
800
932
|
|
|
801
|
-
//
|
|
802
|
-
const
|
|
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}`));
|
|
803
935
|
|
|
804
|
-
for
|
|
805
|
-
|
|
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
|
|
806
940
|
|
|
807
|
-
|
|
808
|
-
|
|
941
|
+
const nextCheckLabel = this._makeLabel("sw_next");
|
|
942
|
+
this.emit(bc, [this.OP.DUP], node);
|
|
809
943
|
this._compileExpr(cas.test, scope, bc);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
+
);
|
|
820
962
|
}
|
|
821
963
|
|
|
822
|
-
// No
|
|
823
|
-
|
|
824
|
-
|
|
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
|
+
);
|
|
825
977
|
|
|
826
|
-
// Body section: compile all case bodies in source order
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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) {
|
|
831
986
|
this._compileStatement(stmt, scope, bc);
|
|
832
987
|
}
|
|
833
988
|
}
|
|
834
989
|
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
// Patch all body jumps
|
|
844
|
-
for (const { cas, jumpIdx } of bodyJumps) {
|
|
845
|
-
bc[jumpIdx][1] = bodyStart.get(cas);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// Exit: pop the discriminant and patch break jumps
|
|
849
|
-
bc.push([this.OP.POP]);
|
|
850
|
-
for (const idx of switchCtx.breakJumps) {
|
|
851
|
-
bc[idx][1] = bc.length - 1; // Point to the POP instruction
|
|
852
|
-
}
|
|
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);
|
|
853
997
|
|
|
854
998
|
this._loopStack.pop();
|
|
855
999
|
break;
|
|
@@ -872,16 +1016,20 @@ class Compiler {
|
|
|
872
1016
|
this._pendingLabel = null; // safety clear if handler didn't consume it
|
|
873
1017
|
} else {
|
|
874
1018
|
// Non-loop labeled statement (e.g. labeled block) -- only break is valid
|
|
1019
|
+
const blockBreakLabel = this._makeLabel("block_break");
|
|
875
1020
|
this._loopStack.push({
|
|
876
1021
|
type: "block",
|
|
877
1022
|
label: _lName,
|
|
878
|
-
|
|
879
|
-
|
|
1023
|
+
breakLabel: blockBreakLabel,
|
|
1024
|
+
continueLabel: blockBreakLabel, // unused
|
|
880
1025
|
});
|
|
881
1026
|
this._compileStatement(_lBody, scope, bc);
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1027
|
+
this._loopStack.pop();
|
|
1028
|
+
this.emit(
|
|
1029
|
+
bc,
|
|
1030
|
+
[null, { type: "defineLabel", label: blockBreakLabel }],
|
|
1031
|
+
node,
|
|
1032
|
+
);
|
|
885
1033
|
}
|
|
886
1034
|
break;
|
|
887
1035
|
}
|
|
@@ -893,7 +1041,7 @@ class Compiler {
|
|
|
893
1041
|
// Evaluate the object expression -> on stack
|
|
894
1042
|
this._compileExpr(node.right, scope, bc);
|
|
895
1043
|
// FOR_IN_SETUP: pops obj, pushes iterator {keys, i}
|
|
896
|
-
|
|
1044
|
+
this.emit(bc, [this.OP.FOR_IN_SETUP], node);
|
|
897
1045
|
|
|
898
1046
|
// Store iterator in a hidden slot so break/continue need no cleanup
|
|
899
1047
|
let emitLoadIter: () => void;
|
|
@@ -901,32 +1049,43 @@ class Compiler {
|
|
|
901
1049
|
if (scope) {
|
|
902
1050
|
// Reserve a hidden local slot (no name mapping needed)
|
|
903
1051
|
const iterSlot = scope._next++;
|
|
904
|
-
emitLoadIter = () =>
|
|
905
|
-
|
|
1052
|
+
emitLoadIter = () =>
|
|
1053
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, iterSlot], node);
|
|
1054
|
+
emitStoreIter = () =>
|
|
1055
|
+
this.emit(bc, [this.OP.STORE_LOCAL, iterSlot], node);
|
|
906
1056
|
} else {
|
|
907
1057
|
// Top level -- use a synthetic global that won't collide with user code
|
|
908
|
-
const iterNameIdx = this.
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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);
|
|
913
1063
|
}
|
|
914
1064
|
emitStoreIter();
|
|
915
1065
|
|
|
1066
|
+
const loopTopLabel = this._makeLabel("forin_top");
|
|
1067
|
+
const exitLabel = this._makeLabel("forin_exit");
|
|
1068
|
+
|
|
916
1069
|
this._loopStack.push({
|
|
917
1070
|
type: "loop",
|
|
918
1071
|
label: _fiLabel,
|
|
919
|
-
|
|
920
|
-
|
|
1072
|
+
breakLabel: exitLabel,
|
|
1073
|
+
continueLabel: loopTopLabel, // continue re-checks the iterator
|
|
921
1074
|
});
|
|
922
|
-
const loopCtxFI = this._loopStack[this._loopStack.length - 1];
|
|
923
1075
|
|
|
924
|
-
|
|
1076
|
+
this.emit(
|
|
1077
|
+
bc,
|
|
1078
|
+
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
1079
|
+
node,
|
|
1080
|
+
);
|
|
925
1081
|
|
|
926
1082
|
// Load iterator, attempt to get next key
|
|
927
1083
|
emitLoadIter();
|
|
928
|
-
|
|
929
|
-
|
|
1084
|
+
this.emit(
|
|
1085
|
+
bc,
|
|
1086
|
+
[this.OP.FOR_IN_NEXT, { type: "label", label: exitLabel }],
|
|
1087
|
+
node,
|
|
1088
|
+
);
|
|
930
1089
|
|
|
931
1090
|
// Assign the key (now on top of stack) to the loop variable
|
|
932
1091
|
if (node.left.type === "VariableDeclaration") {
|
|
@@ -938,21 +1097,26 @@ class Compiler {
|
|
|
938
1097
|
const name = identifier.name;
|
|
939
1098
|
if (scope) {
|
|
940
1099
|
const slot = scope.define(name);
|
|
941
|
-
|
|
1100
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
942
1101
|
} else {
|
|
943
|
-
|
|
1102
|
+
this.emit(
|
|
1103
|
+
bc,
|
|
1104
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(name)],
|
|
1105
|
+
node,
|
|
1106
|
+
);
|
|
944
1107
|
}
|
|
945
1108
|
} else if (node.left.type === "Identifier") {
|
|
946
1109
|
const res = this._resolve(node.left.name, this._currentCtx);
|
|
947
1110
|
if (res.kind === "local") {
|
|
948
|
-
|
|
1111
|
+
this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
|
|
949
1112
|
} else if (res.kind === "upvalue") {
|
|
950
|
-
|
|
1113
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
951
1114
|
} else {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
this.
|
|
955
|
-
|
|
1115
|
+
this.emit(
|
|
1116
|
+
bc,
|
|
1117
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(node.left.name)],
|
|
1118
|
+
node,
|
|
1119
|
+
);
|
|
956
1120
|
}
|
|
957
1121
|
} else {
|
|
958
1122
|
const src = generate(node.left).code;
|
|
@@ -968,15 +1132,101 @@ class Compiler {
|
|
|
968
1132
|
this._compileStatement(stmt, scope, bc);
|
|
969
1133
|
}
|
|
970
1134
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
+
}
|
|
974
1158
|
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
for (const idx of loopCtxFI.breakJumps) bc[idx][1] = exitTargetFI;
|
|
1159
|
+
const catchLabel = this._makeLabel("catch");
|
|
1160
|
+
const afterCatchLabel = this._makeLabel("after_catch");
|
|
978
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
|
+
});
|
|
1178
|
+
|
|
1179
|
+
// Compile try body
|
|
1180
|
+
for (const stmt of node.block.body) {
|
|
1181
|
+
this._compileStatement(stmt, scope, bc);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Done compiling the try body — pop the tracking entry.
|
|
979
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
|
+
);
|
|
980
1230
|
break;
|
|
981
1231
|
}
|
|
982
1232
|
|
|
@@ -994,17 +1244,25 @@ class Compiler {
|
|
|
994
1244
|
switch (node.type) {
|
|
995
1245
|
case "NumericLiteral":
|
|
996
1246
|
case "StringLiteral": {
|
|
997
|
-
|
|
1247
|
+
this.emit(
|
|
1248
|
+
bc,
|
|
1249
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.value)],
|
|
1250
|
+
node,
|
|
1251
|
+
);
|
|
998
1252
|
break;
|
|
999
1253
|
}
|
|
1000
1254
|
|
|
1001
1255
|
case "BooleanLiteral": {
|
|
1002
|
-
|
|
1256
|
+
this.emit(
|
|
1257
|
+
bc,
|
|
1258
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.value)],
|
|
1259
|
+
node,
|
|
1260
|
+
);
|
|
1003
1261
|
break;
|
|
1004
1262
|
}
|
|
1005
1263
|
|
|
1006
1264
|
case "NullLiteral": {
|
|
1007
|
-
|
|
1265
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(null)], node);
|
|
1008
1266
|
break;
|
|
1009
1267
|
}
|
|
1010
1268
|
|
|
@@ -1012,17 +1270,21 @@ class Compiler {
|
|
|
1012
1270
|
// scope=null means we're at the top-level -> always global
|
|
1013
1271
|
const res = this._resolve(node.name, this._currentCtx);
|
|
1014
1272
|
if (res.kind === "local") {
|
|
1015
|
-
|
|
1273
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
1016
1274
|
} else if (res.kind === "upvalue") {
|
|
1017
|
-
|
|
1275
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1018
1276
|
} else {
|
|
1019
|
-
|
|
1277
|
+
this.emit(
|
|
1278
|
+
bc,
|
|
1279
|
+
[this.OP.LOAD_GLOBAL, b.constantOperand(node.name)],
|
|
1280
|
+
node,
|
|
1281
|
+
);
|
|
1020
1282
|
}
|
|
1021
1283
|
break;
|
|
1022
1284
|
}
|
|
1023
1285
|
|
|
1024
1286
|
case "ThisExpression": {
|
|
1025
|
-
|
|
1287
|
+
this.emit(bc, [this.OP.LOAD_THIS], node);
|
|
1026
1288
|
break;
|
|
1027
1289
|
}
|
|
1028
1290
|
|
|
@@ -1030,16 +1292,15 @@ class Compiler {
|
|
|
1030
1292
|
// Push callee, then args -- identical layout to CALL but uses NEW opcode
|
|
1031
1293
|
this._compileExpr(node.callee, scope, bc);
|
|
1032
1294
|
for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
|
|
1033
|
-
|
|
1295
|
+
this.emit(bc, [this.OP.NEW, node.arguments.length], node);
|
|
1034
1296
|
break;
|
|
1035
1297
|
}
|
|
1036
1298
|
|
|
1037
1299
|
case "SequenceExpression": {
|
|
1038
1300
|
// (a, b, c) -> eval a -> POP, eval b -> POP, eval c -> leave on stack
|
|
1039
|
-
// Matches CPython's BINARY_OP / POP_TOP pattern for comma expressions.
|
|
1040
1301
|
for (let i = 0; i < node.expressions.length - 1; i++) {
|
|
1041
1302
|
this._compileExpr(node.expressions[i], scope, bc);
|
|
1042
|
-
|
|
1303
|
+
this.emit(bc, [this.OP.POP], node); // discard intermediate result
|
|
1043
1304
|
}
|
|
1044
1305
|
// Last expression -- its value is the result of the whole sequence
|
|
1045
1306
|
this._compileExpr(
|
|
@@ -1052,21 +1313,23 @@ class Compiler {
|
|
|
1052
1313
|
|
|
1053
1314
|
case "ConditionalExpression": {
|
|
1054
1315
|
// test ? consequent : alternate
|
|
1055
|
-
|
|
1056
|
-
this.
|
|
1316
|
+
const elseLabel = this._makeLabel("ternary_else");
|
|
1317
|
+
const endLabel = this._makeLabel("ternary_end");
|
|
1057
1318
|
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
+
);
|
|
1060
1325
|
|
|
1061
1326
|
this._compileExpr(node.consequent, scope, bc);
|
|
1327
|
+
this.emit(bc, [this.OP.JUMP, { type: "label", label: endLabel }], node);
|
|
1062
1328
|
|
|
1063
|
-
|
|
1064
|
-
const jumpToEnd = bc.length - 1;
|
|
1065
|
-
|
|
1066
|
-
bc[jumpToElse][1] = bc.length; // patch: false -> alternate
|
|
1329
|
+
this.emit(bc, [null, { type: "defineLabel", label: elseLabel }], node);
|
|
1067
1330
|
this._compileExpr(node.alternate, scope, bc);
|
|
1068
1331
|
|
|
1069
|
-
bc[
|
|
1332
|
+
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1070
1333
|
break;
|
|
1071
1334
|
}
|
|
1072
1335
|
|
|
@@ -1081,16 +1344,24 @@ class Compiler {
|
|
|
1081
1344
|
|
|
1082
1345
|
if (node.operator === "||") {
|
|
1083
1346
|
// Short-circuit if LHS is TRUTHY -- keep it, skip RHS
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
);
|
|
1086
1353
|
this._compileExpr(node.right, scope, bc);
|
|
1087
|
-
bc[
|
|
1354
|
+
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1088
1355
|
} else if (node.operator === "&&") {
|
|
1089
1356
|
// Short-circuit if LHS is FALSY -- keep it, skip RHS
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
+
);
|
|
1092
1363
|
this._compileExpr(node.right, scope, bc);
|
|
1093
|
-
bc[
|
|
1364
|
+
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1094
1365
|
} else {
|
|
1095
1366
|
throw new Error(`Unsupported logical operator: ${node.operator}`);
|
|
1096
1367
|
}
|
|
@@ -1129,7 +1400,7 @@ class Compiler {
|
|
|
1129
1400
|
const resolvedOp = arithOp ?? cmpOp;
|
|
1130
1401
|
if (resolvedOp === undefined)
|
|
1131
1402
|
throw new Error(`Unsupported operator: ${node.operator}`);
|
|
1132
|
-
|
|
1403
|
+
this.emit(bc, [resolvedOp], node);
|
|
1133
1404
|
|
|
1134
1405
|
break;
|
|
1135
1406
|
}
|
|
@@ -1137,34 +1408,38 @@ class Compiler {
|
|
|
1137
1408
|
case "UpdateExpression": {
|
|
1138
1409
|
const res = this._resolve(node.argument.name, this._currentCtx);
|
|
1139
1410
|
const bumpOp = node.operator === "++" ? this.OP.ADD : this.OP.SUB;
|
|
1140
|
-
const one =
|
|
1411
|
+
const one = b.constantOperand(1);
|
|
1141
1412
|
|
|
1142
1413
|
// Helper closures: emit load / store for whichever resolution kind we have
|
|
1143
1414
|
const emitLoad = () => {
|
|
1144
|
-
if (res.kind === "local")
|
|
1415
|
+
if (res.kind === "local")
|
|
1416
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
1145
1417
|
else if (res.kind === "upvalue")
|
|
1146
|
-
|
|
1418
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1147
1419
|
else
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
this.
|
|
1151
|
-
|
|
1420
|
+
this.emit(
|
|
1421
|
+
bc,
|
|
1422
|
+
[this.OP.LOAD_GLOBAL, b.constantOperand(node.argument.name)],
|
|
1423
|
+
node,
|
|
1424
|
+
);
|
|
1152
1425
|
};
|
|
1153
1426
|
const emitStore = () => {
|
|
1154
|
-
if (res.kind === "local")
|
|
1427
|
+
if (res.kind === "local")
|
|
1428
|
+
this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
|
|
1155
1429
|
else if (res.kind === "upvalue")
|
|
1156
|
-
|
|
1430
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
1157
1431
|
else
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
this.
|
|
1161
|
-
|
|
1432
|
+
this.emit(
|
|
1433
|
+
bc,
|
|
1434
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(node.argument.name)],
|
|
1435
|
+
node,
|
|
1436
|
+
);
|
|
1162
1437
|
};
|
|
1163
1438
|
|
|
1164
1439
|
emitLoad();
|
|
1165
|
-
if (!node.prefix)
|
|
1166
|
-
|
|
1167
|
-
|
|
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);
|
|
1168
1443
|
emitStore();
|
|
1169
1444
|
if (node.prefix) emitLoad(); // pre: reload new value as result
|
|
1170
1445
|
|
|
@@ -1199,10 +1474,11 @@ class Compiler {
|
|
|
1199
1474
|
if (node.left.computed) {
|
|
1200
1475
|
this._compileExpr(node.left.property, scope, bc); // push key (runtime)
|
|
1201
1476
|
} else {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
this.
|
|
1205
|
-
|
|
1477
|
+
this.emit(
|
|
1478
|
+
bc,
|
|
1479
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)],
|
|
1480
|
+
node,
|
|
1481
|
+
);
|
|
1206
1482
|
}
|
|
1207
1483
|
|
|
1208
1484
|
if (isCompound) {
|
|
@@ -1215,19 +1491,23 @@ class Compiler {
|
|
|
1215
1491
|
if (node.left.computed) {
|
|
1216
1492
|
this._compileExpr(node.left.property, scope, bc);
|
|
1217
1493
|
} else {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1494
|
+
this.emit(
|
|
1495
|
+
bc,
|
|
1496
|
+
[
|
|
1497
|
+
this.OP.LOAD_CONST,
|
|
1498
|
+
b.constantOperand(node.left.property.name),
|
|
1499
|
+
],
|
|
1500
|
+
node,
|
|
1501
|
+
);
|
|
1222
1502
|
}
|
|
1223
|
-
|
|
1503
|
+
this.emit(bc, [this.OP.GET_PROP_COMPUTED], node); // [..., obj, key, currentVal]
|
|
1224
1504
|
this._compileExpr(node.right, scope, bc); // [..., obj, key, currentVal, rhs]
|
|
1225
|
-
|
|
1505
|
+
this.emit(bc, [compoundOp], node); // [..., obj, key, newVal]
|
|
1226
1506
|
} else {
|
|
1227
1507
|
this._compileExpr(node.right, scope, bc); // [..., obj, key, val]
|
|
1228
1508
|
}
|
|
1229
1509
|
|
|
1230
|
-
|
|
1510
|
+
this.emit(bc, [this.OP.SET_PROP], node); // obj[key] = val, leaves val on stack
|
|
1231
1511
|
break;
|
|
1232
1512
|
}
|
|
1233
1513
|
|
|
@@ -1237,34 +1517,35 @@ class Compiler {
|
|
|
1237
1517
|
if (isCompound) {
|
|
1238
1518
|
// Load the current value of the target first
|
|
1239
1519
|
if (res.kind === "local") {
|
|
1240
|
-
|
|
1520
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
1241
1521
|
} else if (res.kind === "upvalue") {
|
|
1242
|
-
|
|
1522
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1243
1523
|
} else {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
this.
|
|
1247
|
-
|
|
1524
|
+
this.emit(
|
|
1525
|
+
bc,
|
|
1526
|
+
[this.OP.LOAD_GLOBAL, b.constantOperand(node.left.name)],
|
|
1527
|
+
node,
|
|
1528
|
+
);
|
|
1248
1529
|
}
|
|
1249
1530
|
}
|
|
1250
1531
|
|
|
1251
1532
|
this._compileExpr(node.right, scope, bc); // push RHS
|
|
1252
1533
|
|
|
1253
1534
|
if (isCompound) {
|
|
1254
|
-
|
|
1535
|
+
this.emit(bc, [compoundOp], node); // apply binary op -> leaves newVal on stack
|
|
1255
1536
|
}
|
|
1256
1537
|
|
|
1257
1538
|
// Store & leave value on stack (assignment is an expression)
|
|
1258
1539
|
if (res.kind === "local") {
|
|
1259
|
-
|
|
1260
|
-
|
|
1540
|
+
this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
|
|
1541
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
1261
1542
|
} else if (res.kind === "upvalue") {
|
|
1262
|
-
|
|
1263
|
-
|
|
1543
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
1544
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1264
1545
|
} else {
|
|
1265
|
-
const nameIdx =
|
|
1266
|
-
|
|
1267
|
-
|
|
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);
|
|
1268
1549
|
}
|
|
1269
1550
|
break;
|
|
1270
1551
|
}
|
|
@@ -1275,16 +1556,16 @@ class Compiler {
|
|
|
1275
1556
|
// Push receiver first (GET_PROP leaves it; CALL_METHOD pops it as `this`)
|
|
1276
1557
|
this._compileExpr(node.callee.object, scope, bc);
|
|
1277
1558
|
const prop = node.callee.property.name;
|
|
1278
|
-
const propIdx =
|
|
1279
|
-
|
|
1280
|
-
|
|
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);
|
|
1281
1562
|
for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
|
|
1282
|
-
|
|
1563
|
+
this.emit(bc, [this.OP.CALL_METHOD, node.arguments.length], node);
|
|
1283
1564
|
} else {
|
|
1284
1565
|
// ── Plain call: add(5, 10)
|
|
1285
1566
|
this._compileExpr(node.callee, scope, bc);
|
|
1286
1567
|
for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
|
|
1287
|
-
|
|
1568
|
+
this.emit(bc, [this.OP.CALL, node.arguments.length], node);
|
|
1288
1569
|
}
|
|
1289
1570
|
break;
|
|
1290
1571
|
}
|
|
@@ -1297,19 +1578,18 @@ class Compiler {
|
|
|
1297
1578
|
const res = this._resolve(node.argument.name, this._currentCtx);
|
|
1298
1579
|
if (res.kind === "global") {
|
|
1299
1580
|
// Potentially undeclared -- let VM guard it
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
this.
|
|
1303
|
-
|
|
1304
|
-
|
|
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);
|
|
1305
1587
|
break;
|
|
1306
1588
|
}
|
|
1307
1589
|
// Known local or upvalue -- safe to load first, then typeof
|
|
1308
1590
|
}
|
|
1309
1591
|
|
|
1310
1592
|
// Special case: delete -- argument must NOT be pre-evaluated.
|
|
1311
|
-
// The generic path below compiles the argument first, which would leave
|
|
1312
|
-
// a stale value on the stack before the delete result, corrupting it.
|
|
1313
1593
|
if (node.operator === "delete") {
|
|
1314
1594
|
const arg = node.argument;
|
|
1315
1595
|
if (arg.type === "MemberExpression") {
|
|
@@ -1317,15 +1597,16 @@ class Compiler {
|
|
|
1317
1597
|
if (arg.computed) {
|
|
1318
1598
|
this._compileExpr(arg.property, scope, bc);
|
|
1319
1599
|
} else {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
this.
|
|
1323
|
-
|
|
1600
|
+
this.emit(
|
|
1601
|
+
bc,
|
|
1602
|
+
[this.OP.LOAD_CONST, b.constantOperand(arg.property.name)],
|
|
1603
|
+
node,
|
|
1604
|
+
);
|
|
1324
1605
|
}
|
|
1325
|
-
|
|
1606
|
+
this.emit(bc, [this.OP.DELETE_PROP], node);
|
|
1326
1607
|
} else {
|
|
1327
1608
|
// delete x, delete 0, etc. -- always true in non-strict, just push true
|
|
1328
|
-
|
|
1609
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(true)], node);
|
|
1329
1610
|
}
|
|
1330
1611
|
break;
|
|
1331
1612
|
}
|
|
@@ -1334,22 +1615,22 @@ class Compiler {
|
|
|
1334
1615
|
this._compileExpr(node.argument, scope, bc);
|
|
1335
1616
|
switch (node.operator) {
|
|
1336
1617
|
case "-":
|
|
1337
|
-
|
|
1618
|
+
this.emit(bc, [this.OP.UNARY_NEG], node);
|
|
1338
1619
|
break;
|
|
1339
1620
|
case "+":
|
|
1340
|
-
|
|
1621
|
+
this.emit(bc, [this.OP.UNARY_POS], node);
|
|
1341
1622
|
break;
|
|
1342
1623
|
case "!":
|
|
1343
|
-
|
|
1624
|
+
this.emit(bc, [this.OP.UNARY_NOT], node);
|
|
1344
1625
|
break;
|
|
1345
1626
|
case "~":
|
|
1346
|
-
|
|
1627
|
+
this.emit(bc, [this.OP.UNARY_BITNOT], node);
|
|
1347
1628
|
break;
|
|
1348
1629
|
case "typeof":
|
|
1349
|
-
|
|
1630
|
+
this.emit(bc, [this.OP.TYPEOF], node);
|
|
1350
1631
|
break;
|
|
1351
1632
|
case "void":
|
|
1352
|
-
|
|
1633
|
+
this.emit(bc, [this.OP.VOID], node);
|
|
1353
1634
|
break;
|
|
1354
1635
|
|
|
1355
1636
|
default:
|
|
@@ -1361,10 +1642,18 @@ class Compiler {
|
|
|
1361
1642
|
case "RegExpLiteral": {
|
|
1362
1643
|
// Emit: new RegExp(pattern, flags)
|
|
1363
1644
|
// Fresh object per evaluation -- correct for stateful g/y flags.
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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);
|
|
1368
1657
|
break;
|
|
1369
1658
|
}
|
|
1370
1659
|
|
|
@@ -1373,7 +1662,12 @@ class Compiler {
|
|
|
1373
1662
|
// but leave the resulting closure ON THE STACK -- no store.
|
|
1374
1663
|
// The surrounding expression (assignment, call arg, return) consumes it.
|
|
1375
1664
|
const desc = this._compileFunctionDecl(node);
|
|
1376
|
-
|
|
1665
|
+
this._emitClosureMetadata(desc, node, bc);
|
|
1666
|
+
this.emit(
|
|
1667
|
+
bc,
|
|
1668
|
+
[this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
|
|
1669
|
+
node,
|
|
1670
|
+
);
|
|
1377
1671
|
break;
|
|
1378
1672
|
}
|
|
1379
1673
|
|
|
@@ -1384,16 +1678,17 @@ class Compiler {
|
|
|
1384
1678
|
this._compileExpr(node.property, scope, bc);
|
|
1385
1679
|
} else {
|
|
1386
1680
|
// point.x -- push key as string, same opcode handles both
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
this.
|
|
1390
|
-
|
|
1681
|
+
this.emit(
|
|
1682
|
+
bc,
|
|
1683
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.property.name)],
|
|
1684
|
+
node,
|
|
1685
|
+
);
|
|
1391
1686
|
}
|
|
1392
1687
|
|
|
1393
1688
|
// GET_PROP_COMPUTED pops the object -- correct for value access.
|
|
1394
1689
|
// GET_PROP (peek) is only used in CallExpression's method call path
|
|
1395
1690
|
// where the receiver must survive on the stack for CALL_METHOD.
|
|
1396
|
-
|
|
1691
|
+
this.emit(bc, [this.OP.GET_PROP_COMPUTED], node);
|
|
1397
1692
|
break;
|
|
1398
1693
|
}
|
|
1399
1694
|
|
|
@@ -1403,45 +1698,114 @@ class Compiler {
|
|
|
1403
1698
|
for (const el of node.elements) {
|
|
1404
1699
|
if (el === null) {
|
|
1405
1700
|
// hole: e.g. [1,,3]
|
|
1406
|
-
|
|
1701
|
+
this.emit(
|
|
1702
|
+
bc,
|
|
1703
|
+
[this.OP.LOAD_CONST, b.constantOperand(undefined)],
|
|
1704
|
+
node,
|
|
1705
|
+
);
|
|
1407
1706
|
} else {
|
|
1408
1707
|
this._compileExpr(el, scope, bc);
|
|
1409
1708
|
}
|
|
1410
1709
|
}
|
|
1411
|
-
|
|
1710
|
+
this.emit(bc, [this.OP.BUILD_ARRAY, node.elements.length], node);
|
|
1412
1711
|
break;
|
|
1413
1712
|
}
|
|
1414
1713
|
case "ObjectExpression": {
|
|
1415
|
-
//
|
|
1416
|
-
|
|
1714
|
+
// Separate regular data properties from ES5 accessor methods (get/set).
|
|
1715
|
+
const regularProps: t.ObjectProperty[] = [];
|
|
1716
|
+
const accessorProps: t.ObjectMethod[] = [];
|
|
1717
|
+
|
|
1417
1718
|
for (const prop of node.properties) {
|
|
1418
1719
|
if (prop.type === "SpreadElement") {
|
|
1419
1720
|
throw new Error("Object spread not supported");
|
|
1420
1721
|
}
|
|
1421
|
-
|
|
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) {
|
|
1422
1740
|
const key = prop.key;
|
|
1423
|
-
let keyStr;
|
|
1741
|
+
let keyStr: string;
|
|
1424
1742
|
if (key.type === "Identifier") {
|
|
1425
|
-
keyStr = key.name;
|
|
1743
|
+
keyStr = key.name;
|
|
1426
1744
|
} else if (
|
|
1427
1745
|
key.type === "StringLiteral" ||
|
|
1428
1746
|
key.type === "NumericLiteral"
|
|
1429
1747
|
) {
|
|
1430
|
-
keyStr = String(key.value);
|
|
1748
|
+
keyStr = String(key.value);
|
|
1431
1749
|
} else {
|
|
1432
1750
|
throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1433
1751
|
}
|
|
1434
|
-
|
|
1435
|
-
// Value -- any expression, including FunctionExpression
|
|
1752
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
|
|
1436
1753
|
this._compileExpr(prop.value, scope, bc);
|
|
1437
1754
|
}
|
|
1438
|
-
|
|
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
|
+
|
|
1439
1804
|
break;
|
|
1440
1805
|
}
|
|
1441
1806
|
|
|
1442
1807
|
default: {
|
|
1443
|
-
|
|
1444
|
-
throw new Error(`Unsupported expression: ${node.type}\n -> ${src}`);
|
|
1808
|
+
throw new Error(`Unsupported expression: ${node.type}`);
|
|
1445
1809
|
}
|
|
1446
1810
|
}
|
|
1447
1811
|
}
|
|
@@ -1449,6 +1813,8 @@ class Compiler {
|
|
|
1449
1813
|
|
|
1450
1814
|
// Serializer
|
|
1451
1815
|
// Turns the compiled output into a commented JS source string.
|
|
1816
|
+
// Expects fully-resolved bytecode (all label refs and constant refs already
|
|
1817
|
+
// converted to plain integers by resolveLabels + resolveConstants passes).
|
|
1452
1818
|
class Serializer {
|
|
1453
1819
|
compiler: Compiler;
|
|
1454
1820
|
|
|
@@ -1472,43 +1838,52 @@ class Serializer {
|
|
|
1472
1838
|
return this.compiler.JUMP_OPS;
|
|
1473
1839
|
}
|
|
1474
1840
|
|
|
1475
|
-
get constants() {
|
|
1476
|
-
return this.compiler.constants.items;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
get fnDescriptors() {
|
|
1480
|
-
return this.compiler.fnDescriptors;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
1841
|
// Produce a JS literal for a constant pool entry
|
|
1484
1842
|
_serializeConst(val) {
|
|
1485
1843
|
if (val === null) return "null";
|
|
1486
1844
|
if (val === undefined) return "undefined";
|
|
1487
|
-
if (typeof val === "object" && val._fnIdx !== undefined) {
|
|
1488
|
-
return `FN[${val._fnIdx}]`; // fn descriptor -> reference by FN index
|
|
1489
|
-
}
|
|
1490
1845
|
return JSON.stringify(val); // number / string / bool
|
|
1491
1846
|
}
|
|
1492
1847
|
|
|
1493
1848
|
// One instruction -> "[op, operand] // MNEMONIC description"
|
|
1494
|
-
|
|
1495
|
-
|
|
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;
|
|
1496
1858
|
|
|
1497
|
-
const [op, operand] = instr;
|
|
1498
1859
|
const name = this.OP_NAME[op] || `OP_${op}`;
|
|
1499
1860
|
let comment = name;
|
|
1500
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
|
+
|
|
1501
1871
|
// Annotate operand with its meaning
|
|
1502
1872
|
if (operand !== undefined) {
|
|
1503
1873
|
switch (op) {
|
|
1504
|
-
case this.OP.LOAD_CONST:
|
|
1505
|
-
case this.OP.MAKE_CLOSURE: {
|
|
1874
|
+
case this.OP.LOAD_CONST: {
|
|
1506
1875
|
const val = constants[operand];
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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}`;
|
|
1882
|
+
break;
|
|
1883
|
+
}
|
|
1884
|
+
case this.OP.DATA: {
|
|
1885
|
+
// Inline function header word — value is a raw integer
|
|
1886
|
+
comment += ` ${operand}`;
|
|
1512
1887
|
break;
|
|
1513
1888
|
}
|
|
1514
1889
|
case this.OP.LOAD_LOCAL:
|
|
@@ -1544,15 +1919,17 @@ class Serializer {
|
|
|
1544
1919
|
}
|
|
1545
1920
|
}
|
|
1546
1921
|
|
|
1922
|
+
comment = comment.padEnd(40) + sourceLocation;
|
|
1923
|
+
|
|
1547
1924
|
// Pack a [op, operand?] instruction pair into a single 32-bit word.
|
|
1548
1925
|
// Shared between the Serializer and the obfuscation path in _compileMain.
|
|
1549
1926
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
|
|
1927
|
+
const instrText = operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
|
|
1928
|
+
const text = `${(instrText + ",").padEnd(12)} ${comment}`;
|
|
1553
1929
|
|
|
1930
|
+
if (!this.options.encodeBytecode) {
|
|
1554
1931
|
return {
|
|
1555
|
-
text:
|
|
1932
|
+
text: text,
|
|
1556
1933
|
value: operand !== undefined ? [op, operand] : [op],
|
|
1557
1934
|
};
|
|
1558
1935
|
}
|
|
@@ -1569,44 +1946,35 @@ class Serializer {
|
|
|
1569
1946
|
}
|
|
1570
1947
|
|
|
1571
1948
|
return {
|
|
1572
|
-
text:
|
|
1949
|
+
text: text,
|
|
1573
1950
|
value: packInstr(instr),
|
|
1574
1951
|
};
|
|
1575
1952
|
}
|
|
1576
1953
|
|
|
1577
|
-
// Serialize
|
|
1578
|
-
|
|
1579
|
-
const lines = [
|
|
1580
|
-
` { // FN[${desc._fnIdx}] -- ${desc.name}`,
|
|
1581
|
-
` paramCount: ${desc.paramCount},`,
|
|
1582
|
-
` localCount: ${desc.localCount},`,
|
|
1583
|
-
` upvalueDescriptors: ${JSON5.stringify(desc.upvalueDescriptors)},`,
|
|
1584
|
-
` startPc: ${desc.startPc},`,
|
|
1585
|
-
` },`,
|
|
1586
|
-
];
|
|
1587
|
-
return lines.join("\n");
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// Serialize the CONSTANTS array, showing FN[n] references
|
|
1591
|
-
_serializeConstants() {
|
|
1954
|
+
// Serialize the CONSTANTS array
|
|
1955
|
+
_serializeConstants(constants: any[]) {
|
|
1592
1956
|
const lines = ["var CONSTANTS = ["];
|
|
1593
|
-
|
|
1957
|
+
constants.forEach((val, idx) => {
|
|
1594
1958
|
lines.push(` /* ${idx} */ ${this._serializeConst(val)},`);
|
|
1595
1959
|
});
|
|
1596
1960
|
lines.push("];");
|
|
1597
1961
|
return lines.join("\n");
|
|
1598
1962
|
}
|
|
1599
1963
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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
|
+
}
|
|
1604
1971
|
|
|
1972
|
+
__serializeBytecode(bytecode: b.Bytecode, constants: any[]) {
|
|
1605
1973
|
let words = [];
|
|
1606
1974
|
|
|
1607
1975
|
// BYTECODE
|
|
1608
1976
|
for (const instr of bytecode) {
|
|
1609
|
-
words.push(this._serializeInstr(instr).value);
|
|
1977
|
+
words.push(this._serializeInstr(instr, constants).value);
|
|
1610
1978
|
}
|
|
1611
1979
|
|
|
1612
1980
|
// Convert packed words -> raw 4-byte little-endian binary -> base64
|
|
@@ -1617,30 +1985,32 @@ class Serializer {
|
|
|
1617
1985
|
buf[i * 4 + 2] = (w >>> 16) & 0xff;
|
|
1618
1986
|
buf[i * 4 + 3] = (w >>> 24) & 0xff;
|
|
1619
1987
|
});
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
return b64;
|
|
1988
|
+
return Buffer.from(buf).toString("base64");
|
|
1623
1989
|
}
|
|
1624
1990
|
|
|
1625
|
-
serialize(bytecode,
|
|
1626
|
-
const
|
|
1991
|
+
serialize(bytecode: b.Bytecode, constants: any[], compiler: Compiler) {
|
|
1992
|
+
const mainStartPc = compiler.mainStartPc;
|
|
1993
|
+
let sections = [];
|
|
1994
|
+
|
|
1995
|
+
var textForm = [];
|
|
1996
|
+
var initBody = [];
|
|
1627
1997
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
for (const
|
|
1631
|
-
|
|
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);
|
|
1632
2003
|
}
|
|
1633
|
-
fnLines.push("];");
|
|
1634
|
-
sections.push(fnLines.join("\n"));
|
|
1635
2004
|
|
|
1636
|
-
|
|
1637
|
-
sections.push(this._serializeConstants());
|
|
2005
|
+
initBody.push(textForm.map((line) => `// ${line}`).join("\n"));
|
|
1638
2006
|
|
|
1639
2007
|
if (this.options.encodeBytecode) {
|
|
1640
|
-
sections.push(
|
|
2008
|
+
sections.push(
|
|
2009
|
+
`var BYTECODE = "${this.__serializeBytecode(bytecodeResult.bytecode, constants)}";`,
|
|
2010
|
+
);
|
|
1641
2011
|
} else {
|
|
1642
2012
|
sections.push(
|
|
1643
|
-
`var BYTECODE = [
|
|
2013
|
+
`var BYTECODE = [${bytecodeResult.bytecode.map((v) => "[" + v[0] + ", " + v[1] + "]").join(",")}]`,
|
|
1644
2014
|
);
|
|
1645
2015
|
}
|
|
1646
2016
|
|
|
@@ -1651,6 +2021,11 @@ class Serializer {
|
|
|
1651
2021
|
// Opcodes
|
|
1652
2022
|
sections.push(`var OP = ${JSON5.stringify(this.OP)};`);
|
|
1653
2023
|
|
|
2024
|
+
// Constants must be defined before the bytecode
|
|
2025
|
+
initBody.push(this._serializeConstants(constants));
|
|
2026
|
+
|
|
2027
|
+
sections = [...initBody, ...sections];
|
|
2028
|
+
|
|
1654
2029
|
// VM runtime
|
|
1655
2030
|
sections.push(VM_RUNTIME);
|
|
1656
2031
|
|
|
@@ -1663,10 +2038,24 @@ export async function compileAndSerialize(
|
|
|
1663
2038
|
options: Options,
|
|
1664
2039
|
) {
|
|
1665
2040
|
const compiler = new Compiler(options);
|
|
1666
|
-
|
|
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
|
+
|
|
1667
2055
|
const output = compiler.serializer.serialize(
|
|
1668
|
-
|
|
1669
|
-
|
|
2056
|
+
finalBytecode,
|
|
2057
|
+
constants,
|
|
2058
|
+
compiler,
|
|
1670
2059
|
);
|
|
1671
2060
|
|
|
1672
2061
|
const finalOutput = await obfuscateRuntime(output, options);
|