js-confuser-vm 0.0.8 → 0.1.0
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/.gitmodules +4 -0
- package/CHANGELOG.md +102 -2
- package/README.md +95 -1
- package/dist/compiler.js +225 -152
- package/dist/runtime.js +200 -143
- package/dist/template.js +142 -0
- package/dist/transforms/bytecode/dispatcher.js +362 -0
- package/dist/transforms/bytecode/macroOpcodes.js +1 -1
- package/dist/transforms/bytecode/resolveLabels.js +21 -18
- package/dist/transforms/bytecode/resolveRegisters.js +212 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +4 -2
- package/dist/types.js +41 -0
- package/dist/utils/op-utils.js +1 -0
- package/index.ts +1 -0
- package/jest.config.js +5 -0
- package/package.json +10 -2
- package/src/compiler.ts +291 -180
- package/src/options.ts +1 -0
- package/src/runtime.ts +222 -141
- package/src/template.ts +141 -0
- package/src/transforms/bytecode/aliasedOpcodes.ts +1 -1
- package/src/transforms/bytecode/dispatcher.ts +398 -0
- package/src/transforms/bytecode/macroOpcodes.ts +2 -2
- package/src/transforms/bytecode/resolveLabels.ts +31 -27
- package/src/transforms/bytecode/resolveRegisters.ts +221 -0
- package/src/transforms/bytecode/specializedOpcodes.ts +5 -9
- package/src/types.ts +64 -4
- package/src/utils/op-utils.ts +2 -0
- package/dist/transforms/utils/op-utils.js +0 -25
- package/dist/transforms/utils/random-utils.js +0 -27
- package/dist/utilts.js +0 -3
package/src/compiler.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { ok } from "assert";
|
|
|
10
10
|
import { obfuscateRuntime } from "./build-runtime.ts";
|
|
11
11
|
import { DEFAULT_OPTIONS, type Options } from "./options.ts";
|
|
12
12
|
import { resolveLabels } from "./transforms/bytecode/resolveLabels.ts";
|
|
13
|
+
import { resolveRegisters } from "./transforms/bytecode/resolveRegisters.ts";
|
|
13
14
|
import { resolveConstants } from "./transforms/bytecode/resolveContants.ts";
|
|
14
15
|
import { selfModifying } from "./transforms/bytecode/selfModifying.ts";
|
|
15
16
|
import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.ts";
|
|
@@ -19,6 +20,7 @@ import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.ts";
|
|
|
19
20
|
import { getRandomInt } from "./utils/random-utils.ts";
|
|
20
21
|
import { U16_MAX } from "./utils/op-utils.ts";
|
|
21
22
|
import { concealConstants } from "./transforms/bytecode/concealConstants.ts";
|
|
23
|
+
import { dispatcher } from "./transforms/bytecode/dispatcher.ts";
|
|
22
24
|
|
|
23
25
|
const traverse = (traverseImport.default ||
|
|
24
26
|
traverseImport) as typeof traverseImport.default;
|
|
@@ -139,78 +141,123 @@ export const OP_ORIGINAL = {
|
|
|
139
141
|
|
|
140
142
|
// ── Debug ─────────────────────────────────────────────────────────────────
|
|
141
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]
|
|
142
150
|
};
|
|
143
151
|
|
|
144
152
|
// ── Scope ─────────────────────────────────────────────────────────────────────
|
|
145
|
-
// Maps variable names to
|
|
146
|
-
// Locals are allocated at compile time; zero name lookups at runtime.
|
|
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.
|
|
147
156
|
class Scope {
|
|
148
157
|
parent: Scope | null;
|
|
149
|
-
_locals: Map<string,
|
|
150
|
-
_next: number;
|
|
158
|
+
_locals: Map<string, b.RegisterOperand>;
|
|
151
159
|
|
|
152
160
|
constructor(parent = null) {
|
|
153
161
|
this.parent = parent;
|
|
154
162
|
this._locals = new Map();
|
|
155
|
-
this._next = 0;
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
define(name: string):
|
|
165
|
+
define(name: string, ctx: FnContext): b.RegisterOperand {
|
|
159
166
|
if (!this._locals.has(name)) {
|
|
160
|
-
this._locals.set(name,
|
|
167
|
+
this._locals.set(name, ctx._newReg());
|
|
161
168
|
}
|
|
162
169
|
return this._locals.get(name)!;
|
|
163
170
|
}
|
|
164
171
|
|
|
165
|
-
resolve(
|
|
172
|
+
resolve(
|
|
173
|
+
name: string,
|
|
174
|
+
): { kind: "local"; reg: b.RegisterOperand } | { kind: "global" } {
|
|
166
175
|
if (this._locals.has(name)) {
|
|
167
|
-
return { kind: "local",
|
|
176
|
+
return { kind: "local", reg: this._locals.get(name)! };
|
|
168
177
|
}
|
|
169
178
|
if (this.parent) return this.parent.resolve(name);
|
|
170
179
|
return { kind: "global" };
|
|
171
180
|
}
|
|
172
|
-
|
|
173
|
-
get localCount() {
|
|
174
|
-
return this._next;
|
|
175
|
-
}
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
// ── FnContext ─────────────────────────────────────────────────────────────────
|
|
179
184
|
// Compiler-side state for the function currently being compiled.
|
|
180
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.
|
|
181
192
|
class FnContext {
|
|
182
|
-
|
|
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
|
+
}[];
|
|
183
199
|
parentCtx: FnContext | null;
|
|
184
200
|
scope: Scope;
|
|
185
201
|
compiler: Compiler;
|
|
186
202
|
bc: b.Instruction[];
|
|
187
203
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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;
|
|
192
208
|
|
|
193
|
-
constructor(
|
|
209
|
+
constructor(
|
|
210
|
+
compiler: Compiler,
|
|
211
|
+
parentCtx: FnContext | null = null,
|
|
212
|
+
fnId: number = 0,
|
|
213
|
+
) {
|
|
194
214
|
this.compiler = compiler;
|
|
195
215
|
this.parentCtx = parentCtx;
|
|
196
216
|
this.scope = new Scope();
|
|
197
217
|
this.bc = [];
|
|
198
218
|
this.upvalues = [];
|
|
219
|
+
this._fnId = fnId;
|
|
199
220
|
}
|
|
200
221
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (this.regTop > this.maxRegTop) this.maxRegTop = this.regTop;
|
|
205
|
-
return r;
|
|
222
|
+
/** Create a new virtual register owned by this function. */
|
|
223
|
+
_newReg(): b.RegisterOperand {
|
|
224
|
+
return b.registerOperand(this._nextId++, this._fnId);
|
|
206
225
|
}
|
|
207
226
|
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
|
|
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)]);
|
|
211
251
|
}
|
|
212
252
|
|
|
213
|
-
|
|
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 {
|
|
214
261
|
const existing = this.upvalues.findIndex((u) => u.name === name);
|
|
215
262
|
if (existing !== -1) return existing;
|
|
216
263
|
const idx = this.upvalues.length;
|
|
@@ -233,6 +280,7 @@ interface FnDescriptor {
|
|
|
233
280
|
* Only populated AFTER resolveLabels
|
|
234
281
|
*/
|
|
235
282
|
startPc?: number;
|
|
283
|
+
ctx?: FnContext;
|
|
236
284
|
}
|
|
237
285
|
|
|
238
286
|
// ── Compiler ──────────────────────────────────────────────────────────────────
|
|
@@ -285,7 +333,17 @@ export class Compiler {
|
|
|
285
333
|
|
|
286
334
|
constants: any[];
|
|
287
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
|
+
|
|
288
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
|
+
}
|
|
289
347
|
bc.push(instr);
|
|
290
348
|
instr[SOURCE_NODE_SYM] = node;
|
|
291
349
|
}
|
|
@@ -344,13 +402,13 @@ export class Compiler {
|
|
|
344
402
|
name: string,
|
|
345
403
|
ctx: FnContext | null,
|
|
346
404
|
):
|
|
347
|
-
| { kind: "local";
|
|
405
|
+
| { kind: "local"; reg: b.RegisterOperand }
|
|
348
406
|
| { kind: "upvalue"; index: number }
|
|
349
407
|
| { kind: "global" } {
|
|
350
408
|
if (!ctx) return { kind: "global" };
|
|
351
409
|
|
|
352
410
|
if (ctx.scope._locals.has(name)) {
|
|
353
|
-
return { kind: "local",
|
|
411
|
+
return { kind: "local", reg: ctx.scope._locals.get(name)! };
|
|
354
412
|
}
|
|
355
413
|
|
|
356
414
|
if (!ctx.parentCtx) return { kind: "global" };
|
|
@@ -359,31 +417,30 @@ export class Compiler {
|
|
|
359
417
|
if (parentResult.kind === "global") return { kind: "global" };
|
|
360
418
|
|
|
361
419
|
const isLocal = parentResult.kind === "local";
|
|
362
|
-
const index = isLocal ? parentResult.
|
|
420
|
+
const index = isLocal ? parentResult.reg : parentResult.index;
|
|
363
421
|
const uvIdx = ctx.addUpvalue(name, isLocal ? 1 : 0, index);
|
|
364
422
|
return { kind: "upvalue", index: uvIdx };
|
|
365
423
|
}
|
|
366
424
|
|
|
367
425
|
// ── Variable hoisting ──────────────────────────────────────────────────────
|
|
368
|
-
// Pre-scan a statement list and reserve
|
|
369
|
-
// function declaration, for-in iterator, and try-catch binding.
|
|
370
|
-
// Must be called before
|
|
371
|
-
|
|
372
|
-
_hoistVars(stmts: t.Statement[], scope: Scope): void {
|
|
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 {
|
|
373
430
|
for (const stmt of stmts) {
|
|
374
431
|
switch (stmt.type) {
|
|
375
432
|
case "VariableDeclaration":
|
|
376
433
|
for (const decl of stmt.declarations) {
|
|
377
|
-
if (decl.id.type === "Identifier") scope.define(decl.id.name);
|
|
434
|
+
if (decl.id.type === "Identifier") scope.define(decl.id.name, ctx);
|
|
378
435
|
}
|
|
379
436
|
break;
|
|
380
437
|
|
|
381
438
|
case "FunctionDeclaration":
|
|
382
|
-
if (stmt.id) scope.define(stmt.id.name);
|
|
439
|
+
if (stmt.id) scope.define(stmt.id.name, ctx);
|
|
383
440
|
break;
|
|
384
441
|
|
|
385
442
|
case "BlockStatement":
|
|
386
|
-
this._hoistVars(stmt.body, scope);
|
|
443
|
+
this._hoistVars(stmt.body, scope, ctx);
|
|
387
444
|
break;
|
|
388
445
|
|
|
389
446
|
case "IfStatement": {
|
|
@@ -391,13 +448,13 @@ export class Compiler {
|
|
|
391
448
|
stmt.consequent.type === "BlockStatement"
|
|
392
449
|
? stmt.consequent.body
|
|
393
450
|
: [stmt.consequent];
|
|
394
|
-
this._hoistVars(cons, scope);
|
|
451
|
+
this._hoistVars(cons, scope, ctx);
|
|
395
452
|
if (stmt.alternate) {
|
|
396
453
|
const alt =
|
|
397
454
|
stmt.alternate.type === "BlockStatement"
|
|
398
455
|
? stmt.alternate.body
|
|
399
456
|
: [stmt.alternate];
|
|
400
|
-
this._hoistVars(alt, scope);
|
|
457
|
+
this._hoistVars(alt, scope, ctx);
|
|
401
458
|
}
|
|
402
459
|
break;
|
|
403
460
|
}
|
|
@@ -406,56 +463,58 @@ export class Compiler {
|
|
|
406
463
|
case "DoWhileStatement": {
|
|
407
464
|
const body =
|
|
408
465
|
stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
409
|
-
this._hoistVars(body, scope);
|
|
466
|
+
this._hoistVars(body, scope, ctx);
|
|
410
467
|
break;
|
|
411
468
|
}
|
|
412
469
|
|
|
413
470
|
case "ForStatement": {
|
|
414
471
|
if (stmt.init?.type === "VariableDeclaration") {
|
|
415
472
|
for (const decl of stmt.init.declarations) {
|
|
416
|
-
if (decl.id.type === "Identifier")
|
|
473
|
+
if (decl.id.type === "Identifier")
|
|
474
|
+
scope.define(decl.id.name, ctx);
|
|
417
475
|
}
|
|
418
476
|
}
|
|
419
477
|
const body =
|
|
420
478
|
stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
421
|
-
this._hoistVars(body, scope);
|
|
479
|
+
this._hoistVars(body, scope, ctx);
|
|
422
480
|
break;
|
|
423
481
|
}
|
|
424
482
|
|
|
425
483
|
case "ForInStatement": {
|
|
426
|
-
// Reserve a hidden register for the iterator object.
|
|
427
|
-
(stmt as any)._iterSlot =
|
|
484
|
+
// Reserve a hidden virtual register for the iterator object.
|
|
485
|
+
(stmt as any)._iterSlot = ctx._newReg();
|
|
428
486
|
if (stmt.left.type === "VariableDeclaration") {
|
|
429
487
|
for (const decl of stmt.left.declarations) {
|
|
430
|
-
if (decl.id.type === "Identifier")
|
|
488
|
+
if (decl.id.type === "Identifier")
|
|
489
|
+
scope.define(decl.id.name, ctx);
|
|
431
490
|
}
|
|
432
491
|
}
|
|
433
492
|
const body =
|
|
434
493
|
stmt.body.type === "BlockStatement" ? stmt.body.body : [stmt.body];
|
|
435
|
-
this._hoistVars(body, scope);
|
|
494
|
+
this._hoistVars(body, scope, ctx);
|
|
436
495
|
break;
|
|
437
496
|
}
|
|
438
497
|
|
|
439
498
|
case "SwitchStatement":
|
|
440
|
-
for (const c of stmt.cases) this._hoistVars(c.consequent, scope);
|
|
499
|
+
for (const c of stmt.cases) this._hoistVars(c.consequent, scope, ctx);
|
|
441
500
|
break;
|
|
442
501
|
|
|
443
502
|
case "TryStatement":
|
|
444
|
-
this._hoistVars(stmt.block.body, scope);
|
|
503
|
+
this._hoistVars(stmt.block.body, scope, ctx);
|
|
445
504
|
if (stmt.handler) {
|
|
446
505
|
if (stmt.handler.param?.type === "Identifier") {
|
|
447
506
|
// Catch parameter IS the exception register.
|
|
448
|
-
scope.define((stmt.handler.param as t.Identifier).name);
|
|
507
|
+
scope.define((stmt.handler.param as t.Identifier).name, ctx);
|
|
449
508
|
} else {
|
|
450
|
-
// No catch binding – reserve a dummy
|
|
451
|
-
(stmt as any)._exceptionSlot =
|
|
509
|
+
// No catch binding – reserve a dummy virtual register for the exception value.
|
|
510
|
+
(stmt as any)._exceptionSlot = ctx._newReg();
|
|
452
511
|
}
|
|
453
|
-
this._hoistVars(stmt.handler.body.body, scope);
|
|
512
|
+
this._hoistVars(stmt.handler.body.body, scope, ctx);
|
|
454
513
|
}
|
|
455
514
|
break;
|
|
456
515
|
|
|
457
516
|
case "LabeledStatement":
|
|
458
|
-
this._hoistVars([stmt.body], scope);
|
|
517
|
+
this._hoistVars([stmt.body], scope, ctx);
|
|
459
518
|
break;
|
|
460
519
|
}
|
|
461
520
|
}
|
|
@@ -463,7 +522,10 @@ export class Compiler {
|
|
|
463
522
|
|
|
464
523
|
// ── Entry point ───────────────────────────────────────────────────────────
|
|
465
524
|
compile(source: string) {
|
|
466
|
-
const ast = parse(source, {
|
|
525
|
+
const ast = parse(source, {
|
|
526
|
+
sourceType: "script",
|
|
527
|
+
allowReturnOutsideFunction: true,
|
|
528
|
+
});
|
|
467
529
|
return this.compileAST(ast);
|
|
468
530
|
}
|
|
469
531
|
|
|
@@ -482,32 +544,28 @@ export class Compiler {
|
|
|
482
544
|
var desc: FnDescriptor = {};
|
|
483
545
|
this.fnDescriptors.push(desc);
|
|
484
546
|
|
|
485
|
-
const ctx = new FnContext(this, this._currentCtx);
|
|
547
|
+
const ctx = new FnContext(this, this._currentCtx, fnIdx);
|
|
486
548
|
const savedCtx = this._currentCtx;
|
|
487
549
|
this._currentCtx = ctx;
|
|
488
550
|
|
|
489
551
|
const savedLoopStack = this._loopStack;
|
|
490
552
|
this._loopStack = [];
|
|
491
553
|
|
|
492
|
-
// 1. Define parameters (occupy
|
|
554
|
+
// 1. Define parameters as virtual registers (occupy the first IDs in order).
|
|
493
555
|
for (const param of node.params) {
|
|
494
556
|
let identifier = param.type === "AssignmentPattern" ? param.left : param;
|
|
495
557
|
ok(
|
|
496
558
|
identifier.type === "Identifier",
|
|
497
559
|
"Only simple identifiers allowed as parameters",
|
|
498
560
|
);
|
|
499
|
-
ctx.scope.define((identifier as t.Identifier).name);
|
|
561
|
+
ctx.scope.define((identifier as t.Identifier).name, ctx);
|
|
500
562
|
}
|
|
501
563
|
|
|
502
|
-
// 2. Reserve the `arguments`
|
|
503
|
-
ctx.scope.define("arguments");
|
|
564
|
+
// 2. Reserve the `arguments` virtual register (immediately after params).
|
|
565
|
+
ctx.scope.define("arguments", ctx);
|
|
504
566
|
|
|
505
|
-
// 3. Hoist all var declarations so
|
|
506
|
-
this._hoistVars(node.body.body, ctx.scope);
|
|
507
|
-
|
|
508
|
-
// 4. Temps now start above all locals.
|
|
509
|
-
ctx.regTop = ctx.scope.localCount;
|
|
510
|
-
ctx.maxRegTop = ctx.regTop;
|
|
567
|
+
// 3. Hoist all var declarations so locals are allocated before any temps.
|
|
568
|
+
this._hoistVars(node.body.body, ctx.scope, ctx);
|
|
511
569
|
|
|
512
570
|
// 5. Emit default-value guards.
|
|
513
571
|
for (const param of node.params) {
|
|
@@ -569,21 +627,23 @@ export class Compiler {
|
|
|
569
627
|
desc.bytecode = ctx.bc as b.Bytecode;
|
|
570
628
|
desc._fnIdx = fnIdx;
|
|
571
629
|
desc.paramCount = node.params.length;
|
|
572
|
-
|
|
630
|
+
// regCount is NOT set here — resolveRegisters() fills it after liveness analysis.
|
|
573
631
|
desc.upvalues = ctx.upvalues.slice();
|
|
632
|
+
desc.ctx = ctx;
|
|
574
633
|
|
|
575
634
|
return desc;
|
|
576
635
|
}
|
|
577
636
|
|
|
578
637
|
// Emit MAKE_CLOSURE with all metadata as inline operands.
|
|
579
638
|
// Layout: dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, …]
|
|
639
|
+
// regCount is emitted as a fnRegCount IR operand; resolveRegisters() fills it.
|
|
580
640
|
_emitMakeClosure(desc: any, node: t.Node, bc: b.Bytecode) {
|
|
581
641
|
const ctx = this._currentCtx!;
|
|
582
642
|
const dst = ctx.allocReg();
|
|
583
|
-
const uvOperands:
|
|
643
|
+
const uvOperands: b.InstrOperand[] = [];
|
|
584
644
|
for (const uv of desc.upvalues) {
|
|
585
645
|
uvOperands.push(uv.isLocal ? 1 : 0);
|
|
586
|
-
uvOperands.push(uv.index);
|
|
646
|
+
uvOperands.push(uv.index); // RegisterOperand if isLocal, number if upvalue chain
|
|
587
647
|
}
|
|
588
648
|
this.emit(
|
|
589
649
|
bc,
|
|
@@ -592,7 +652,7 @@ export class Compiler {
|
|
|
592
652
|
dst,
|
|
593
653
|
{ type: "label", label: desc.entryLabel },
|
|
594
654
|
desc.paramCount,
|
|
595
|
-
desc.
|
|
655
|
+
b.fnRegCountOperand(desc._fnIdx), // resolved by resolveRegisters()
|
|
596
656
|
desc.upvalues.length,
|
|
597
657
|
...uvOperands,
|
|
598
658
|
] as b.Instruction,
|
|
@@ -612,7 +672,7 @@ export class Compiler {
|
|
|
612
672
|
async: false,
|
|
613
673
|
generator: false,
|
|
614
674
|
params: [],
|
|
615
|
-
id:
|
|
675
|
+
id: t.identifier("main"),
|
|
616
676
|
body: t.blockStatement([...body]),
|
|
617
677
|
});
|
|
618
678
|
|
|
@@ -626,7 +686,7 @@ export class Compiler {
|
|
|
626
686
|
}
|
|
627
687
|
}
|
|
628
688
|
|
|
629
|
-
|
|
689
|
+
// mainRegCount is set by resolveRegisters() after the pipeline runs.
|
|
630
690
|
this.mainFn = desc;
|
|
631
691
|
this._currentCtx = savedCtx;
|
|
632
692
|
}
|
|
@@ -689,7 +749,7 @@ export class Compiler {
|
|
|
689
749
|
}
|
|
690
750
|
|
|
691
751
|
case "ReturnStatement": {
|
|
692
|
-
let reg:
|
|
752
|
+
let reg: b.RegisterOperand;
|
|
693
753
|
if (node.argument) {
|
|
694
754
|
reg = this._compileExpr(node.argument, scope, bc);
|
|
695
755
|
} else {
|
|
@@ -730,18 +790,12 @@ export class Compiler {
|
|
|
730
790
|
this.emit(bc, [this.OP.MOVE, slot, srcReg], node);
|
|
731
791
|
}
|
|
732
792
|
} else {
|
|
733
|
-
|
|
734
|
-
// Load undefined into the just-allocated temp, then move.
|
|
735
|
-
// Actually: just emit LOAD_CONST directly into slot.
|
|
736
|
-
// Undo the allocReg – instead emit directly:
|
|
737
|
-
ctx.regTop--; // undo the allocReg above
|
|
738
|
-
const tmp = ctx.allocReg();
|
|
793
|
+
// No initializer: var x; → load undefined directly into the local's register.
|
|
739
794
|
this.emit(
|
|
740
795
|
bc,
|
|
741
|
-
[this.OP.LOAD_CONST,
|
|
796
|
+
[this.OP.LOAD_CONST, slot, b.constantOperand(undefined)],
|
|
742
797
|
node,
|
|
743
798
|
);
|
|
744
|
-
if (tmp !== slot) this.emit(bc, [this.OP.MOVE, slot, tmp], node);
|
|
745
799
|
}
|
|
746
800
|
} else {
|
|
747
801
|
if (decl.init) {
|
|
@@ -772,7 +826,6 @@ export class Compiler {
|
|
|
772
826
|
case "IfStatement": {
|
|
773
827
|
const elseOrEndLabel = this._makeLabel("if_else");
|
|
774
828
|
|
|
775
|
-
const savedTop = ctx.regTop;
|
|
776
829
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
777
830
|
this.emit(
|
|
778
831
|
bc,
|
|
@@ -783,7 +836,6 @@ export class Compiler {
|
|
|
783
836
|
],
|
|
784
837
|
node,
|
|
785
838
|
);
|
|
786
|
-
ctx.regTop = savedTop; // free test temps
|
|
787
839
|
|
|
788
840
|
const consequentBody =
|
|
789
841
|
node.consequent.type === "BlockStatement"
|
|
@@ -843,14 +895,12 @@ export class Compiler {
|
|
|
843
895
|
node,
|
|
844
896
|
);
|
|
845
897
|
|
|
846
|
-
const savedTop = ctx.regTop;
|
|
847
898
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
848
899
|
this.emit(
|
|
849
900
|
bc,
|
|
850
901
|
[this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: exitLabel }],
|
|
851
902
|
node,
|
|
852
903
|
);
|
|
853
|
-
ctx.regTop = savedTop;
|
|
854
904
|
|
|
855
905
|
const whileBody =
|
|
856
906
|
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
@@ -902,14 +952,13 @@ export class Compiler {
|
|
|
902
952
|
node,
|
|
903
953
|
);
|
|
904
954
|
|
|
905
|
-
const savedTop = ctx.regTop;
|
|
906
955
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
907
956
|
this.emit(
|
|
908
957
|
bc,
|
|
909
958
|
[this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: exitLabel }],
|
|
910
959
|
node,
|
|
911
960
|
);
|
|
912
|
-
|
|
961
|
+
|
|
913
962
|
this.emit(
|
|
914
963
|
bc,
|
|
915
964
|
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
@@ -954,7 +1003,6 @@ export class Compiler {
|
|
|
954
1003
|
);
|
|
955
1004
|
|
|
956
1005
|
if (node.test) {
|
|
957
|
-
const savedTop = ctx.regTop;
|
|
958
1006
|
const testReg = this._compileExpr(node.test, scope, bc);
|
|
959
1007
|
this.emit(
|
|
960
1008
|
bc,
|
|
@@ -965,7 +1013,6 @@ export class Compiler {
|
|
|
965
1013
|
],
|
|
966
1014
|
node,
|
|
967
1015
|
);
|
|
968
|
-
ctx.regTop = savedTop;
|
|
969
1016
|
}
|
|
970
1017
|
|
|
971
1018
|
const forBody =
|
|
@@ -1103,7 +1150,6 @@ export class Compiler {
|
|
|
1103
1150
|
if (cas.test === null) continue;
|
|
1104
1151
|
|
|
1105
1152
|
const nextCheckLabel = this._makeLabel("sw_next");
|
|
1106
|
-
const savedTop = ctx.regTop;
|
|
1107
1153
|
const caseValReg = this._compileExpr(cas.test, scope, bc);
|
|
1108
1154
|
const cmpReg = ctx.allocReg();
|
|
1109
1155
|
this.emit(bc, [this.OP.EQ, cmpReg, discReg, caseValReg], node);
|
|
@@ -1116,7 +1162,7 @@ export class Compiler {
|
|
|
1116
1162
|
],
|
|
1117
1163
|
node,
|
|
1118
1164
|
);
|
|
1119
|
-
|
|
1165
|
+
|
|
1120
1166
|
this.emit(
|
|
1121
1167
|
bc,
|
|
1122
1168
|
[this.OP.JUMP, { type: "label", label: caseLabels[i] }],
|
|
@@ -1202,7 +1248,7 @@ export class Compiler {
|
|
|
1202
1248
|
this._pendingLabel = null;
|
|
1203
1249
|
|
|
1204
1250
|
// Iterator register was reserved by _hoistVars.
|
|
1205
|
-
const iterSlot:
|
|
1251
|
+
const iterSlot: b.RegisterOperand = (node as any)._iterSlot;
|
|
1206
1252
|
|
|
1207
1253
|
// FOR_IN_SETUP dst, src
|
|
1208
1254
|
const objReg = this._compileExpr(node.right, scope, bc);
|
|
@@ -1259,8 +1305,8 @@ export class Compiler {
|
|
|
1259
1305
|
} else if (node.left.type === "Identifier") {
|
|
1260
1306
|
const res = this._resolve(node.left.name, this._currentCtx);
|
|
1261
1307
|
if (res.kind === "local") {
|
|
1262
|
-
if (keyReg !== res.
|
|
1263
|
-
this.emit(bc, [this.OP.MOVE, res.
|
|
1308
|
+
if (keyReg !== res.reg)
|
|
1309
|
+
this.emit(bc, [this.OP.MOVE, res.reg, keyReg], node);
|
|
1264
1310
|
} else if (res.kind === "upvalue") {
|
|
1265
1311
|
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, keyReg], node);
|
|
1266
1312
|
} else {
|
|
@@ -1366,17 +1412,37 @@ export class Compiler {
|
|
|
1366
1412
|
}
|
|
1367
1413
|
|
|
1368
1414
|
// ── Expressions ───────────────────────────────────────────────────────────
|
|
1369
|
-
// Returns the
|
|
1370
|
-
// For local variables: returns their
|
|
1371
|
-
// For all others: allocates a fresh
|
|
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),
|
|
1372
1418
|
// and returns the allocated register.
|
|
1373
1419
|
_compileExpr(
|
|
1374
1420
|
node: t.Expression | t.Node,
|
|
1375
1421
|
scope: Scope | null,
|
|
1376
1422
|
bc: b.Bytecode,
|
|
1377
|
-
):
|
|
1423
|
+
): b.RegisterOperand {
|
|
1378
1424
|
const ctx = this._currentCtx!;
|
|
1379
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
|
+
|
|
1380
1446
|
switch ((node as any).type) {
|
|
1381
1447
|
case "NumericLiteral":
|
|
1382
1448
|
case "StringLiteral":
|
|
@@ -1401,7 +1467,7 @@ export class Compiler {
|
|
|
1401
1467
|
(node as t.Identifier).name,
|
|
1402
1468
|
this._currentCtx,
|
|
1403
1469
|
);
|
|
1404
|
-
if (res.kind === "local") return res.
|
|
1470
|
+
if (res.kind === "local") return res.reg; // register IS the local
|
|
1405
1471
|
if (res.kind === "upvalue") {
|
|
1406
1472
|
const dst = ctx.allocReg();
|
|
1407
1473
|
this.emit(bc, [this.OP.LOAD_UPVALUE, dst, res.index], node);
|
|
@@ -1454,9 +1520,7 @@ export class Compiler {
|
|
|
1454
1520
|
case "SequenceExpression": {
|
|
1455
1521
|
const exprs = (node as t.SequenceExpression).expressions;
|
|
1456
1522
|
for (let i = 0; i < exprs.length - 1; i++) {
|
|
1457
|
-
|
|
1458
|
-
this._compileExpr(exprs[i], scope, bc);
|
|
1459
|
-
ctx.regTop = savedTop; // discard intermediate result
|
|
1523
|
+
this._compileExpr(exprs[i], scope, bc); // result discarded; virtual reg is unused
|
|
1460
1524
|
}
|
|
1461
1525
|
return this._compileExpr(exprs[exprs.length - 1], scope, bc);
|
|
1462
1526
|
}
|
|
@@ -1466,17 +1530,14 @@ export class Compiler {
|
|
|
1466
1530
|
const elseLabel = this._makeLabel("ternary_else");
|
|
1467
1531
|
const endLabel = this._makeLabel("ternary_end");
|
|
1468
1532
|
|
|
1469
|
-
// Compile test; free its temps after the jump is emitted.
|
|
1470
|
-
const baseTop = ctx.regTop;
|
|
1471
1533
|
const testReg = this._compileExpr(n.test, scope, bc);
|
|
1472
1534
|
this.emit(
|
|
1473
1535
|
bc,
|
|
1474
1536
|
[this.OP.JUMP_IF_FALSE, testReg, { type: "label", label: elseLabel }],
|
|
1475
1537
|
node,
|
|
1476
1538
|
);
|
|
1477
|
-
ctx.regTop = baseTop; // free test temps
|
|
1478
1539
|
|
|
1479
|
-
//
|
|
1540
|
+
// reg_result is a stable virtual register both branches write into.
|
|
1480
1541
|
const reg_result = ctx.allocReg();
|
|
1481
1542
|
|
|
1482
1543
|
// Consequent branch.
|
|
@@ -1485,18 +1546,14 @@ export class Compiler {
|
|
|
1485
1546
|
this.emit(bc, [this.OP.MOVE, reg_result, consReg], node);
|
|
1486
1547
|
this.emit(bc, [this.OP.JUMP, { type: "label", label: endLabel }], node);
|
|
1487
1548
|
|
|
1488
|
-
// Alternate branch
|
|
1549
|
+
// Alternate branch — each allocReg() gets a unique virtual ID so no
|
|
1550
|
+
// slot collision is possible; no need to "re-occupy" reg_result.
|
|
1489
1551
|
this.emit(bc, [null, { type: "defineLabel", label: elseLabel }], node);
|
|
1490
|
-
ctx.regTop = baseTop;
|
|
1491
|
-
ctx.allocReg(); // re-occupy reg_result slot
|
|
1492
1552
|
const altReg = this._compileExpr(n.alternate, scope, bc);
|
|
1493
1553
|
if (altReg !== reg_result)
|
|
1494
1554
|
this.emit(bc, [this.OP.MOVE, reg_result, altReg], node);
|
|
1495
1555
|
|
|
1496
1556
|
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1497
|
-
|
|
1498
|
-
// Leave reg_result allocated above baseTop.
|
|
1499
|
-
ctx.regTop = baseTop + 1;
|
|
1500
1557
|
return reg_result;
|
|
1501
1558
|
}
|
|
1502
1559
|
|
|
@@ -1507,9 +1564,7 @@ export class Compiler {
|
|
|
1507
1564
|
if (!isOr && n.operator !== "&&")
|
|
1508
1565
|
throw new Error(`Unsupported logical operator: ${n.operator}`);
|
|
1509
1566
|
|
|
1510
|
-
const baseTop = ctx.regTop;
|
|
1511
1567
|
const lhsReg = this._compileExpr(n.left, scope, bc);
|
|
1512
|
-
ctx.regTop = baseTop;
|
|
1513
1568
|
const reg_result = ctx.allocReg();
|
|
1514
1569
|
if (lhsReg !== reg_result)
|
|
1515
1570
|
this.emit(bc, [this.OP.MOVE, reg_result, lhsReg], node);
|
|
@@ -1527,15 +1582,11 @@ export class Compiler {
|
|
|
1527
1582
|
);
|
|
1528
1583
|
|
|
1529
1584
|
// Compile RHS into reg_result.
|
|
1530
|
-
ctx.regTop = baseTop;
|
|
1531
|
-
ctx.allocReg(); // re-occupy reg_result
|
|
1532
1585
|
const rhsReg = this._compileExpr(n.right, scope, bc);
|
|
1533
1586
|
if (rhsReg !== reg_result)
|
|
1534
1587
|
this.emit(bc, [this.OP.MOVE, reg_result, rhsReg], node);
|
|
1535
1588
|
|
|
1536
1589
|
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1537
|
-
|
|
1538
|
-
ctx.regTop = baseTop + 1;
|
|
1539
1590
|
return reg_result;
|
|
1540
1591
|
}
|
|
1541
1592
|
|
|
@@ -1622,9 +1673,11 @@ export class Compiler {
|
|
|
1622
1673
|
const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
|
|
1623
1674
|
|
|
1624
1675
|
// Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
|
|
1625
|
-
const applyBump = (
|
|
1676
|
+
const applyBump = (
|
|
1677
|
+
curReg: b.RegisterOperand,
|
|
1678
|
+
): [b.RegisterOperand, b.RegisterOperand] => {
|
|
1626
1679
|
const postfixReg = n.prefix
|
|
1627
|
-
?
|
|
1680
|
+
? curReg // prefix: postfix copy unused; caller returns newReg instead
|
|
1628
1681
|
: (() => {
|
|
1629
1682
|
const r = ctx.allocReg();
|
|
1630
1683
|
this.emit(bc, [this.OP.MOVE, r, curReg], node as t.Node);
|
|
@@ -1644,7 +1697,7 @@ export class Compiler {
|
|
|
1644
1697
|
if (n.argument.type === "MemberExpression") {
|
|
1645
1698
|
const mem = n.argument as t.MemberExpression;
|
|
1646
1699
|
const objReg = this._compileExpr(mem.object, scope, bc);
|
|
1647
|
-
let keyReg:
|
|
1700
|
+
let keyReg: b.RegisterOperand;
|
|
1648
1701
|
if (mem.computed) {
|
|
1649
1702
|
keyReg = this._compileExpr(mem.property as t.Expression, scope, bc);
|
|
1650
1703
|
} else {
|
|
@@ -1681,9 +1734,9 @@ export class Compiler {
|
|
|
1681
1734
|
const name = (n.argument as t.Identifier).name;
|
|
1682
1735
|
const res = this._resolve(name, this._currentCtx);
|
|
1683
1736
|
|
|
1684
|
-
let curReg:
|
|
1737
|
+
let curReg: b.RegisterOperand;
|
|
1685
1738
|
if (res.kind === "local") {
|
|
1686
|
-
curReg = res.
|
|
1739
|
+
curReg = res.reg;
|
|
1687
1740
|
} else if (res.kind === "upvalue") {
|
|
1688
1741
|
curReg = ctx.allocReg();
|
|
1689
1742
|
this.emit(
|
|
@@ -1703,7 +1756,7 @@ export class Compiler {
|
|
|
1703
1756
|
const [postfixReg, newReg] = applyBump(curReg);
|
|
1704
1757
|
|
|
1705
1758
|
if (res.kind === "local") {
|
|
1706
|
-
this.emit(bc, [this.OP.MOVE, res.
|
|
1759
|
+
this.emit(bc, [this.OP.MOVE, res.reg, newReg], node as t.Node);
|
|
1707
1760
|
} else if (res.kind === "upvalue") {
|
|
1708
1761
|
this.emit(
|
|
1709
1762
|
bc,
|
|
@@ -1747,7 +1800,7 @@ export class Compiler {
|
|
|
1747
1800
|
if (n.left.type === "MemberExpression") {
|
|
1748
1801
|
const objReg = this._compileExpr(n.left.object, scope, bc);
|
|
1749
1802
|
|
|
1750
|
-
let keyReg:
|
|
1803
|
+
let keyReg: b.RegisterOperand;
|
|
1751
1804
|
if (n.left.computed) {
|
|
1752
1805
|
keyReg = this._compileExpr(
|
|
1753
1806
|
n.left.property as t.Expression,
|
|
@@ -1767,7 +1820,7 @@ export class Compiler {
|
|
|
1767
1820
|
);
|
|
1768
1821
|
}
|
|
1769
1822
|
|
|
1770
|
-
let valReg:
|
|
1823
|
+
let valReg: b.RegisterOperand;
|
|
1771
1824
|
if (isCompound) {
|
|
1772
1825
|
const curReg = ctx.allocReg();
|
|
1773
1826
|
this.emit(bc, [this.OP.GET_PROP, curReg, objReg, keyReg], node);
|
|
@@ -1788,12 +1841,12 @@ export class Compiler {
|
|
|
1788
1841
|
this._currentCtx,
|
|
1789
1842
|
);
|
|
1790
1843
|
|
|
1791
|
-
let rhsReg:
|
|
1844
|
+
let rhsReg: b.RegisterOperand;
|
|
1792
1845
|
if (isCompound) {
|
|
1793
1846
|
// Load current value of the variable.
|
|
1794
|
-
let curReg:
|
|
1847
|
+
let curReg: b.RegisterOperand;
|
|
1795
1848
|
if (res.kind === "local") {
|
|
1796
|
-
curReg = res.
|
|
1849
|
+
curReg = res.reg;
|
|
1797
1850
|
} else if (res.kind === "upvalue") {
|
|
1798
1851
|
curReg = ctx.allocReg();
|
|
1799
1852
|
this.emit(bc, [this.OP.LOAD_UPVALUE, curReg, res.index], node);
|
|
@@ -1818,9 +1871,9 @@ export class Compiler {
|
|
|
1818
1871
|
|
|
1819
1872
|
// Store result and return it.
|
|
1820
1873
|
if (res.kind === "local") {
|
|
1821
|
-
if (rhsReg !== res.
|
|
1822
|
-
this.emit(bc, [this.OP.MOVE, res.
|
|
1823
|
-
return res.
|
|
1874
|
+
if (rhsReg !== res.reg)
|
|
1875
|
+
this.emit(bc, [this.OP.MOVE, res.reg, rhsReg], node);
|
|
1876
|
+
return res.reg;
|
|
1824
1877
|
} else if (res.kind === "upvalue") {
|
|
1825
1878
|
this.emit(bc, [this.OP.STORE_UPVALUE, res.index, rhsReg], node);
|
|
1826
1879
|
return rhsReg;
|
|
@@ -1838,7 +1891,7 @@ export class Compiler {
|
|
|
1838
1891
|
// Method call: receiver.method(args)
|
|
1839
1892
|
const receiverReg = this._compileExpr(n.callee.object, scope, bc);
|
|
1840
1893
|
|
|
1841
|
-
let methodKeyReg:
|
|
1894
|
+
let methodKeyReg: b.RegisterOperand;
|
|
1842
1895
|
if (n.callee.computed) {
|
|
1843
1896
|
methodKeyReg = this._compileExpr(
|
|
1844
1897
|
n.callee.property as t.Expression,
|
|
@@ -1924,7 +1977,7 @@ export class Compiler {
|
|
|
1924
1977
|
const arg = n.argument;
|
|
1925
1978
|
if (arg.type === "MemberExpression") {
|
|
1926
1979
|
const objReg = this._compileExpr(arg.object, scope, bc);
|
|
1927
|
-
let keyReg:
|
|
1980
|
+
let keyReg: b.RegisterOperand;
|
|
1928
1981
|
if (arg.computed) {
|
|
1929
1982
|
keyReg = this._compileExpr(
|
|
1930
1983
|
arg.property as t.Expression,
|
|
@@ -2017,7 +2070,7 @@ export class Compiler {
|
|
|
2017
2070
|
case "MemberExpression": {
|
|
2018
2071
|
const n = node as t.MemberExpression;
|
|
2019
2072
|
const objReg = this._compileExpr(n.object, scope, bc);
|
|
2020
|
-
let keyReg:
|
|
2073
|
+
let keyReg: b.RegisterOperand;
|
|
2021
2074
|
if (n.computed) {
|
|
2022
2075
|
keyReg = this._compileExpr(n.property as t.Expression, scope, bc);
|
|
2023
2076
|
} else {
|
|
@@ -2084,7 +2137,7 @@ export class Compiler {
|
|
|
2084
2137
|
}
|
|
2085
2138
|
|
|
2086
2139
|
// Build flat [key, val, key, val, …] register list.
|
|
2087
|
-
const pairRegs:
|
|
2140
|
+
const pairRegs: b.RegisterOperand[] = [];
|
|
2088
2141
|
for (const prop of regularProps) {
|
|
2089
2142
|
let keyStr: string;
|
|
2090
2143
|
const key = prop.key;
|
|
@@ -2197,6 +2250,7 @@ class Serializer {
|
|
|
2197
2250
|
const v = constants[idx];
|
|
2198
2251
|
if (!key) return v;
|
|
2199
2252
|
if (typeof v === "number") return v ^ key;
|
|
2253
|
+
if (typeof v !== "string") return v;
|
|
2200
2254
|
// String: base64 → u16 LE byte pairs → XOR with (key + i) (mirrors _readConstant)
|
|
2201
2255
|
const bytes = Buffer.from(v as string, "base64");
|
|
2202
2256
|
let out = "";
|
|
@@ -2207,21 +2261,36 @@ class Serializer {
|
|
|
2207
2261
|
return out;
|
|
2208
2262
|
}
|
|
2209
2263
|
|
|
2210
|
-
|
|
2264
|
+
_generateComment(instr: b.Instruction) {
|
|
2211
2265
|
const op = instr[0] as number;
|
|
2212
2266
|
const operands = instr.slice(1) as number[];
|
|
2213
2267
|
|
|
2268
|
+
if (op === null && (operands[0] as any)?.type === "defineLabel") {
|
|
2269
|
+
const label = (operands[0] as any).label;
|
|
2270
|
+
return `${label}:`;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2214
2273
|
const constants = this.compiler.constants;
|
|
2215
2274
|
|
|
2216
|
-
const
|
|
2217
|
-
|
|
2218
|
-
|
|
2275
|
+
const emittedOperands = operands.filter(
|
|
2276
|
+
(operand) => (operand as any)?.placeholder !== true,
|
|
2277
|
+
);
|
|
2219
2278
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
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
|
+
});
|
|
2225
2294
|
|
|
2226
2295
|
let name = this.OP_NAME[op];
|
|
2227
2296
|
if (!name || name.includes("{")) {
|
|
@@ -2241,71 +2310,85 @@ class Serializer {
|
|
|
2241
2310
|
.join("-")
|
|
2242
2311
|
: "";
|
|
2243
2312
|
|
|
2244
|
-
if (
|
|
2313
|
+
if (displayOperands.length > 0) {
|
|
2245
2314
|
// Operand[0] is always `dst` for instruction types that produce a value.
|
|
2246
|
-
const dst =
|
|
2315
|
+
const dst = displayOperands[0];
|
|
2247
2316
|
|
|
2248
2317
|
switch (op) {
|
|
2249
2318
|
case this.OP.LOAD_CONST: {
|
|
2250
2319
|
// resolvedOperands: [dst, constIdx, concealKey]
|
|
2251
2320
|
const val = this._decryptConst(
|
|
2252
2321
|
constants,
|
|
2253
|
-
|
|
2254
|
-
|
|
2322
|
+
displayOperands[1],
|
|
2323
|
+
displayOperands[2],
|
|
2255
2324
|
);
|
|
2256
2325
|
comment += ` reg[${dst}] = ${this._serializeConst(val)}`;
|
|
2257
2326
|
break;
|
|
2258
2327
|
}
|
|
2328
|
+
|
|
2329
|
+
case this.OP.LOAD_INT: {
|
|
2330
|
+
// resolvedOperands: [dst, intValue]
|
|
2331
|
+
comment += ` reg[${dst}] = ${displayOperands[1]}`;
|
|
2332
|
+
break;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2259
2335
|
case this.OP.LOAD_GLOBAL:
|
|
2260
2336
|
// resolvedOperands: [dst, constIdx, concealKey]
|
|
2261
|
-
comment += ` reg[${dst}] = ${this._decryptConst(constants,
|
|
2337
|
+
comment += ` reg[${dst}] = ${this._decryptConst(constants, displayOperands[1], displayOperands[2])}`;
|
|
2262
2338
|
break;
|
|
2263
2339
|
case this.OP.STORE_GLOBAL:
|
|
2264
2340
|
// resolvedOperands: [constIdx, concealKey, srcReg]
|
|
2265
|
-
comment += ` ${this._decryptConst(constants,
|
|
2341
|
+
comment += ` ${this._decryptConst(constants, displayOperands[0], displayOperands[1])} = reg[${displayOperands[2]}]`;
|
|
2266
2342
|
break;
|
|
2267
2343
|
case this.OP.LOAD_UPVALUE:
|
|
2268
|
-
comment += ` reg[${dst}] = upvalue[${
|
|
2344
|
+
comment += ` reg[${dst}] = upvalue[${displayOperands[1]}]`;
|
|
2269
2345
|
break;
|
|
2270
2346
|
case this.OP.STORE_UPVALUE:
|
|
2271
|
-
comment += ` upvalue[${
|
|
2347
|
+
comment += ` upvalue[${displayOperands[0]}] = reg[${displayOperands[1]}]`;
|
|
2272
2348
|
break;
|
|
2273
2349
|
case this.OP.MOVE:
|
|
2274
|
-
comment += ` reg[${dst}] = reg[${
|
|
2350
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}]`;
|
|
2275
2351
|
break;
|
|
2276
2352
|
case this.OP.MAKE_CLOSURE:
|
|
2277
|
-
comment += ` reg[${dst}] PC=${
|
|
2353
|
+
comment += ` reg[${dst}] PC=${displayOperands[1]} (params=${displayOperands[2]} regs=${displayOperands[3]} upvalues=${displayOperands[4]})`;
|
|
2278
2354
|
break;
|
|
2279
2355
|
case this.OP.CALL:
|
|
2280
|
-
comment += ` reg[${dst}] = reg[${
|
|
2356
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}](${displayOperands
|
|
2357
|
+
.slice(3)
|
|
2358
|
+
.map((v) => `reg[${v}]`)
|
|
2359
|
+
.join(", ")})`;
|
|
2281
2360
|
break;
|
|
2282
2361
|
case this.OP.CALL_METHOD:
|
|
2283
|
-
comment += ` reg[${dst}] = reg[${
|
|
2362
|
+
comment += ` reg[${dst}] = reg[${displayOperands[2]}](recv=reg[${displayOperands[1]}], ${displayOperands[3]} args)`;
|
|
2284
2363
|
break;
|
|
2285
2364
|
case this.OP.NEW:
|
|
2286
|
-
comment += ` reg[${dst}] = new reg[${
|
|
2365
|
+
comment += ` reg[${dst}] = new reg[${displayOperands[1]}](${displayOperands[2]} args)`;
|
|
2287
2366
|
break;
|
|
2288
2367
|
case this.OP.RETURN:
|
|
2289
|
-
comment += ` reg[${
|
|
2368
|
+
comment += ` reg[${displayOperands[0]}]`;
|
|
2290
2369
|
break;
|
|
2291
2370
|
case this.OP.BUILD_ARRAY:
|
|
2292
|
-
comment += ` reg[${dst}] = [${
|
|
2371
|
+
comment += ` reg[${dst}] = [${displayOperands[2]} elems]`;
|
|
2293
2372
|
break;
|
|
2294
2373
|
case this.OP.BUILD_OBJECT:
|
|
2295
|
-
comment += ` reg[${dst}] = {${
|
|
2374
|
+
comment += ` reg[${dst}] = {${displayOperands[1]} pairs}`;
|
|
2296
2375
|
break;
|
|
2297
2376
|
case this.OP.GET_PROP:
|
|
2298
|
-
comment += ` reg[${dst}] = reg[${
|
|
2377
|
+
comment += ` reg[${dst}] = reg[${displayOperands[1]}][reg[${displayOperands[2]}]]`;
|
|
2299
2378
|
break;
|
|
2300
2379
|
case this.OP.SET_PROP:
|
|
2301
|
-
comment += ` reg[${
|
|
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]}]`;
|
|
2302
2385
|
break;
|
|
2303
2386
|
|
|
2304
2387
|
default:
|
|
2305
2388
|
comment +=
|
|
2306
|
-
|
|
2307
|
-
? ` ${
|
|
2308
|
-
: ` [${
|
|
2389
|
+
displayOperands.length === 1
|
|
2390
|
+
? ` ${displayOperands[0]}`
|
|
2391
|
+
: ` [${displayOperands.join(", ")}]`;
|
|
2309
2392
|
}
|
|
2310
2393
|
}
|
|
2311
2394
|
|
|
@@ -2315,7 +2398,7 @@ class Serializer {
|
|
|
2315
2398
|
const instrText = `[${values.join(", ")}]`;
|
|
2316
2399
|
const text = `${(instrText + ",").padEnd(20)} ${comment}`;
|
|
2317
2400
|
|
|
2318
|
-
return
|
|
2401
|
+
return text;
|
|
2319
2402
|
}
|
|
2320
2403
|
|
|
2321
2404
|
_serializeConstants(constants: any[]) {
|
|
@@ -2333,20 +2416,29 @@ class Serializer {
|
|
|
2333
2416
|
): { bytecode: b.Bytecode } {
|
|
2334
2417
|
const serialized = [];
|
|
2335
2418
|
for (const instr of bytecode) {
|
|
2336
|
-
|
|
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
|
+
);
|
|
2337
2427
|
|
|
2338
2428
|
const specializedOpInfo = compiler.SPECIALIZED_OPS[instr[0]];
|
|
2339
2429
|
if (specializedOpInfo) {
|
|
2340
|
-
const operands = instr.slice(1);
|
|
2341
|
-
|
|
2342
|
-
const resolvedValues = operands.map(
|
|
2343
|
-
(o) => (o as any)?.resolvedValue ?? o,
|
|
2344
|
-
);
|
|
2345
2430
|
const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
|
|
2346
2431
|
compiler.OP_NAME[instr[0]] =
|
|
2347
2432
|
`${originalName}_${resolvedValues.join("_")}`;
|
|
2348
2433
|
}
|
|
2349
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
|
+
|
|
2350
2442
|
serialized.push(instr);
|
|
2351
2443
|
}
|
|
2352
2444
|
return { bytecode: serialized };
|
|
@@ -2409,6 +2501,14 @@ export async function compileAndSerialize(
|
|
|
2409
2501
|
const compiler = new Compiler(options);
|
|
2410
2502
|
let bytecode = compiler.compile(sourceCode);
|
|
2411
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
|
+
|
|
2412
2512
|
const passes = [];
|
|
2413
2513
|
|
|
2414
2514
|
passes.push(concealConstants);
|
|
@@ -2425,10 +2525,6 @@ export async function compileAndSerialize(
|
|
|
2425
2525
|
passes.push(macroOpcodes);
|
|
2426
2526
|
}
|
|
2427
2527
|
|
|
2428
|
-
if (options.selfModifying) {
|
|
2429
|
-
passes.push(selfModifying);
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
2528
|
if (options.aliasedOpcodes) {
|
|
2433
2529
|
passes.push(aliasedOpcodes);
|
|
2434
2530
|
}
|
|
@@ -2438,6 +2534,21 @@ export async function compileAndSerialize(
|
|
|
2438
2534
|
bytecode = passResult.bytecode;
|
|
2439
2535
|
}
|
|
2440
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
|
+
|
|
2441
2552
|
// Resolve label references to flat bytecode indices.
|
|
2442
2553
|
const labelsResult = resolveLabels(bytecode, compiler);
|
|
2443
2554
|
bytecode = labelsResult.bytecode;
|
|
@@ -2463,8 +2574,8 @@ export async function compileAndSerialize(
|
|
|
2463
2574
|
const generateBytecodeComment = () => {
|
|
2464
2575
|
var lines = [];
|
|
2465
2576
|
for (const instr of bytecode) {
|
|
2466
|
-
const
|
|
2467
|
-
lines.push("// " +
|
|
2577
|
+
const comment = compiler.serializer._generateComment(instr);
|
|
2578
|
+
lines.push("// " + comment);
|
|
2468
2579
|
}
|
|
2469
2580
|
|
|
2470
2581
|
return lines.join("\n");
|