js-confuser-vm 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -147
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +714 -265
- package/dist/disassembler.js +367 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +160 -119
- package/dist/template.js +163 -42
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/concealConstants.js +2 -2
- package/dist/transforms/bytecode/controlFlowFlattening.js +569 -0
- package/dist/transforms/bytecode/dispatcher.js +15 -111
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/{src/transforms/bytecode/resolveContants.ts → dist/transforms/bytecode/resolveConstants.js} +30 -56
- package/dist/transforms/bytecode/resolveRegisters.js +23 -4
- package/dist/transforms/bytecode/selfModifying.js +88 -21
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +23 -12
- package/dist/transforms/bytecode/stringConcealing.js +288 -0
- package/dist/transforms/runtime/classObfuscation.js +43 -0
- package/dist/transforms/runtime/handlerTable.js +91 -0
- package/dist/transforms/runtime/semanticOpcodes.js +35 -0
- package/dist/transforms/runtime/specializedOpcodes.js +11 -5
- package/dist/types.js +1 -1
- package/dist/utils/ast-utils.js +75 -0
- package/dist/utils/op-utils.js +1 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/package.json +8 -1
- package/.gitmodules +0 -4
- package/.prettierignore +0 -1
- package/CHANGELOG.md +0 -335
- package/babel-plugin-inline-runtime.cjs +0 -34
- package/babel.config.json +0 -23
- package/index.ts +0 -38
- package/jest-strip-types.js +0 -10
- package/jest.config.js +0 -52
- package/src/build-runtime.ts +0 -78
- package/src/compiler.ts +0 -2593
- package/src/index.ts +0 -14
- package/src/minify.ts +0 -21
- package/src/options.ts +0 -18
- package/src/runtime.ts +0 -923
- package/src/template.ts +0 -141
- package/src/transforms/bytecode/aliasedOpcodes.ts +0 -148
- package/src/transforms/bytecode/concealConstants.ts +0 -52
- package/src/transforms/bytecode/dispatcher.ts +0 -398
- package/src/transforms/bytecode/macroOpcodes.ts +0 -193
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/bytecode/resolveLabels.ts +0 -112
- package/src/transforms/bytecode/resolveRegisters.ts +0 -221
- package/src/transforms/bytecode/selfModifying.ts +0 -121
- package/src/transforms/bytecode/specializedOpcodes.ts +0 -153
- package/src/transforms/runtime/aliasedOpcodes.ts +0 -191
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/macroOpcodes.ts +0 -138
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- package/src/transforms/runtime/minify.ts +0 -1
- package/src/transforms/runtime/shuffleOpcodes.ts +0 -24
- package/src/transforms/runtime/specializedOpcodes.ts +0 -156
- package/src/types.ts +0 -93
- package/src/utils/op-utils.ts +0 -48
- package/src/utils/random-utils.ts +0 -31
- package/tsconfig.json +0 -12
package/src/compiler.ts
DELETED
|
@@ -1,2593 +0,0 @@
|
|
|
1
|
-
import * as t from "@babel/types";
|
|
2
|
-
import * as b from "./types.ts";
|
|
3
|
-
import { parse } from "@babel/parser";
|
|
4
|
-
import traverseImport from "@babel/traverse";
|
|
5
|
-
import { generate } from "@babel/generator";
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
import { readFileSync } from "fs";
|
|
8
|
-
import { stripTypeScriptTypes } from "module";
|
|
9
|
-
import { ok } from "assert";
|
|
10
|
-
import { obfuscateRuntime } from "./build-runtime.ts";
|
|
11
|
-
import { DEFAULT_OPTIONS, type Options } from "./options.ts";
|
|
12
|
-
import { resolveLabels } from "./transforms/bytecode/resolveLabels.ts";
|
|
13
|
-
import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.ts";
|
|
14
|
-
import { resolveConstants } from "./transforms/bytecode/resolveContants.ts";
|
|
15
|
-
import { selfModifying } from "./transforms/bytecode/selfModifying.ts";
|
|
16
|
-
import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.ts";
|
|
17
|
-
import { microOpcodes } from "./transforms/bytecode/microOpcodes.ts";
|
|
18
|
-
import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.ts";
|
|
19
|
-
import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.ts";
|
|
20
|
-
import { getRandomInt } from "./utils/random-utils.ts";
|
|
21
|
-
import { U16_MAX } from "./utils/op-utils.ts";
|
|
22
|
-
import { concealConstants } from "./transforms/bytecode/concealConstants.ts";
|
|
23
|
-
import { dispatcher } from "./transforms/bytecode/dispatcher.ts";
|
|
24
|
-
|
|
25
|
-
const traverse = (traverseImport.default ||
|
|
26
|
-
traverseImport) as typeof traverseImport.default;
|
|
27
|
-
|
|
28
|
-
const readVMRuntimeFile = () => {
|
|
29
|
-
let code;
|
|
30
|
-
try {
|
|
31
|
-
code = readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8");
|
|
32
|
-
} catch (e) {
|
|
33
|
-
code = readFileSync(join(import.meta.dirname, "./runtime.js"), "utf-8");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return stripTypeScriptTypes?.(code) || code;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
|
|
40
|
-
export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE");
|
|
41
|
-
|
|
42
|
-
// ── Opcodes ──────────────────────────────────────────────────────────────────
|
|
43
|
-
// Register-based encoding. Operand convention (x86 / CPython style):
|
|
44
|
-
// destination register first, then source registers, then immediates.
|
|
45
|
-
//
|
|
46
|
-
// dst – register index that receives the result
|
|
47
|
-
// src – register index holding an input value
|
|
48
|
-
// imm/Idx – immediate integer (constant-pool index, upvalue index, argc …)
|
|
49
|
-
//
|
|
50
|
-
// Every arithmetic/comparison/unary instruction: [op, dst, src1, src2?]
|
|
51
|
-
// Every load: [op, dst, ...]
|
|
52
|
-
// Every store: [op, target, src]
|
|
53
|
-
// Calls: CALL [op, dst, callee, argc, arg0, arg1, …]
|
|
54
|
-
// CALL_METHOD [op, dst, receiver, callee, argc, arg0, …]
|
|
55
|
-
export const OP_ORIGINAL = {
|
|
56
|
-
// ── Loads ─────────────────────────────────────────────────────────────────
|
|
57
|
-
LOAD_CONST: 0, // dst, constIdx regs[dst] = constants[constIdx]
|
|
58
|
-
LOAD_INT: 1, // dst, imm regs[dst] = imm (raw u16 literal)
|
|
59
|
-
LOAD_GLOBAL: 2, // dst, nameIdx regs[dst] = globals[constants[nameIdx]]
|
|
60
|
-
LOAD_UPVALUE: 3, // dst, uvIdx regs[dst] = upvalues[uvIdx].read()
|
|
61
|
-
LOAD_THIS: 4, // dst regs[dst] = frame.thisVal
|
|
62
|
-
MOVE: 5, // dst, src regs[dst] = regs[src]
|
|
63
|
-
|
|
64
|
-
// ── Stores ────────────────────────────────────────────────────────────────
|
|
65
|
-
STORE_GLOBAL: 6, // nameIdx, src globals[constants[nameIdx]] = regs[src]
|
|
66
|
-
STORE_UPVALUE: 7, // uvIdx, src upvalues[uvIdx].write(regs[src])
|
|
67
|
-
|
|
68
|
-
// ── Property access ───────────────────────────────────────────────────────
|
|
69
|
-
GET_PROP: 8, // dst, obj, key regs[dst] = regs[obj][regs[key]]
|
|
70
|
-
SET_PROP: 9, // obj, key, val regs[obj][regs[key]] = regs[val] (result stays in val reg)
|
|
71
|
-
DELETE_PROP: 10, // dst, obj, key regs[dst] = delete regs[obj][regs[key]]
|
|
72
|
-
|
|
73
|
-
// ── Arithmetic / bitwise (dst, src1, src2) ───────────────────────────────
|
|
74
|
-
ADD: 11,
|
|
75
|
-
SUB: 12,
|
|
76
|
-
MUL: 13,
|
|
77
|
-
DIV: 14,
|
|
78
|
-
MOD: 15,
|
|
79
|
-
BAND: 16,
|
|
80
|
-
BOR: 17,
|
|
81
|
-
BXOR: 18,
|
|
82
|
-
SHL: 19,
|
|
83
|
-
SHR: 20,
|
|
84
|
-
USHR: 21,
|
|
85
|
-
|
|
86
|
-
// ── Comparison (dst, src1, src2) ─────────────────────────────────────────
|
|
87
|
-
LT: 22,
|
|
88
|
-
GT: 23,
|
|
89
|
-
LTE: 24,
|
|
90
|
-
GTE: 25,
|
|
91
|
-
EQ: 26,
|
|
92
|
-
NEQ: 27,
|
|
93
|
-
LOOSE_EQ: 28,
|
|
94
|
-
LOOSE_NEQ: 29,
|
|
95
|
-
IN: 30,
|
|
96
|
-
INSTANCEOF: 31,
|
|
97
|
-
|
|
98
|
-
// ── Unary (dst, src) ─────────────────────────────────────────────────────
|
|
99
|
-
UNARY_NEG: 32,
|
|
100
|
-
UNARY_POS: 33,
|
|
101
|
-
UNARY_NOT: 34,
|
|
102
|
-
UNARY_BITNOT: 35,
|
|
103
|
-
TYPEOF: 36, // dst, src
|
|
104
|
-
VOID: 37, // dst, src – regs[dst] = undefined (src evaluated for side-effects)
|
|
105
|
-
TYPEOF_SAFE: 38, // dst, nameConstIdx – safe typeof for potentially-undeclared globals
|
|
106
|
-
|
|
107
|
-
// ── Control flow ──────────────────────────────────────────────────────────
|
|
108
|
-
JUMP: 39, // target
|
|
109
|
-
JUMP_IF_FALSE: 40, // src, target if !regs[src] then pc = target
|
|
110
|
-
JUMP_IF_TRUE: 41, // src, target if regs[src] then pc = target (|| short-circuit)
|
|
111
|
-
|
|
112
|
-
// ── Calls & constructors ──────────────────────────────────────────────────
|
|
113
|
-
CALL: 42, // dst, callee, argc, [argRegs…]
|
|
114
|
-
CALL_METHOD: 43, // dst, receiver, callee, argc, [argRegs…]
|
|
115
|
-
NEW: 44, // dst, callee, argc, [argRegs…]
|
|
116
|
-
RETURN: 45, // src
|
|
117
|
-
THROW: 46, // src
|
|
118
|
-
|
|
119
|
-
// ── Closures ──────────────────────────────────────────────────────────────
|
|
120
|
-
// dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
|
|
121
|
-
MAKE_CLOSURE: 47,
|
|
122
|
-
|
|
123
|
-
// ── Collections ───────────────────────────────────────────────────────────
|
|
124
|
-
BUILD_ARRAY: 48, // dst, count, [elemRegs…]
|
|
125
|
-
BUILD_OBJECT: 49, // dst, pairCount, [keyReg, valReg, …]
|
|
126
|
-
|
|
127
|
-
// ── Property definitions (getters / setters) ──────────────────────────────
|
|
128
|
-
DEFINE_GETTER: 50, // obj, key, fn
|
|
129
|
-
DEFINE_SETTER: 51, // obj, key, fn
|
|
130
|
-
|
|
131
|
-
// ── For-in iteration ──────────────────────────────────────────────────────
|
|
132
|
-
FOR_IN_SETUP: 52, // dst, src dst = { _keys: enumKeys(src), i: 0 }
|
|
133
|
-
FOR_IN_NEXT: 53, // dst, iter, exitTarget
|
|
134
|
-
|
|
135
|
-
// ── Exception handling ────────────────────────────────────────────────────
|
|
136
|
-
TRY_SETUP: 54, // handlerPc, exceptionReg
|
|
137
|
-
TRY_END: 55,
|
|
138
|
-
|
|
139
|
-
// ── Self-modifying bytecode ───────────────────────────────────────────────
|
|
140
|
-
PATCH: 56, // destPc, sliceStart, sliceEnd
|
|
141
|
-
|
|
142
|
-
// ── Debug ─────────────────────────────────────────────────────────────────
|
|
143
|
-
DEBUGGER: 57,
|
|
144
|
-
|
|
145
|
-
// ── Indirect jump (register-addressed) ───────────────────────────────────
|
|
146
|
-
// Used by the jumpDispatcher pass. The target PC is read from a register
|
|
147
|
-
// rather than encoded as a bytecode immediate, so static analysis cannot
|
|
148
|
-
// determine the destination without tracking register values at runtime.
|
|
149
|
-
JUMP_REG: 58, // src — frame._pc = regs[src]
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
// ── Scope ─────────────────────────────────────────────────────────────────────
|
|
153
|
-
// Maps variable names to virtual RegisterOperands.
|
|
154
|
-
// Locals are allocated at compile time via ctx._newReg(); zero name lookups at runtime.
|
|
155
|
-
// resolveRegisters() assigns concrete slot indices before serialisation.
|
|
156
|
-
class Scope {
|
|
157
|
-
parent: Scope | null;
|
|
158
|
-
_locals: Map<string, b.RegisterOperand>;
|
|
159
|
-
|
|
160
|
-
constructor(parent = null) {
|
|
161
|
-
this.parent = parent;
|
|
162
|
-
this._locals = new Map();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
define(name: string, ctx: FnContext): b.RegisterOperand {
|
|
166
|
-
if (!this._locals.has(name)) {
|
|
167
|
-
this._locals.set(name, ctx._newReg());
|
|
168
|
-
}
|
|
169
|
-
return this._locals.get(name)!;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
resolve(
|
|
173
|
-
name: string,
|
|
174
|
-
): { kind: "local"; reg: b.RegisterOperand } | { kind: "global" } {
|
|
175
|
-
if (this._locals.has(name)) {
|
|
176
|
-
return { kind: "local", reg: this._locals.get(name)! };
|
|
177
|
-
}
|
|
178
|
-
if (this.parent) return this.parent.resolve(name);
|
|
179
|
-
return { kind: "global" };
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ── FnContext ─────────────────────────────────────────────────────────────────
|
|
184
|
-
// Compiler-side state for the function currently being compiled.
|
|
185
|
-
// Distinct from the runtime Frame — this is compile-time only.
|
|
186
|
-
//
|
|
187
|
-
// Virtual-register model (Lua/LLVM style):
|
|
188
|
-
// Every allocReg() / _newReg() call returns a fresh RegisterOperand with a
|
|
189
|
-
// unique (fnId, id) pair. IDs are never reused — resolveRegisters() does
|
|
190
|
-
// liveness-aware slot assignment and sets desc.regCount at the end of the
|
|
191
|
-
// pipeline, just like resolveLabels() fills in jump targets.
|
|
192
|
-
class FnContext {
|
|
193
|
-
// index: RegisterOperand if isLocal (register in parent frame), number if upvalue chain
|
|
194
|
-
upvalues: {
|
|
195
|
-
name: string;
|
|
196
|
-
isLocal: number;
|
|
197
|
-
index: number | b.RegisterOperand;
|
|
198
|
-
}[];
|
|
199
|
-
parentCtx: FnContext | null;
|
|
200
|
-
scope: Scope;
|
|
201
|
-
compiler: Compiler;
|
|
202
|
-
bc: b.Instruction[];
|
|
203
|
-
|
|
204
|
-
// Unique ID for this function — matches the index in compiler.fnDescriptors.
|
|
205
|
-
_fnId: number;
|
|
206
|
-
// Monotonically increasing counter; each call to _newReg() bumps it.
|
|
207
|
-
_nextId: number = 0;
|
|
208
|
-
|
|
209
|
-
constructor(
|
|
210
|
-
compiler: Compiler,
|
|
211
|
-
parentCtx: FnContext | null = null,
|
|
212
|
-
fnId: number = 0,
|
|
213
|
-
) {
|
|
214
|
-
this.compiler = compiler;
|
|
215
|
-
this.parentCtx = parentCtx;
|
|
216
|
-
this.scope = new Scope();
|
|
217
|
-
this.bc = [];
|
|
218
|
-
this.upvalues = [];
|
|
219
|
-
this._fnId = fnId;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Create a new virtual register owned by this function. */
|
|
223
|
-
_newReg(): b.RegisterOperand {
|
|
224
|
-
return b.registerOperand(this._nextId++, this._fnId);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Allocate a short-lived temporary register (pool "temp::").
|
|
229
|
-
* resolveRegisters() will reuse its concrete slot once its live range ends.
|
|
230
|
-
* Do NOT use for named locals or upvalue-captured variables — use _newReg()
|
|
231
|
-
* via scope.define() for those, so they stay in the stable "local::" pool.
|
|
232
|
-
*/
|
|
233
|
-
allocReg(): b.RegisterOperand {
|
|
234
|
-
return b.registerOperand(this._nextId++, this._fnId, { kind: "temp" });
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Emit a freeReg pseudo-instruction to explicitly end a temporary's live range.
|
|
239
|
-
*
|
|
240
|
-
* NOTE: This is extraneous for any programmatically generated IR.
|
|
241
|
-
* resolveRegisters() already computes lastUse as the last instruction index
|
|
242
|
-
* where the register appears as a real operand — which is always the tightest
|
|
243
|
-
* correct bound when you stop emitting a register after its last logical use.
|
|
244
|
-
* freeReg is only needed in the rare case where a register has a late syntactic
|
|
245
|
-
* appearance that does NOT represent its true logical death (e.g. a dummy read
|
|
246
|
-
* emitted for side-effects long after the value is logically dead). No current
|
|
247
|
-
* pass in this codebase uses it; it is kept as an extension point only.
|
|
248
|
-
*/
|
|
249
|
-
freeReg(bc: b.Bytecode, reg: b.RegisterOperand): void {
|
|
250
|
-
bc.push([null, b.freeRegOperand(reg)]);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** No-op kept for call-site compatibility; liveness is handled by resolveRegisters. */
|
|
254
|
-
resetTemps(): void {}
|
|
255
|
-
|
|
256
|
-
addUpvalue(
|
|
257
|
-
name: string,
|
|
258
|
-
isLocal: number,
|
|
259
|
-
index: number | b.RegisterOperand,
|
|
260
|
-
): number {
|
|
261
|
-
const existing = this.upvalues.findIndex((u) => u.name === name);
|
|
262
|
-
if (existing !== -1) return existing;
|
|
263
|
-
const idx = this.upvalues.length;
|
|
264
|
-
this.upvalues.push({ name, isLocal, index });
|
|
265
|
-
return idx;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
interface FnDescriptor {
|
|
270
|
-
name?: string;
|
|
271
|
-
entryLabel?: string;
|
|
272
|
-
startLabel?: string;
|
|
273
|
-
bytecode?: b.Bytecode;
|
|
274
|
-
paramCount?: number;
|
|
275
|
-
regCount?: number;
|
|
276
|
-
upvalues?: any[];
|
|
277
|
-
_fnIdx?: number;
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Only populated AFTER resolveLabels
|
|
281
|
-
*/
|
|
282
|
-
startPc?: number;
|
|
283
|
-
ctx?: FnContext;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// ── Compiler ──────────────────────────────────────────────────────────────────
|
|
287
|
-
export class Compiler {
|
|
288
|
-
fnDescriptors: FnDescriptor[];
|
|
289
|
-
bytecode: b.Bytecode;
|
|
290
|
-
mainRegCount: number;
|
|
291
|
-
mainFn: ReturnType<typeof this._compileFunctionDecl>;
|
|
292
|
-
mainStartPc: number;
|
|
293
|
-
|
|
294
|
-
_currentCtx: FnContext | null;
|
|
295
|
-
_pendingLabel: string | null;
|
|
296
|
-
_forInCount: number;
|
|
297
|
-
_labelCount: number;
|
|
298
|
-
_loopStack: {
|
|
299
|
-
type: "loop" | "switch" | "block";
|
|
300
|
-
label: string | null;
|
|
301
|
-
breakLabel: string;
|
|
302
|
-
continueLabel: string;
|
|
303
|
-
}[];
|
|
304
|
-
|
|
305
|
-
options: Options;
|
|
306
|
-
serializer: Serializer;
|
|
307
|
-
|
|
308
|
-
OP: Partial<typeof OP_ORIGINAL>;
|
|
309
|
-
MACRO_OPS: Record<number, number[]>;
|
|
310
|
-
SPECIALIZED_OPS: Record<
|
|
311
|
-
number,
|
|
312
|
-
{
|
|
313
|
-
originalOp: number;
|
|
314
|
-
operands: b.InstrOperand[];
|
|
315
|
-
}
|
|
316
|
-
>;
|
|
317
|
-
ALIASED_OPS: Record<number, { originalOp: number; order: number[] }>;
|
|
318
|
-
MICRO_OPS: Record<
|
|
319
|
-
number,
|
|
320
|
-
{ originalOp: number; stmtIndex: number; irOperandCount: number }
|
|
321
|
-
>;
|
|
322
|
-
|
|
323
|
-
/** Internal variable slot registry.
|
|
324
|
-
* globally: shared name→index pool (written on first sight; reused by non-random mode or by 50% chance in random mode).
|
|
325
|
-
* opcodes: per-opcode source-of-truth — all assignment lookups are read/written here. */
|
|
326
|
-
_internals: {
|
|
327
|
-
globally: Map<string, number>;
|
|
328
|
-
opcodes: Map<number, Map<string, number>>;
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
OP_NAME: Record<number, string>;
|
|
332
|
-
JUMP_OPS: Set<number>;
|
|
333
|
-
|
|
334
|
-
constants: any[];
|
|
335
|
-
|
|
336
|
-
_cloneRegisterOperand<T extends b.InstrOperand>(operand: T): T {
|
|
337
|
-
if (!operand || typeof operand !== "object") return operand;
|
|
338
|
-
if ((operand as any).type !== "register") return operand;
|
|
339
|
-
|
|
340
|
-
return JSON.parse(JSON.stringify(operand)) as T;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
emit(bc: b.Bytecode, instr: b.Instruction, node: t.Node) {
|
|
344
|
-
for (let i = 1; i < instr.length; i++) {
|
|
345
|
-
instr[i] = this._cloneRegisterOperand(instr[i]);
|
|
346
|
-
}
|
|
347
|
-
bc.push(instr);
|
|
348
|
-
instr[SOURCE_NODE_SYM] = node;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
constructor(options: Options = DEFAULT_OPTIONS) {
|
|
352
|
-
this.options = options;
|
|
353
|
-
this.fnDescriptors = [];
|
|
354
|
-
this.bytecode = [];
|
|
355
|
-
this.mainStartPc = 0;
|
|
356
|
-
this.mainRegCount = 0;
|
|
357
|
-
this._currentCtx = null;
|
|
358
|
-
this._loopStack = [];
|
|
359
|
-
this._pendingLabel = null;
|
|
360
|
-
this._forInCount = 0;
|
|
361
|
-
this._labelCount = 0;
|
|
362
|
-
|
|
363
|
-
this.serializer = new Serializer(this);
|
|
364
|
-
this.MACRO_OPS = {};
|
|
365
|
-
this.MICRO_OPS = {};
|
|
366
|
-
this.SPECIALIZED_OPS = {};
|
|
367
|
-
this.ALIASED_OPS = {};
|
|
368
|
-
this._internals = { globally: new Map(), opcodes: new Map() };
|
|
369
|
-
|
|
370
|
-
this.OP = { ...OP_ORIGINAL };
|
|
371
|
-
|
|
372
|
-
if (this.options.randomizeOpcodes) {
|
|
373
|
-
let usedNumbers = new Set<number>();
|
|
374
|
-
for (const key in this.OP) {
|
|
375
|
-
let val;
|
|
376
|
-
do {
|
|
377
|
-
val = getRandomInt(0, U16_MAX);
|
|
378
|
-
} while (usedNumbers.has(val));
|
|
379
|
-
usedNumbers.add(val);
|
|
380
|
-
this.OP[key] = val;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
this.OP_NAME = Object.fromEntries(
|
|
385
|
-
Object.entries(this.OP).map(([k, v]) => [v, k]),
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
this.JUMP_OPS = new Set([
|
|
389
|
-
this.OP.JUMP,
|
|
390
|
-
this.OP.JUMP_IF_FALSE,
|
|
391
|
-
this.OP.JUMP_IF_TRUE,
|
|
392
|
-
this.OP.FOR_IN_NEXT,
|
|
393
|
-
this.OP.TRY_SETUP,
|
|
394
|
-
]);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
_makeLabel(hint = ""): string {
|
|
398
|
-
return `${hint || "L"}_${this._labelCount++}`;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
_resolve(
|
|
402
|
-
name: string,
|
|
403
|
-
ctx: FnContext | null,
|
|
404
|
-
):
|
|
405
|
-
| { kind: "local"; reg: b.RegisterOperand }
|
|
406
|
-
| { kind: "upvalue"; index: number }
|
|
407
|
-
| { kind: "global" } {
|
|
408
|
-
if (!ctx) return { kind: "global" };
|
|
409
|
-
|
|
410
|
-
if (ctx.scope._locals.has(name)) {
|
|
411
|
-
return { kind: "local", reg: ctx.scope._locals.get(name)! };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (!ctx.parentCtx) return { kind: "global" };
|
|
415
|
-
|
|
416
|
-
const parentResult = this._resolve(name, ctx.parentCtx);
|
|
417
|
-
if (parentResult.kind === "global") return { kind: "global" };
|
|
418
|
-
|
|
419
|
-
const isLocal = parentResult.kind === "local";
|
|
420
|
-
const index = isLocal ? parentResult.reg : parentResult.index;
|
|
421
|
-
const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
|
|
422
|
-
return { kind: "upvalue", index: uvIdx };
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// ── Variable hoisting ──────────────────────────────────────────────────────
|
|
426
|
-
// Pre-scan a statement list and reserve virtual registers for every var
|
|
427
|
-
// declaration, function declaration, for-in iterator, and try-catch binding.
|
|
428
|
-
// Must be called before any emit so that locals are allocated before temps.
|
|
429
|
-
_hoistVars(stmts: t.Statement[], scope: Scope, ctx: FnContext): void {
|
|
430
|
-
for (const stmt of stmts) {
|
|
431
|
-
switch (stmt.type) {
|
|
432
|
-
case "VariableDeclaration":
|
|
433
|
-
for (const decl of stmt.declarations) {
|
|
434
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
435
|
-
}
|
|
436
|
-
break;
|
|
437
|
-
|
|
438
|
-
case "FunctionDeclaration":
|
|
439
|
-
if (stmt.id) scope.define(stmt.id.name, ctx);
|
|
440
|
-
break;
|
|
441
|
-
|
|
442
|
-
case "BlockStatement":
|
|
443
|
-
this._hoistVars(stmt.body, scope, ctx);
|
|
444
|
-
break;
|
|
445
|
-
|
|
446
|
-
case "IfStatement": {
|
|
447
|
-
const cons =
|
|
448
|
-
stmt.consequent.type === "BlockStatement"
|
|
449
|
-
? stmt.consequent.body
|
|
450
|
-
: [stmt.consequent];
|
|
451
|
-
this._hoistVars(cons, scope, ctx);
|
|
452
|
-
if (stmt.alternate) {
|
|
453
|
-
const alt =
|
|
454
|
-
stmt.alternate.type === "BlockStatement"
|
|
455
|
-
? stmt.alternate.body
|
|
456
|
-
: [stmt.alternate];
|
|
457
|
-
this._hoistVars(alt, scope, ctx);
|
|
458
|
-
}
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
case "WhileStatement":
|
|
463
|
-
case "DoWhileStatement": {
|
|
464
|
-
const body =
|
|
465
|
-
stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
466
|
-
this._hoistVars(body, scope, ctx);
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
case "ForStatement": {
|
|
471
|
-
if (stmt.init?.type === "VariableDeclaration") {
|
|
472
|
-
for (const decl of stmt.init.declarations) {
|
|
473
|
-
if (decl.id.type === "Identifier")
|
|
474
|
-
scope.define(decl.id.name, ctx);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
const body =
|
|
478
|
-
stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
479
|
-
this._hoistVars(body, scope, ctx);
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
case "ForInStatement": {
|
|
484
|
-
// Reserve a hidden virtual register for the iterator object.
|
|
485
|
-
(stmt as any)._iterSlot = ctx._newReg();
|
|
486
|
-
if (stmt.left.type === "VariableDeclaration") {
|
|
487
|
-
for (const decl of stmt.left.declarations) {
|
|
488
|
-
if (decl.id.type === "Identifier")
|
|
489
|
-
scope.define(decl.id.name, ctx);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
const body =
|
|
493
|
-
stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
494
|
-
this._hoistVars(body, scope, ctx);
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
case "SwitchStatement":
|
|
499
|
-
for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
|
|
500
|
-
break;
|
|
501
|
-
|
|
502
|
-
case "TryStatement":
|
|
503
|
-
this._hoistVars(stmt.block.body, scope, ctx);
|
|
504
|
-
if (stmt.handler) {
|
|
505
|
-
if (stmt.handler.param?.type === "Identifier") {
|
|
506
|
-
// Catch parameter IS the exception register.
|
|
507
|
-
scope.define((stmt.handler.param as t.Identifier).name, ctx);
|
|
508
|
-
} else {
|
|
509
|
-
// No catch binding – reserve a dummy virtual register for the exception value.
|
|
510
|
-
(stmt as any)._exceptionSlot = ctx._newReg();
|
|
511
|
-
}
|
|
512
|
-
this._hoistVars(stmt.handler.body.body, scope, ctx);
|
|
513
|
-
}
|
|
514
|
-
break;
|
|
515
|
-
|
|
516
|
-
case "LabeledStatement":
|
|
517
|
-
this._hoistVars([stmt.body], scope, ctx);
|
|
518
|
-
break;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ── Entry point ───────────────────────────────────────────────────────────
|
|
524
|
-
compile(source: string) {
|
|
525
|
-
const ast = parse(source, {
|
|
526
|
-
sourceType: "script",
|
|
527
|
-
allowReturnOutsideFunction: true,
|
|
528
|
-
});
|
|
529
|
-
return this.compileAST(ast);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
compileAST(ast: t.File) {
|
|
533
|
-
this._compileMain(ast.program.body);
|
|
534
|
-
return this.bytecode;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// ── Function compilation ───────────────────────────────────────────────────
|
|
538
|
-
_compileFunctionDecl(node: t.FunctionDeclaration | t.FunctionExpression) {
|
|
539
|
-
ok(!node.generator, "Generator functions are not supported");
|
|
540
|
-
ok(!node.async, "Async functions are not supported");
|
|
541
|
-
|
|
542
|
-
var fnIdx = this.fnDescriptors.length;
|
|
543
|
-
const entryLabel = this._makeLabel(`fn_${fnIdx}`);
|
|
544
|
-
var desc: FnDescriptor = {};
|
|
545
|
-
this.fnDescriptors.push(desc);
|
|
546
|
-
|
|
547
|
-
const ctx = new FnContext(this, this._currentCtx, fnIdx);
|
|
548
|
-
const savedCtx = this._currentCtx;
|
|
549
|
-
this._currentCtx = ctx;
|
|
550
|
-
|
|
551
|
-
const savedLoopStack = this._loopStack;
|
|
552
|
-
this._loopStack = [];
|
|
553
|
-
|
|
554
|
-
// 1. Define parameters as virtual registers (occupy the first IDs in order).
|
|
555
|
-
for (const param of node.params) {
|
|
556
|
-
let identifier = param.type === "AssignmentPattern" ? param.left : param;
|
|
557
|
-
ok(
|
|
558
|
-
identifier.type === "Identifier",
|
|
559
|
-
"Only simple identifiers allowed as parameters",
|
|
560
|
-
);
|
|
561
|
-
ctx.scope.define((identifier as t.Identifier).name, ctx);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// 2. Reserve the `arguments` virtual register (immediately after params).
|
|
565
|
-
ctx.scope.define("arguments", ctx);
|
|
566
|
-
|
|
567
|
-
// 3. Hoist all var declarations so locals are allocated before any temps.
|
|
568
|
-
this._hoistVars(node.body.body, ctx.scope, ctx);
|
|
569
|
-
|
|
570
|
-
// 5. Emit default-value guards.
|
|
571
|
-
for (const param of node.params) {
|
|
572
|
-
if (param.type !== "AssignmentPattern") continue;
|
|
573
|
-
|
|
574
|
-
const slot = ctx.scope._locals.get((param.left as t.Identifier).name)!;
|
|
575
|
-
const skipLabel = this._makeLabel("param_skip");
|
|
576
|
-
|
|
577
|
-
// if (param === undefined) param = <default>
|
|
578
|
-
const reg_undef = ctx.allocReg();
|
|
579
|
-
this.emit(
|
|
580
|
-
ctx.bc,
|
|
581
|
-
[this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)],
|
|
582
|
-
param,
|
|
583
|
-
);
|
|
584
|
-
const reg_cmp = ctx.allocReg();
|
|
585
|
-
this.emit(ctx.bc, [this.OP.EQ, reg_cmp, slot, reg_undef], param);
|
|
586
|
-
this.emit(
|
|
587
|
-
ctx.bc,
|
|
588
|
-
[this.OP.JUMP_IF_FALSE, reg_cmp, { type: "label", label: skipLabel }],
|
|
589
|
-
param,
|
|
590
|
-
);
|
|
591
|
-
ctx.resetTemps();
|
|
592
|
-
|
|
593
|
-
const srcReg = this._compileExpr(param.right, ctx.scope, ctx.bc);
|
|
594
|
-
if (srcReg !== slot) {
|
|
595
|
-
this.emit(ctx.bc, [this.OP.MOVE, slot, srcReg], param);
|
|
596
|
-
}
|
|
597
|
-
ctx.resetTemps();
|
|
598
|
-
|
|
599
|
-
this.emit(
|
|
600
|
-
ctx.bc,
|
|
601
|
-
[null, { type: "defineLabel", label: skipLabel }],
|
|
602
|
-
param,
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// 6. Compile body.
|
|
607
|
-
for (const stmt of node.body.body) {
|
|
608
|
-
this._compileStatement(stmt, ctx.scope, ctx.bc);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Implicit return undefined at end of function.
|
|
612
|
-
const reg_undef = ctx.allocReg();
|
|
613
|
-
this.emit(
|
|
614
|
-
ctx.bc,
|
|
615
|
-
[this.OP.LOAD_CONST, reg_undef, b.constantOperand(undefined)],
|
|
616
|
-
node,
|
|
617
|
-
);
|
|
618
|
-
this.emit(ctx.bc, [this.OP.RETURN, reg_undef], node);
|
|
619
|
-
|
|
620
|
-
this._currentCtx = savedCtx;
|
|
621
|
-
this._loopStack = savedLoopStack;
|
|
622
|
-
|
|
623
|
-
(node as any)._fnIdx = fnIdx;
|
|
624
|
-
|
|
625
|
-
desc.name = (node as any).id?.name || "<anonymous>";
|
|
626
|
-
desc.entryLabel = entryLabel;
|
|
627
|
-
desc.bytecode = ctx.bc as b.Bytecode;
|
|
628
|
-
desc._fnIdx = fnIdx;
|
|
629
|
-
desc.paramCount = node.params.length;
|
|
630
|
-
// regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
|
|
631
|
-
desc.upvalues = ctx.upvalues.slice();
|
|
632
|
-
desc.ctx = ctx;
|
|
633
|
-
|
|
634
|
-
return desc;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Emit MAKE_CLOSURE with all metadata as inline operands.
|
|
638
|
-
// Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
|
|
639
|
-
// regCount is emitted as a fnRegCount IR operand; resolveRegisters() fills it.
|
|
640
|
-
_emitMakeClosure(desc: any, node: t.Node, bc: b.Bytecode) {
|
|
641
|
-
const ctx = this._currentCtx!;
|
|
642
|
-
const dst = ctx.allocReg();
|
|
643
|
-
const uvOperands: b.InstrOperand[] = [];
|
|
644
|
-
for (const uv of desc.upvalues) {
|
|
645
|
-
uvOperands.push(uv.isLocal ? 1 : 0);
|
|
646
|
-
uvOperands.push(uv.index); // RegisterOperand if isLocal, number if upvalue chain
|
|
647
|
-
}
|
|
648
|
-
this.emit(
|
|
649
|
-
bc,
|
|
650
|
-
[
|
|
651
|
-
this.OP.MAKE_CLOSURE,
|
|
652
|
-
dst,
|
|
653
|
-
{ type: "label", label: desc.entryLabel },
|
|
654
|
-
desc.paramCount,
|
|
655
|
-
b.fnRegCountOperand(desc._fnIdx), // resolved by resolveRegisters()
|
|
656
|
-
desc.upvalues.length,
|
|
657
|
-
...uvOperands,
|
|
658
|
-
] as b.Instruction,
|
|
659
|
-
node,
|
|
660
|
-
);
|
|
661
|
-
return dst;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// ── Main (top-level) ───────────────────────────────────────────────────────
|
|
665
|
-
_compileMain(body: t.Statement[]) {
|
|
666
|
-
const mainCtx = new FnContext(this, null);
|
|
667
|
-
const savedCtx = this._currentCtx;
|
|
668
|
-
this._currentCtx = mainCtx;
|
|
669
|
-
|
|
670
|
-
var desc = this._compileFunctionDecl({
|
|
671
|
-
type: "FunctionDeclaration",
|
|
672
|
-
async: false,
|
|
673
|
-
generator: false,
|
|
674
|
-
params: [],
|
|
675
|
-
id: t.identifier("main"),
|
|
676
|
-
body: t.blockStatement([...body]),
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
for (const descriptor of this.fnDescriptors) {
|
|
680
|
-
this.bytecode.push([
|
|
681
|
-
null,
|
|
682
|
-
{ type: "defineLabel", label: descriptor.entryLabel },
|
|
683
|
-
]);
|
|
684
|
-
for (const instr of descriptor.bytecode) {
|
|
685
|
-
this.bytecode.push(instr);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// mainRegCount is set by resolveRegisters() after the pipeline runs.
|
|
690
|
-
this.mainFn = desc;
|
|
691
|
-
this._currentCtx = savedCtx;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// ── Statements ────────────────────────────────────────────────────────────
|
|
695
|
-
// Wrapper that resets temps after every statement so that short-lived
|
|
696
|
-
// expression temps don't accumulate across statements.
|
|
697
|
-
_compileStatement(node: t.Statement, scope: Scope | null, bc: b.Bytecode) {
|
|
698
|
-
this._compileStatementImpl(node, scope, bc);
|
|
699
|
-
this._currentCtx?.resetTemps();
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
_compileStatementImpl(
|
|
703
|
-
node: t.Statement,
|
|
704
|
-
scope: Scope | null,
|
|
705
|
-
bc: b.Bytecode,
|
|
706
|
-
) {
|
|
707
|
-
const ctx = this._currentCtx!;
|
|
708
|
-
|
|
709
|
-
switch (node.type) {
|
|
710
|
-
case "EmptyStatement":
|
|
711
|
-
break;
|
|
712
|
-
|
|
713
|
-
case "DebuggerStatement":
|
|
714
|
-
this.emit(bc, [this.OP.DEBUGGER], node);
|
|
715
|
-
break;
|
|
716
|
-
|
|
717
|
-
case "BlockStatement":
|
|
718
|
-
for (const stmt of node.body) {
|
|
719
|
-
this._compileStatement(stmt, scope, bc);
|
|
720
|
-
}
|
|
721
|
-
break;
|
|
722
|
-
|
|
723
|
-
case "FunctionDeclaration": {
|
|
724
|
-
const desc = this._compileFunctionDecl(node);
|
|
725
|
-
const closureReg = this._emitMakeClosure(desc, node, bc);
|
|
726
|
-
if (scope) {
|
|
727
|
-
const slot = scope._locals.get(node.id!.name)!;
|
|
728
|
-
if (closureReg !== slot) {
|
|
729
|
-
this.emit(bc, [this.OP.MOVE, slot, closureReg], node);
|
|
730
|
-
}
|
|
731
|
-
} else {
|
|
732
|
-
this.emit(
|
|
733
|
-
bc,
|
|
734
|
-
[
|
|
735
|
-
this.OP.STORE_GLOBAL,
|
|
736
|
-
b.constantOperand(node.id!.name),
|
|
737
|
-
closureReg,
|
|
738
|
-
],
|
|
739
|
-
node,
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
break;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
case "ThrowStatement": {
|
|
746
|
-
const reg = this._compileExpr(node.argument, scope, bc);
|
|
747
|
-
this.emit(bc, [this.OP.THROW, reg], node);
|
|
748
|
-
break;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
case "ReturnStatement": {
|
|
752
|
-
let reg: b.RegisterOperand;
|
|
753
|
-
if (node.argument) {
|
|
754
|
-
reg = this._compileExpr(node.argument, scope, bc);
|
|
755
|
-
} else {
|
|
756
|
-
reg = ctx.allocReg();
|
|
757
|
-
this.emit(
|
|
758
|
-
bc,
|
|
759
|
-
[this.OP.LOAD_CONST, reg, b.constantOperand(undefined)],
|
|
760
|
-
node,
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
|
|
764
|
-
if ((this._loopStack[_ri].type as any) === "try") {
|
|
765
|
-
this.emit(bc, [this.OP.TRY_END], node);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
this.emit(bc, [this.OP.RETURN, reg], node);
|
|
769
|
-
break;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
case "ExpressionStatement":
|
|
773
|
-
this._compileExpr(node.expression, scope, bc);
|
|
774
|
-
// Result is discarded; resetTemps in the wrapper handles cleanup.
|
|
775
|
-
break;
|
|
776
|
-
|
|
777
|
-
case "VariableDeclaration": {
|
|
778
|
-
for (const decl of node.declarations) {
|
|
779
|
-
ok(
|
|
780
|
-
decl.id.type === "Identifier",
|
|
781
|
-
"Only simple identifiers can be declared",
|
|
782
|
-
);
|
|
783
|
-
const name = (decl.id as t.Identifier).name;
|
|
784
|
-
|
|
785
|
-
if (scope) {
|
|
786
|
-
const slot = scope._locals.get(name)!; // already defined by _hoistVars
|
|
787
|
-
if (decl.init) {
|
|
788
|
-
const srcReg = this._compileExpr(decl.init, scope, bc);
|
|
789
|
-
if (srcReg !== slot) {
|
|
790
|
-
this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
|
|
791
|
-
}
|
|
792
|
-
} else {
|
|
793
|
-
// No initializer: var x; → load undefined directly into the local's register.
|
|
794
|
-
this.emit(
|
|
795
|
-
bc,
|
|
796
|
-
[this.OP.LOAD_CONST, slot, b.constantOperand(undefined)],
|
|
797
|
-
node,
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
} else {
|
|
801
|
-
if (decl.init) {
|
|
802
|
-
const srcReg = this._compileExpr(decl.init, scope, bc);
|
|
803
|
-
this.emit(
|
|
804
|
-
bc,
|
|
805
|
-
[this.OP.STORE_GLOBAL, b.constantOperand(name), srcReg],
|
|
806
|
-
node,
|
|
807
|
-
);
|
|
808
|
-
} else {
|
|
809
|
-
const tmp = ctx.allocReg();
|
|
810
|
-
this.emit(
|
|
811
|
-
bc,
|
|
812
|
-
[this.OP.LOAD_CONST, tmp, b.constantOperand(undefined)],
|
|
813
|
-
node,
|
|
814
|
-
);
|
|
815
|
-
this.emit(
|
|
816
|
-
bc,
|
|
817
|
-
[this.OP.STORE_GLOBAL, b.constantOperand(name), tmp],
|
|
818
|
-
node,
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
break;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
case "IfStatement": {
|
|
827
|
-
const elseOrEndLabel = this._makeLabel("if_else");
|
|
828
|
-
|
|
829
|
-
const testReg = this._compileExpr(node.test, scope, bc);
|
|
830
|
-
this.emit(
|
|
831
|
-
bc,
|
|
832
|
-
[
|
|
833
|
-
this.OP.JUMP_IF_FALSE,
|
|
834
|
-
testReg,
|
|
835
|
-
{ type: "label", label: elseOrEndLabel },
|
|
836
|
-
],
|
|
837
|
-
node,
|
|
838
|
-
);
|
|
839
|
-
|
|
840
|
-
const consequentBody =
|
|
841
|
-
node.consequent.type === "BlockStatement"
|
|
842
|
-
? node.consequent.body
|
|
843
|
-
: [node.consequent];
|
|
844
|
-
for (const stmt of consequentBody) {
|
|
845
|
-
this._compileStatement(stmt, scope, bc);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (node.alternate) {
|
|
849
|
-
const endLabel = this._makeLabel("if_end");
|
|
850
|
-
this.emit(
|
|
851
|
-
bc,
|
|
852
|
-
[this.OP.JUMP, { type: "label", label: endLabel }],
|
|
853
|
-
node,
|
|
854
|
-
);
|
|
855
|
-
this.emit(
|
|
856
|
-
bc,
|
|
857
|
-
[null, { type: "defineLabel", label: elseOrEndLabel }],
|
|
858
|
-
node,
|
|
859
|
-
);
|
|
860
|
-
const altBody =
|
|
861
|
-
node.alternate.type === "BlockStatement"
|
|
862
|
-
? node.alternate.body
|
|
863
|
-
: [node.alternate];
|
|
864
|
-
for (const stmt of altBody) {
|
|
865
|
-
this._compileStatement(stmt, scope, bc);
|
|
866
|
-
}
|
|
867
|
-
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
868
|
-
} else {
|
|
869
|
-
this.emit(
|
|
870
|
-
bc,
|
|
871
|
-
[null, { type: "defineLabel", label: elseOrEndLabel }],
|
|
872
|
-
node,
|
|
873
|
-
);
|
|
874
|
-
}
|
|
875
|
-
break;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
case "WhileStatement": {
|
|
879
|
-
const _wLabel = this._pendingLabel;
|
|
880
|
-
this._pendingLabel = null;
|
|
881
|
-
|
|
882
|
-
const loopTopLabel = this._makeLabel("while_top");
|
|
883
|
-
const exitLabel = this._makeLabel("while_exit");
|
|
884
|
-
|
|
885
|
-
this._loopStack.push({
|
|
886
|
-
type: "loop",
|
|
887
|
-
label: _wLabel,
|
|
888
|
-
breakLabel: exitLabel,
|
|
889
|
-
continueLabel: loopTopLabel,
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
this.emit(
|
|
893
|
-
bc,
|
|
894
|
-
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
895
|
-
node,
|
|
896
|
-
);
|
|
897
|
-
|
|
898
|
-
const testReg = this._compileExpr(node.test, scope, bc);
|
|
899
|
-
this.emit(
|
|
900
|
-
bc,
|
|
901
|
-
[this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: exitLabel }],
|
|
902
|
-
node,
|
|
903
|
-
);
|
|
904
|
-
|
|
905
|
-
const whileBody =
|
|
906
|
-
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
907
|
-
for (const stmt of whileBody) {
|
|
908
|
-
this._compileStatement(stmt, scope, bc);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
this.emit(
|
|
912
|
-
bc,
|
|
913
|
-
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
914
|
-
node,
|
|
915
|
-
);
|
|
916
|
-
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
917
|
-
|
|
918
|
-
this._loopStack.pop();
|
|
919
|
-
break;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
case "DoWhileStatement": {
|
|
923
|
-
const _dwLabel = this._pendingLabel;
|
|
924
|
-
this._pendingLabel = null;
|
|
925
|
-
|
|
926
|
-
const loopTopLabel = this._makeLabel("dowhile_top");
|
|
927
|
-
const continueLabel = this._makeLabel("dowhile_cont");
|
|
928
|
-
const exitLabel = this._makeLabel("dowhile_exit");
|
|
929
|
-
|
|
930
|
-
this._loopStack.push({
|
|
931
|
-
type: "loop",
|
|
932
|
-
label: _dwLabel,
|
|
933
|
-
breakLabel: exitLabel,
|
|
934
|
-
continueLabel: continueLabel,
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
this.emit(
|
|
938
|
-
bc,
|
|
939
|
-
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
940
|
-
node,
|
|
941
|
-
);
|
|
942
|
-
|
|
943
|
-
const doWhileBody =
|
|
944
|
-
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
945
|
-
for (const stmt of doWhileBody) {
|
|
946
|
-
this._compileStatement(stmt, scope, bc);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
this.emit(
|
|
950
|
-
bc,
|
|
951
|
-
[null, { type: "defineLabel", label: continueLabel }],
|
|
952
|
-
node,
|
|
953
|
-
);
|
|
954
|
-
|
|
955
|
-
const testReg = this._compileExpr(node.test, scope, bc);
|
|
956
|
-
this.emit(
|
|
957
|
-
bc,
|
|
958
|
-
[this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: exitLabel }],
|
|
959
|
-
node,
|
|
960
|
-
);
|
|
961
|
-
|
|
962
|
-
this.emit(
|
|
963
|
-
bc,
|
|
964
|
-
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
965
|
-
node,
|
|
966
|
-
);
|
|
967
|
-
|
|
968
|
-
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
969
|
-
this._loopStack.pop();
|
|
970
|
-
break;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
case "ForStatement": {
|
|
974
|
-
const _fLabel = this._pendingLabel;
|
|
975
|
-
this._pendingLabel = null;
|
|
976
|
-
|
|
977
|
-
const loopTopLabel = this._makeLabel("for_top");
|
|
978
|
-
const exitLabel = this._makeLabel("for_exit");
|
|
979
|
-
const updateLabel = node.update
|
|
980
|
-
? this._makeLabel("for_update")
|
|
981
|
-
: loopTopLabel;
|
|
982
|
-
|
|
983
|
-
this._loopStack.push({
|
|
984
|
-
type: "loop",
|
|
985
|
-
label: _fLabel,
|
|
986
|
-
breakLabel: exitLabel,
|
|
987
|
-
continueLabel: updateLabel,
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
if (node.init) {
|
|
991
|
-
if (node.init.type === "VariableDeclaration") {
|
|
992
|
-
this._compileStatement(node.init, scope, bc);
|
|
993
|
-
} else {
|
|
994
|
-
this._compileExpr(node.init as t.Expression, scope, bc);
|
|
995
|
-
// result discarded; resetTemps in next iteration
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
this.emit(
|
|
1000
|
-
bc,
|
|
1001
|
-
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
1002
|
-
node,
|
|
1003
|
-
);
|
|
1004
|
-
|
|
1005
|
-
if (node.test) {
|
|
1006
|
-
const testReg = this._compileExpr(node.test, scope, bc);
|
|
1007
|
-
this.emit(
|
|
1008
|
-
bc,
|
|
1009
|
-
[
|
|
1010
|
-
this.OP.JUMP_IF_FALSE,
|
|
1011
|
-
testReg,
|
|
1012
|
-
{ type: "label", label: exitLabel },
|
|
1013
|
-
],
|
|
1014
|
-
node,
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const forBody =
|
|
1019
|
-
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
1020
|
-
for (const stmt of forBody) {
|
|
1021
|
-
this._compileStatement(stmt, scope, bc);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
if (node.update) {
|
|
1025
|
-
this.emit(
|
|
1026
|
-
bc,
|
|
1027
|
-
[null, { type: "defineLabel", label: updateLabel }],
|
|
1028
|
-
node,
|
|
1029
|
-
);
|
|
1030
|
-
this._compileExpr(node.update, scope, bc);
|
|
1031
|
-
ctx.resetTemps(); // discard update expression result
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
this.emit(
|
|
1035
|
-
bc,
|
|
1036
|
-
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
1037
|
-
node,
|
|
1038
|
-
);
|
|
1039
|
-
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
1040
|
-
|
|
1041
|
-
this._loopStack.pop();
|
|
1042
|
-
break;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
case "BreakStatement": {
|
|
1046
|
-
let _bTargetIdx = -1;
|
|
1047
|
-
if (node.label) {
|
|
1048
|
-
const _bLabelName = node.label.name;
|
|
1049
|
-
for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
|
|
1050
|
-
if (this._loopStack[_bi].label === _bLabelName) {
|
|
1051
|
-
_bTargetIdx = _bi;
|
|
1052
|
-
break;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
if (_bTargetIdx === -1)
|
|
1056
|
-
throw new Error(`Label '${node.label.name}' not found`);
|
|
1057
|
-
} else {
|
|
1058
|
-
for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
|
|
1059
|
-
if ((this._loopStack[_bi].type as any) !== "try") {
|
|
1060
|
-
_bTargetIdx = _bi;
|
|
1061
|
-
break;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (_bTargetIdx === -1) throw new Error("break outside loop");
|
|
1065
|
-
}
|
|
1066
|
-
for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
|
|
1067
|
-
if ((this._loopStack[_bi].type as any) === "try") {
|
|
1068
|
-
this.emit(bc, [this.OP.TRY_END], node);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
this.emit(
|
|
1072
|
-
bc,
|
|
1073
|
-
[
|
|
1074
|
-
this.OP.JUMP,
|
|
1075
|
-
{ type: "label", label: this._loopStack[_bTargetIdx].breakLabel },
|
|
1076
|
-
],
|
|
1077
|
-
node,
|
|
1078
|
-
);
|
|
1079
|
-
break;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
case "ContinueStatement": {
|
|
1083
|
-
let _cTargetIdx = -1;
|
|
1084
|
-
if (node.label) {
|
|
1085
|
-
const _cLabelName = node.label.name;
|
|
1086
|
-
for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
|
|
1087
|
-
if (
|
|
1088
|
-
this._loopStack[_ci].label === _cLabelName &&
|
|
1089
|
-
this._loopStack[_ci].type === "loop"
|
|
1090
|
-
) {
|
|
1091
|
-
_cTargetIdx = _ci;
|
|
1092
|
-
break;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
if (_cTargetIdx === -1)
|
|
1096
|
-
throw new Error(
|
|
1097
|
-
`Label '${node.label.name}' not found for continue`,
|
|
1098
|
-
);
|
|
1099
|
-
} else {
|
|
1100
|
-
for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
|
|
1101
|
-
if (this._loopStack[_ci].type === "loop") {
|
|
1102
|
-
_cTargetIdx = _ci;
|
|
1103
|
-
break;
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
if (_cTargetIdx === -1) throw new Error("continue outside loop");
|
|
1107
|
-
}
|
|
1108
|
-
for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
|
|
1109
|
-
if ((this._loopStack[_ci].type as any) === "try") {
|
|
1110
|
-
this.emit(bc, [this.OP.TRY_END], node);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
this.emit(
|
|
1114
|
-
bc,
|
|
1115
|
-
[
|
|
1116
|
-
this.OP.JUMP,
|
|
1117
|
-
{
|
|
1118
|
-
type: "label",
|
|
1119
|
-
label: this._loopStack[_cTargetIdx].continueLabel,
|
|
1120
|
-
},
|
|
1121
|
-
],
|
|
1122
|
-
node,
|
|
1123
|
-
);
|
|
1124
|
-
break;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
case "SwitchStatement": {
|
|
1128
|
-
const _swLabel = this._pendingLabel;
|
|
1129
|
-
this._pendingLabel = null;
|
|
1130
|
-
|
|
1131
|
-
const switchBreakLabel = this._makeLabel("sw_break");
|
|
1132
|
-
|
|
1133
|
-
this._loopStack.push({
|
|
1134
|
-
type: "switch",
|
|
1135
|
-
label: _swLabel,
|
|
1136
|
-
breakLabel: switchBreakLabel,
|
|
1137
|
-
continueLabel: switchBreakLabel,
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
|
-
// Compile discriminant into a register that lives for the whole switch.
|
|
1141
|
-
const discReg = this._compileExpr(node.discriminant, scope, bc);
|
|
1142
|
-
|
|
1143
|
-
const cases = node.cases;
|
|
1144
|
-
const defaultIdx = cases.findIndex((c) => c.test === null);
|
|
1145
|
-
const caseLabels = cases.map((_, i) => this._makeLabel(`sw_case_${i}`));
|
|
1146
|
-
|
|
1147
|
-
// Dispatch: for each non-default case, test and jump.
|
|
1148
|
-
for (let i = 0; i < cases.length; i++) {
|
|
1149
|
-
const cas = cases[i];
|
|
1150
|
-
if (cas.test === null) continue;
|
|
1151
|
-
|
|
1152
|
-
const nextCheckLabel = this._makeLabel("sw_next");
|
|
1153
|
-
const caseValReg = this._compileExpr(cas.test, scope, bc);
|
|
1154
|
-
const cmpReg = ctx.allocReg();
|
|
1155
|
-
this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
|
|
1156
|
-
this.emit(
|
|
1157
|
-
bc,
|
|
1158
|
-
[
|
|
1159
|
-
this.OP.JUMP_IF_FALSE,
|
|
1160
|
-
cmpReg,
|
|
1161
|
-
{ type: "label", label: nextCheckLabel },
|
|
1162
|
-
],
|
|
1163
|
-
node,
|
|
1164
|
-
);
|
|
1165
|
-
|
|
1166
|
-
this.emit(
|
|
1167
|
-
bc,
|
|
1168
|
-
[this.OP.JUMP, { type: "label", label: caseLabels[i] }],
|
|
1169
|
-
node,
|
|
1170
|
-
);
|
|
1171
|
-
this.emit(
|
|
1172
|
-
bc,
|
|
1173
|
-
[null, { type: "defineLabel", label: nextCheckLabel }],
|
|
1174
|
-
node,
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
this.emit(
|
|
1179
|
-
bc,
|
|
1180
|
-
[
|
|
1181
|
-
this.OP.JUMP,
|
|
1182
|
-
{
|
|
1183
|
-
type: "label",
|
|
1184
|
-
label:
|
|
1185
|
-
defaultIdx !== -1 ? caseLabels[defaultIdx] : switchBreakLabel,
|
|
1186
|
-
},
|
|
1187
|
-
],
|
|
1188
|
-
node,
|
|
1189
|
-
);
|
|
1190
|
-
|
|
1191
|
-
for (let i = 0; i < cases.length; i++) {
|
|
1192
|
-
this.emit(
|
|
1193
|
-
bc,
|
|
1194
|
-
[null, { type: "defineLabel", label: caseLabels[i] }],
|
|
1195
|
-
node,
|
|
1196
|
-
);
|
|
1197
|
-
for (const stmt of cases[i].consequent) {
|
|
1198
|
-
this._compileStatement(stmt, scope, bc);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// Break lands here – discriminant register is simply abandoned.
|
|
1203
|
-
this.emit(
|
|
1204
|
-
bc,
|
|
1205
|
-
[null, { type: "defineLabel", label: switchBreakLabel }],
|
|
1206
|
-
node,
|
|
1207
|
-
);
|
|
1208
|
-
|
|
1209
|
-
this._loopStack.pop();
|
|
1210
|
-
break;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
case "LabeledStatement": {
|
|
1214
|
-
const _lName = node.label.name;
|
|
1215
|
-
const _lBody = node.body;
|
|
1216
|
-
const _lIsLoop =
|
|
1217
|
-
_lBody.type === "ForStatement" ||
|
|
1218
|
-
_lBody.type === "WhileStatement" ||
|
|
1219
|
-
_lBody.type === "DoWhileStatement" ||
|
|
1220
|
-
_lBody.type === "ForInStatement";
|
|
1221
|
-
const _lIsSwitch = _lBody.type === "SwitchStatement";
|
|
1222
|
-
|
|
1223
|
-
if (_lIsLoop || _lIsSwitch) {
|
|
1224
|
-
this._pendingLabel = _lName;
|
|
1225
|
-
this._compileStatement(_lBody, scope, bc);
|
|
1226
|
-
this._pendingLabel = null;
|
|
1227
|
-
} else {
|
|
1228
|
-
const blockBreakLabel = this._makeLabel("block_break");
|
|
1229
|
-
this._loopStack.push({
|
|
1230
|
-
type: "block",
|
|
1231
|
-
label: _lName,
|
|
1232
|
-
breakLabel: blockBreakLabel,
|
|
1233
|
-
continueLabel: blockBreakLabel,
|
|
1234
|
-
});
|
|
1235
|
-
this._compileStatement(_lBody, scope, bc);
|
|
1236
|
-
this._loopStack.pop();
|
|
1237
|
-
this.emit(
|
|
1238
|
-
bc,
|
|
1239
|
-
[null, { type: "defineLabel", label: blockBreakLabel }],
|
|
1240
|
-
node,
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
break;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
case "ForInStatement": {
|
|
1247
|
-
const _fiLabel = this._pendingLabel;
|
|
1248
|
-
this._pendingLabel = null;
|
|
1249
|
-
|
|
1250
|
-
// Iterator register was reserved by _hoistVars.
|
|
1251
|
-
const iterSlot: b.RegisterOperand = (node as any)._iterSlot;
|
|
1252
|
-
|
|
1253
|
-
// FOR_IN_SETUP dst, src
|
|
1254
|
-
const objReg = this._compileExpr(node.right, scope, bc);
|
|
1255
|
-
this.emit(bc, [this.OP.FOR_IN_SETUP, iterSlot, objReg], node);
|
|
1256
|
-
|
|
1257
|
-
const loopTopLabel = this._makeLabel("forin_top");
|
|
1258
|
-
const exitLabel = this._makeLabel("forin_exit");
|
|
1259
|
-
|
|
1260
|
-
this._loopStack.push({
|
|
1261
|
-
type: "loop",
|
|
1262
|
-
label: _fiLabel,
|
|
1263
|
-
breakLabel: exitLabel,
|
|
1264
|
-
continueLabel: loopTopLabel,
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
this.emit(
|
|
1268
|
-
bc,
|
|
1269
|
-
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
1270
|
-
node,
|
|
1271
|
-
);
|
|
1272
|
-
|
|
1273
|
-
// FOR_IN_NEXT keyDst, iter, exitTarget
|
|
1274
|
-
const keyReg = ctx.allocReg();
|
|
1275
|
-
this.emit(
|
|
1276
|
-
bc,
|
|
1277
|
-
[
|
|
1278
|
-
this.OP.FOR_IN_NEXT,
|
|
1279
|
-
keyReg,
|
|
1280
|
-
iterSlot,
|
|
1281
|
-
{ type: "label", label: exitLabel },
|
|
1282
|
-
],
|
|
1283
|
-
node,
|
|
1284
|
-
);
|
|
1285
|
-
|
|
1286
|
-
// Assign the key to the loop variable.
|
|
1287
|
-
if (node.left.type === "VariableDeclaration") {
|
|
1288
|
-
const identifier = node.left.declarations[0].id;
|
|
1289
|
-
ok(
|
|
1290
|
-
identifier.type === "Identifier",
|
|
1291
|
-
"Only simple identifiers can be declared in for-in loops",
|
|
1292
|
-
);
|
|
1293
|
-
const name = (identifier as t.Identifier).name;
|
|
1294
|
-
if (scope) {
|
|
1295
|
-
const slot = scope._locals.get(name)!;
|
|
1296
|
-
if (keyReg !== slot)
|
|
1297
|
-
this.emit(bc, [this.OP.MOVE, slot, keyReg], node);
|
|
1298
|
-
} else {
|
|
1299
|
-
this.emit(
|
|
1300
|
-
bc,
|
|
1301
|
-
[this.OP.STORE_GLOBAL, b.constantOperand(name), keyReg],
|
|
1302
|
-
node,
|
|
1303
|
-
);
|
|
1304
|
-
}
|
|
1305
|
-
} else if (node.left.type === "Identifier") {
|
|
1306
|
-
const res = this._resolve(node.left.name, this._currentCtx);
|
|
1307
|
-
if (res.kind === "local") {
|
|
1308
|
-
if (keyReg !== res.reg)
|
|
1309
|
-
this.emit(bc, [this.OP.MOVE, res.reg, keyReg], node);
|
|
1310
|
-
} else if (res.kind === "upvalue") {
|
|
1311
|
-
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
|
|
1312
|
-
} else {
|
|
1313
|
-
this.emit(
|
|
1314
|
-
bc,
|
|
1315
|
-
[this.OP.STORE_GLOBAL, b.constantOperand(node.left.name), keyReg],
|
|
1316
|
-
node,
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
} else {
|
|
1320
|
-
const src = generate(node.left).code;
|
|
1321
|
-
throw new Error(
|
|
1322
|
-
`Unsupported for-in left-hand side: ${node.left.type}\n -> ${src}`,
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
const fiBody =
|
|
1327
|
-
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
1328
|
-
for (const stmt of fiBody) {
|
|
1329
|
-
this._compileStatement(stmt, scope, bc);
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
this.emit(
|
|
1333
|
-
bc,
|
|
1334
|
-
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
1335
|
-
node,
|
|
1336
|
-
);
|
|
1337
|
-
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
1338
|
-
|
|
1339
|
-
this._loopStack.pop();
|
|
1340
|
-
break;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
case "TryStatement": {
|
|
1344
|
-
if (node.finalizer) {
|
|
1345
|
-
throw new Error("try..finally is not supported");
|
|
1346
|
-
}
|
|
1347
|
-
if (!node.handler) {
|
|
1348
|
-
throw new Error("try without catch is not supported");
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
const catchLabel = this._makeLabel("catch");
|
|
1352
|
-
const afterCatchLabel = this._makeLabel("after_catch");
|
|
1353
|
-
|
|
1354
|
-
// Determine where the caught exception is written.
|
|
1355
|
-
const exceptionReg =
|
|
1356
|
-
node.handler.param?.type === "Identifier"
|
|
1357
|
-
? (scope?._locals.get((node.handler.param as t.Identifier).name) ??
|
|
1358
|
-
ctx.allocReg()) // shouldn't normally reach here
|
|
1359
|
-
: (node as any)._exceptionSlot;
|
|
1360
|
-
|
|
1361
|
-
this.emit(
|
|
1362
|
-
bc,
|
|
1363
|
-
[
|
|
1364
|
-
this.OP.TRY_SETUP,
|
|
1365
|
-
{ type: "label", label: catchLabel },
|
|
1366
|
-
exceptionReg,
|
|
1367
|
-
],
|
|
1368
|
-
node,
|
|
1369
|
-
);
|
|
1370
|
-
|
|
1371
|
-
this._loopStack.push({
|
|
1372
|
-
type: "try" as any,
|
|
1373
|
-
label: null,
|
|
1374
|
-
breakLabel: "",
|
|
1375
|
-
continueLabel: "",
|
|
1376
|
-
});
|
|
1377
|
-
|
|
1378
|
-
for (const stmt of node.block.body) {
|
|
1379
|
-
this._compileStatement(stmt, scope, bc);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
this._loopStack.pop();
|
|
1383
|
-
|
|
1384
|
-
this.emit(bc, [this.OP.TRY_END], node);
|
|
1385
|
-
this.emit(
|
|
1386
|
-
bc,
|
|
1387
|
-
[this.OP.JUMP, { type: "label", label: afterCatchLabel }],
|
|
1388
|
-
node,
|
|
1389
|
-
);
|
|
1390
|
-
|
|
1391
|
-
// Catch block: exceptionReg already holds the caught value.
|
|
1392
|
-
this.emit(bc, [null, { type: "defineLabel", label: catchLabel }], node);
|
|
1393
|
-
|
|
1394
|
-
// If no param binding, just ignore the exception (it's in the dummy slot).
|
|
1395
|
-
for (const stmt of node.handler!.body.body) {
|
|
1396
|
-
this._compileStatement(stmt, scope, bc);
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
this.emit(
|
|
1400
|
-
bc,
|
|
1401
|
-
[null, { type: "defineLabel", label: afterCatchLabel }],
|
|
1402
|
-
node,
|
|
1403
|
-
);
|
|
1404
|
-
break;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
default: {
|
|
1408
|
-
const src = generate(node).code;
|
|
1409
|
-
throw new Error(`Unsupported statement: ${node.type}\n -> ${src}`);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// ── Expressions ───────────────────────────────────────────────────────────
|
|
1415
|
-
// Returns the virtual RegisterOperand that holds the result.
|
|
1416
|
-
// For local variables: returns their RegisterOperand directly (no instruction emitted).
|
|
1417
|
-
// For all others: allocates a fresh virtual register, emits the instruction(s),
|
|
1418
|
-
// and returns the allocated register.
|
|
1419
|
-
_compileExpr(
|
|
1420
|
-
node: t.Expression | t.Node,
|
|
1421
|
-
scope: Scope | null,
|
|
1422
|
-
bc: b.Bytecode,
|
|
1423
|
-
): b.RegisterOperand {
|
|
1424
|
-
const ctx = this._currentCtx!;
|
|
1425
|
-
|
|
1426
|
-
// Intrinsic for emitting raw bytecode, useful for emitting register address
|
|
1427
|
-
if (
|
|
1428
|
-
node.type === "CallExpression" &&
|
|
1429
|
-
node.callee.type === "Identifier" &&
|
|
1430
|
-
node.callee.name === "_VM_"
|
|
1431
|
-
) {
|
|
1432
|
-
const argJSONStrng = (node.arguments[0] as t.StringLiteral).value;
|
|
1433
|
-
console.log("Emitting raw bytecode from _VM_ call:", argJSONStrng);
|
|
1434
|
-
const arg = JSON.parse(argJSONStrng);
|
|
1435
|
-
console.log("Parsed bytecode:", arg);
|
|
1436
|
-
|
|
1437
|
-
const dst = ctx.allocReg();
|
|
1438
|
-
|
|
1439
|
-
let operand = arg[0];
|
|
1440
|
-
|
|
1441
|
-
this.emit(bc, [this.OP.MOVE, dst, operand], node); // emit a breakpoint for easy inspection
|
|
1442
|
-
|
|
1443
|
-
return dst;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
switch ((node as any).type) {
|
|
1447
|
-
case "NumericLiteral":
|
|
1448
|
-
case "StringLiteral":
|
|
1449
|
-
case "BooleanLiteral": {
|
|
1450
|
-
const dst = ctx.allocReg();
|
|
1451
|
-
this.emit(
|
|
1452
|
-
bc,
|
|
1453
|
-
[this.OP.LOAD_CONST, dst, b.constantOperand((node as any).value)],
|
|
1454
|
-
node,
|
|
1455
|
-
);
|
|
1456
|
-
return dst;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
case "NullLiteral": {
|
|
1460
|
-
const dst = ctx.allocReg();
|
|
1461
|
-
this.emit(bc, [this.OP.LOAD_CONST, dst, b.constantOperand(null)], node);
|
|
1462
|
-
return dst;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
case "Identifier": {
|
|
1466
|
-
const res = this._resolve(
|
|
1467
|
-
(node as t.Identifier).name,
|
|
1468
|
-
this._currentCtx,
|
|
1469
|
-
);
|
|
1470
|
-
if (res.kind === "local") return res.reg; // register IS the local
|
|
1471
|
-
if (res.kind === "upvalue") {
|
|
1472
|
-
const dst = ctx.allocReg();
|
|
1473
|
-
this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
|
|
1474
|
-
return dst;
|
|
1475
|
-
}
|
|
1476
|
-
// global
|
|
1477
|
-
const dst = ctx.allocReg();
|
|
1478
|
-
this.emit(
|
|
1479
|
-
bc,
|
|
1480
|
-
[
|
|
1481
|
-
this.OP.LOAD_GLOBAL,
|
|
1482
|
-
dst,
|
|
1483
|
-
b.constantOperand((node as t.Identifier).name),
|
|
1484
|
-
],
|
|
1485
|
-
node,
|
|
1486
|
-
);
|
|
1487
|
-
return dst;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
case "ThisExpression": {
|
|
1491
|
-
const dst = ctx.allocReg();
|
|
1492
|
-
this.emit(bc, [this.OP.LOAD_THIS, dst], node);
|
|
1493
|
-
return dst;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
case "NewExpression": {
|
|
1497
|
-
const calleeReg = this._compileExpr(
|
|
1498
|
-
(node as t.NewExpression).callee,
|
|
1499
|
-
scope,
|
|
1500
|
-
bc,
|
|
1501
|
-
);
|
|
1502
|
-
const argRegs = (node as t.NewExpression).arguments.map((a) =>
|
|
1503
|
-
this._compileExpr(a as t.Expression, scope, bc),
|
|
1504
|
-
);
|
|
1505
|
-
const dst = ctx.allocReg();
|
|
1506
|
-
this.emit(
|
|
1507
|
-
bc,
|
|
1508
|
-
[
|
|
1509
|
-
this.OP.NEW,
|
|
1510
|
-
dst,
|
|
1511
|
-
calleeReg,
|
|
1512
|
-
(node as t.NewExpression).arguments.length,
|
|
1513
|
-
...argRegs,
|
|
1514
|
-
],
|
|
1515
|
-
node,
|
|
1516
|
-
);
|
|
1517
|
-
return dst;
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
case "SequenceExpression": {
|
|
1521
|
-
const exprs = (node as t.SequenceExpression).expressions;
|
|
1522
|
-
for (let i = 0; i < exprs.length - 1; i++) {
|
|
1523
|
-
this._compileExpr(exprs[i], scope, bc); // result discarded; virtual reg is unused
|
|
1524
|
-
}
|
|
1525
|
-
return this._compileExpr(exprs[exprs.length - 1], scope, bc);
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
case "ConditionalExpression": {
|
|
1529
|
-
const n = node as t.ConditionalExpression;
|
|
1530
|
-
const elseLabel = this._makeLabel("ternary_else");
|
|
1531
|
-
const endLabel = this._makeLabel("ternary_end");
|
|
1532
|
-
|
|
1533
|
-
const testReg = this._compileExpr(n.test, scope, bc);
|
|
1534
|
-
this.emit(
|
|
1535
|
-
bc,
|
|
1536
|
-
[this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: elseLabel }],
|
|
1537
|
-
node,
|
|
1538
|
-
);
|
|
1539
|
-
|
|
1540
|
-
// reg_result is a stable virtual register both branches write into.
|
|
1541
|
-
const reg_result = ctx.allocReg();
|
|
1542
|
-
|
|
1543
|
-
// Consequent branch.
|
|
1544
|
-
const consReg = this._compileExpr(n.consequent, scope, bc);
|
|
1545
|
-
if (consReg !== reg_result)
|
|
1546
|
-
this.emit(bc, [this.OP.MOVE, reg_result, consReg], node);
|
|
1547
|
-
this.emit(bc, [this.OP.JUMP, { type: "label", label: endLabel }], node);
|
|
1548
|
-
|
|
1549
|
-
// Alternate branch — each allocReg() gets a unique virtual ID so no
|
|
1550
|
-
// slot collision is possible; no need to "re-occupy" reg_result.
|
|
1551
|
-
this.emit(bc, [null, { type: "defineLabel", label: elseLabel }], node);
|
|
1552
|
-
const altReg = this._compileExpr(n.alternate, scope, bc);
|
|
1553
|
-
if (altReg !== reg_result)
|
|
1554
|
-
this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
|
|
1555
|
-
|
|
1556
|
-
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1557
|
-
return reg_result;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
case "LogicalExpression": {
|
|
1561
|
-
const n = node as t.LogicalExpression;
|
|
1562
|
-
const endLabel = this._makeLabel("logical_end");
|
|
1563
|
-
const isOr = n.operator === "||";
|
|
1564
|
-
if (!isOr && n.operator !== "&&")
|
|
1565
|
-
throw new Error(`Unsupported logical operator: ${n.operator}`);
|
|
1566
|
-
|
|
1567
|
-
const lhsReg = this._compileExpr(n.left, scope, bc);
|
|
1568
|
-
const reg_result = ctx.allocReg();
|
|
1569
|
-
if (lhsReg !== reg_result)
|
|
1570
|
-
this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
|
|
1571
|
-
|
|
1572
|
-
// For ||: if truthy keep LHS, jump past RHS.
|
|
1573
|
-
// For &&: if falsy keep LHS, jump past RHS.
|
|
1574
|
-
this.emit(
|
|
1575
|
-
bc,
|
|
1576
|
-
[
|
|
1577
|
-
isOr ? this.OP.JUMP_IF_TRUE : this.OP.JUMP_IF_FALSE,
|
|
1578
|
-
reg_result,
|
|
1579
|
-
{ type: "label", label: endLabel },
|
|
1580
|
-
],
|
|
1581
|
-
node,
|
|
1582
|
-
);
|
|
1583
|
-
|
|
1584
|
-
// Compile RHS into reg_result.
|
|
1585
|
-
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1586
|
-
if (rhsReg !== reg_result)
|
|
1587
|
-
this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
|
|
1588
|
-
|
|
1589
|
-
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1590
|
-
return reg_result;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
case "TemplateLiteral": {
|
|
1594
|
-
const n = node as t.TemplateLiteral;
|
|
1595
|
-
// Fold: quasi[0] + expr[0] + quasi[1] + ... + quasi[last]
|
|
1596
|
-
let acc = ctx.allocReg();
|
|
1597
|
-
this.emit(
|
|
1598
|
-
bc,
|
|
1599
|
-
[
|
|
1600
|
-
this.OP.LOAD_CONST,
|
|
1601
|
-
acc,
|
|
1602
|
-
b.constantOperand(n.quasis[0].value.cooked ?? ""),
|
|
1603
|
-
],
|
|
1604
|
-
node,
|
|
1605
|
-
);
|
|
1606
|
-
for (let i = 0; i < n.expressions.length; i++) {
|
|
1607
|
-
const exprReg = this._compileExpr(
|
|
1608
|
-
n.expressions[i] as t.Expression,
|
|
1609
|
-
scope,
|
|
1610
|
-
bc,
|
|
1611
|
-
);
|
|
1612
|
-
const t1 = ctx.allocReg();
|
|
1613
|
-
this.emit(bc, [this.OP.ADD, t1, acc, exprReg], node);
|
|
1614
|
-
acc = t1;
|
|
1615
|
-
const quasiReg = ctx.allocReg();
|
|
1616
|
-
this.emit(
|
|
1617
|
-
bc,
|
|
1618
|
-
[
|
|
1619
|
-
this.OP.LOAD_CONST,
|
|
1620
|
-
quasiReg,
|
|
1621
|
-
b.constantOperand(n.quasis[i + 1].value.cooked ?? ""),
|
|
1622
|
-
],
|
|
1623
|
-
node,
|
|
1624
|
-
);
|
|
1625
|
-
const t2 = ctx.allocReg();
|
|
1626
|
-
this.emit(bc, [this.OP.ADD, t2, acc, quasiReg], node);
|
|
1627
|
-
acc = t2;
|
|
1628
|
-
}
|
|
1629
|
-
return acc;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
case "BinaryExpression": {
|
|
1633
|
-
const n = node as t.BinaryExpression;
|
|
1634
|
-
const lhsReg = this._compileExpr(n.left as t.Expression, scope, bc);
|
|
1635
|
-
const rhsReg = this._compileExpr(n.right as t.Expression, scope, bc);
|
|
1636
|
-
const dst = ctx.allocReg();
|
|
1637
|
-
|
|
1638
|
-
const op = (
|
|
1639
|
-
{
|
|
1640
|
-
"+": this.OP.ADD,
|
|
1641
|
-
"-": this.OP.SUB,
|
|
1642
|
-
"*": this.OP.MUL,
|
|
1643
|
-
"/": this.OP.DIV,
|
|
1644
|
-
"%": this.OP.MOD,
|
|
1645
|
-
"&": this.OP.BAND,
|
|
1646
|
-
"|": this.OP.BOR,
|
|
1647
|
-
"^": this.OP.BXOR,
|
|
1648
|
-
"<<": this.OP.SHL,
|
|
1649
|
-
">>": this.OP.SHR,
|
|
1650
|
-
">>>": this.OP.USHR,
|
|
1651
|
-
"<": this.OP.LT,
|
|
1652
|
-
">": this.OP.GT,
|
|
1653
|
-
"===": this.OP.EQ,
|
|
1654
|
-
"==": this.OP.LOOSE_EQ,
|
|
1655
|
-
"<=": this.OP.LTE,
|
|
1656
|
-
">=": this.OP.GTE,
|
|
1657
|
-
"!==": this.OP.NEQ,
|
|
1658
|
-
"!=": this.OP.LOOSE_NEQ,
|
|
1659
|
-
in: this.OP.IN,
|
|
1660
|
-
instanceof: this.OP.INSTANCEOF,
|
|
1661
|
-
} as Record<string, number | undefined>
|
|
1662
|
-
)[n.operator];
|
|
1663
|
-
|
|
1664
|
-
if (op === undefined)
|
|
1665
|
-
throw new Error(`Unsupported operator: ${n.operator}`);
|
|
1666
|
-
|
|
1667
|
-
this.emit(bc, [op, dst, lhsReg, rhsReg], node);
|
|
1668
|
-
return dst;
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
case "UpdateExpression": {
|
|
1672
|
-
const n = node as t.UpdateExpression;
|
|
1673
|
-
const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
|
|
1674
|
-
|
|
1675
|
-
// Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
|
|
1676
|
-
const applyBump = (
|
|
1677
|
-
curReg: b.RegisterOperand,
|
|
1678
|
-
): [b.RegisterOperand, b.RegisterOperand] => {
|
|
1679
|
-
const postfixReg = n.prefix
|
|
1680
|
-
? curReg // prefix: postfix copy unused; caller returns newReg instead
|
|
1681
|
-
: (() => {
|
|
1682
|
-
const r = ctx.allocReg();
|
|
1683
|
-
this.emit(bc, [this.OP.MOVE, r, curReg], node as t.Node);
|
|
1684
|
-
return r;
|
|
1685
|
-
})();
|
|
1686
|
-
const oneReg = ctx.allocReg();
|
|
1687
|
-
this.emit(
|
|
1688
|
-
bc,
|
|
1689
|
-
[this.OP.LOAD_CONST, oneReg, b.constantOperand(1)],
|
|
1690
|
-
node as t.Node,
|
|
1691
|
-
);
|
|
1692
|
-
const newReg = ctx.allocReg();
|
|
1693
|
-
this.emit(bc, [bumpOp, newReg, curReg, oneReg], node as t.Node);
|
|
1694
|
-
return [postfixReg, newReg];
|
|
1695
|
-
};
|
|
1696
|
-
|
|
1697
|
-
if (n.argument.type === "MemberExpression") {
|
|
1698
|
-
const mem = n.argument as t.MemberExpression;
|
|
1699
|
-
const objReg = this._compileExpr(mem.object, scope, bc);
|
|
1700
|
-
let keyReg: b.RegisterOperand;
|
|
1701
|
-
if (mem.computed) {
|
|
1702
|
-
keyReg = this._compileExpr(mem.property as t.Expression, scope, bc);
|
|
1703
|
-
} else {
|
|
1704
|
-
keyReg = ctx.allocReg();
|
|
1705
|
-
this.emit(
|
|
1706
|
-
bc,
|
|
1707
|
-
[
|
|
1708
|
-
this.OP.LOAD_CONST,
|
|
1709
|
-
keyReg,
|
|
1710
|
-
b.constantOperand((mem.property as t.Identifier).name),
|
|
1711
|
-
],
|
|
1712
|
-
node as t.Node,
|
|
1713
|
-
);
|
|
1714
|
-
}
|
|
1715
|
-
const curReg = ctx.allocReg();
|
|
1716
|
-
this.emit(
|
|
1717
|
-
bc,
|
|
1718
|
-
[this.OP.GET_PROP, curReg, objReg, keyReg],
|
|
1719
|
-
node as t.Node,
|
|
1720
|
-
);
|
|
1721
|
-
const [postfixReg, newReg] = applyBump(curReg);
|
|
1722
|
-
this.emit(
|
|
1723
|
-
bc,
|
|
1724
|
-
[this.OP.SET_PROP, objReg, keyReg, newReg],
|
|
1725
|
-
node as t.Node,
|
|
1726
|
-
);
|
|
1727
|
-
return n.prefix ? newReg : postfixReg;
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
ok(
|
|
1731
|
-
n.argument.type === "Identifier",
|
|
1732
|
-
"UpdateExpression requires identifier or member expression",
|
|
1733
|
-
);
|
|
1734
|
-
const name = (n.argument as t.Identifier).name;
|
|
1735
|
-
const res = this._resolve(name, this._currentCtx);
|
|
1736
|
-
|
|
1737
|
-
let curReg: b.RegisterOperand;
|
|
1738
|
-
if (res.kind === "local") {
|
|
1739
|
-
curReg = res.reg;
|
|
1740
|
-
} else if (res.kind === "upvalue") {
|
|
1741
|
-
curReg = ctx.allocReg();
|
|
1742
|
-
this.emit(
|
|
1743
|
-
bc,
|
|
1744
|
-
[this.OP.LOAD_UPVALUE, curReg, res.index],
|
|
1745
|
-
node as t.Node,
|
|
1746
|
-
);
|
|
1747
|
-
} else {
|
|
1748
|
-
curReg = ctx.allocReg();
|
|
1749
|
-
this.emit(
|
|
1750
|
-
bc,
|
|
1751
|
-
[this.OP.LOAD_GLOBAL, curReg, b.constantOperand(name)],
|
|
1752
|
-
node as t.Node,
|
|
1753
|
-
);
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
const [postfixReg, newReg] = applyBump(curReg);
|
|
1757
|
-
|
|
1758
|
-
if (res.kind === "local") {
|
|
1759
|
-
this.emit(bc, [this.OP.MOVE, res.reg, newReg], node as t.Node);
|
|
1760
|
-
} else if (res.kind === "upvalue") {
|
|
1761
|
-
this.emit(
|
|
1762
|
-
bc,
|
|
1763
|
-
[this.OP.STORE_UPVALUE, res.index, newReg],
|
|
1764
|
-
node as t.Node,
|
|
1765
|
-
);
|
|
1766
|
-
} else {
|
|
1767
|
-
this.emit(
|
|
1768
|
-
bc,
|
|
1769
|
-
[this.OP.STORE_GLOBAL, b.constantOperand(name), newReg],
|
|
1770
|
-
node as t.Node,
|
|
1771
|
-
);
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
return n.prefix ? newReg : postfixReg;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
case "AssignmentExpression": {
|
|
1778
|
-
const n = node as t.AssignmentExpression;
|
|
1779
|
-
const compoundOp = (
|
|
1780
|
-
{
|
|
1781
|
-
"+=": this.OP.ADD,
|
|
1782
|
-
"-=": this.OP.SUB,
|
|
1783
|
-
"*=": this.OP.MUL,
|
|
1784
|
-
"/=": this.OP.DIV,
|
|
1785
|
-
"%=": this.OP.MOD,
|
|
1786
|
-
"&=": this.OP.BAND,
|
|
1787
|
-
"|=": this.OP.BOR,
|
|
1788
|
-
"^=": this.OP.BXOR,
|
|
1789
|
-
"<<=": this.OP.SHL,
|
|
1790
|
-
">>=": this.OP.SHR,
|
|
1791
|
-
">>>=": this.OP.USHR,
|
|
1792
|
-
} as Record<string, number | undefined>
|
|
1793
|
-
)[n.operator];
|
|
1794
|
-
const isCompound = compoundOp !== undefined;
|
|
1795
|
-
|
|
1796
|
-
if (n.operator !== "=" && !isCompound)
|
|
1797
|
-
throw new Error(`Unsupported assignment operator: ${n.operator}`);
|
|
1798
|
-
|
|
1799
|
-
// Member assignment: obj.x = val or arr[i] = val
|
|
1800
|
-
if (n.left.type === "MemberExpression") {
|
|
1801
|
-
const objReg = this._compileExpr(n.left.object, scope, bc);
|
|
1802
|
-
|
|
1803
|
-
let keyReg: b.RegisterOperand;
|
|
1804
|
-
if (n.left.computed) {
|
|
1805
|
-
keyReg = this._compileExpr(
|
|
1806
|
-
n.left.property as t.Expression,
|
|
1807
|
-
scope,
|
|
1808
|
-
bc,
|
|
1809
|
-
);
|
|
1810
|
-
} else {
|
|
1811
|
-
keyReg = ctx.allocReg();
|
|
1812
|
-
this.emit(
|
|
1813
|
-
bc,
|
|
1814
|
-
[
|
|
1815
|
-
this.OP.LOAD_CONST,
|
|
1816
|
-
keyReg,
|
|
1817
|
-
b.constantOperand((n.left.property as t.Identifier).name),
|
|
1818
|
-
],
|
|
1819
|
-
node,
|
|
1820
|
-
);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
let valReg: b.RegisterOperand;
|
|
1824
|
-
if (isCompound) {
|
|
1825
|
-
const curReg = ctx.allocReg();
|
|
1826
|
-
this.emit(bc, [this.OP.GET_PROP, curReg, objReg, keyReg], node);
|
|
1827
|
-
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1828
|
-
valReg = ctx.allocReg();
|
|
1829
|
-
this.emit(bc, [compoundOp!, valReg, curReg, rhsReg], node);
|
|
1830
|
-
} else {
|
|
1831
|
-
valReg = this._compileExpr(n.right, scope, bc);
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
this.emit(bc, [this.OP.SET_PROP, objReg, keyReg, valReg], node);
|
|
1835
|
-
return valReg;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
// Plain identifier assignment.
|
|
1839
|
-
const res = this._resolve(
|
|
1840
|
-
(n.left as t.Identifier).name,
|
|
1841
|
-
this._currentCtx,
|
|
1842
|
-
);
|
|
1843
|
-
|
|
1844
|
-
let rhsReg: b.RegisterOperand;
|
|
1845
|
-
if (isCompound) {
|
|
1846
|
-
// Load current value of the variable.
|
|
1847
|
-
let curReg: b.RegisterOperand;
|
|
1848
|
-
if (res.kind === "local") {
|
|
1849
|
-
curReg = res.reg;
|
|
1850
|
-
} else if (res.kind === "upvalue") {
|
|
1851
|
-
curReg = ctx.allocReg();
|
|
1852
|
-
this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
|
|
1853
|
-
} else {
|
|
1854
|
-
curReg = ctx.allocReg();
|
|
1855
|
-
this.emit(
|
|
1856
|
-
bc,
|
|
1857
|
-
[
|
|
1858
|
-
this.OP.LOAD_GLOBAL,
|
|
1859
|
-
curReg,
|
|
1860
|
-
b.constantOperand((n.left as t.Identifier).name),
|
|
1861
|
-
],
|
|
1862
|
-
node,
|
|
1863
|
-
);
|
|
1864
|
-
}
|
|
1865
|
-
const rhs2 = this._compileExpr(n.right, scope, bc);
|
|
1866
|
-
rhsReg = ctx.allocReg();
|
|
1867
|
-
this.emit(bc, [compoundOp!, rhsReg, curReg, rhs2], node);
|
|
1868
|
-
} else {
|
|
1869
|
-
rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
// Store result and return it.
|
|
1873
|
-
if (res.kind === "local") {
|
|
1874
|
-
if (rhsReg !== res.reg)
|
|
1875
|
-
this.emit(bc, [this.OP.MOVE, res.reg, rhsReg], node);
|
|
1876
|
-
return res.reg;
|
|
1877
|
-
} else if (res.kind === "upvalue") {
|
|
1878
|
-
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
|
|
1879
|
-
return rhsReg;
|
|
1880
|
-
} else {
|
|
1881
|
-
const nameIdx = b.constantOperand((n.left as t.Identifier).name);
|
|
1882
|
-
this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx, rhsReg], node);
|
|
1883
|
-
return rhsReg;
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
case "CallExpression": {
|
|
1888
|
-
const n = node as t.CallExpression;
|
|
1889
|
-
|
|
1890
|
-
if (n.callee.type === "MemberExpression") {
|
|
1891
|
-
// Method call: receiver.method(args)
|
|
1892
|
-
const receiverReg = this._compileExpr(n.callee.object, scope, bc);
|
|
1893
|
-
|
|
1894
|
-
let methodKeyReg: b.RegisterOperand;
|
|
1895
|
-
if (n.callee.computed) {
|
|
1896
|
-
methodKeyReg = this._compileExpr(
|
|
1897
|
-
n.callee.property as t.Expression,
|
|
1898
|
-
scope,
|
|
1899
|
-
bc,
|
|
1900
|
-
);
|
|
1901
|
-
} else {
|
|
1902
|
-
methodKeyReg = ctx.allocReg();
|
|
1903
|
-
this.emit(
|
|
1904
|
-
bc,
|
|
1905
|
-
[
|
|
1906
|
-
this.OP.LOAD_CONST,
|
|
1907
|
-
methodKeyReg,
|
|
1908
|
-
b.constantOperand((n.callee.property as t.Identifier).name),
|
|
1909
|
-
],
|
|
1910
|
-
node,
|
|
1911
|
-
);
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
const calleeReg = ctx.allocReg();
|
|
1915
|
-
this.emit(
|
|
1916
|
-
bc,
|
|
1917
|
-
[this.OP.GET_PROP, calleeReg, receiverReg, methodKeyReg],
|
|
1918
|
-
node,
|
|
1919
|
-
);
|
|
1920
|
-
|
|
1921
|
-
const argRegs = n.arguments.map((a) =>
|
|
1922
|
-
this._compileExpr(a as t.Expression, scope, bc),
|
|
1923
|
-
);
|
|
1924
|
-
const dst = ctx.allocReg();
|
|
1925
|
-
this.emit(
|
|
1926
|
-
bc,
|
|
1927
|
-
[
|
|
1928
|
-
this.OP.CALL_METHOD,
|
|
1929
|
-
dst,
|
|
1930
|
-
receiverReg,
|
|
1931
|
-
calleeReg,
|
|
1932
|
-
n.arguments.length,
|
|
1933
|
-
...argRegs,
|
|
1934
|
-
],
|
|
1935
|
-
node,
|
|
1936
|
-
);
|
|
1937
|
-
return dst;
|
|
1938
|
-
} else {
|
|
1939
|
-
// Plain call: fn(args)
|
|
1940
|
-
const calleeReg = this._compileExpr(
|
|
1941
|
-
n.callee as t.Expression,
|
|
1942
|
-
scope,
|
|
1943
|
-
bc,
|
|
1944
|
-
);
|
|
1945
|
-
const argRegs = n.arguments.map((a) =>
|
|
1946
|
-
this._compileExpr(a as t.Expression, scope, bc),
|
|
1947
|
-
);
|
|
1948
|
-
const dst = ctx.allocReg();
|
|
1949
|
-
this.emit(
|
|
1950
|
-
bc,
|
|
1951
|
-
[this.OP.CALL, dst, calleeReg, n.arguments.length, ...argRegs],
|
|
1952
|
-
node,
|
|
1953
|
-
);
|
|
1954
|
-
return dst;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
case "UnaryExpression": {
|
|
1959
|
-
const n = node as t.UnaryExpression;
|
|
1960
|
-
|
|
1961
|
-
// typeof on a potentially-undeclared global -- safe guard.
|
|
1962
|
-
if (n.operator === "typeof" && n.argument.type === "Identifier") {
|
|
1963
|
-
const res = this._resolve(n.argument.name, this._currentCtx);
|
|
1964
|
-
if (res.kind === "global") {
|
|
1965
|
-
const dst = ctx.allocReg();
|
|
1966
|
-
this.emit(
|
|
1967
|
-
bc,
|
|
1968
|
-
[this.OP.TYPEOF_SAFE, dst, b.constantOperand(n.argument.name)],
|
|
1969
|
-
node,
|
|
1970
|
-
);
|
|
1971
|
-
return dst;
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
// delete expression.
|
|
1976
|
-
if (n.operator === "delete") {
|
|
1977
|
-
const arg = n.argument;
|
|
1978
|
-
if (arg.type === "MemberExpression") {
|
|
1979
|
-
const objReg = this._compileExpr(arg.object, scope, bc);
|
|
1980
|
-
let keyReg: b.RegisterOperand;
|
|
1981
|
-
if (arg.computed) {
|
|
1982
|
-
keyReg = this._compileExpr(
|
|
1983
|
-
arg.property as t.Expression,
|
|
1984
|
-
scope,
|
|
1985
|
-
bc,
|
|
1986
|
-
);
|
|
1987
|
-
} else {
|
|
1988
|
-
keyReg = ctx.allocReg();
|
|
1989
|
-
this.emit(
|
|
1990
|
-
bc,
|
|
1991
|
-
[
|
|
1992
|
-
this.OP.LOAD_CONST,
|
|
1993
|
-
keyReg,
|
|
1994
|
-
b.constantOperand((arg.property as t.Identifier).name),
|
|
1995
|
-
],
|
|
1996
|
-
node,
|
|
1997
|
-
);
|
|
1998
|
-
}
|
|
1999
|
-
const dst = ctx.allocReg();
|
|
2000
|
-
this.emit(bc, [this.OP.DELETE_PROP, dst, objReg, keyReg], node);
|
|
2001
|
-
return dst;
|
|
2002
|
-
} else {
|
|
2003
|
-
// delete x or delete 0 -- always true in sloppy mode.
|
|
2004
|
-
const dst = ctx.allocReg();
|
|
2005
|
-
this.emit(
|
|
2006
|
-
bc,
|
|
2007
|
-
[this.OP.LOAD_CONST, dst, b.constantOperand(true)],
|
|
2008
|
-
node,
|
|
2009
|
-
);
|
|
2010
|
-
return dst;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
// All other unary operators.
|
|
2015
|
-
const srcReg = this._compileExpr(n.argument, scope, bc);
|
|
2016
|
-
const dst = ctx.allocReg();
|
|
2017
|
-
const unaryOp = (
|
|
2018
|
-
{
|
|
2019
|
-
"-": this.OP.UNARY_NEG,
|
|
2020
|
-
"+": this.OP.UNARY_POS,
|
|
2021
|
-
"!": this.OP.UNARY_NOT,
|
|
2022
|
-
"~": this.OP.UNARY_BITNOT,
|
|
2023
|
-
typeof: this.OP.TYPEOF,
|
|
2024
|
-
void: this.OP.VOID,
|
|
2025
|
-
} as Record<string, number | undefined>
|
|
2026
|
-
)[n.operator];
|
|
2027
|
-
|
|
2028
|
-
if (unaryOp === undefined)
|
|
2029
|
-
throw new Error(`Unsupported unary operator: ${n.operator}`);
|
|
2030
|
-
|
|
2031
|
-
this.emit(bc, [unaryOp, dst, srcReg], node);
|
|
2032
|
-
return dst;
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
case "RegExpLiteral": {
|
|
2036
|
-
const n = node as t.RegExpLiteral;
|
|
2037
|
-
// new RegExp(pattern, flags)
|
|
2038
|
-
const regExpReg = ctx.allocReg();
|
|
2039
|
-
this.emit(
|
|
2040
|
-
bc,
|
|
2041
|
-
[this.OP.LOAD_GLOBAL, regExpReg, b.constantOperand("RegExp")],
|
|
2042
|
-
node,
|
|
2043
|
-
);
|
|
2044
|
-
const patternReg = ctx.allocReg();
|
|
2045
|
-
this.emit(
|
|
2046
|
-
bc,
|
|
2047
|
-
[this.OP.LOAD_CONST, patternReg, b.constantOperand(n.pattern)],
|
|
2048
|
-
node,
|
|
2049
|
-
);
|
|
2050
|
-
const flagsReg = ctx.allocReg();
|
|
2051
|
-
this.emit(
|
|
2052
|
-
bc,
|
|
2053
|
-
[this.OP.LOAD_CONST, flagsReg, b.constantOperand(n.flags)],
|
|
2054
|
-
node,
|
|
2055
|
-
);
|
|
2056
|
-
const dst = ctx.allocReg();
|
|
2057
|
-
this.emit(
|
|
2058
|
-
bc,
|
|
2059
|
-
[this.OP.NEW, dst, regExpReg, 2, patternReg, flagsReg],
|
|
2060
|
-
node,
|
|
2061
|
-
);
|
|
2062
|
-
return dst;
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
case "FunctionExpression": {
|
|
2066
|
-
const desc = this._compileFunctionDecl(node as t.FunctionExpression);
|
|
2067
|
-
return this._emitMakeClosure(desc, node, bc);
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
case "MemberExpression": {
|
|
2071
|
-
const n = node as t.MemberExpression;
|
|
2072
|
-
const objReg = this._compileExpr(n.object, scope, bc);
|
|
2073
|
-
let keyReg: b.RegisterOperand;
|
|
2074
|
-
if (n.computed) {
|
|
2075
|
-
keyReg = this._compileExpr(n.property as t.Expression, scope, bc);
|
|
2076
|
-
} else {
|
|
2077
|
-
keyReg = ctx.allocReg();
|
|
2078
|
-
this.emit(
|
|
2079
|
-
bc,
|
|
2080
|
-
[
|
|
2081
|
-
this.OP.LOAD_CONST,
|
|
2082
|
-
keyReg,
|
|
2083
|
-
b.constantOperand((n.property as t.Identifier).name),
|
|
2084
|
-
],
|
|
2085
|
-
node,
|
|
2086
|
-
);
|
|
2087
|
-
}
|
|
2088
|
-
const dst = ctx.allocReg();
|
|
2089
|
-
this.emit(bc, [this.OP.GET_PROP, dst, objReg, keyReg], node);
|
|
2090
|
-
return dst;
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
case "ArrayExpression": {
|
|
2094
|
-
const n = node as t.ArrayExpression;
|
|
2095
|
-
const elemRegs = n.elements.map((el) => {
|
|
2096
|
-
if (el === null) {
|
|
2097
|
-
const r = ctx.allocReg();
|
|
2098
|
-
this.emit(
|
|
2099
|
-
bc,
|
|
2100
|
-
[this.OP.LOAD_CONST, r, b.constantOperand(undefined)],
|
|
2101
|
-
node,
|
|
2102
|
-
);
|
|
2103
|
-
return r;
|
|
2104
|
-
}
|
|
2105
|
-
return this._compileExpr(el as t.Expression, scope, bc);
|
|
2106
|
-
});
|
|
2107
|
-
const dst = ctx.allocReg();
|
|
2108
|
-
this.emit(
|
|
2109
|
-
bc,
|
|
2110
|
-
[this.OP.BUILD_ARRAY, dst, n.elements.length, ...elemRegs],
|
|
2111
|
-
node,
|
|
2112
|
-
);
|
|
2113
|
-
return dst;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
case "ObjectExpression": {
|
|
2117
|
-
const n = node as t.ObjectExpression;
|
|
2118
|
-
const regularProps: t.ObjectProperty[] = [];
|
|
2119
|
-
const accessorProps: t.ObjectMethod[] = [];
|
|
2120
|
-
|
|
2121
|
-
for (const prop of n.properties) {
|
|
2122
|
-
if (prop.type === "SpreadElement")
|
|
2123
|
-
throw new Error("Object spread not supported");
|
|
2124
|
-
if (prop.type === "ObjectMethod") {
|
|
2125
|
-
if (prop.kind === "get" || prop.kind === "set") {
|
|
2126
|
-
if (prop.computed)
|
|
2127
|
-
throw new Error(
|
|
2128
|
-
"Computed getter/setter keys are not supported",
|
|
2129
|
-
);
|
|
2130
|
-
accessorProps.push(prop);
|
|
2131
|
-
} else {
|
|
2132
|
-
throw new Error("Shorthand method syntax is not supported");
|
|
2133
|
-
}
|
|
2134
|
-
} else {
|
|
2135
|
-
regularProps.push(prop as t.ObjectProperty);
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
// Build flat [key, val, key, val, …] register list.
|
|
2140
|
-
const pairRegs: b.RegisterOperand[] = [];
|
|
2141
|
-
for (const prop of regularProps) {
|
|
2142
|
-
let keyStr: string;
|
|
2143
|
-
const key = prop.key;
|
|
2144
|
-
if (key.type === "Identifier") keyStr = key.name;
|
|
2145
|
-
else if (
|
|
2146
|
-
key.type === "StringLiteral" ||
|
|
2147
|
-
key.type === "NumericLiteral"
|
|
2148
|
-
)
|
|
2149
|
-
keyStr = String(key.value);
|
|
2150
|
-
else throw new Error(`Unsupported object key type: ${key.type}`);
|
|
2151
|
-
|
|
2152
|
-
const keyReg = ctx.allocReg();
|
|
2153
|
-
this.emit(
|
|
2154
|
-
bc,
|
|
2155
|
-
[this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)],
|
|
2156
|
-
node,
|
|
2157
|
-
);
|
|
2158
|
-
const valReg = this._compileExpr(
|
|
2159
|
-
prop.value as t.Expression,
|
|
2160
|
-
scope,
|
|
2161
|
-
bc,
|
|
2162
|
-
);
|
|
2163
|
-
pairRegs.push(keyReg, valReg);
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
const dst = ctx.allocReg();
|
|
2167
|
-
this.emit(
|
|
2168
|
-
bc,
|
|
2169
|
-
[this.OP.BUILD_OBJECT, dst, regularProps.length, ...pairRegs],
|
|
2170
|
-
node,
|
|
2171
|
-
);
|
|
2172
|
-
|
|
2173
|
-
// Define accessors on the object now sitting in `dst`.
|
|
2174
|
-
for (const prop of accessorProps) {
|
|
2175
|
-
const key = prop.key;
|
|
2176
|
-
let keyStr: string;
|
|
2177
|
-
if (key.type === "Identifier") keyStr = key.name;
|
|
2178
|
-
else if (
|
|
2179
|
-
key.type === "StringLiteral" ||
|
|
2180
|
-
key.type === "NumericLiteral"
|
|
2181
|
-
)
|
|
2182
|
-
keyStr = String(key.value);
|
|
2183
|
-
else throw new Error(`Unsupported object key type: ${key.type}`);
|
|
2184
|
-
|
|
2185
|
-
const keyReg = ctx.allocReg();
|
|
2186
|
-
this.emit(
|
|
2187
|
-
bc,
|
|
2188
|
-
[this.OP.LOAD_CONST, keyReg, b.constantOperand(keyStr)],
|
|
2189
|
-
node,
|
|
2190
|
-
);
|
|
2191
|
-
const fnReg = this._emitMakeClosure(
|
|
2192
|
-
this._compileFunctionDecl(prop as any),
|
|
2193
|
-
prop as any,
|
|
2194
|
-
bc,
|
|
2195
|
-
);
|
|
2196
|
-
this.emit(
|
|
2197
|
-
bc,
|
|
2198
|
-
[
|
|
2199
|
-
prop.kind === "get"
|
|
2200
|
-
? this.OP.DEFINE_GETTER
|
|
2201
|
-
: this.OP.DEFINE_SETTER,
|
|
2202
|
-
dst,
|
|
2203
|
-
keyReg,
|
|
2204
|
-
fnReg,
|
|
2205
|
-
],
|
|
2206
|
-
node,
|
|
2207
|
-
);
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
return dst;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
default: {
|
|
2214
|
-
throw new Error(`Unsupported expression: ${(node as any).type}`);
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
// ── Serializer ────────────────────────────────────────────────────────────────
|
|
2221
|
-
class Serializer {
|
|
2222
|
-
compiler: Compiler;
|
|
2223
|
-
|
|
2224
|
-
constructor(compiler: Compiler) {
|
|
2225
|
-
this.compiler = compiler;
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
get options() {
|
|
2229
|
-
return this.compiler.options;
|
|
2230
|
-
}
|
|
2231
|
-
get OP() {
|
|
2232
|
-
return this.compiler.OP;
|
|
2233
|
-
}
|
|
2234
|
-
get OP_NAME() {
|
|
2235
|
-
return this.compiler.OP_NAME;
|
|
2236
|
-
}
|
|
2237
|
-
get JUMP_OPS() {
|
|
2238
|
-
return this.compiler.JUMP_OPS;
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
_serializeConst(val: any) {
|
|
2242
|
-
if (val === null) return "null";
|
|
2243
|
-
if (val === undefined) return "undefined";
|
|
2244
|
-
return JSON.stringify(val);
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
// Reverse the concealment applied by resolveConstants so disassembly comments
|
|
2248
|
-
// always show the plaintext value regardless of the concealConstants option.
|
|
2249
|
-
_decryptConst(constants: any[], idx: number, key: number): any {
|
|
2250
|
-
const v = constants[idx];
|
|
2251
|
-
if (!key) return v;
|
|
2252
|
-
if (typeof v === "number") return v ^ key;
|
|
2253
|
-
if (typeof v !== "string") return v;
|
|
2254
|
-
// String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
|
|
2255
|
-
const bytes = Buffer.from(v as string, "base64");
|
|
2256
|
-
let out = "";
|
|
2257
|
-
for (let i = 0; i < bytes.length / 2; i++) {
|
|
2258
|
-
const code = bytes[i * 2] | (bytes[i * 2 + 1] << 8);
|
|
2259
|
-
out += String.fromCharCode(code ^ ((key + i) & 0xffff));
|
|
2260
|
-
}
|
|
2261
|
-
return out;
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
_generateComment(instr: b.Instruction) {
|
|
2265
|
-
const op = instr[0] as number;
|
|
2266
|
-
const operands = instr.slice(1) as number[];
|
|
2267
|
-
|
|
2268
|
-
if (op === null && (operands[0] as any)?.type === "defineLabel") {
|
|
2269
|
-
const label = (operands[0] as any).label;
|
|
2270
|
-
return `${label}:`;
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
const constants = this.compiler.constants;
|
|
2274
|
-
|
|
2275
|
-
const emittedOperands = operands.filter(
|
|
2276
|
-
(operand) => (operand as any)?.placeholder !== true,
|
|
2277
|
-
);
|
|
2278
|
-
|
|
2279
|
-
const resolvedOperands = emittedOperands.map(
|
|
2280
|
-
(o) => (o as any)?.resolvedValue ?? o,
|
|
2281
|
-
);
|
|
2282
|
-
|
|
2283
|
-
const displayOperands = operands.map((o, i) => {
|
|
2284
|
-
const resolvedValue = resolvedOperands[i];
|
|
2285
|
-
const label = (o as any)?.label;
|
|
2286
|
-
|
|
2287
|
-
let displayOperand = resolvedValue;
|
|
2288
|
-
if (label) {
|
|
2289
|
-
return label;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
return displayOperand;
|
|
2293
|
-
});
|
|
2294
|
-
|
|
2295
|
-
let name = this.OP_NAME[op];
|
|
2296
|
-
if (!name || name.includes("{")) {
|
|
2297
|
-
name = `OP_${op}`;
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
let comment = name;
|
|
2301
|
-
|
|
2302
|
-
function formatLoc(loc: t.Node["loc"]["start"]) {
|
|
2303
|
-
return loc ? `${loc.line}:${loc.column}` : "";
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
const sourceNode = instr[SOURCE_NODE_SYM];
|
|
2307
|
-
const sourceLocation = sourceNode?.loc
|
|
2308
|
-
? [formatLoc(sourceNode.loc.start), formatLoc(sourceNode.loc.end)]
|
|
2309
|
-
.filter(Boolean)
|
|
2310
|
-
.join("-")
|
|
2311
|
-
: "";
|
|
2312
|
-
|
|
2313
|
-
if (displayOperands.length > 0) {
|
|
2314
|
-
// Operand[0] is always `dst` for instruction types that produce a value.
|
|
2315
|
-
const dst = displayOperands[0];
|
|
2316
|
-
|
|
2317
|
-
switch (op) {
|
|
2318
|
-
case this.OP.LOAD_CONST: {
|
|
2319
|
-
// resolvedOperands: [dst, constIdx, concealKey]
|
|
2320
|
-
const val = this._decryptConst(
|
|
2321
|
-
constants,
|
|
2322
|
-
displayOperands[1],
|
|
2323
|
-
displayOperands[2],
|
|
2324
|
-
);
|
|
2325
|
-
comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
|
|
2326
|
-
break;
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
case this.OP.LOAD_INT: {
|
|
2330
|
-
// resolvedOperands: [dst, intValue]
|
|
2331
|
-
comment += ` reg[${dst}] = ${displayOperands[1]}`;
|
|
2332
|
-
break;
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
case this.OP.LOAD_GLOBAL:
|
|
2336
|
-
// resolvedOperands: [dst, constIdx, concealKey]
|
|
2337
|
-
comment += ` reg[${dst}] = ${this._decryptConst(constants, displayOperands[1], displayOperands[2])}`;
|
|
2338
|
-
break;
|
|
2339
|
-
case this.OP.STORE_GLOBAL:
|
|
2340
|
-
// resolvedOperands: [constIdx, concealKey, srcReg]
|
|
2341
|
-
comment += ` ${this._decryptConst(constants, displayOperands[0], displayOperands[1])} = reg[${displayOperands[2]}]`;
|
|
2342
|
-
break;
|
|
2343
|
-
case this.OP.LOAD_UPVALUE:
|
|
2344
|
-
comment += ` reg[${dst}] = upvalue[${displayOperands[1]}]`;
|
|
2345
|
-
break;
|
|
2346
|
-
case this.OP.STORE_UPVALUE:
|
|
2347
|
-
comment += ` upvalue[${displayOperands[0]}] = reg[${displayOperands[1]}]`;
|
|
2348
|
-
break;
|
|
2349
|
-
case this.OP.MOVE:
|
|
2350
|
-
comment += ` reg[${dst}] = reg[${displayOperands[1]}]`;
|
|
2351
|
-
break;
|
|
2352
|
-
case this.OP.MAKE_CLOSURE:
|
|
2353
|
-
comment += ` reg[${dst}] PC=${displayOperands[1]} (params=${displayOperands[2]} regs=${displayOperands[3]} upvalues=${displayOperands[4]})`;
|
|
2354
|
-
break;
|
|
2355
|
-
case this.OP.CALL:
|
|
2356
|
-
comment += ` reg[${dst}] = reg[${displayOperands[1]}](${displayOperands
|
|
2357
|
-
.slice(3)
|
|
2358
|
-
.map((v) => `reg[${v}]`)
|
|
2359
|
-
.join(", ")})`;
|
|
2360
|
-
break;
|
|
2361
|
-
case this.OP.CALL_METHOD:
|
|
2362
|
-
comment += ` reg[${dst}] = reg[${displayOperands[2]}](recv=reg[${displayOperands[1]}], ${displayOperands[3]} args)`;
|
|
2363
|
-
break;
|
|
2364
|
-
case this.OP.NEW:
|
|
2365
|
-
comment += ` reg[${dst}] = new reg[${displayOperands[1]}](${displayOperands[2]} args)`;
|
|
2366
|
-
break;
|
|
2367
|
-
case this.OP.RETURN:
|
|
2368
|
-
comment += ` reg[${displayOperands[0]}]`;
|
|
2369
|
-
break;
|
|
2370
|
-
case this.OP.BUILD_ARRAY:
|
|
2371
|
-
comment += ` reg[${dst}] = [${displayOperands[2]} elems]`;
|
|
2372
|
-
break;
|
|
2373
|
-
case this.OP.BUILD_OBJECT:
|
|
2374
|
-
comment += ` reg[${dst}] = {${displayOperands[1]} pairs}`;
|
|
2375
|
-
break;
|
|
2376
|
-
case this.OP.GET_PROP:
|
|
2377
|
-
comment += ` reg[${dst}] = reg[${displayOperands[1]}][reg[${displayOperands[2]}]]`;
|
|
2378
|
-
break;
|
|
2379
|
-
case this.OP.SET_PROP:
|
|
2380
|
-
comment += ` reg[${displayOperands[0]}][reg[${displayOperands[1]}]] = reg[${displayOperands[2]}]`;
|
|
2381
|
-
break;
|
|
2382
|
-
|
|
2383
|
-
case this.OP.JUMP_REG:
|
|
2384
|
-
comment += ` PC = reg[${displayOperands[0]}]`;
|
|
2385
|
-
break;
|
|
2386
|
-
|
|
2387
|
-
default:
|
|
2388
|
-
comment +=
|
|
2389
|
-
displayOperands.length === 1
|
|
2390
|
-
? ` ${displayOperands[0]}`
|
|
2391
|
-
: ` [${displayOperands.join(", ")}]`;
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
comment = comment.padEnd(50) + sourceLocation;
|
|
2396
|
-
|
|
2397
|
-
const values = [op, ...resolvedOperands];
|
|
2398
|
-
const instrText = `[${values.join(", ")}]`;
|
|
2399
|
-
const text = `${(instrText + ",").padEnd(20)} ${comment}`;
|
|
2400
|
-
|
|
2401
|
-
return text;
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
_serializeConstants(constants: any[]) {
|
|
2405
|
-
const lines = ["var CONSTANTS = ["];
|
|
2406
|
-
constants.forEach((val, idx) => {
|
|
2407
|
-
lines.push(` /* ${idx} */ ${this._serializeConst(val)},`);
|
|
2408
|
-
});
|
|
2409
|
-
lines.push("];");
|
|
2410
|
-
return lines.join("\n");
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
_serializeBytecode(
|
|
2414
|
-
bytecode: b.Bytecode,
|
|
2415
|
-
compiler: Compiler,
|
|
2416
|
-
): { bytecode: b.Bytecode } {
|
|
2417
|
-
const serialized = [];
|
|
2418
|
-
for (const instr of bytecode) {
|
|
2419
|
-
const op = instr[0];
|
|
2420
|
-
const operands = instr.slice(1);
|
|
2421
|
-
|
|
2422
|
-
if (instr[0] === null) continue; // null opcodes are not emitted
|
|
2423
|
-
|
|
2424
|
-
const resolvedValues = operands.map(
|
|
2425
|
-
(o) => (o as any)?.resolvedValue ?? o,
|
|
2426
|
-
);
|
|
2427
|
-
|
|
2428
|
-
const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
|
|
2429
|
-
if (specializedOpInfo) {
|
|
2430
|
-
const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
|
|
2431
|
-
compiler.OP_NAME[instr[0]] =
|
|
2432
|
-
`${originalName}_${resolvedValues.join("_")}`;
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
// Validate no opcode or operand exceeds u16 limit
|
|
2436
|
-
for (const o of resolvedValues) {
|
|
2437
|
-
ok(typeof o === "number", "Unresolved operand: " + JSON.stringify(o));
|
|
2438
|
-
ok(o >= 0 && o <= 0xffff, `Operand overflow (max 0xFFFF u16): ${o}`);
|
|
2439
|
-
}
|
|
2440
|
-
ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
|
|
2441
|
-
|
|
2442
|
-
serialized.push(instr);
|
|
2443
|
-
}
|
|
2444
|
-
return { bytecode: serialized };
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
_encodeBytecode(flat: number[]) {
|
|
2448
|
-
const buf = new Uint8Array(flat.length * 2);
|
|
2449
|
-
flat.forEach((w, i) => {
|
|
2450
|
-
buf[i * 2] = w & 0xff;
|
|
2451
|
-
buf[i * 2 + 1] = (w >>> 8) & 0xff;
|
|
2452
|
-
});
|
|
2453
|
-
return Buffer.from(buf).toString("base64");
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
serialize(bytecode: b.Bytecode, constants: any[], compiler: Compiler) {
|
|
2457
|
-
const mainStartPc = compiler.mainStartPc;
|
|
2458
|
-
const mainRegCount = compiler.mainRegCount;
|
|
2459
|
-
let sections = [];
|
|
2460
|
-
|
|
2461
|
-
var initBody = [];
|
|
2462
|
-
var bytecodeResult = this._serializeBytecode(bytecode, compiler);
|
|
2463
|
-
|
|
2464
|
-
const flat = bytecodeResult.bytecode.flatMap((instr) => {
|
|
2465
|
-
let filtered = instr.filter((x) => (x as any)?.placeholder !== true);
|
|
2466
|
-
let resolved = filtered.map((x) => (x as any)?.resolvedValue ?? x);
|
|
2467
|
-
return resolved as number[];
|
|
2468
|
-
});
|
|
2469
|
-
|
|
2470
|
-
if (this.options.encodeBytecode) {
|
|
2471
|
-
sections.push(`var BYTECODE = "${this._encodeBytecode(flat)}";`);
|
|
2472
|
-
} else {
|
|
2473
|
-
sections.push(`var BYTECODE = [${flat.join(",")}]`);
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
sections.push(`var MAIN_START_PC = ${mainStartPc};`);
|
|
2477
|
-
sections.push(`var MAIN_REG_COUNT = ${mainRegCount};`);
|
|
2478
|
-
sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
|
|
2479
|
-
sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
|
|
2480
|
-
|
|
2481
|
-
const object = t.objectExpression(
|
|
2482
|
-
Object.entries(this.OP).map(([name, value]) =>
|
|
2483
|
-
t.objectProperty(t.identifier(name), t.numericLiteral(value)),
|
|
2484
|
-
),
|
|
2485
|
-
);
|
|
2486
|
-
sections.push(`var OP = ${generate(object).code};`);
|
|
2487
|
-
|
|
2488
|
-
initBody.push(this._serializeConstants(constants));
|
|
2489
|
-
|
|
2490
|
-
sections = [...initBody, ...sections];
|
|
2491
|
-
sections.push(VM_RUNTIME);
|
|
2492
|
-
|
|
2493
|
-
return sections.join("\n\n");
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
export async function compileAndSerialize(
|
|
2498
|
-
sourceCode: string,
|
|
2499
|
-
options: Options,
|
|
2500
|
-
) {
|
|
2501
|
-
const compiler = new Compiler(options);
|
|
2502
|
-
let bytecode = compiler.compile(sourceCode);
|
|
2503
|
-
|
|
2504
|
-
// jumpDispatcher must run before resolveRegisters so that the new rDisp/rKey
|
|
2505
|
-
// RegisterOperand objects it injects are visible to the liveness analysis.
|
|
2506
|
-
// It must also run before resolveLabels since it emits encodedLabel IR operands.
|
|
2507
|
-
if (options.dispatcher) {
|
|
2508
|
-
const dispatcherResult = dispatcher(bytecode, compiler);
|
|
2509
|
-
bytecode = dispatcherResult.bytecode;
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
const passes = [];
|
|
2513
|
-
|
|
2514
|
-
passes.push(concealConstants);
|
|
2515
|
-
|
|
2516
|
-
if (options.specializedOpcodes) {
|
|
2517
|
-
passes.push(specializedOpcodes);
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
if (options.microOpcodes) {
|
|
2521
|
-
passes.push(microOpcodes);
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
if (options.macroOpcodes) {
|
|
2525
|
-
passes.push(macroOpcodes);
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
if (options.aliasedOpcodes) {
|
|
2529
|
-
passes.push(aliasedOpcodes);
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
for (const pass of passes) {
|
|
2533
|
-
const passResult = pass(bytecode, compiler);
|
|
2534
|
-
bytecode = passResult.bytecode;
|
|
2535
|
-
}
|
|
2536
|
-
|
|
2537
|
-
// Resolve virtual registers to concrete slot indices and set regCount per fn.
|
|
2538
|
-
// Must run BEFORE selfModifying: that pass moves body instructions to the end
|
|
2539
|
-
// of the bytecode while leaving RETURN in place, splitting a function's code
|
|
2540
|
-
// into two non-contiguous regions. Linear-scan liveness then sees incorrect
|
|
2541
|
-
// firstUse/lastUse for registers that span the gap, causing slot collisions.
|
|
2542
|
-
const regsResult = resolveRegisters(bytecode, compiler);
|
|
2543
|
-
bytecode = regsResult.bytecode;
|
|
2544
|
-
|
|
2545
|
-
// selfModifying runs after register resolution so concrete slot indices are
|
|
2546
|
-
// already in place; only label operands remain unresolved at this stage.
|
|
2547
|
-
if (options.selfModifying) {
|
|
2548
|
-
const smResult = selfModifying(bytecode, compiler);
|
|
2549
|
-
bytecode = smResult.bytecode;
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
// Resolve label references to flat bytecode indices.
|
|
2553
|
-
const labelsResult = resolveLabels(bytecode, compiler);
|
|
2554
|
-
bytecode = labelsResult.bytecode;
|
|
2555
|
-
|
|
2556
|
-
// Set mainStartPc from the first function descriptor (or 0 for top-level start).
|
|
2557
|
-
compiler.mainStartPc = compiler.mainFn.startPc;
|
|
2558
|
-
|
|
2559
|
-
// Resolve constant references to pool indices (+ conceal key operand).
|
|
2560
|
-
const constResult = resolveConstants(bytecode, compiler);
|
|
2561
|
-
bytecode = constResult.bytecode;
|
|
2562
|
-
compiler.constants = constResult.constants;
|
|
2563
|
-
|
|
2564
|
-
// Build and obfuscate the runtime.
|
|
2565
|
-
const runtimeSource = compiler.serializer.serialize(
|
|
2566
|
-
bytecode,
|
|
2567
|
-
constResult.constants,
|
|
2568
|
-
compiler,
|
|
2569
|
-
);
|
|
2570
|
-
|
|
2571
|
-
// This part was purposefully pulled out Serializer as OP_NAME's get resolved during obfuscateRuntime
|
|
2572
|
-
// So for the most useful comments, it's ran absolutely last
|
|
2573
|
-
// Tests also rely on correct comments so it's required
|
|
2574
|
-
const generateBytecodeComment = () => {
|
|
2575
|
-
var lines = [];
|
|
2576
|
-
for (const instr of bytecode) {
|
|
2577
|
-
const comment = compiler.serializer._generateComment(instr);
|
|
2578
|
-
lines.push("// " + comment);
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
return lines.join("\n");
|
|
2582
|
-
};
|
|
2583
|
-
|
|
2584
|
-
const code = await obfuscateRuntime(
|
|
2585
|
-
runtimeSource,
|
|
2586
|
-
bytecode,
|
|
2587
|
-
options,
|
|
2588
|
-
compiler,
|
|
2589
|
-
generateBytecodeComment,
|
|
2590
|
-
);
|
|
2591
|
-
|
|
2592
|
-
return { code };
|
|
2593
|
-
}
|