js-confuser-vm 0.0.9 → 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.
Files changed (64) hide show
  1. package/.gitmodules +4 -0
  2. package/CHANGELOG.md +125 -2
  3. package/README.md +128 -53
  4. package/bench.ts +146 -0
  5. package/disassemble.ts +12 -0
  6. package/dist/build-runtime.js +41 -15
  7. package/dist/compiler.js +328 -181
  8. package/dist/disassembler.js +317 -0
  9. package/dist/index.js +7 -2
  10. package/dist/runtime.js +255 -176
  11. package/dist/template.js +258 -0
  12. package/dist/transforms/bytecode/aliasedOpcodes.js +4 -1
  13. package/dist/transforms/bytecode/controlFlowFlattening.js +451 -0
  14. package/dist/transforms/bytecode/dispatcher.js +266 -0
  15. package/dist/transforms/bytecode/macroOpcodes.js +3 -3
  16. package/dist/transforms/bytecode/resolveConstants.js +100 -0
  17. package/dist/transforms/bytecode/resolveLabels.js +21 -18
  18. package/dist/transforms/bytecode/resolveRegisters.js +216 -0
  19. package/dist/transforms/bytecode/semanticOpcodes.js +162 -0
  20. package/dist/transforms/bytecode/specializedOpcodes.js +22 -12
  21. package/dist/transforms/bytecode/stringConcealing.js +110 -0
  22. package/dist/transforms/runtime/classObfuscation.js +43 -0
  23. package/dist/transforms/runtime/handlerTable.js +91 -0
  24. package/dist/transforms/runtime/semanticOpcodes.js +35 -0
  25. package/dist/transforms/runtime/specializedOpcodes.js +11 -5
  26. package/dist/types.js +42 -1
  27. package/dist/utils/ast-utils.js +14 -0
  28. package/dist/utils/op-utils.js +1 -2
  29. package/dist/utils/pass-utils.js +100 -0
  30. package/dist/utils/profile-utils.js +3 -0
  31. package/index.ts +22 -16
  32. package/jest.config.js +19 -2
  33. package/output.disassembled.js +41 -0
  34. package/package.json +2 -1
  35. package/src/build-runtime.ts +113 -78
  36. package/src/compiler.ts +2703 -2482
  37. package/src/disassembler.ts +329 -0
  38. package/src/index.ts +12 -2
  39. package/src/options.ts +8 -1
  40. package/src/runtime.ts +294 -180
  41. package/src/template.ts +265 -0
  42. package/src/transforms/bytecode/aliasedOpcodes.ts +5 -2
  43. package/src/transforms/bytecode/controlFlowFlattening.ts +566 -0
  44. package/src/transforms/bytecode/dispatcher.ts +292 -0
  45. package/src/transforms/bytecode/macroOpcodes.ts +4 -4
  46. package/src/transforms/bytecode/resolveLabels.ts +31 -27
  47. package/src/transforms/bytecode/resolveRegisters.ts +226 -0
  48. package/src/transforms/bytecode/specializedOpcodes.ts +27 -20
  49. package/src/transforms/bytecode/stringConcealing.ts +130 -0
  50. package/src/transforms/runtime/classObfuscation.ts +59 -0
  51. package/src/transforms/runtime/specializedOpcodes.ts +14 -9
  52. package/src/types.ts +106 -5
  53. package/src/utils/ast-utils.ts +19 -0
  54. package/src/utils/op-utils.ts +2 -2
  55. package/src/utils/pass-utils.ts +126 -0
  56. package/src/utils/profile-utils.ts +3 -0
  57. package/tsconfig.json +1 -1
  58. package/dist/transforms/utils/op-utils.js +0 -25
  59. package/dist/transforms/utils/random-utils.js +0 -27
  60. package/dist/utilts.js +0 -3
  61. package/src/transforms/bytecode/microOpcodes.ts +0 -291
  62. package/src/transforms/runtime/internalVariables.ts +0 -270
  63. package/src/transforms/runtime/microOpcodes.ts +0 -93
  64. /package/src/transforms/bytecode/{resolveContants.ts → resolveConstants.ts} +0 -0
