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/template.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Template
|
|
2
|
+
// Compiles a JS code snippet into raw IR bytecode that can be spliced into the
|
|
3
|
+
// parent compiler's bytecode stream at any point before resolveRegisters /
|
|
4
|
+
// resolveLabels run.
|
|
5
|
+
//
|
|
6
|
+
// ── Usage ─────────────────────────────────────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// const tmpl = new Template(`
|
|
9
|
+
// function {name}(x, y) {
|
|
10
|
+
// return x + y;
|
|
11
|
+
// }
|
|
12
|
+
// `);
|
|
13
|
+
//
|
|
14
|
+
// const bc = tmpl.compile({ name: "myHelper" }, parentCompiler);
|
|
15
|
+
// result.push(...bc);
|
|
16
|
+
//
|
|
17
|
+
// ── How it works ──────────────────────────────────────────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// 1. {name} placeholders are replaced with the caller-supplied string values.
|
|
20
|
+
// 2. A fresh child Compiler is created, inheriting the parent's OP table so
|
|
21
|
+
// opcode numbers match exactly (including randomizeOpcodes mappings).
|
|
22
|
+
// 3. The child compiles the snippet to raw IR (no passes, no label/register
|
|
23
|
+
// resolution).
|
|
24
|
+
// 4. Post-processing makes the child's bytecode compatible with the parent:
|
|
25
|
+
//
|
|
26
|
+
// Labels — every label string is renamed via parentCompiler._makeLabel()
|
|
27
|
+
// so names never collide with existing or future labels.
|
|
28
|
+
//
|
|
29
|
+
// FnIds — the child's main scope (fnDescriptors[0]) is mapped to
|
|
30
|
+
// targetFnId (default 0). Any inner functions (closures
|
|
31
|
+
// declared inside the template) are appended to
|
|
32
|
+
// parentCompiler.fnDescriptors with fresh indices.
|
|
33
|
+
//
|
|
34
|
+
// 5. The main function's entry defineLabel is stripped from the output — it is
|
|
35
|
+
// a synthetic wrapper added by _compileMain and is not part of the injected
|
|
36
|
+
// code. All other instructions (including the implicit RETURN at the end of
|
|
37
|
+
// the main scope and any inner-function blocks) are returned as-is so the
|
|
38
|
+
// caller can append them wherever appropriate.
|
|
39
|
+
//
|
|
40
|
+
// ── Limitations (MVP) ─────────────────────────────────────────────────────────
|
|
41
|
+
// • Variables are plain string/number interpolation only — no AST-node
|
|
42
|
+
// substitution.
|
|
43
|
+
// • Templates that reference upvalue-captured registers from the call site are
|
|
44
|
+
// not supported (inner functions closing over template-local variables work).
|
|
45
|
+
// • Opcodes with no JS equivalent (JUMP_REG, BXOR used as decode, etc.) cannot
|
|
46
|
+
// be expressed in a template; write those instruction arrays manually.
|
|
47
|
+
|
|
48
|
+
import { Compiler } from "./compiler.ts";
|
|
49
|
+
import { DEFAULT_OPTIONS } from "./options.ts";
|
|
50
|
+
import type { Bytecode, Instruction } from "./types.ts";
|
|
51
|
+
|
|
52
|
+
export class Template {
|
|
53
|
+
private readonly _source: string;
|
|
54
|
+
|
|
55
|
+
constructor(source: string) {
|
|
56
|
+
this._source = source;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── String interpolation ──────────────────────────────────────────────────
|
|
60
|
+
private _interpolate(variables: Record<string, string | number>): string {
|
|
61
|
+
return this._source.replace(/\{(\w+)\}/g, (match, name) => {
|
|
62
|
+
if (!(name in variables)) {
|
|
63
|
+
throw new Error(`Template: missing variable {${name}}`);
|
|
64
|
+
}
|
|
65
|
+
return String(variables[name]);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Main entry point ───────────────────────────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Compile the template and return the inner (non-main) function descriptors
|
|
72
|
+
* and their bytecode blocks, ready to splice into the parent compiler's
|
|
73
|
+
* instruction stream.
|
|
74
|
+
*
|
|
75
|
+
* The template source should declare one or more named functions. The
|
|
76
|
+
* top-level ("main") scope of the template is discarded — it exists only as
|
|
77
|
+
* a syntactic wrapper so that function declarations parse correctly.
|
|
78
|
+
*
|
|
79
|
+
* Each inner function is registered in parentCompiler.fnDescriptors with a
|
|
80
|
+
* fresh fnIdx, and its bytecode block (defineLabel + body instructions) is
|
|
81
|
+
* returned so the caller can append it to the parent bytecode stream at the
|
|
82
|
+
* desired location (typically at the end, after all function bodies).
|
|
83
|
+
*
|
|
84
|
+
* @param variables Substitution map for {name} placeholders.
|
|
85
|
+
* @param parentCompiler The Compiler whose OP table, label counter, and
|
|
86
|
+
* fnDescriptors are shared.
|
|
87
|
+
*
|
|
88
|
+
* @returns
|
|
89
|
+
* functions — ordered list of inner FnDescriptors (index 0 = first named
|
|
90
|
+
* function in the template source). Use .entryLabel and
|
|
91
|
+
* ._fnIdx to build MAKE_CLOSURE operands.
|
|
92
|
+
* bytecode — IR bytecode blocks for all inner functions, ready to splice
|
|
93
|
+
* after the parent's function bodies. Does NOT include the
|
|
94
|
+
* template's main-scope instructions.
|
|
95
|
+
*/
|
|
96
|
+
compile(
|
|
97
|
+
variables: Record<string, string | number>,
|
|
98
|
+
parentCompiler: Compiler,
|
|
99
|
+
): { functions: any[]; bytecode: Bytecode } {
|
|
100
|
+
// ── 1. Interpolate ────────────────────────────────────────────────────
|
|
101
|
+
const code = this._interpolate(variables);
|
|
102
|
+
|
|
103
|
+
// ── 2. Create child compiler, inherit parent's OP table ───────────────
|
|
104
|
+
// randomizeOpcodes is disabled — we copy the parent's already-randomized
|
|
105
|
+
// mapping directly so all opcode numbers are identical.
|
|
106
|
+
const child = new Compiler({ ...DEFAULT_OPTIONS, randomizeOpcodes: false });
|
|
107
|
+
child.OP = { ...parentCompiler.OP };
|
|
108
|
+
child.OP_NAME = { ...parentCompiler.OP_NAME };
|
|
109
|
+
child.JUMP_OPS = new Set(parentCompiler.JUMP_OPS);
|
|
110
|
+
|
|
111
|
+
child._makeLabel = parentCompiler._makeLabel.bind(parentCompiler);
|
|
112
|
+
|
|
113
|
+
// Record how many descriptors the parent already has so we can find the
|
|
114
|
+
// child's main (index = startIdx) and inner functions (startIdx+1 …).
|
|
115
|
+
const startIdx = parentCompiler.fnDescriptors.length;
|
|
116
|
+
child.fnDescriptors = parentCompiler.fnDescriptors; // share — inner functions auto-register
|
|
117
|
+
|
|
118
|
+
// ── 3. Compile to raw IR (no passes) ──────────────────────────────────
|
|
119
|
+
child.compile(code);
|
|
120
|
+
|
|
121
|
+
// parentCompiler.fnDescriptors[startIdx] → child's main (discard)
|
|
122
|
+
// parentCompiler.fnDescriptors[startIdx+1…] → inner helper functions
|
|
123
|
+
const innerDescs = parentCompiler.fnDescriptors.slice(startIdx + 1);
|
|
124
|
+
|
|
125
|
+
// Build bytecode blocks for inner functions only.
|
|
126
|
+
// child.bytecode was assembled by _compileMain from ALL fnDescriptors
|
|
127
|
+
// starting at startIdx. We rebuild it here from the inner descs only.
|
|
128
|
+
const innerBytecode: Bytecode = [];
|
|
129
|
+
for (const desc of innerDescs) {
|
|
130
|
+
innerBytecode.push([
|
|
131
|
+
null,
|
|
132
|
+
{ type: "defineLabel", label: desc.entryLabel },
|
|
133
|
+
] as Instruction);
|
|
134
|
+
for (const instr of (desc as any).bytecode as Bytecode) {
|
|
135
|
+
innerBytecode.push(instr);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { functions: innerDescs, bytecode: innerBytecode };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Bytecode, InstrOperand, Instruction } from "../../types.ts";
|
|
2
2
|
import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
|
|
3
|
-
import { nextFreeSlot
|
|
3
|
+
import { nextFreeSlot } from "../../utils/op-utils.ts";
|
|
4
4
|
import { shuffle } from "../../utils/random-utils.ts";
|
|
5
5
|
|
|
6
6
|
// Opcodes that must not be aliased.
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// Routes simple unconditional and conditional jumps through a per-function
|
|
2
|
+
// central dispatcher block so that static analysis cannot read jump targets
|
|
3
|
+
// directly from the bytecode operands.
|
|
4
|
+
//
|
|
5
|
+
// ── How it works ─────────────────────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Each function that contains at least one routable jump gets:
|
|
8
|
+
//
|
|
9
|
+
// rDisp — a stable register shared across the whole function.
|
|
10
|
+
// At every jump site, the per-site encoded target PC is written
|
|
11
|
+
// here before jumping to the dispatcher block.
|
|
12
|
+
// rKey — a stable register written at every jump site with that site's
|
|
13
|
+
// unique XOR key. The dispatcher passes it to the decode closure.
|
|
14
|
+
// rClosure — holds the decode closure, created ONCE at function entry
|
|
15
|
+
// (hoisted). All dispatch calls reuse the same closure object.
|
|
16
|
+
//
|
|
17
|
+
// Dispatcher block (appended after the function body, never reached by fall-through):
|
|
18
|
+
//
|
|
19
|
+
// <dispatcher_N>:
|
|
20
|
+
// CALL rDisp, rClosure, 2, rDisp, rKey // rDisp = decode(rDisp, rKey)
|
|
21
|
+
// JUMP_REG rDisp // indirect jump to recovered PC
|
|
22
|
+
//
|
|
23
|
+
// The decode function is compiled ONCE PER FUNCTION from a Template that
|
|
24
|
+
// embeds a per-function constant (fnSalt). Every function gets its own
|
|
25
|
+
// distinct decode closure body, so identifying one does not help with others.
|
|
26
|
+
//
|
|
27
|
+
// function decode(x, k) { return ((x ^ k) + FN_SALT) & 0xFFFF; }
|
|
28
|
+
//
|
|
29
|
+
// Jump site transformations (each site has its own random siteKey):
|
|
30
|
+
//
|
|
31
|
+
// Original: JUMP target_label
|
|
32
|
+
// Becomes: LOAD_INT rDisp, (target_label_pc - fnSalt) ^ siteKey
|
|
33
|
+
// LOAD_INT rKey, siteKey
|
|
34
|
+
// JUMP <dispatcher_N>
|
|
35
|
+
//
|
|
36
|
+
// Original: JUMP_IF_FALSE cond, target_label
|
|
37
|
+
// Becomes: JUMP_IF_TRUE cond, <skip_N>
|
|
38
|
+
// LOAD_INT rDisp, (target_label_pc - fnSalt) ^ siteKey
|
|
39
|
+
// LOAD_INT rKey, siteKey
|
|
40
|
+
// JUMP <dispatcher_N>
|
|
41
|
+
// <skip_N>:
|
|
42
|
+
//
|
|
43
|
+
// Original: JUMP_IF_TRUE cond, target_label
|
|
44
|
+
// Becomes: JUMP_IF_FALSE cond, <skip_N>
|
|
45
|
+
// LOAD_INT rDisp, (target_label_pc - fnSalt) ^ siteKey
|
|
46
|
+
// LOAD_INT rKey, siteKey
|
|
47
|
+
// JUMP <dispatcher_N>
|
|
48
|
+
// <skip_N>:
|
|
49
|
+
//
|
|
50
|
+
// ── Encoding scheme ──────────────────────────────────────────────────────────
|
|
51
|
+
// Two-key mixed encoding: XOR (per-site) + SUB/ADD (per-function).
|
|
52
|
+
//
|
|
53
|
+
// encode(pc, siteKey, fnSalt) = (pc - fnSalt) ^ siteKey
|
|
54
|
+
// decode(x, k, fnSalt) = (x ^ k) + fnSalt
|
|
55
|
+
//
|
|
56
|
+
// The siteKey is a random nonzero u16 unique per jump site — stored as a plain
|
|
57
|
+
// integer operand in the bytecode.
|
|
58
|
+
// The fnSalt is a random nonzero u16 unique per function — it is never stored
|
|
59
|
+
// as an operand anywhere; it is compiled as a literal constant inside the
|
|
60
|
+
// function's own decode Template body.
|
|
61
|
+
//
|
|
62
|
+
// Attack resistance:
|
|
63
|
+
// • Brute-forcing a single jump requires enumerating siteKey × fnSalt
|
|
64
|
+
// (~4 billion combinations) rather than just siteKey (65 535).
|
|
65
|
+
// • Assuming pure XOR fails: un-XOR-ing with siteKey yields (pc - fnSalt),
|
|
66
|
+
// not pc. Valid-PC heuristics produce wrong answers.
|
|
67
|
+
// • Each function emits its own decode closure bytecode with a different
|
|
68
|
+
// fnSalt literal baked in. There is no shared signature to fingerprint.
|
|
69
|
+
// • The encode and decode operations differ structurally (SUB vs ADD),
|
|
70
|
+
// removing the self-inverse property that makes XOR-only schemes obvious.
|
|
71
|
+
//
|
|
72
|
+
// To change the scheme:
|
|
73
|
+
// 1. Change the Template source in processFunctionBlock() to match new decode.
|
|
74
|
+
// 2. Change applyEncoding() to return the matching encode transform.
|
|
75
|
+
// Only these two places need updating; everything else is scheme-agnostic.
|
|
76
|
+
//
|
|
77
|
+
// ── Pipeline position ─────────────────────────────────────────────────────────
|
|
78
|
+
// Runs BEFORE resolveRegisters (so injected RegisterOperands are picked up by
|
|
79
|
+
// liveness analysis) and BEFORE resolveLabels (so label operands with transforms
|
|
80
|
+
// are resolved as part of the normal label-resolution pass).
|
|
81
|
+
//
|
|
82
|
+
// Enabled by options.dispatcher = true.
|
|
83
|
+
|
|
84
|
+
import type {
|
|
85
|
+
Bytecode,
|
|
86
|
+
Instruction,
|
|
87
|
+
RegisterOperand,
|
|
88
|
+
InstrOperand,
|
|
89
|
+
} from "../../types.ts";
|
|
90
|
+
import * as b from "../../types.ts";
|
|
91
|
+
import { Compiler } from "../../compiler.ts";
|
|
92
|
+
import { getRandomInt } from "../../utils/random-utils.ts";
|
|
93
|
+
import { U16_MAX } from "../../utils/op-utils.ts";
|
|
94
|
+
import { Template } from "../../template.ts";
|
|
95
|
+
|
|
96
|
+
// VERY IMPORTANT: All object operands should be unique objects for the entire compilation process.
|
|
97
|
+
// This ensures that other passes that may reference/modify operands (e.g. specializedOpcodes) don't accidentally break behavior by mutating cloned objects.
|
|
98
|
+
function ref(r: RegisterOperand): RegisterOperand {
|
|
99
|
+
return b.registerOperand(r.id, r.fnId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Monotonically increasing counter that makes every encoded label operand
|
|
103
|
+
// JSON.stringify-distinguishable. specializedOpcodes keys candidates by
|
|
104
|
+
// JSON.stringify(operands), which drops the transform function. Without this
|
|
105
|
+
// counter, two LOAD_INT instructions for the same label but different siteKeys
|
|
106
|
+
// would serialize identically and be coalesced into one specialized opcode
|
|
107
|
+
// sharing a single operand object — causing both sites to decode with the
|
|
108
|
+
// first site's key rather than their own.
|
|
109
|
+
let _encodedLabelId = 0;
|
|
110
|
+
|
|
111
|
+
function encodedLabelOperand(
|
|
112
|
+
label: string,
|
|
113
|
+
siteKey: number,
|
|
114
|
+
fnSalt: number,
|
|
115
|
+
): InstrOperand {
|
|
116
|
+
return {
|
|
117
|
+
type: "label",
|
|
118
|
+
label,
|
|
119
|
+
_id: _encodedLabelId++, // unique per site — survives JSON.stringify
|
|
120
|
+
transform: (pc) => applyEncoding(pc, siteKey, fnSalt),
|
|
121
|
+
} as InstrOperand;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Encoding scheme (XOR + SUB/ADD, u16 modular) ────────────────────────────
|
|
125
|
+
// applyEncoding(pc, siteKey, fnSalt): the value stored in rDisp at the jump site.
|
|
126
|
+
// Must be the inverse of the decode function compiled by the Template.
|
|
127
|
+
// encode: ((pc - fnSalt) & 0xFFFF) ^ siteKey → always a valid u16
|
|
128
|
+
// decode: ((x ^ siteKey) + fnSalt) & 0xFFFF ← compiled into the per-function Template
|
|
129
|
+
// The & 0xFFFF mask keeps both sides in [0, 65535], preventing negative LOAD_INT operands.
|
|
130
|
+
function applyEncoding(pc: number, siteKey: number, fnSalt: number): number {
|
|
131
|
+
return ((pc - fnSalt) & U16_MAX) ^ siteKey;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Register allocation helpers ───────────────────────────────────────────────
|
|
135
|
+
// At pass time FnContext objects are gone; we allocate new virtual registers by
|
|
136
|
+
// scanning the bytecode for the highest existing id per fnId and incrementing.
|
|
137
|
+
function buildMaxIdMap(bc: Bytecode): Map<number, number> {
|
|
138
|
+
const maxId = new Map<number, number>();
|
|
139
|
+
for (const instr of bc) {
|
|
140
|
+
for (let j = 1; j < instr.length; j++) {
|
|
141
|
+
const op = instr[j] as any;
|
|
142
|
+
if (op && op.type === "register") {
|
|
143
|
+
const cur = maxId.get(op.fnId) ?? -1;
|
|
144
|
+
if (op.id > cur) maxId.set(op.fnId, op.id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return maxId;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Allocate a new virtual register for fnId, updating maxId in-place.
|
|
152
|
+
function allocReg(fnId: number, maxId: Map<number, number>): RegisterOperand {
|
|
153
|
+
const next = (maxId.get(fnId) ?? -1) + 1;
|
|
154
|
+
maxId.set(fnId, next);
|
|
155
|
+
return b.registerOperand(next, fnId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Label operand extraction ──────────────────────────────────────────────────
|
|
159
|
+
// Returns the label string if the operand is a { type:"label" } object,
|
|
160
|
+
// otherwise returns null. Used to identify routable jump targets.
|
|
161
|
+
function extractLabel(op: InstrOperand | undefined): string | null {
|
|
162
|
+
if (op && typeof op === "object" && (op as any).type === "label")
|
|
163
|
+
return (op as any).label as string;
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// buildDispatcherBlock: emits the dispatcher label + call + indirect jump.
|
|
168
|
+
// rClosure is already live (created at function entry); this block simply
|
|
169
|
+
// calls the decode closure and jumps to the result.
|
|
170
|
+
function buildDispatcherBlock(
|
|
171
|
+
compiler: Compiler,
|
|
172
|
+
rDisp: RegisterOperand,
|
|
173
|
+
rKey: RegisterOperand,
|
|
174
|
+
rClosure: RegisterOperand,
|
|
175
|
+
dispatcherLabel: string,
|
|
176
|
+
): Instruction[] {
|
|
177
|
+
const OP = compiler.OP;
|
|
178
|
+
return [
|
|
179
|
+
[null, { type: "defineLabel", label: dispatcherLabel }],
|
|
180
|
+
|
|
181
|
+
// decode(rDisp, rKey) → rDisp. Args are read before dst is written.
|
|
182
|
+
[
|
|
183
|
+
OP.CALL!,
|
|
184
|
+
ref(rDisp), // dst — receives decoded PC
|
|
185
|
+
ref(rClosure), // the hoisted decode closure
|
|
186
|
+
2, // argc
|
|
187
|
+
ref(rDisp), // arg[0] = encoded value
|
|
188
|
+
ref(rKey), // arg[1] = per-site key
|
|
189
|
+
],
|
|
190
|
+
|
|
191
|
+
[OP.JUMP_REG!, ref(rDisp)],
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Per-function transformation ───────────────────────────────────────────────
|
|
196
|
+
// Returns the transformed instruction stream and the template bytecode block
|
|
197
|
+
// for the per-function decode closure (to be appended at the end of the output).
|
|
198
|
+
function processFunctionBlock(
|
|
199
|
+
instrs: Bytecode,
|
|
200
|
+
fnId: number,
|
|
201
|
+
compiler: Compiler,
|
|
202
|
+
maxId: Map<number, number>,
|
|
203
|
+
labelCounter: () => string,
|
|
204
|
+
): { instrs: Bytecode; templateBytecode: Bytecode } {
|
|
205
|
+
const OP = compiler.OP;
|
|
206
|
+
|
|
207
|
+
// Only transform functions that actually contain simple jumps.
|
|
208
|
+
const hasRoutableJump = instrs.some((instr) => {
|
|
209
|
+
const op = instr[0];
|
|
210
|
+
return op === OP.JUMP || op === OP.JUMP_IF_FALSE || op === OP.JUMP_IF_TRUE;
|
|
211
|
+
});
|
|
212
|
+
if (!hasRoutableJump) return { instrs, templateBytecode: [] };
|
|
213
|
+
|
|
214
|
+
// Per-function salt baked into this function's decode Template.
|
|
215
|
+
// Never stored as an operand — lives only inside the decode closure body.
|
|
216
|
+
const fnSalt = getRandomInt(1, U16_MAX);
|
|
217
|
+
|
|
218
|
+
// Compile a unique decode closure for this function.
|
|
219
|
+
// The fnSalt literal is inlined into the source so each function's closure
|
|
220
|
+
// body is structurally distinct; no single signature covers all functions.
|
|
221
|
+
const tmpl = new Template(
|
|
222
|
+
`function decode(x, k) { return ((x ^ k) + ${fnSalt}) & ${U16_MAX}; }`,
|
|
223
|
+
).compile({}, compiler);
|
|
224
|
+
const decodeDesc = tmpl.functions[0];
|
|
225
|
+
|
|
226
|
+
const dispatcherLabel = labelCounter();
|
|
227
|
+
const rDisp = allocReg(fnId, maxId); // carries encoded PC to dispatcher
|
|
228
|
+
const rKey = allocReg(fnId, maxId); // carries per-site key to dispatcher
|
|
229
|
+
const rClosure = allocReg(fnId, maxId); // holds the hoisted decode closure
|
|
230
|
+
|
|
231
|
+
const out: Bytecode = [];
|
|
232
|
+
|
|
233
|
+
// ── Hoist: create the decode closure once at function entry ───────────────
|
|
234
|
+
out.push([
|
|
235
|
+
OP.MAKE_CLOSURE!,
|
|
236
|
+
ref(rClosure),
|
|
237
|
+
{ type: "label", label: decodeDesc.entryLabel },
|
|
238
|
+
decodeDesc.paramCount, // 2 (x, k)
|
|
239
|
+
b.fnRegCountOperand(decodeDesc._fnIdx), // resolved by resolveRegisters()
|
|
240
|
+
0, // no upvalues
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
// ── Transform each instruction ────────────────────────────────────────────
|
|
244
|
+
for (const instr of instrs) {
|
|
245
|
+
const op = instr[0];
|
|
246
|
+
|
|
247
|
+
if (op === OP.JUMP) {
|
|
248
|
+
// [JUMP, label] → [LOAD_INT rDisp, encoded] + [LOAD_INT rKey, siteKey] + [JUMP dispatcher]
|
|
249
|
+
const targetLabel = extractLabel(instr[1]);
|
|
250
|
+
if (targetLabel === null) {
|
|
251
|
+
out.push(instr);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const siteKey = getRandomInt(1, U16_MAX);
|
|
256
|
+
out.push([
|
|
257
|
+
OP.LOAD_INT!,
|
|
258
|
+
ref(rDisp),
|
|
259
|
+
encodedLabelOperand(targetLabel, siteKey, fnSalt),
|
|
260
|
+
]);
|
|
261
|
+
out.push([OP.LOAD_INT!, ref(rKey), siteKey]);
|
|
262
|
+
out.push([OP.JUMP!, { type: "label", label: dispatcherLabel }]);
|
|
263
|
+
} else if (op === OP.JUMP_IF_FALSE) {
|
|
264
|
+
// Invert to JUMP_IF_TRUE so the false path (jump taken) falls into dispatch.
|
|
265
|
+
const cond = instr[1] as RegisterOperand;
|
|
266
|
+
const targetLabel = extractLabel(instr[2]);
|
|
267
|
+
if (targetLabel === null) {
|
|
268
|
+
out.push(instr);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const siteKey = getRandomInt(1, U16_MAX);
|
|
273
|
+
const skipLabel = compiler._makeLabel(targetLabel + "_skip");
|
|
274
|
+
out.push([OP.JUMP_IF_TRUE!, cond, { type: "label", label: skipLabel }]);
|
|
275
|
+
out.push([
|
|
276
|
+
OP.LOAD_INT!,
|
|
277
|
+
ref(rDisp),
|
|
278
|
+
encodedLabelOperand(targetLabel, siteKey, fnSalt),
|
|
279
|
+
]);
|
|
280
|
+
out.push([OP.LOAD_INT!, ref(rKey), siteKey]);
|
|
281
|
+
out.push([OP.JUMP!, { type: "label", label: dispatcherLabel }]);
|
|
282
|
+
out.push([null, { type: "defineLabel", label: skipLabel }]);
|
|
283
|
+
} else if (op === OP.JUMP_IF_TRUE) {
|
|
284
|
+
// Invert to JUMP_IF_FALSE so the true path (jump taken) falls into dispatch.
|
|
285
|
+
const cond = instr[1] as RegisterOperand;
|
|
286
|
+
const targetLabel = extractLabel(instr[2]);
|
|
287
|
+
if (targetLabel === null) {
|
|
288
|
+
out.push(instr);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const siteKey = getRandomInt(1, U16_MAX);
|
|
293
|
+
const skipLabel = compiler._makeLabel(targetLabel + "_skip");
|
|
294
|
+
out.push([OP.JUMP_IF_FALSE!, cond, { type: "label", label: skipLabel }]);
|
|
295
|
+
out.push([
|
|
296
|
+
OP.LOAD_INT!,
|
|
297
|
+
ref(rDisp),
|
|
298
|
+
encodedLabelOperand(targetLabel, siteKey, fnSalt),
|
|
299
|
+
]);
|
|
300
|
+
out.push([OP.LOAD_INT!, ref(rKey), siteKey]);
|
|
301
|
+
out.push([OP.JUMP!, { type: "label", label: dispatcherLabel }]);
|
|
302
|
+
out.push([null, { type: "defineLabel", label: skipLabel }]);
|
|
303
|
+
} else {
|
|
304
|
+
out.push(instr);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Dispatcher block appended after the function body. Never reached by
|
|
309
|
+
// fall-through; all entries are via the JUMP dispatcher instructions above.
|
|
310
|
+
out.push(
|
|
311
|
+
...buildDispatcherBlock(compiler, rDisp, rKey, rClosure, dispatcherLabel),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
return { instrs: out, templateBytecode: tmpl.bytecode };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Pass entry point ──────────────────────────────────────────────────────────
|
|
318
|
+
export function dispatcher(
|
|
319
|
+
bc: Bytecode,
|
|
320
|
+
compiler: Compiler,
|
|
321
|
+
): { bytecode: Bytecode } {
|
|
322
|
+
// Pre-compute max virtual register id per function across the whole bytecode.
|
|
323
|
+
const maxId = buildMaxIdMap(bc);
|
|
324
|
+
|
|
325
|
+
// Label factory that delegates to the compiler's own counter so labels
|
|
326
|
+
// produced here never collide with compiler-generated or pass-generated ones.
|
|
327
|
+
const labelCounter = () => compiler._makeLabel("dispatcher");
|
|
328
|
+
|
|
329
|
+
// Build a set of entry labels so we can detect function boundaries.
|
|
330
|
+
const entryLabels = new Set(compiler.fnDescriptors.map((d) => d.entryLabel));
|
|
331
|
+
// Build a map from entry label → fnId.
|
|
332
|
+
const entryLabelToFnId = new Map(
|
|
333
|
+
compiler.fnDescriptors.map((d) => [d.entryLabel!, d._fnIdx!]),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const result: Bytecode = [];
|
|
337
|
+
// Collect each function's decode Template bytecode; appended at the end so
|
|
338
|
+
// all MAKE_CLOSURE instructions can reference their entryLabels regardless
|
|
339
|
+
// of where in the bytecode the function appears.
|
|
340
|
+
const decodeBytecodes: Bytecode[] = [];
|
|
341
|
+
let i = 0;
|
|
342
|
+
|
|
343
|
+
while (i < bc.length) {
|
|
344
|
+
const instr = bc[i];
|
|
345
|
+
const [op, operand0] = instr;
|
|
346
|
+
const isEntryLabel =
|
|
347
|
+
op === null &&
|
|
348
|
+
(operand0 as any)?.type === "defineLabel" &&
|
|
349
|
+
entryLabels.has((operand0 as any).label);
|
|
350
|
+
|
|
351
|
+
if (!isEntryLabel) {
|
|
352
|
+
result.push(instr);
|
|
353
|
+
i++;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Found a function entry label. Collect all instructions belonging to
|
|
358
|
+
// this function (until the next entry label or end of bytecode).
|
|
359
|
+
const entryLabel = (operand0 as any).label as string;
|
|
360
|
+
const fnId = entryLabelToFnId.get(entryLabel)!;
|
|
361
|
+
i++; // step past the defineLabel itself
|
|
362
|
+
|
|
363
|
+
const fnInstrs: Bytecode = [];
|
|
364
|
+
while (i < bc.length) {
|
|
365
|
+
const next = bc[i];
|
|
366
|
+
const [nextOp, nextOp0] = next;
|
|
367
|
+
if (
|
|
368
|
+
nextOp === null &&
|
|
369
|
+
(nextOp0 as any)?.type === "defineLabel" &&
|
|
370
|
+
entryLabels.has((nextOp0 as any).label)
|
|
371
|
+
)
|
|
372
|
+
break; // next function starts here
|
|
373
|
+
fnInstrs.push(next);
|
|
374
|
+
i++;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Emit the entry defineLabel, then the (potentially transformed) body.
|
|
378
|
+
result.push(instr); // the defineLabel
|
|
379
|
+
const { instrs: processed, templateBytecode } = processFunctionBlock(
|
|
380
|
+
fnInstrs,
|
|
381
|
+
fnId,
|
|
382
|
+
compiler,
|
|
383
|
+
maxId,
|
|
384
|
+
labelCounter,
|
|
385
|
+
);
|
|
386
|
+
result.push(...processed);
|
|
387
|
+
if (templateBytecode.length > 0) decodeBytecodes.push(templateBytecode);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Append all per-function decode closure bodies at the end of the bytecode.
|
|
391
|
+
// Each block defines the entryLabel that the corresponding MAKE_CLOSURE
|
|
392
|
+
// instruction references.
|
|
393
|
+
for (const tb of decodeBytecodes) {
|
|
394
|
+
result.push(...tb);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { bytecode: result };
|
|
398
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Bytecode, Instruction } from "../../types.ts";
|
|
2
2
|
import { Compiler, SOURCE_NODE_SYM } from "../../compiler.ts";
|
|
3
|
-
import { nextFreeSlot
|
|
4
|
-
import { ok } from "
|
|
3
|
+
import { nextFreeSlot } from "../../utils/op-utils.ts";
|
|
4
|
+
import { ok } from "assert";
|
|
5
5
|
|
|
6
6
|
// Opcodes that must not appear in a non-terminal position inside a macro window.
|
|
7
7
|
// Jump ops: modifying frame._pc mid-execution causes the macro handler to
|
|
@@ -35,6 +35,7 @@ export function resolveLabels(
|
|
|
35
35
|
for (const instr of bc) {
|
|
36
36
|
const op = instr[0];
|
|
37
37
|
const operand = instr[1];
|
|
38
|
+
|
|
38
39
|
if (
|
|
39
40
|
op === null &&
|
|
40
41
|
operand !== null &&
|
|
@@ -42,11 +43,14 @@ export function resolveLabels(
|
|
|
42
43
|
(operand as any).type === "defineLabel"
|
|
43
44
|
) {
|
|
44
45
|
labelToPc.set((operand as any).label, realPc);
|
|
45
|
-
|
|
46
|
-
// Each instruction occupies 1 slot for the opcode + 1 per operand.
|
|
47
|
-
// IMPORTANT: 'placeholder' operands are not counted
|
|
48
|
-
realPc += instr.filter((x) => (x as any)?.placeholder !== true).length;
|
|
46
|
+
continue;
|
|
49
47
|
}
|
|
48
|
+
|
|
49
|
+
if (op === null) continue; // "null" opcodes are never emitted
|
|
50
|
+
|
|
51
|
+
// Each instruction occupies 1 slot for the opcode + 1 per operand.
|
|
52
|
+
// IMPORTANT: 'placeholder' operands are not counted
|
|
53
|
+
realPc += instr.filter((x) => (x as any)?.placeholder !== true).length;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
// Pass 2 – build the resolved instruction list.
|
|
@@ -55,44 +59,44 @@ export function resolveLabels(
|
|
|
55
59
|
for (const instr of bc) {
|
|
56
60
|
const [op, ...operands] = instr;
|
|
57
61
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
(operands[0] as any)?.type === "defineLabel"
|
|
63
|
-
) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Replace label-ref operands with their resolved flat PC (any position).
|
|
62
|
+
// Replace label-ref and encodedLabel operands with resolved flat PCs.
|
|
63
|
+
// encodedLabel applies an encoding to the PC before emission so that raw
|
|
64
|
+
// jump targets are hidden; the dispatcher block reverses it at runtime.
|
|
65
|
+
// To change the encoding scheme, update both here and in jumpDispatcher.ts.
|
|
68
66
|
const newOperands = operands.map((operand) => {
|
|
69
67
|
if (
|
|
70
|
-
operand
|
|
71
|
-
operand
|
|
72
|
-
typeof operand
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
operand === undefined ||
|
|
69
|
+
operand === null ||
|
|
70
|
+
typeof operand !== "object"
|
|
71
|
+
)
|
|
72
|
+
return operand;
|
|
73
|
+
|
|
74
|
+
const type = (operand as any).type;
|
|
75
|
+
|
|
76
|
+
if (type === "label") {
|
|
75
77
|
const pc = labelToPc.get((operand as any).label);
|
|
76
78
|
if (pc === undefined)
|
|
77
79
|
throw new Error(`Undefined label: ${(operand as any).label}`);
|
|
78
80
|
|
|
81
|
+
let resolvedValue = pc + ((operand as any).offset ?? 0);
|
|
82
|
+
if ((operand as any).transform) {
|
|
83
|
+
resolvedValue = (operand as any).transform(resolvedValue);
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
const newOperand = {
|
|
80
87
|
type: "number",
|
|
81
|
-
resolvedValue:
|
|
88
|
+
resolvedValue: resolvedValue,
|
|
82
89
|
};
|
|
83
|
-
|
|
84
|
-
// Mutate original object so that references are also updated
|
|
85
|
-
if (typeof operand === "object" && operand !== null) {
|
|
86
|
-
return Object.assign(operand, newOperand);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return newOperand;
|
|
90
|
+
return Object.assign(operand, newOperand);
|
|
90
91
|
}
|
|
92
|
+
|
|
91
93
|
return operand;
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
const newInstr = [op, ...newOperands];
|
|
95
97
|
(newInstr as any)[SOURCE_NODE_SYM] = (instr as any)[SOURCE_NODE_SYM];
|
|
98
|
+
|
|
99
|
+
// Pseudo-op "defineLabel"s are kept within this bytecode as the Serializer is responsible for dropping it, and its useful information for comment generation
|
|
96
100
|
resolved.push(newInstr);
|
|
97
101
|
}
|
|
98
102
|
|