js-confuser-vm 0.1.0 → 0.1.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/CHANGELOG.md +23 -0
- package/README.md +75 -94
- package/bench.ts +146 -0
- package/disassemble.ts +12 -0
- package/dist/build-runtime.js +41 -15
- package/dist/compiler.js +134 -60
- package/dist/disassembler.js +317 -0
- package/dist/index.js +7 -2
- package/dist/runtime.js +68 -46
- package/dist/template.js +116 -0
- package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
- package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
- package/dist/transforms/bytecode/dispatcher.js +13 -109
- package/dist/transforms/bytecode/macroOpcodes.js +2 -2
- package/dist/transforms/bytecode/resolveConstants.js +100 -0
- package/dist/transforms/bytecode/resolveRegisters.js +4 -0
- package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
- package/dist/transforms/bytecode/specializedOpcodes.js +18 -10
- package/dist/transforms/bytecode/stringConcealing.js +110 -0
- package/dist/transforms/runtime/classObfuscation.js +43 -0
- package/dist/transforms/runtime/handlerTable.js +91 -0
- package/dist/transforms/runtime/semanticOpcodes.js +35 -0
- package/dist/transforms/runtime/specializedOpcodes.js +11 -5
- package/dist/types.js +1 -1
- package/dist/utils/ast-utils.js +14 -0
- package/dist/utils/op-utils.js +0 -2
- package/dist/utils/pass-utils.js +100 -0
- package/dist/utils/profile-utils.js +3 -0
- package/index.ts +22 -17
- package/jest.config.js +14 -2
- package/output.disassembled.js +41 -0
- package/package.json +2 -1
- package/src/build-runtime.ts +113 -78
- package/src/compiler.ts +2703 -2593
- package/src/disassembler.ts +329 -0
- package/src/index.ts +12 -2
- package/src/options.ts +7 -1
- package/src/runtime.ts +84 -51
- package/src/template.ts +125 -1
- package/src/transforms/bytecode/aliasedOpcodes.ts +4 -1
- package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
- package/src/transforms/bytecode/dispatcher.ts +19 -125
- package/src/transforms/bytecode/macroOpcodes.ts +2 -2
- package/src/transforms/bytecode/resolveRegisters.ts +5 -0
- package/src/transforms/bytecode/specializedOpcodes.ts +22 -11
- package/src/transforms/bytecode/stringConcealing.ts +130 -0
- package/src/transforms/runtime/classObfuscation.ts +59 -0
- package/src/transforms/runtime/specializedOpcodes.ts +14 -9
- package/src/types.ts +42 -1
- package/src/utils/ast-utils.ts +19 -0
- package/src/utils/op-utils.ts +0 -2
- package/src/utils/pass-utils.ts +126 -0
- package/src/utils/profile-utils.ts +3 -0
- package/tsconfig.json +1 -1
- package/src/transforms/bytecode/microOpcodes.ts +0 -291
- package/src/transforms/runtime/internalVariables.ts +0 -270
- package/src/transforms/runtime/microOpcodes.ts +0 -93
- /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
package/src/runtime.ts
CHANGED
|
@@ -8,18 +8,27 @@ const TIMING_CHECKS = false;
|
|
|
8
8
|
// The text above is not included in the compiled output - for type intellisense only
|
|
9
9
|
// @START
|
|
10
10
|
|
|
11
|
+
function base64ToBytes(s) {
|
|
12
|
+
return typeof Buffer !== "undefined"
|
|
13
|
+
? Buffer.from(s, "base64")
|
|
14
|
+
: Uint8Array.from(atob(s), function (c) {
|
|
15
|
+
return c.charCodeAt(0);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
function decodeBytecode(s) {
|
|
12
20
|
if (!ENCODE_BYTECODE) return s;
|
|
13
21
|
|
|
14
|
-
var b =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
var b = base64ToBytes(s);
|
|
23
|
+
// Each slot is a u32 stored as 4 little-endian bytes.
|
|
24
|
+
var r = new Uint32Array(b.length / 4);
|
|
25
|
+
for (var i = 0; i < r.length; i++)
|
|
26
|
+
r[i] =
|
|
27
|
+
(b[i * 4] |
|
|
28
|
+
(b[i * 4 + 1] << 8) |
|
|
29
|
+
(b[i * 4 + 2] << 16) |
|
|
30
|
+
(b[i * 4 + 3] << 24)) >>>
|
|
31
|
+
0;
|
|
23
32
|
return r;
|
|
24
33
|
}
|
|
25
34
|
|
|
@@ -100,7 +109,6 @@ function VM(bytecode, mainStartPc, mainRegCount, constants, globals) {
|
|
|
100
109
|
0,
|
|
101
110
|
0,
|
|
102
111
|
);
|
|
103
|
-
this._internals = {};
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
// Consume the next slot from the flat bytecode stream and advance the PC.
|
|
@@ -138,12 +146,7 @@ VM.prototype._constant = function (idxIn, keyIn) {
|
|
|
138
146
|
if (!key) return v;
|
|
139
147
|
if (typeof v === "number") return v ^ key;
|
|
140
148
|
// String: base64-decode to u16 LE byte pairs, then XOR each code with (key+i).
|
|
141
|
-
var b =
|
|
142
|
-
typeof Buffer !== "undefined"
|
|
143
|
-
? Buffer.from(v, "base64")
|
|
144
|
-
: Uint8Array.from(atob(v), function (c) {
|
|
145
|
-
return c.charCodeAt(0);
|
|
146
|
-
});
|
|
149
|
+
var b = base64ToBytes(v);
|
|
147
150
|
var out = "";
|
|
148
151
|
for (var i = 0; i < b.length / 2; i++) {
|
|
149
152
|
var code = b[i * 2] | (b[i * 2 + 1] << 8); // u16 LE
|
|
@@ -212,6 +215,7 @@ VM.prototype.run = function () {
|
|
|
212
215
|
try {
|
|
213
216
|
var regs = this._regs;
|
|
214
217
|
var base = frame._base;
|
|
218
|
+
|
|
215
219
|
/* @SWITCH */
|
|
216
220
|
switch (op) {
|
|
217
221
|
case OP.LOAD_CONST: {
|
|
@@ -257,8 +261,7 @@ VM.prototype.run = function () {
|
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
case OP.STORE_GLOBAL: {
|
|
260
|
-
//
|
|
261
|
-
// transform can rewrite this._constant() consistently.
|
|
264
|
+
// globals[globalName] = regs[src]
|
|
262
265
|
this.globals[this._constant()] = regs[base + this._operand()];
|
|
263
266
|
break;
|
|
264
267
|
}
|
|
@@ -283,13 +286,14 @@ VM.prototype.run = function () {
|
|
|
283
286
|
var obj = regs[base + this._operand()];
|
|
284
287
|
var key = regs[base + this._operand()];
|
|
285
288
|
var val = regs[base + this._operand()];
|
|
286
|
-
// Reflect.set performs [[Set]] without throwing on failure
|
|
287
|
-
// correctly simulating sloppy-mode assignment from a strict-mode host.
|
|
289
|
+
// Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior)
|
|
288
290
|
Reflect.set(obj, key, val);
|
|
289
291
|
break;
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
case OP.DELETE_PROP: {
|
|
295
|
+
// regs[dst] = delete regs[obj][regs[key]]
|
|
296
|
+
// The delete operator returns true if successful which is most cases
|
|
293
297
|
var dst = this._operand();
|
|
294
298
|
var obj = regs[base + this._operand()];
|
|
295
299
|
var key = regs[base + this._operand()];
|
|
@@ -297,7 +301,7 @@ VM.prototype.run = function () {
|
|
|
297
301
|
break;
|
|
298
302
|
}
|
|
299
303
|
|
|
300
|
-
//
|
|
304
|
+
// Arithmetic (dst, src1, src2)
|
|
301
305
|
case OP.ADD: {
|
|
302
306
|
var dst = this._operand();
|
|
303
307
|
var a = regs[base + this._operand()];
|
|
@@ -365,7 +369,7 @@ VM.prototype.run = function () {
|
|
|
365
369
|
break;
|
|
366
370
|
}
|
|
367
371
|
|
|
368
|
-
//
|
|
372
|
+
// Comparison (dst, src1, src2)
|
|
369
373
|
case OP.LT: {
|
|
370
374
|
var dst = this._operand();
|
|
371
375
|
var a = regs[base + this._operand()];
|
|
@@ -421,13 +425,14 @@ VM.prototype.run = function () {
|
|
|
421
425
|
break;
|
|
422
426
|
}
|
|
423
427
|
case OP.INSTANCEOF: {
|
|
428
|
+
// regs[dst] = regs[obj] instanceof regs[ctor]
|
|
424
429
|
var dst = this._operand();
|
|
425
430
|
var obj = regs[base + this._operand()];
|
|
426
431
|
var ctor = regs[base + this._operand()];
|
|
427
432
|
if (typeof ctor === "function") {
|
|
428
433
|
regs[base + dst] = obj instanceof ctor;
|
|
429
434
|
} else {
|
|
430
|
-
//
|
|
435
|
+
// TODO: Why is this needed?
|
|
431
436
|
var proto = ctor.prototype;
|
|
432
437
|
var target = Object.getPrototypeOf(obj);
|
|
433
438
|
var result = false;
|
|
@@ -443,7 +448,7 @@ VM.prototype.run = function () {
|
|
|
443
448
|
break;
|
|
444
449
|
}
|
|
445
450
|
|
|
446
|
-
//
|
|
451
|
+
// Unary (dst, src)
|
|
447
452
|
case OP.UNARY_NEG: {
|
|
448
453
|
var dst = this._operand();
|
|
449
454
|
regs[base + dst] = -regs[base + this._operand()];
|
|
@@ -471,12 +476,13 @@ VM.prototype.run = function () {
|
|
|
471
476
|
}
|
|
472
477
|
case OP.VOID: {
|
|
473
478
|
var dst = this._operand();
|
|
474
|
-
this._operand(); //
|
|
479
|
+
this._operand(); // consumes argument (intended)
|
|
475
480
|
regs[base + dst] = undefined;
|
|
476
481
|
break;
|
|
477
482
|
}
|
|
478
483
|
case OP.TYPEOF_SAFE: {
|
|
479
|
-
// dst
|
|
484
|
+
// regs[dst] = typeof window[name]
|
|
485
|
+
// Never throws ReferenceError, instead returns undefined for undeclared variables
|
|
480
486
|
var dst = this._operand();
|
|
481
487
|
var name = this._constant();
|
|
482
488
|
var val = Object.prototype.hasOwnProperty.call(this.globals, name)
|
|
@@ -486,7 +492,7 @@ VM.prototype.run = function () {
|
|
|
486
492
|
break;
|
|
487
493
|
}
|
|
488
494
|
|
|
489
|
-
//
|
|
495
|
+
// Control flow
|
|
490
496
|
case OP.JUMP:
|
|
491
497
|
frame._pc = this._operand();
|
|
492
498
|
break;
|
|
@@ -506,7 +512,7 @@ VM.prototype.run = function () {
|
|
|
506
512
|
break;
|
|
507
513
|
}
|
|
508
514
|
|
|
509
|
-
//
|
|
515
|
+
// Calls
|
|
510
516
|
case OP.CALL: {
|
|
511
517
|
// dst, calleeReg, argc, [argReg...]
|
|
512
518
|
var dst = this._operand();
|
|
@@ -528,8 +534,14 @@ VM.prototype.run = function () {
|
|
|
528
534
|
dst,
|
|
529
535
|
newBase,
|
|
530
536
|
);
|
|
531
|
-
|
|
532
|
-
|
|
537
|
+
if (closure.fn.hasRest) {
|
|
538
|
+
var restSlot = closure.fn.paramCount - 1;
|
|
539
|
+
for (var i = 0; i < restSlot; i++)
|
|
540
|
+
this._regs[newBase + i] = i < args.length ? args[i] : undefined;
|
|
541
|
+
this._regs[newBase + restSlot] = args.slice(restSlot);
|
|
542
|
+
} else {
|
|
543
|
+
for (var i = 0; i < args.length && i < closure.fn.regCount; i++)
|
|
544
|
+
this._regs[newBase + i] = args[i];
|
|
533
545
|
}
|
|
534
546
|
if (closure.fn.paramCount < closure.fn.regCount) {
|
|
535
547
|
this._regs[newBase + closure.fn.paramCount] = args;
|
|
@@ -564,8 +576,14 @@ VM.prototype.run = function () {
|
|
|
564
576
|
dst,
|
|
565
577
|
newBase,
|
|
566
578
|
);
|
|
567
|
-
|
|
568
|
-
|
|
579
|
+
if (closure.fn.hasRest) {
|
|
580
|
+
var restSlot = closure.fn.paramCount - 1;
|
|
581
|
+
for (var i = 0; i < restSlot; i++)
|
|
582
|
+
this._regs[newBase + i] = i < args.length ? args[i] : undefined;
|
|
583
|
+
this._regs[newBase + restSlot] = args.slice(restSlot);
|
|
584
|
+
} else {
|
|
585
|
+
for (var i = 0; i < args.length && i < closure.fn.regCount; i++)
|
|
586
|
+
this._regs[newBase + i] = args[i];
|
|
569
587
|
}
|
|
570
588
|
if (closure.fn.paramCount < closure.fn.regCount) {
|
|
571
589
|
this._regs[newBase + closure.fn.paramCount] = args;
|
|
@@ -593,8 +611,14 @@ VM.prototype.run = function () {
|
|
|
593
611
|
this._ensureRegisterWindow(newBase, closure.fn.regCount);
|
|
594
612
|
this._regsTop = newBase + closure.fn.regCount;
|
|
595
613
|
var f = new Frame(closure, frame._pc, frame, newObj, dst, newBase);
|
|
596
|
-
|
|
597
|
-
|
|
614
|
+
if (closure.fn.hasRest) {
|
|
615
|
+
var restSlot = closure.fn.paramCount - 1;
|
|
616
|
+
for (var i = 0; i < restSlot; i++)
|
|
617
|
+
this._regs[newBase + i] = i < args.length ? args[i] : undefined;
|
|
618
|
+
this._regs[newBase + restSlot] = args.slice(restSlot);
|
|
619
|
+
} else {
|
|
620
|
+
for (var i = 0; i < args.length && i < closure.fn.regCount; i++)
|
|
621
|
+
this._regs[newBase + i] = args[i];
|
|
598
622
|
}
|
|
599
623
|
if (closure.fn.paramCount < closure.fn.regCount) {
|
|
600
624
|
this._regs[newBase + closure.fn.paramCount] = args;
|
|
@@ -613,11 +637,16 @@ VM.prototype.run = function () {
|
|
|
613
637
|
case OP.RETURN: {
|
|
614
638
|
var retVal = regs[base + this._operand()];
|
|
615
639
|
this._closeUpvaluesFor(frame); // must happen before frame is abandoned
|
|
640
|
+
|
|
641
|
+
// Zero out callee's register window to limit exposing runtime values
|
|
642
|
+
var hi = frame._base + frame.closure.fn.regCount;
|
|
643
|
+
for (var i = frame._base as number; i < hi; i++)
|
|
644
|
+
this._regs[i] = undefined;
|
|
616
645
|
this._regsTop = frame._base;
|
|
617
646
|
|
|
618
647
|
if (this._frameStack.length === 0) return retVal; // main script returning
|
|
619
648
|
|
|
620
|
-
//
|
|
649
|
+
// NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object)
|
|
621
650
|
if (frame._newObj !== null) {
|
|
622
651
|
if (typeof retVal !== "object" || retVal === null)
|
|
623
652
|
retVal = frame._newObj;
|
|
@@ -632,14 +661,15 @@ VM.prototype.run = function () {
|
|
|
632
661
|
case OP.THROW:
|
|
633
662
|
throw regs[base + this._operand()];
|
|
634
663
|
|
|
635
|
-
//
|
|
664
|
+
// Closures
|
|
636
665
|
case OP.MAKE_CLOSURE: {
|
|
637
|
-
// dst, startPc, paramCount, regCount, uvCount, [isLocal, idx, ...]
|
|
666
|
+
// dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...]
|
|
638
667
|
var dst = this._operand();
|
|
639
668
|
var startPc = this._operand();
|
|
640
669
|
var paramCount = this._operand();
|
|
641
670
|
var regCount = this._operand();
|
|
642
671
|
var uvCount = this._operand();
|
|
672
|
+
var hasRest = this._operand(); // 1 if last param is a rest element
|
|
643
673
|
|
|
644
674
|
var uvDescs = new Array(uvCount);
|
|
645
675
|
for (var i = 0; i < uvCount; i++) {
|
|
@@ -653,6 +683,7 @@ VM.prototype.run = function () {
|
|
|
653
683
|
regCount: regCount,
|
|
654
684
|
startPc: startPc,
|
|
655
685
|
upvalueDescriptors: uvDescs,
|
|
686
|
+
hasRest: hasRest,
|
|
656
687
|
};
|
|
657
688
|
|
|
658
689
|
var closure = new Closure(fn);
|
|
@@ -688,8 +719,14 @@ VM.prototype.run = function () {
|
|
|
688
719
|
0,
|
|
689
720
|
);
|
|
690
721
|
sub._currentFrame = f;
|
|
691
|
-
|
|
692
|
-
|
|
722
|
+
if (c.fn.hasRest) {
|
|
723
|
+
var restSlot = c.fn.paramCount - 1;
|
|
724
|
+
for (var i = 0; i < restSlot; i++)
|
|
725
|
+
sub._regs[i] = i < args.length ? args[i] : undefined;
|
|
726
|
+
sub._regs[restSlot] = args.slice(restSlot);
|
|
727
|
+
} else {
|
|
728
|
+
for (var i = 0; i < args.length && i < c.fn.regCount; i++)
|
|
729
|
+
sub._regs[i] = args[i];
|
|
693
730
|
}
|
|
694
731
|
if (c.fn.paramCount < c.fn.regCount) {
|
|
695
732
|
sub._regs[c.fn.paramCount] = args;
|
|
@@ -703,7 +740,7 @@ VM.prototype.run = function () {
|
|
|
703
740
|
break;
|
|
704
741
|
}
|
|
705
742
|
|
|
706
|
-
//
|
|
743
|
+
// Collections
|
|
707
744
|
case OP.BUILD_ARRAY: {
|
|
708
745
|
// dst, count, [elemReg...]
|
|
709
746
|
var dst = this._operand();
|
|
@@ -729,7 +766,7 @@ VM.prototype.run = function () {
|
|
|
729
766
|
break;
|
|
730
767
|
}
|
|
731
768
|
|
|
732
|
-
//
|
|
769
|
+
// Object methods (getters / setters)
|
|
733
770
|
case OP.DEFINE_GETTER: {
|
|
734
771
|
// obj, key, fn
|
|
735
772
|
var obj = regs[base + this._operand()];
|
|
@@ -825,7 +862,7 @@ VM.prototype.run = function () {
|
|
|
825
862
|
break;
|
|
826
863
|
}
|
|
827
864
|
|
|
828
|
-
//
|
|
865
|
+
// Self-modifying bytecode
|
|
829
866
|
case OP.PATCH: {
|
|
830
867
|
// destPc, sliceStart, sliceEnd
|
|
831
868
|
var destPc = this._operand();
|
|
@@ -838,10 +875,7 @@ VM.prototype.run = function () {
|
|
|
838
875
|
}
|
|
839
876
|
|
|
840
877
|
case OP.JUMP_REG: {
|
|
841
|
-
// Indirect jump:
|
|
842
|
-
// bytecode immediate. Used by the jumpDispatcher pass so that static
|
|
843
|
-
// analysis cannot determine the jump destination without tracking the
|
|
844
|
-
// register value (which contains an encoded PC resolved at runtime).
|
|
878
|
+
// Indirect jump: allows VM to jump based on runtime values.
|
|
845
879
|
frame._pc = regs[base + this._operand()];
|
|
846
880
|
break;
|
|
847
881
|
}
|
|
@@ -857,9 +891,8 @@ VM.prototype.run = function () {
|
|
|
857
891
|
);
|
|
858
892
|
}
|
|
859
893
|
} catch (err) {
|
|
860
|
-
// Exception handler unwinding
|
|
861
|
-
// Walk from the current frame upward until we find a frame that has an open
|
|
862
|
-
// exception handler (TRY_SETUP without a matching TRY_END).
|
|
894
|
+
// Exception handler unwinding
|
|
895
|
+
// Walk from the current frame upward until we find a frame that has an open exception handler (TRY_SETUP without a matching TRY_END).
|
|
863
896
|
// For every frame we abandon along the way, close its captured upvalues.
|
|
864
897
|
var handledFrame = null;
|
|
865
898
|
var searchFrame = this._currentFrame;
|
|
@@ -876,7 +909,7 @@ VM.prototype.run = function () {
|
|
|
876
909
|
this._currentFrame = searchFrame;
|
|
877
910
|
}
|
|
878
911
|
|
|
879
|
-
if (!handledFrame) throw err; // no handler
|
|
912
|
+
if (!handledFrame) throw err; // if there's no handler, propagate back to host
|
|
880
913
|
|
|
881
914
|
var h = handledFrame._handlerStack.pop();
|
|
882
915
|
// Discard any call-frames that were pushed inside the try body.
|
|
@@ -891,7 +924,7 @@ VM.prototype.run = function () {
|
|
|
891
924
|
}
|
|
892
925
|
};
|
|
893
926
|
|
|
894
|
-
//
|
|
927
|
+
/* @BOOT */ // <- This comment can't be removed!
|
|
895
928
|
var globals: any = {}; // global object for globals
|
|
896
929
|
|
|
897
930
|
// Always pull built-ins from globalThis so eval() scoping can't shadow them
|
package/src/template.ts
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
import { Compiler } from "./compiler.ts";
|
|
49
49
|
import { DEFAULT_OPTIONS } from "./options.ts";
|
|
50
|
-
import type { Bytecode, Instruction } from "./types.ts";
|
|
50
|
+
import type { Bytecode, Instruction, RegisterOperand } from "./types.ts";
|
|
51
51
|
|
|
52
52
|
export class Template {
|
|
53
53
|
private readonly _source: string;
|
|
@@ -138,4 +138,128 @@ export class Template {
|
|
|
138
138
|
|
|
139
139
|
return { functions: innerDescs, bytecode: innerBytecode };
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
// ── Inline compilation ───────────────────────────────────────────────────
|
|
143
|
+
/**
|
|
144
|
+
* Compile the template and return the **main scope** bytecode, with all
|
|
145
|
+
* register operands remapped to belong to `targetFnId`. This allows
|
|
146
|
+
* bytecode transforms to express high-level JS control flow (while-loops,
|
|
147
|
+
* if-chains, variable declarations) via Template and splice the result
|
|
148
|
+
* directly into an existing function's instruction stream.
|
|
149
|
+
*
|
|
150
|
+
* The implicit trailing RETURN added by _compileFunctionDecl is stripped —
|
|
151
|
+
* inline code should flow into the surrounding bytecode, not return.
|
|
152
|
+
*
|
|
153
|
+
* @param variables Substitution map for {name} placeholders.
|
|
154
|
+
* @param parentCompiler The Compiler whose OP table, label counter, and
|
|
155
|
+
* fnDescriptors are shared.
|
|
156
|
+
* @param targetFnId The function whose register file the template's
|
|
157
|
+
* registers should be remapped into.
|
|
158
|
+
* @param maxId Live map of max register id per fnId — updated
|
|
159
|
+
* in-place as new registers are allocated.
|
|
160
|
+
*
|
|
161
|
+
* @returns
|
|
162
|
+
* bytecode — main-scope IR (no entry defineLabel, no trailing RETURN),
|
|
163
|
+
* ready to splice into the target function's instruction stream.
|
|
164
|
+
* registers — mapping of JS variable names → remapped RegisterOperands,
|
|
165
|
+
* so the caller can reference template-declared variables
|
|
166
|
+
* (e.g. the `state` variable in CFF).
|
|
167
|
+
* functions — inner function descriptors (same as compile()).
|
|
168
|
+
* innerBytecode — inner function bytecode blocks (same as compile()).
|
|
169
|
+
*/
|
|
170
|
+
compileInline(
|
|
171
|
+
variables: Record<string, string | number>,
|
|
172
|
+
parentCompiler: Compiler,
|
|
173
|
+
targetFnId: number,
|
|
174
|
+
maxId: Map<number, number>,
|
|
175
|
+
): {
|
|
176
|
+
bytecode: Bytecode;
|
|
177
|
+
registers: Map<string, RegisterOperand>;
|
|
178
|
+
functions: any[];
|
|
179
|
+
innerBytecode: Bytecode;
|
|
180
|
+
} {
|
|
181
|
+
const code = this._interpolate(variables);
|
|
182
|
+
|
|
183
|
+
const child = new Compiler({ ...DEFAULT_OPTIONS, randomizeOpcodes: false });
|
|
184
|
+
child.OP = { ...parentCompiler.OP };
|
|
185
|
+
child.OP_NAME = { ...parentCompiler.OP_NAME };
|
|
186
|
+
child.JUMP_OPS = new Set(parentCompiler.JUMP_OPS);
|
|
187
|
+
child._makeLabel = parentCompiler._makeLabel.bind(parentCompiler);
|
|
188
|
+
|
|
189
|
+
const startIdx = parentCompiler.fnDescriptors.length;
|
|
190
|
+
child.fnDescriptors = parentCompiler.fnDescriptors;
|
|
191
|
+
|
|
192
|
+
child.compile(code);
|
|
193
|
+
|
|
194
|
+
const mainDesc = parentCompiler.fnDescriptors[startIdx] as any;
|
|
195
|
+
const mainFnId: number = mainDesc._fnIdx;
|
|
196
|
+
const mainBc = mainDesc.bytecode as Bytecode;
|
|
197
|
+
|
|
198
|
+
// ── Remap registers from the template's main fnId → targetFnId ────────
|
|
199
|
+
// Build a mapping: old register id → new RegisterOperand in targetFnId.
|
|
200
|
+
const regRemap = new Map<number, RegisterOperand>();
|
|
201
|
+
const remapReg = (id: number): RegisterOperand => {
|
|
202
|
+
if (!regRemap.has(id)) {
|
|
203
|
+
const next = (maxId.get(targetFnId) ?? -1) + 1;
|
|
204
|
+
maxId.set(targetFnId, next);
|
|
205
|
+
regRemap.set(id, { type: "register", id: next, fnId: targetFnId });
|
|
206
|
+
}
|
|
207
|
+
return regRemap.get(id)!;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
for (const instr of mainBc) {
|
|
211
|
+
for (let j = 1; j < instr.length; j++) {
|
|
212
|
+
const op = instr[j] as any;
|
|
213
|
+
if (op && typeof op === "object" && op.type === "register" && op.fnId === mainFnId) {
|
|
214
|
+
const mapped = remapReg(op.id);
|
|
215
|
+
op.id = mapped.id;
|
|
216
|
+
op.fnId = mapped.fnId;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Build variable name → remapped register mapping ───────────────────
|
|
222
|
+
const registers = new Map<string, RegisterOperand>();
|
|
223
|
+
const locals: Map<string, RegisterOperand> = mainDesc.ctx.scope._locals;
|
|
224
|
+
for (const [name, reg] of locals) {
|
|
225
|
+
const mapped = regRemap.get(reg.id);
|
|
226
|
+
if (mapped) registers.set(name, mapped);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Strip entry defineLabel and trailing implicit RETURN ───────────────
|
|
230
|
+
let bytecode = mainBc.filter((instr) => {
|
|
231
|
+
const op0 = instr[1] as any;
|
|
232
|
+
return !(
|
|
233
|
+
instr[0] === null &&
|
|
234
|
+
op0?.type === "defineLabel" &&
|
|
235
|
+
op0.label === mainDesc.entryLabel
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Remove trailing LOAD_CONST undefined + RETURN (implicit return added
|
|
240
|
+
// by _compileFunctionDecl).
|
|
241
|
+
const OP = parentCompiler.OP;
|
|
242
|
+
if (
|
|
243
|
+
bytecode.length >= 2 &&
|
|
244
|
+
bytecode[bytecode.length - 1][0] === OP.RETURN &&
|
|
245
|
+
bytecode[bytecode.length - 2][0] === OP.LOAD_CONST
|
|
246
|
+
) {
|
|
247
|
+
bytecode = bytecode.slice(0, -2);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Inner function bytecode (same as compile()) ───────────────────────
|
|
251
|
+
const innerDescs = parentCompiler.fnDescriptors.slice(startIdx + 1);
|
|
252
|
+
const innerBytecode: Bytecode = [];
|
|
253
|
+
for (const desc of innerDescs) {
|
|
254
|
+
innerBytecode.push([
|
|
255
|
+
null,
|
|
256
|
+
{ type: "defineLabel", label: desc.entryLabel },
|
|
257
|
+
] as Instruction);
|
|
258
|
+
for (const instr of (desc as any).bytecode as Bytecode) {
|
|
259
|
+
innerBytecode.push(instr);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { bytecode, registers, functions: innerDescs, innerBytecode };
|
|
264
|
+
}
|
|
141
265
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Bytecode, InstrOperand, Instruction } from "../../types.ts";
|
|
2
|
-
import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
|
|
2
|
+
import { Compiler, OP_ORIGINAL, SOURCE_NODE_SYM } from "../../compiler.ts";
|
|
3
3
|
import { nextFreeSlot } from "../../utils/op-utils.ts";
|
|
4
4
|
import { shuffle } from "../../utils/random-utils.ts";
|
|
5
5
|
|
|
@@ -58,6 +58,9 @@ export function aliasedOpcodes(
|
|
|
58
58
|
const arity = instr.length - 1;
|
|
59
59
|
if (arity < 1) continue; // 0-operand opcodes have nothing to permute
|
|
60
60
|
|
|
61
|
+
const opName = compiler.OP_NAME[op];
|
|
62
|
+
if (!OP_ORIGINAL[opName]) continue; // only consider original ops, not already-specialized ones
|
|
63
|
+
|
|
61
64
|
const existing = opStats.get(op);
|
|
62
65
|
if (!existing) {
|
|
63
66
|
opStats.set(op, { freq: 1, arity });
|