@@ -1,12 +1,6 @@
1
1
  import type { Bytecode, InstrOperand, Instruction } from "../../types.ts";
2
2
  import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
3
- import {
4
- getInstructionSize,
5
- nextFreeSlot,
6
- U16_MAX,
7
- } from "../../utils/op-utils.ts";
8
- import * as t from "@babel/types";
9
- import * as b from "../../types.ts";
3
+ import { getInstructionSize, nextFreeSlot } from "../../utils/op-utils.ts";
10
4
 
11
5
  export const nSizedOps = [
12
6
  "MAKE_CLOSURE",
@@ -19,9 +13,11 @@ export const nSizedOps = [
19
13
 
20
14
  // Creates specialized opcodes for the most frequent (OPCODE + single_integer_operand) pairs.
21
15
  // Example: [OP.LOAD_CONST, 1] becomes [SPECIALIZED_LOAD_CONST_1].
22
- // Only instructions with *exactly one numeric operand* are considered.
16
+ // Only instructions that are fixed-sized are considered.
23
17
  // MAKE_CLOSURE and other N-sized instructions cannot be specialized
24
- // Runs after selfModifying but before resolveLabels (operands stay plain numbers).
18
+ // Operands are converted into objects and marked as 'placeholder' - other passes can mutate and the reference stays intact
19
+ // We need a reference throughout the pipeline so that final AST generation can place the actual value
20
+ // The 'placeholder' flag drops the operand from the final bytecode - any size calculation must not count these
25
21
  export function specializedOpcodes(
26
22
  bc: Bytecode,
27
23
  compiler: Compiler,
@@ -39,6 +35,8 @@ export function specializedOpcodes(
39
35
  }
40
36
  >();
41
37
 
38
+ const instrToOperandKey = new WeakMap<Instruction, string>();
39
+
42
40
  for (const instr of bc) {
43
41
  const op = instr[0];
44
42
  if (op === null || disallowedOps.has(op)) continue;
@@ -49,21 +47,26 @@ export function specializedOpcodes(
49
47
 
50
48
  // Convert numbers into operand objects so they can be modified elsewhere and preserved
51
49
  const oldOperands = instr.slice(1);
52
- const operands = oldOperands.map((operand) => {
50
+
51
+ let operands = [];
52
+
53
+ for (const operand of oldOperands) {
53
54
  if (typeof operand === "number") {
54
- return {
55
+ operands.push({
55
56
  type: "number",
56
57
  value: operand,
57
58
  resolvedValue: operand,
58
- } as InstrOperand;
59
+ } as InstrOperand);
60
+ } else {
61
+ operands.push(operand as InstrOperand);
59
62
  }
60
- return operand;
61
- });
63
+ }
62
64
 
63
65
  instr.length = 1;
64
66
  instr.push(...operands);
65
67
 
66
68
  const operandsKey = JSON.stringify(operands);
69
+ instrToOperandKey.set(instr, operandsKey);
67
70
 
68
71
  const key = `${op},${operandsKey}`;
69
72
  const entry = freqMap.get(key);
@@ -82,7 +85,8 @@ export function specializedOpcodes(
82
85
  // ── Step 2: keep combinations that appear >= 2 times, sort by frequency ───
83
86
  const candidates = Array.from(freqMap.values())
84
87
  .filter((e) => e.occurences >= 1)
85
- .sort((a, b) => b.occurences - a.occurences);
88
+ .sort((a, b) => b.occurences - a.occurences)
89
+ .slice(0, 1000);
86
90
 
87
91
  if (candidates.length === 0) return { bytecode: bc };
88
92
 
@@ -90,10 +94,10 @@ export function specializedOpcodes(
90
94
  const sigToSpecial = new Map<string, number>();
91
95
  const specializedOps: Compiler["SPECIALIZED_OPS"] = {};
92
96
 
93
- for (let i = 0; i < candidates.length; i++) {
97
+ for (const candidate of candidates) {
94
98
  const specialOp = nextFreeSlot(compiler);
95
99
  if (specialOp === -1) break;
96
- const { op: originalOp, operands, operandsKey } = candidates[i];
100
+ const { op: originalOp, operands, operandsKey } = candidate;
97
101
 
98
102
  const key = `${originalOp},${operandsKey}`;
99
103
  sigToSpecial.set(key, specialOp);
@@ -102,8 +106,7 @@ export function specializedOpcodes(
102
106
 
103
107
  // Register a human-readable name for disassembly / debugging
104
108
  const originalName = compiler.OP_NAME[originalOp] ?? `OP_${originalOp}`;
105
- compiler.OP_NAME[specialOp] =
106
- `${originalName}_${JSON.stringify(operandsKey)}`;
109
+ compiler.OP_NAME[specialOp] = `${originalName}_${operandsKey}`;
107
110
  }
108
111
 
109
112
  // Store mapping so the interpreter knows how to dispatch the specialized op
@@ -121,7 +124,11 @@ export function specializedOpcodes(
121
124
  }
122
125
 
123
126
  const operands = instr.slice(1);
124
- const operandsKey = JSON.stringify(operands);
127
+ const operandsKey = instrToOperandKey.get(instr);
128
+ if (!operandsKey) {
129
+ result.push(instr);
130
+ continue;
131
+ }
125
132
 
126
133
  const key = `${op},${operandsKey}`;
127
134
 
@@ -0,0 +1,130 @@
1
+ // String Concealing
2
+ //
3
+ // Encodes every string constant in each function with base64, then inserts a
4
+ // decode closure (atob) that is called immediately after each LOAD_CONST to
5
+ // recover the original value at runtime.
6
+ //
7
+ // ── How it works ─────────────────────────────────────────────────────────────
8
+ //
9
+ // Each function that contains at least one string LOAD_CONST gets:
10
+ //
11
+ // rClosure — a register holding the decode closure, created ONCE at function
12
+ // entry (hoisted). All decode calls within the function reuse it.
13
+ //
14
+ // The decode function is compiled ONCE (shared across all functions) from a
15
+ // Template:
16
+ //
17
+ // function decode(encoded) { return atob(encoded); }
18
+ //
19
+ // String constant transformations:
20
+ //
21
+ // Original: LOAD_CONST rDst, "hello"
22
+ // Becomes: LOAD_CONST rDst, "aGVsbG8=" (base64-encoded)
23
+ // CALL rDst, rClosure, 1, rDst (decode in-place)
24
+ //
25
+ // ── Pipeline position ─────────────────────────────────────────────────────────
26
+ // Runs BEFORE resolveRegisters and resolveLabels (same slot as Dispatcher/CFF).
27
+
28
+ import { Compiler } from "../../compiler.ts";
29
+ import { Template } from "../../template.ts";
30
+ import type { Bytecode, RegisterOperand } from "../../types.ts";
31
+ import * as b from "../../types.ts";
32
+ import { ref, buildMaxIdMap, allocReg, forEachFunction } from "../../utils/pass-utils.ts";
33
+
34
+ // ── Per-function transformation ──────────────────────────────────────────────
35
+
36
+ function processFunctionBlock(
37
+ instrs: Bytecode,
38
+ fnId: number,
39
+ compiler: Compiler,
40
+ maxId: Map<number, number>,
41
+ decodeDesc: any,
42
+ ): { instrs: Bytecode } {
43
+ const OP = compiler.OP;
44
+
45
+ // Only transform functions that contain string constants.
46
+ const hasStringConst = instrs.some((instr) => {
47
+ if (instr[0] !== OP.LOAD_CONST) return false;
48
+ const operands = instr.slice(1);
49
+ return (
50
+ operands.length === 2 &&
51
+ (operands[1] as any)?.type === "constant" &&
52
+ typeof (operands[1] as any).value === "string"
53
+ );
54
+ });
55
+ if (!hasStringConst) return { instrs };
56
+
57
+ const rClosure = allocReg(fnId, maxId);
58
+ const out: Bytecode = [];
59
+
60
+ // Hoist: create the decode closure once at function entry.
61
+ out.push([
62
+ OP.MAKE_CLOSURE!,
63
+ ref(rClosure),
64
+ { type: "label", label: decodeDesc.entryLabel },
65
+ decodeDesc.paramCount, // 1 (encoded)
66
+ b.fnRegCountOperand(decodeDesc._fnIdx),
67
+ 0, // no upvalues
68
+ 0, // hasRest = false
69
+ ]);
70
+
71
+ // Transform each instruction.
72
+ for (const instr of instrs) {
73
+ if (
74
+ instr[0] === OP.LOAD_CONST &&
75
+ instr.length === 3 &&
76
+ (instr[2] as any)?.type === "constant" &&
77
+ typeof (instr[2] as any).value === "string"
78
+ ) {
79
+ const dst = instr[1] as RegisterOperand;
80
+ const constOp = instr[2] as any;
81
+
82
+ // Encode the string in-place.
83
+ constOp.value = Buffer.from(constOp.value as string, "utf-8").toString(
84
+ "base64",
85
+ );
86
+
87
+ out.push(instr);
88
+
89
+ // Decode: rDst = decode(rDst)
90
+ out.push([
91
+ OP.CALL!,
92
+ ref(dst), // dst — receives decoded string
93
+ ref(rClosure), // the hoisted decode closure
94
+ 1, // argc
95
+ ref(dst), // arg[0] = encoded value
96
+ ]);
97
+ } else {
98
+ out.push(instr);
99
+ }
100
+ }
101
+
102
+ return { instrs: out };
103
+ }
104
+
105
+ // ── Pass entry point ──────────────────────────────────────────────────────────
106
+ export function stringConcealing(
107
+ bc: Bytecode,
108
+ compiler: Compiler,
109
+ ): { bytecode: Bytecode } {
110
+ const maxId = buildMaxIdMap(bc);
111
+
112
+ // Compile the decode function ONCE — all functions share the same closure body.
113
+ const decodeTemplate = new Template(`
114
+ function decode(encoded) {
115
+ return atob(encoded);
116
+ }
117
+ `).compile({}, compiler);
118
+ const decodeDesc = decodeTemplate.functions[0];
119
+
120
+ const { bytecode } = forEachFunction(bc, compiler, (fnInstrs, fnId) =>
121
+ processFunctionBlock(fnInstrs, fnId, compiler, maxId, decodeDesc),
122
+ );
123
+
124
+ // Append the decode function's bytecode at the end (defines its entryLabel).
125
+ // This is a single shared closure, not per-function, so it lives outside
126
+ // forEachFunction's tail mechanism.
127
+ bytecode.push(...decodeTemplate.bytecode);
128
+
129
+ return { bytecode };
130
+ }
@@ -0,0 +1,59 @@
1
+ import { Compiler } from "../../compiler.ts";
2
+ import * as t from "@babel/types";
3
+ import { shuffle } from "../../utils/random-utils.ts";
4
+
5
+ function hasComment(node: t.Node, text: string): boolean {
6
+ const all = [
7
+ ...((node as any).leadingComments ?? []),
8
+ ...((node as any).innerComments ?? []),
9
+ ...((node as any).trailingComments ?? []),
10
+ ];
11
+ return all.some((c) => c.value.includes(text));
12
+ }
13
+
14
+ function isPrototypeAssignment(stmt: t.Statement): boolean {
15
+ if (!t.isExpressionStatement(stmt)) return false;
16
+ const expr = stmt.expression;
17
+ if (!t.isAssignmentExpression(expr)) return false;
18
+ const left = expr.left;
19
+ return (
20
+ t.isMemberExpression(left) &&
21
+ t.isMemberExpression(left.object) &&
22
+ t.isIdentifier((left.object as t.MemberExpression).property, {
23
+ name: "prototype",
24
+ })
25
+ );
26
+ }
27
+
28
+ export function applyClassObfuscation(ast: t.File, _compiler: Compiler): void {
29
+ const body = ast.program.body;
30
+
31
+ // Split at the first statement that carries the @BOOT comment.
32
+ // Everything from that statement onward is the boot section and must stay last.
33
+ let bootIdx = body.findIndex((stmt) => hasComment(stmt, "@BOOT"));
34
+ if (bootIdx === -1) bootIdx = body.length;
35
+
36
+ const shufflable = body.slice(0, bootIdx);
37
+ const boot = body.slice(bootIdx);
38
+
39
+ // Partition the shufflable section into two independent groups.
40
+ // Group A: variable/function declarations (constructors, standalone vars).
41
+ // Group B: prototype method assignments (X.prototype.Y = ...).
42
+ // Both groups are shuffled independently; A always precedes B so that
43
+ // constructors are defined before methods reference them.
44
+ const varDecls: t.Statement[] = [];
45
+ const methodDefs: t.Statement[] = [];
46
+
47
+ for (const stmt of shufflable) {
48
+ if (isPrototypeAssignment(stmt)) {
49
+ methodDefs.push(stmt);
50
+ } else {
51
+ varDecls.push(stmt);
52
+ }
53
+ }
54
+
55
+ shuffle(varDecls);
56
+ shuffle(methodDefs);
57
+
58
+ ast.program.body = [...varDecls, ...methodDefs, ...boot];
59
+ }
@@ -27,6 +27,8 @@ function extractCaseBody(switchCase: t.SwitchCase): t.Statement[] {
27
27
  // *exactly one* numeric operand, every `_operand()` call inside the original
28
28
  // handler is replaced by the constant value that was baked into the opcode.
29
29
  function inlineFixedOperands(
30
+ newName: string, // for debugging
31
+ info: any,
30
32
  bodyStmts: t.Statement[],
31
33
  resolvedValues: number[],
32
34
  ): void {
@@ -66,10 +68,12 @@ function inlineFixedOperands(
66
68
  },
67
69
  });
68
70
 
69
- ok(
70
- replaced === resolvedValues.length,
71
- `Expected to replace ${resolvedValues.length} operands, but replaced ${replaced}`,
72
- );
71
+ if (replaced !== resolvedValues.length) {
72
+ console.error(resolvedValues, info);
73
+ throw new Error(
74
+ `Specialized Opcode Inline Error: Given ${resolvedValues.length} operands to replace, but only found ${replaced} for ${newName}`,
75
+ );
76
+ }
73
77
  }
74
78
 
75
79
  // Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
@@ -119,12 +123,13 @@ export function applySpecializedOpcodes(ast: t.File, compiler: Compiler): void {
119
123
  const placedOperands = info.operands;
120
124
  ok(placedOperands, `Could not find operand for original opcode ${newName}`);
121
125
 
122
- const resolvedValues = placedOperands.map((placedOperand) => {
123
- return (placedOperand as any)?.resolvedValue ?? placedOperand;
124
- });
126
+ const resolvedValues = placedOperands
127
+ // .filter((x) => !(x as any)?.placeholder)
128
+ .map((placedOperand) => {
129
+ return (placedOperand as any)?.resolvedValue ?? placedOperand;
130
+ });
125
131
 
126
132
  if (resolvedValues.find((v) => typeof v !== "number")) {
127
- console.error(info);
128
133
  throw new Error("Expected all resolved operand values to be numbers");
129
134
  }
130
135
 
@@ -132,7 +137,7 @@ export function applySpecializedOpcodes(ast: t.File, compiler: Compiler): void {
132
137
  compiler.OP_NAME[specialOpCode] = newName;
133
138
 
134
139
  // Replace this._operand() with the baked-in constant
135
- inlineFixedOperands(bodyStmts, resolvedValues);
140
+ inlineFixedOperands(newName, info, bodyStmts, resolvedValues);
136
141
 
137
142
  // Add a leading comment so the generated source stays readable
138
143
  if (bodyStmts.length > 0) {
package/src/types.ts CHANGED
@@ -1,22 +1,68 @@
1
1
  // Bytecode supports both real instructions and IR pseudo-instructions
2
- // Real instruction: [OP.ADD, 5] or multi-operand: [OP.MAKE_CLOSURE, labelRef, 2, 3, 0]
2
+ // Real instruction: [OP.ADD, 5] or multi-operand: [OP.MAKE_CLOSURE, labelRef, 2, 3, 0, 0]
3
3
  // IR instruction: [null, { type: "defineLabel", label: "FN_ENTRY_1" }]
4
4
 
5
5
  // IR instructions are used to hold symbolic information during compilation
6
6
  // All "null" instructions are dropped before assembly time.
7
7
  // Instructions may carry any number of operands; the flat output serializes
8
8
  // each operand as a separate u16 slot in the bytecode array.
9
+ // A virtual register reference emitted by the compiler.
10
+ // fnId identifies which function's register file this belongs to.
11
+ // resolveRegisters() replaces these with concrete slot indices (type:"number").
12
+ export type RegisterOperand = Op<{
13
+ type: "register";
14
+ id: number;
15
+ fnId: number;
16
+ kind?: string;
17
+ scopeId?: string | number;
18
+ // If true, resolveRegisters always assigns this register to the "local::"
19
+ // pool (no slot reuse). Set by passes that emit registers whose live ranges
20
+ // span CFF dispatch-loop back-edges — regions the linear-scan liveness
21
+ // analysis cannot reason about.
22
+ pinned?: boolean;
23
+ }>;
24
+
25
+ // A placeholder for a function's concrete regCount, emitted in MAKE_CLOSURE.
26
+ // resolveRegisters() fills resolvedValue once it knows the concrete slot count.
27
+ export type FnRegCountOperand = Op<{ type: "fnRegCount"; fnId: number }>;
28
+
29
+ // IR pseudo-instruction that marks the end of a register's live range.
30
+ // Emitted as [null, FreeRegOperand] so it is dropped before final assembly.
31
+ //
32
+ // NOTE: resolveRegisters() already computes correct lastUse from the last real
33
+ // operand appearance, so freeReg is EXTRANEOUS for any programmatically generated
34
+ // IR — the scanner will find the tightest possible range without it.
35
+ // It is only useful when a register has a late syntactic appearance that does
36
+ // NOT reflect its true logical end-of-life (e.g. a read emitted purely for
37
+ // side-effects long after the value is logically dead). No current pass in this
38
+ // codebase emits freeReg; it is kept as an extension point only.
39
+ export type FreeRegOperand = Op<{
40
+ type: "freeReg";
41
+ fnId: number;
42
+ id: number;
43
+ kind?: string;
44
+ scopeId?: string | number;
45
+ }>;
46
+
9
47
  export type InstrOperand =
10
48
  | number
11
49
  | Op<{ type: "number"; value?: number }>
12
- | Op<{ type: "label"; label: string; offset?: number }>
50
+ | Op<{
51
+ type: "label";
52
+ label: string;
53
+ offset?: number;
54
+ transform?: (resolvedPC: number) => number;
55
+ }>
13
56
  | Op<{ type: "defineLabel"; label: string }>
14
- | Op<{ type: "constant"; value: any }>;
57
+ | Op<{ type: "constant"; value: any }>
58
+ | RegisterOperand
59
+ | FnRegCountOperand
60
+ | FreeRegOperand;
15
61
 
16
62
  export interface Operand {
17
63
  type: string;
18
- placeholder?: boolean;
19
- resolvedValue?: number;
64
+ placeholder?: boolean; // This operand will not be emitted in the final bytecode, but used as a reference
65
+ resolvedValue?: number; // This operand knows its resolved value, but kept as a object to keep metadata info available
20
66
  }
21
67
 
22
68
  type Op<T extends object> = Operand & T;
@@ -31,3 +77,58 @@ export function constantOperand(value: any): Instruction[1] {
31
77
  value: value,
32
78
  };
33
79
  }
80
+
81
+ export function registerOperand(
82
+ id: number,
83
+ fnId: number,
84
+ metadata: Partial<Pick<RegisterOperand, "kind" | "scopeId">> = {},
85
+ ): RegisterOperand {
86
+ return { type: "register", id, fnId, ...metadata };
87
+ }
88
+
89
+ export function fnRegCountOperand(fnId: number): FnRegCountOperand {
90
+ return { type: "fnRegCount", fnId };
91
+ }
92
+
93
+ export function freeRegOperand(reg: RegisterOperand): FreeRegOperand {
94
+ const op: FreeRegOperand = { type: "freeReg", fnId: reg.fnId, id: reg.id };
95
+ if (reg.kind !== undefined) op.kind = reg.kind;
96
+ if (reg.scopeId !== undefined) op.scopeId = reg.scopeId;
97
+ return op;
98
+ }
99
+
100
+ export interface ObfuscationResult {
101
+ code: string;
102
+ profileData?: {
103
+ handlerCount: number;
104
+
105
+ /**
106
+ * How long the entire obfuscation process takes (ms)
107
+ */
108
+ obfuscationTime?: number;
109
+
110
+ /**
111
+ * How long @babel/parser takes (ms)
112
+ */
113
+ parseTime?: number;
114
+
115
+ /**
116
+ * How long @babel/generator takes (ms)
117
+ */
118
+ generateTime?: number;
119
+
120
+ /**
121
+ * How long the Compiler#compileAST takes (ms)
122
+ */
123
+ compileTime?: number;
124
+
125
+ transforms: {
126
+ [transformName: string]: {
127
+ fileSize?: number;
128
+ bytecodeSize?: number;
129
+ transformTime?: number;
130
+ handlerCount?: number;
131
+ };
132
+ };
133
+ };
134
+ }
@@ -0,0 +1,19 @@
1
+ import * as t from "@babel/types";
2
+ import traverseImport from "@babel/traverse";
3
+
4
+ const traverse = (traverseImport.default ||
5
+ traverseImport) as typeof traverseImport.default;
6
+
7
+ export function getSwitchStatement(ast: t.File) {
8
+ let switchStatement: t.SwitchStatement | null = null;
9
+ traverse(ast, {
10
+ SwitchStatement(path) {
11
+ if (path.node.leadingComments?.some((c) => c.value.includes("@SWITCH"))) {
12
+ switchStatement = path.node;
13
+ path.stop();
14
+ }
15
+ },
16
+ });
17
+
18
+ return switchStatement;
19
+ }
@@ -21,7 +21,6 @@ export function nextFreeSlot(compiler: Compiler): number {
21
21
  while (attempts++ < 512) {
22
22
  const candidate = getRandomInt(0, U16_MAX);
23
23
  if (!usedOpcodes.has(candidate)) {
24
- usedOpcodes.add(candidate);
25
24
  return candidate;
26
25
  }
27
26
  }
@@ -32,7 +31,6 @@ export function nextFreeSlot(compiler: Compiler): number {
32
31
  for (let i = 0; i <= U16_MAX; i++) {
33
32
  const v = (start + i) & U16_MAX;
34
33
  if (!usedOpcodes.has(v)) {
35
- usedOpcodes.add(v);
36
34
  return v;
37
35
  }
38
36
  }
@@ -40,6 +38,8 @@ export function nextFreeSlot(compiler: Compiler): number {
40
38
  }
41
39
 
42
40
  export function getInstructionSize(instr: b.Instruction): number {
41
+ if (instr[0] === null) return 0;
42
+
43
43
  const size = instr.filter((op) => (op as any)?.placeholder !== true).length;
44
44
 
45
45
  return size;
@@ -0,0 +1,126 @@
1
+ // Shared utilities for bytecode transformation passes.
2
+ //
3
+ // All three patterns below are identical across dispatcher, controlFlowFlattening,
4
+ // and stringConcealing. Centralising them here keeps each pass focused on its
5
+ // own logic and makes the shared contract explicit.
6
+
7
+ import type { Bytecode, RegisterOperand, InstrOperand } from "../types.ts";
8
+ import * as b from "../types.ts";
9
+ import { Compiler } from "../compiler.ts";
10
+
11
+ // Return a fresh RegisterOperand object with the same (id, fnId).
12
+ // IMPORTANT: operand objects must be unique throughout compilation —
13
+ // other passes (e.g. specializedOpcodes) mutate operands in-place and a
14
+ // shared reference would corrupt both sites.
15
+ export function ref(r: RegisterOperand): RegisterOperand {
16
+ return b.registerOperand(r.id, r.fnId);
17
+ }
18
+
19
+ // Scan bc and return the highest virtual register id seen for each fnId.
20
+ // Used by passes that allocate new registers after the compiler has finished.
21
+ export function buildMaxIdMap(bc: Bytecode): Map<number, number> {
22
+ const maxId = new Map<number, number>();
23
+ for (const instr of bc) {
24
+ for (let j = 1; j < instr.length; j++) {
25
+ const op = instr[j] as any;
26
+ if (op && op.type === "register") {
27
+ const cur = maxId.get(op.fnId) ?? -1;
28
+ if (op.id > cur) maxId.set(op.fnId, op.id);
29
+ }
30
+ }
31
+ }
32
+ return maxId;
33
+ }
34
+
35
+ // Allocate the next virtual register id for fnId, updating maxId in-place.
36
+ export function allocReg(
37
+ fnId: number,
38
+ maxId: Map<number, number>,
39
+ ): RegisterOperand {
40
+ const next = (maxId.get(fnId) ?? -1) + 1;
41
+ maxId.set(fnId, next);
42
+ return b.registerOperand(next, fnId);
43
+ }
44
+
45
+ // Return the label string if the operand is a { type:"label" } object,
46
+ // otherwise return null. Used by passes that need to identify jump targets.
47
+ export function extractLabel(op: InstrOperand | undefined): string | null {
48
+ if (op && typeof op === "object" && (op as any).type === "label")
49
+ return (op as any).label as string;
50
+ return null;
51
+ }
52
+
53
+ // Walk bc, call transform() for every function body, and reassemble the output.
54
+ //
55
+ // For each function entry label the scanner collects all instructions up to the
56
+ // next entry label (or end-of-bytecode) into fnInstrs and passes them to
57
+ // transform() along with the function's fnId.
58
+ //
59
+ // The transform callback returns:
60
+ // instrs — the (possibly rewritten) function body to emit in place of fnInstrs
61
+ // tail — optional bytecode to append AFTER all function bodies
62
+ // (e.g. template-compiled decode closures)
63
+ //
64
+ // Instructions that appear before any entry label (the top-level preamble) are
65
+ // passed through unchanged.
66
+ export function forEachFunction(
67
+ bc: Bytecode,
68
+ compiler: Compiler,
69
+ transform: (
70
+ fnInstrs: Bytecode,
71
+ fnId: number,
72
+ ) => { instrs: Bytecode; tail?: Bytecode },
73
+ ): { bytecode: Bytecode } {
74
+ const entryLabels = new Set(compiler.fnDescriptors.map((d) => d.entryLabel));
75
+ const entryLabelToFnId = new Map(
76
+ compiler.fnDescriptors.map((d) => [d.entryLabel!, d._fnIdx!]),
77
+ );
78
+
79
+ const result: Bytecode = [];
80
+ const tails: Bytecode[] = [];
81
+ let i = 0;
82
+
83
+ while (i < bc.length) {
84
+ const instr = bc[i];
85
+ const [op, operand0] = instr;
86
+ const isEntryLabel =
87
+ op === null &&
88
+ (operand0 as any)?.type === "defineLabel" &&
89
+ entryLabels.has((operand0 as any).label);
90
+
91
+ if (!isEntryLabel) {
92
+ result.push(instr);
93
+ i++;
94
+ continue;
95
+ }
96
+
97
+ const entryLabel = (operand0 as any).label as string;
98
+ const fnId = entryLabelToFnId.get(entryLabel)!;
99
+ i++; // step past the defineLabel itself
100
+
101
+ const fnInstrs: Bytecode = [];
102
+ while (i < bc.length) {
103
+ const next = bc[i];
104
+ const [nextOp, nextOp0] = next;
105
+ if (
106
+ nextOp === null &&
107
+ (nextOp0 as any)?.type === "defineLabel" &&
108
+ entryLabels.has((nextOp0 as any).label)
109
+ )
110
+ break;
111
+ fnInstrs.push(next);
112
+ i++;
113
+ }
114
+
115
+ result.push(instr); // emit the entry defineLabel
116
+ const { instrs, tail } = transform(fnInstrs, fnId);
117
+ result.push(...instrs);
118
+ if (tail && tail.length > 0) tails.push(tail);
119
+ }
120
+
121
+ for (const tail of tails) {
122
+ result.push(...tail);
123
+ }
124
+
125
+ return { bytecode: result };
126
+ }
@@ -0,0 +1,3 @@
1
+ export function now() {
2
+ return performance?.now() || Date.now();
3
+ }
package/tsconfig.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "rootDir": "./",
7
7
  "strict": false,
8
8
  "noEmit": true,
9
- "types": ["node"],
9
+ "types": ["node", "jest"],
10
10
  "allowImportingTsExtensions": true
11
11
  }
12
12
  }