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