js-confuser-vm 0.0.1 → 0.0.3
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 +28 -0
- package/README.MD +197 -0
- package/babel-plugin-inline-runtime.cjs +34 -0
- package/babel.config.json +23 -0
- package/dist/compiler.js +1771 -0
- package/dist/index.js +10 -0
- package/dist/minify.js +18 -0
- package/dist/options.js +1 -0
- package/dist/random.js +27 -0
- package/dist/runtime.js +755 -0
- package/dist/runtimeObf.js +56 -0
- package/dist/transforms/controlFlowFlattening.js +22 -0
- package/dist/transforms/resolveContants.js +33 -0
- package/dist/transforms/resolveLabels.js +59 -0
- package/dist/transforms/selfModifying.js +107 -0
- package/dist/types.js +13 -0
- package/dist/utilts.js +3 -0
- package/index.ts +17 -12
- package/jest.config.js +26 -5
- package/package.json +13 -6
- package/src/compiler.ts +1122 -673
- package/src/index.ts +14 -0
- package/src/minify.ts +21 -0
- package/src/options.ts +12 -0
- package/src/random.ts +31 -0
- package/src/runtime.ts +609 -461
- package/src/runtimeObf.ts +62 -0
- package/src/transforms/controlFlowFlattening.ts +30 -0
- package/src/transforms/resolveContants.ts +42 -0
- package/src/transforms/resolveLabels.ts +83 -0
- package/src/transforms/selfModifying.ts +124 -0
- package/src/types.ts +24 -0
- package/src/utilts.ts +3 -0
- package/.claude/settings.local.json +0 -8
- package/ReadME.MD +0 -164
- package/input.js +0 -15
- package/minify.js +0 -17
- package/minify_empty_externs.js +0 -4
- package/obfuscate.js +0 -12
- package/src/index.js +0 -5
- package/src/random.js +0 -3
package/src/compiler.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
2
|
import traverseImport from "@babel/traverse";
|
|
3
3
|
import { generate } from "@babel/generator";
|
|
4
4
|
|
|
@@ -6,33 +6,53 @@ import { readFileSync } from "fs";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { stripTypeScriptTypes } from "module";
|
|
8
8
|
import JSON5 from "json5";
|
|
9
|
+
import * as t from "@babel/types";
|
|
10
|
+
import { ok } from "assert";
|
|
11
|
+
import { obfuscateRuntime } from "./runtimeObf.ts";
|
|
12
|
+
import { DEFAULT_OPTIONS, type Options } from "./options.ts";
|
|
13
|
+
import { resolveLabels } from "./transforms/resolveLabels.ts";
|
|
14
|
+
import { resolveConstants } from "./transforms/resolveContants.ts";
|
|
15
|
+
import { selfModifying } from "./transforms/selfModifying.ts";
|
|
16
|
+
import * as b from "./types.ts";
|
|
17
|
+
|
|
18
|
+
const traverse = (traverseImport.default ||
|
|
19
|
+
traverseImport) as typeof traverseImport.default;
|
|
20
|
+
|
|
21
|
+
const readVMRuntimeFile = () => {
|
|
22
|
+
let code;
|
|
23
|
+
try {
|
|
24
|
+
code = readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
code = readFileSync(join(import.meta.dirname, "./runtime.js"), "utf-8");
|
|
27
|
+
}
|
|
9
28
|
|
|
10
|
-
|
|
29
|
+
return stripTypeScriptTypes?.(code) || code;
|
|
30
|
+
};
|
|
11
31
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
32
|
+
const VM_RUNTIME = readVMRuntimeFile().split("@START")[1];
|
|
33
|
+
export const SOURCE_NODE_SYM = Symbol("SOURCE_NODE"); // Attach source node location to pseudo bytecode instructions
|
|
14
34
|
|
|
15
|
-
//
|
|
16
|
-
const OP_ORIGINAL = {
|
|
35
|
+
// Opcodes
|
|
36
|
+
export const OP_ORIGINAL = {
|
|
17
37
|
LOAD_CONST: 0,
|
|
18
38
|
LOAD_LOCAL: 1,
|
|
19
39
|
STORE_LOCAL: 2,
|
|
20
40
|
LOAD_GLOBAL: 3,
|
|
21
41
|
STORE_GLOBAL: 4,
|
|
22
42
|
GET_PROP: 5,
|
|
23
|
-
ADD: 6,
|
|
24
|
-
SUB: 7,
|
|
25
|
-
MUL: 8,
|
|
26
|
-
DIV: 9,
|
|
43
|
+
ADD: 6, // a + b (both are popped)
|
|
44
|
+
SUB: 7, // a - b
|
|
45
|
+
MUL: 8, // a * b
|
|
46
|
+
DIV: 9, // a / b
|
|
27
47
|
MAKE_CLOSURE: 10,
|
|
28
48
|
CALL: 11,
|
|
29
49
|
CALL_METHOD: 12,
|
|
30
50
|
RETURN: 13,
|
|
31
|
-
POP: 14,
|
|
32
|
-
LT: 15, // pop b, pop a
|
|
33
|
-
GT: 16,
|
|
34
|
-
EQ: 17,
|
|
35
|
-
JUMP: 18, // unconditional
|
|
51
|
+
POP: 14, // discard top of stack
|
|
52
|
+
LT: 15, // pop b, pop a -> push (a < b)
|
|
53
|
+
GT: 16, // pop b, pop a -> push (a > b)
|
|
54
|
+
EQ: 17, // pop b, pop a -> push (a === b)
|
|
55
|
+
JUMP: 18, // unconditional - operand = absolute bytecode index
|
|
36
56
|
JUMP_IF_FALSE: 19, // pop value; jump if falsy
|
|
37
57
|
LTE: 20, // a <= b
|
|
38
58
|
GTE: 21, // a >= b
|
|
@@ -40,19 +60,19 @@ const OP_ORIGINAL = {
|
|
|
40
60
|
LOAD_UPVALUE: 23, // push frame.closure.upvalues[operand].read()
|
|
41
61
|
STORE_UPVALUE: 24, // frame.closure.upvalues[operand].write(pop())
|
|
42
62
|
|
|
43
|
-
//
|
|
63
|
+
// Unary
|
|
44
64
|
UNARY_NEG: 25, // -x
|
|
45
65
|
UNARY_POS: 26, // +x
|
|
46
66
|
UNARY_NOT: 27, // !x
|
|
47
67
|
UNARY_BITNOT: 28, // ~x
|
|
48
68
|
TYPEOF: 29, // typeof x
|
|
49
|
-
VOID: 30, // void x
|
|
69
|
+
VOID: 30, // void x -> always undefined
|
|
50
70
|
|
|
51
|
-
TYPEOF_SAFE: 31, // operand = name constIdx
|
|
52
|
-
BUILD_ARRAY: 32, // operand = element count
|
|
53
|
-
BUILD_OBJECT: 33, // operand = pair count
|
|
54
|
-
SET_PROP: 34, // pop val, pop key, peek obj
|
|
55
|
-
GET_PROP_COMPUTED: 35, // pop key, peek obj
|
|
71
|
+
TYPEOF_SAFE: 31, // operand = name constIdx - typeof guard for undeclared globals
|
|
72
|
+
BUILD_ARRAY: 32, // operand = element count - pops N values -> pushes array
|
|
73
|
+
BUILD_OBJECT: 33, // operand = pair count - pops N*2 (key,val) -> pushes object
|
|
74
|
+
SET_PROP: 34, // pop val, pop key, peek obj -> obj[key] = val (obj stays on stack)
|
|
75
|
+
GET_PROP_COMPUTED: 35, // pop key, peek obj -> push obj[key] (computed: nums[i])
|
|
56
76
|
|
|
57
77
|
MOD: 36, // a % b
|
|
58
78
|
BAND: 37, // a & b
|
|
@@ -62,91 +82,50 @@ const OP_ORIGINAL = {
|
|
|
62
82
|
SHR: 41, // a >> b
|
|
63
83
|
USHR: 42, // a >>> b
|
|
64
84
|
|
|
65
|
-
JUMP_IF_FALSE_OR_POP: 43, // &&
|
|
66
|
-
JUMP_IF_TRUE_OR_POP: 44, // ||
|
|
85
|
+
JUMP_IF_FALSE_OR_POP: 43, // && - if top falsy: jump (keep), else: pop, eval RHS
|
|
86
|
+
JUMP_IF_TRUE_OR_POP: 44, // || - if top truthy: jump (keep), else: pop, eval RHS
|
|
67
87
|
|
|
68
88
|
DELETE_PROP: 45,
|
|
69
89
|
IN: 46, // a in b
|
|
70
90
|
INSTANCEOF: 47, // a instanceof b
|
|
71
91
|
|
|
72
|
-
//
|
|
92
|
+
// NEW
|
|
73
93
|
LOAD_THIS: 48, // push frame.thisVal
|
|
74
|
-
NEW: 49, // operand = argCount
|
|
94
|
+
NEW: 49, // operand = argCount - construct a new object
|
|
75
95
|
DUP: 50, // duplicate top of stack
|
|
76
96
|
THROW: 51, // pop value, throw it
|
|
77
97
|
LOOSE_EQ: 52, // a == b (abstract equality)
|
|
78
98
|
LOOSE_NEQ: 53, // a != b (abstract inequality)
|
|
79
99
|
|
|
80
|
-
FOR_IN_SETUP: 54, // pop obj
|
|
81
|
-
FOR_IN_NEXT: 55, // operand=exit_pc; pop iter; if done
|
|
100
|
+
FOR_IN_SETUP: 54, // pop obj -> build enumerable-key iterator -> push {keys,i}
|
|
101
|
+
FOR_IN_NEXT: 55, // operand=exit_pc; pop iter; if done->jump; else push next key
|
|
82
102
|
|
|
83
|
-
//
|
|
103
|
+
// Self-modifying bytecode
|
|
84
104
|
PATCH: 56, // pop destPc; constants[operand]=word[]; write words into bytecode[destPc..]
|
|
85
|
-
};
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
let used = new Set();
|
|
91
|
-
for (const key in OP_ORIGINAL) {
|
|
92
|
-
let val;
|
|
93
|
-
do {
|
|
94
|
-
val = Math.floor(Math.random() * 256);
|
|
95
|
-
} while (used.has(val));
|
|
96
|
-
used.add(val);
|
|
97
|
-
OP[key] = val;
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
OP = OP_ORIGINAL;
|
|
101
|
-
}
|
|
106
|
+
// Try-Catch
|
|
107
|
+
TRY_SETUP: 57, // operand = catch_pc; push exception handler onto frame._handlerStack
|
|
108
|
+
TRY_END: 58, // pop exception handler (normal exit from try body)
|
|
102
109
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const JUMP_OPS = new Set([
|
|
107
|
-
OP.JUMP,
|
|
108
|
-
OP.JUMP_IF_FALSE,
|
|
109
|
-
OP.JUMP_IF_TRUE_OR_POP,
|
|
110
|
-
OP.JUMP_IF_FALSE_OR_POP,
|
|
111
|
-
OP.FOR_IN_NEXT,
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
// ─────────────────────────────────────────────────────────────────
|
|
115
|
-
// Constant Pool
|
|
116
|
-
// Primitives (string/number/bool) are interned (deduped).
|
|
117
|
-
// Object entries (fn descriptors) are always appended — no dedup.
|
|
118
|
-
// ─────────────────────────────────────────────────────────────────
|
|
119
|
-
class ConstantPool {
|
|
120
|
-
items: any[];
|
|
121
|
-
_index: Map<string, number>;
|
|
122
|
-
|
|
123
|
-
constructor() {
|
|
124
|
-
this.items = []; // ordered pool entries
|
|
125
|
-
this._index = new Map(); // primitive dedup map
|
|
126
|
-
}
|
|
110
|
+
// Getter / Setter (ES5 object literal accessor syntax)
|
|
111
|
+
DEFINE_GETTER: 59, // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {get: fn})
|
|
112
|
+
DEFINE_SETTER: 60, // pop fn, pop key, pop obj -> Object.defineProperty(obj, key, {set: fn})
|
|
127
113
|
|
|
128
|
-
|
|
129
|
-
// Only intern primitives — objects must use addObject()
|
|
130
|
-
const key = `${typeof val}:${val}`;
|
|
131
|
-
if (this._index.has(key)) return this._index.get(key);
|
|
132
|
-
const idx = this.items.length;
|
|
133
|
-
this.items.push(val);
|
|
134
|
-
this._index.set(key, idx);
|
|
135
|
-
return idx;
|
|
136
|
-
}
|
|
114
|
+
DEBUGGER: 61, // for dev/testing -- emits a "debugger" statement with a comment of the original source location
|
|
137
115
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
116
|
+
// Push the raw integer operand directly onto the stack (no constant pool lookup).
|
|
117
|
+
// Identical pipeline to JUMP ops: {type:"label"} pseudo-operands resolve to a
|
|
118
|
+
// raw PC number that becomes the operand, which is pushed as-is at runtime.
|
|
119
|
+
LOAD_INT: 62,
|
|
120
|
+
|
|
121
|
+
// Reserved / unused opcode slot (formerly the inline DATA header word).
|
|
122
|
+
// Kept to avoid renumbering; should never appear in compiled output.
|
|
123
|
+
DATA: 63,
|
|
124
|
+
};
|
|
144
125
|
|
|
145
|
-
// ─────────────────────────────────────────────────────────────────
|
|
146
126
|
// Scope
|
|
147
127
|
// Each function call gets its own Scope. Locals are resolved to
|
|
148
|
-
// numeric slots at compile time
|
|
149
|
-
// ─────────────────────────────────────────────────────────────────
|
|
128
|
+
// numeric slots at compile time -- zero name lookups at runtime.
|
|
150
129
|
class Scope {
|
|
151
130
|
parent: Scope | null;
|
|
152
131
|
_locals: Map<string, number>;
|
|
@@ -154,7 +133,7 @@ class Scope {
|
|
|
154
133
|
|
|
155
134
|
constructor(parent = null) {
|
|
156
135
|
this.parent = parent;
|
|
157
|
-
this._locals = new Map(); // name
|
|
136
|
+
this._locals = new Map(); // name -> slot index
|
|
158
137
|
this._next = 0;
|
|
159
138
|
}
|
|
160
139
|
|
|
@@ -165,7 +144,7 @@ class Scope {
|
|
|
165
144
|
return this._locals.get(name);
|
|
166
145
|
}
|
|
167
146
|
|
|
168
|
-
// Walk up scope chain. If we fall off the top
|
|
147
|
+
// Walk up scope chain. If we fall off the top -> global.
|
|
169
148
|
resolve(name) {
|
|
170
149
|
if (this._locals.has(name)) {
|
|
171
150
|
return { kind: "local", slot: this._locals.get(name) };
|
|
@@ -179,17 +158,15 @@ class Scope {
|
|
|
179
158
|
}
|
|
180
159
|
}
|
|
181
160
|
|
|
182
|
-
// ─────────────────────────────────────────────────────────────────
|
|
183
161
|
// FnContext
|
|
184
162
|
// Compiler-side state for the function currently being compiled.
|
|
185
|
-
// Distinct from runtime Frame
|
|
186
|
-
// ─────────────────────────────────────────────────────────────────
|
|
163
|
+
// Distinct from runtime Frame -- this is compile-time only.
|
|
187
164
|
class FnContext {
|
|
188
165
|
upvalues: { name: string; isLocal: number; index: number }[];
|
|
189
166
|
parentCtx: FnContext | null;
|
|
190
167
|
scope: Scope;
|
|
191
168
|
compiler: Compiler;
|
|
192
|
-
bc:
|
|
169
|
+
bc: b.Instruction[];
|
|
193
170
|
|
|
194
171
|
constructor(compiler, parentCtx = null) {
|
|
195
172
|
this.compiler = compiler;
|
|
@@ -201,8 +178,8 @@ class FnContext {
|
|
|
201
178
|
}
|
|
202
179
|
|
|
203
180
|
// Find or register a captured variable as an upvalue.
|
|
204
|
-
// isLocal=true
|
|
205
|
-
// isLocal=false
|
|
181
|
+
// isLocal=true -> captured directly from parent's locals[index]
|
|
182
|
+
// isLocal=false -> relayed from parent's own upvalue list[index]
|
|
206
183
|
addUpvalue(name, isLocal, index) {
|
|
207
184
|
const existing = this.upvalues.findIndex((u) => u.name === name);
|
|
208
185
|
if (existing !== -1) return existing;
|
|
@@ -212,45 +189,95 @@ class FnContext {
|
|
|
212
189
|
}
|
|
213
190
|
}
|
|
214
191
|
|
|
215
|
-
// ─────────────────────────────────────────────────────────────────
|
|
216
192
|
// Compiler
|
|
217
|
-
|
|
218
|
-
class Compiler {
|
|
219
|
-
constants: ConstantPool;
|
|
193
|
+
export class Compiler {
|
|
220
194
|
fnDescriptors: any[];
|
|
221
|
-
bytecode:
|
|
195
|
+
bytecode: b.Bytecode;
|
|
222
196
|
mainStartPc: number;
|
|
223
197
|
|
|
224
198
|
_currentCtx: FnContext | null;
|
|
225
199
|
_pendingLabel: string | null;
|
|
226
200
|
_forInCount: number;
|
|
201
|
+
_labelCount: number;
|
|
227
202
|
_loopStack: {
|
|
228
203
|
type: "loop" | "switch" | "block";
|
|
229
204
|
label: string | null;
|
|
230
|
-
|
|
231
|
-
|
|
205
|
+
// Label that break statements targeting this entry should jump to.
|
|
206
|
+
breakLabel: string;
|
|
207
|
+
// Label that continue statements targeting this entry should jump to.
|
|
208
|
+
continueLabel: string;
|
|
232
209
|
}[];
|
|
233
210
|
|
|
234
211
|
options: Options;
|
|
235
212
|
serializer: Serializer;
|
|
236
213
|
|
|
237
|
-
|
|
214
|
+
OP: Partial<typeof OP_ORIGINAL>;
|
|
215
|
+
OP_NAME: Record<number, string>;
|
|
216
|
+
JUMP_OPS: Set<number>;
|
|
217
|
+
|
|
218
|
+
emit(bc: b.Bytecode, instr: b.Instruction, node: t.Node) {
|
|
219
|
+
bc.push(instr);
|
|
220
|
+
|
|
221
|
+
instr[SOURCE_NODE_SYM] = node;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// DO NOT USE THIS KEY UNLESS YOU ARE "RESOLVE CONSTANTS"
|
|
225
|
+
// CONSTANTS DURING COMPILATION MUST BE USED BY REFERENCE WITH b.constantOperand("myConstantHere")
|
|
226
|
+
constants: any[];
|
|
227
|
+
|
|
228
|
+
constructor(options: Options = DEFAULT_OPTIONS) {
|
|
238
229
|
this.options = options;
|
|
239
|
-
this.constants = new ConstantPool();
|
|
240
230
|
this.fnDescriptors = []; // populated in pass 1
|
|
241
231
|
this.bytecode = [];
|
|
242
232
|
this.mainStartPc = 0;
|
|
243
233
|
this._currentCtx = null; // FnContext of the function being compiled, null at top-level
|
|
244
|
-
this._loopStack = []; //
|
|
234
|
+
this._loopStack = []; // per active loop/switch/block/try
|
|
245
235
|
this._pendingLabel = null;
|
|
246
236
|
this._forInCount = 0; // counter for synthetic for-in iterator global names
|
|
237
|
+
this._labelCount = 0; // monotonically increasing counter for unique label names
|
|
247
238
|
|
|
248
239
|
this.serializer = new Serializer(this);
|
|
240
|
+
|
|
241
|
+
this.OP = {};
|
|
242
|
+
// Construct randomized opcode mapping
|
|
243
|
+
if (this.options.randomizeOpcodes) {
|
|
244
|
+
let usedNumbers = new Set<number>();
|
|
245
|
+
for (const key in OP_ORIGINAL) {
|
|
246
|
+
let val;
|
|
247
|
+
do {
|
|
248
|
+
val = Math.floor(Math.random() * 256);
|
|
249
|
+
} while (usedNumbers.has(val));
|
|
250
|
+
usedNumbers.add(val);
|
|
251
|
+
this.OP[key] = val;
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
this.OP = OP_ORIGINAL;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Reverse map for comment generation
|
|
258
|
+
this.OP_NAME = Object.fromEntries(
|
|
259
|
+
Object.entries(this.OP).map(([k, v]) => [v, k]),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
this.JUMP_OPS = new Set([
|
|
263
|
+
this.OP.JUMP,
|
|
264
|
+
this.OP.JUMP_IF_FALSE,
|
|
265
|
+
this.OP.JUMP_IF_TRUE_OR_POP,
|
|
266
|
+
this.OP.JUMP_IF_FALSE_OR_POP,
|
|
267
|
+
this.OP.FOR_IN_NEXT,
|
|
268
|
+
this.OP.TRY_SETUP, // catch_pc operand needs offset adjustment like jump targets
|
|
269
|
+
]);
|
|
249
270
|
}
|
|
250
271
|
|
|
251
|
-
//
|
|
272
|
+
// Generate a globally unique label string with an optional hint for readability.
|
|
273
|
+
_makeLabel(hint = ""): string {
|
|
274
|
+
var id = this._labelCount++;
|
|
275
|
+
return `${hint || "L"}_${id}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Variable resolution
|
|
252
279
|
// Walks up the FnContext chain. Crossing a context boundary means
|
|
253
|
-
// we're capturing from an outer function
|
|
280
|
+
// we're capturing from an outer function - register an upvalue.
|
|
254
281
|
_resolve(name, ctx) {
|
|
255
282
|
if (!ctx) return { kind: "global" };
|
|
256
283
|
|
|
@@ -259,14 +286,14 @@ class Compiler {
|
|
|
259
286
|
return { kind: "local", slot: ctx.scope._locals.get(name) };
|
|
260
287
|
}
|
|
261
288
|
|
|
262
|
-
// 2. No parent context
|
|
289
|
+
// 2. No parent context -> must be global
|
|
263
290
|
if (!ctx.parentCtx) return { kind: "global" };
|
|
264
291
|
|
|
265
|
-
// 3. Ask parent
|
|
292
|
+
// 3. Ask parent -- recurse up the chain
|
|
266
293
|
const parentResult = this._resolve(name, ctx.parentCtx);
|
|
267
294
|
if (parentResult.kind === "global") return { kind: "global" };
|
|
268
295
|
|
|
269
|
-
// 4. Parent has it (as local or upvalue)
|
|
296
|
+
// 4. Parent has it (as local or upvalue) -- register an upvalue here.
|
|
270
297
|
// isLocal=true means "take it straight from parent's locals[index]"
|
|
271
298
|
// isLocal=false means "relay parent's upvalue[index]" (multi-level capture)
|
|
272
299
|
const isLocal = parentResult.kind === "local";
|
|
@@ -275,16 +302,15 @@ class Compiler {
|
|
|
275
302
|
return { kind: "upvalue", index: uvIdx };
|
|
276
303
|
}
|
|
277
304
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const ast = parser.parse(source, { sourceType: "script" });
|
|
305
|
+
// Entry point
|
|
306
|
+
compile(source: string) {
|
|
307
|
+
const ast = parse(source, { sourceType: "script" });
|
|
282
308
|
|
|
283
309
|
return this.compileAST(ast);
|
|
284
310
|
}
|
|
285
311
|
|
|
286
|
-
compileAST(ast) {
|
|
287
|
-
// Pass 1
|
|
312
|
+
compileAST(ast: t.File) {
|
|
313
|
+
// Pass 1 - compile every FunctionDeclaration into a descriptor.
|
|
288
314
|
// Traverse finds them regardless of nesting depth.
|
|
289
315
|
traverse(ast, {
|
|
290
316
|
FunctionDeclaration: (path) => {
|
|
@@ -296,58 +322,81 @@ class Compiler {
|
|
|
296
322
|
},
|
|
297
323
|
});
|
|
298
324
|
|
|
299
|
-
// Pass 2
|
|
325
|
+
// Pass 2 -- compile top-level statements into BYTECODE.
|
|
300
326
|
this._compileMain(ast.program.body);
|
|
301
327
|
|
|
302
|
-
return
|
|
303
|
-
bytecode: this.bytecode,
|
|
304
|
-
mainStartPc: this.mainStartPc,
|
|
305
|
-
};
|
|
328
|
+
return this.bytecode;
|
|
306
329
|
}
|
|
307
330
|
|
|
308
|
-
//
|
|
331
|
+
// Function Declaration
|
|
332
|
+
|
|
333
|
+
_compileFunctionDecl(node: t.FunctionDeclaration | t.FunctionExpression) {
|
|
334
|
+
// Reserve a slot in fnDescriptors NOW, before compiling the body, so that
|
|
335
|
+
// any nested _compileFunctionDecl calls see the correct .length and get a
|
|
336
|
+
// distinct _fnIdx. The placeholder object is mutated in-place below once
|
|
337
|
+
// the body and header are ready.
|
|
338
|
+
var fnIdx = this.fnDescriptors.length;
|
|
339
|
+
const entryLabel = this._makeLabel(`fn_${fnIdx}`);
|
|
340
|
+
var desc: any = {}; // placeholder — filled in after compilation
|
|
341
|
+
this.fnDescriptors.push(desc);
|
|
309
342
|
|
|
310
|
-
_compileFunctionDecl(node) {
|
|
311
343
|
// Create a context whose parent is whatever we're currently compiling.
|
|
312
344
|
// This is what lets _resolve cross function boundaries correctly.
|
|
313
345
|
const ctx = new FnContext(this, this._currentCtx);
|
|
314
346
|
const savedCtx = this._currentCtx;
|
|
315
347
|
this._currentCtx = ctx;
|
|
316
348
|
|
|
349
|
+
// Isolate the loop stack so that try/loop entries from the outer scope
|
|
350
|
+
// don't cause spurious TRY_END / extra jumps inside this function body.
|
|
351
|
+
const savedLoopStack = this._loopStack;
|
|
352
|
+
this._loopStack = [];
|
|
353
|
+
|
|
317
354
|
// Params occupy the first N local slots (args are copied in on CALL)
|
|
318
355
|
for (const param of node.params) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
356
|
+
let identifier = param.type === "AssignmentPattern" ? param.left : param;
|
|
357
|
+
ok(
|
|
358
|
+
identifier.type === "Identifier",
|
|
359
|
+
"Only simple identifiers allowed as parameters",
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
ctx.scope.define(identifier.name);
|
|
324
363
|
}
|
|
325
364
|
|
|
326
365
|
// Reserve the next slot for the implicit `arguments` object.
|
|
327
366
|
// Slot index will always equal paramCount (params are 0..paramCount-1).
|
|
328
367
|
ctx.scope.define("arguments");
|
|
329
368
|
|
|
330
|
-
//
|
|
369
|
+
// Pass 2: emit default-value guards at top of fn body
|
|
331
370
|
// Mirrors what JS engines do: if the caller passed undefined (or
|
|
332
371
|
// nothing), evaluate the default expression and overwrite the slot.
|
|
333
|
-
// Default expressions are full expressions, so f(x = a + b) and
|
|
334
|
-
// f(x = foo()) both work correctly.
|
|
335
372
|
for (const param of node.params) {
|
|
336
373
|
if (param.type !== "AssignmentPattern") continue;
|
|
337
374
|
|
|
338
|
-
const slot = ctx.scope._locals.get(param.left.name);
|
|
375
|
+
const slot = ctx.scope._locals.get((param.left as t.Identifier).name);
|
|
376
|
+
const skipLabel = this._makeLabel("param_skip");
|
|
339
377
|
|
|
340
378
|
// if (param === undefined) param = <default expr>
|
|
341
|
-
ctx.bc.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
379
|
+
this.emit(ctx.bc, [this.OP.LOAD_LOCAL, slot], param);
|
|
380
|
+
this.emit(
|
|
381
|
+
ctx.bc,
|
|
382
|
+
[this.OP.LOAD_CONST, b.constantOperand(undefined)],
|
|
383
|
+
param,
|
|
384
|
+
);
|
|
385
|
+
this.emit(ctx.bc, [this.OP.EQ], param);
|
|
386
|
+
this.emit(
|
|
387
|
+
ctx.bc,
|
|
388
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: skipLabel }],
|
|
389
|
+
param,
|
|
390
|
+
);
|
|
346
391
|
|
|
347
392
|
this._compileExpr(param.right, ctx.scope, ctx.bc); // eval default
|
|
348
|
-
ctx.bc.
|
|
393
|
+
this.emit(ctx.bc, [this.OP.STORE_LOCAL, slot], param);
|
|
349
394
|
|
|
350
|
-
|
|
395
|
+
this.emit(
|
|
396
|
+
ctx.bc,
|
|
397
|
+
[null, { type: "defineLabel", label: skipLabel }],
|
|
398
|
+
param,
|
|
399
|
+
);
|
|
351
400
|
}
|
|
352
401
|
|
|
353
402
|
for (const stmt of node.body.body) {
|
|
@@ -355,92 +404,85 @@ class Compiler {
|
|
|
355
404
|
}
|
|
356
405
|
|
|
357
406
|
// If we fall off the end of the function, implicitly return undefined.
|
|
358
|
-
ctx.bc.
|
|
359
|
-
ctx.bc.
|
|
407
|
+
this.emit(ctx.bc, [this.OP.LOAD_CONST, b.constantOperand(undefined)], node);
|
|
408
|
+
this.emit(ctx.bc, [this.OP.RETURN], node);
|
|
360
409
|
|
|
361
410
|
this._currentCtx = savedCtx; // restore before touching fnDescriptors
|
|
411
|
+
this._loopStack = savedLoopStack;
|
|
412
|
+
|
|
413
|
+
(node as any)._fnIdx = fnIdx;
|
|
414
|
+
|
|
415
|
+
// Fill the placeholder that was reserved at the top of this function.
|
|
416
|
+
// Metadata (paramCount, localCount, upvalues) is stored on desc and emitted
|
|
417
|
+
// as LOAD_INT instructions onto the value stack at each MAKE_CLOSURE call
|
|
418
|
+
// site — the runtime reads them from the stack, not from DATA words.
|
|
419
|
+
desc.name = node.id?.name || "<anonymous>";
|
|
420
|
+
desc.entryLabel = entryLabel;
|
|
421
|
+
desc.bytecode = ctx.bc as b.Bytecode;
|
|
422
|
+
desc._fnIdx = fnIdx;
|
|
423
|
+
desc.paramCount = node.params.length;
|
|
424
|
+
desc.localCount = ctx.scope.localCount;
|
|
425
|
+
desc.upvalues = ctx.upvalues.slice();
|
|
362
426
|
|
|
363
|
-
var fnIdx = this.fnDescriptors.length;
|
|
364
|
-
node._fnIdx = fnIdx; // for error messages
|
|
365
|
-
|
|
366
|
-
const desc = {
|
|
367
|
-
name: node.id?.name || "<anonymous>",
|
|
368
|
-
paramCount: node.params.length,
|
|
369
|
-
localCount: ctx.scope.localCount,
|
|
370
|
-
upvalueDescriptors: ctx.upvalues.map((u) => ({
|
|
371
|
-
isLocal: u.isLocal,
|
|
372
|
-
_index: u.index,
|
|
373
|
-
})),
|
|
374
|
-
bytecode: ctx.bc,
|
|
375
|
-
// Indices assigned after pushing into the pool
|
|
376
|
-
_fnIdx: this.fnDescriptors.length,
|
|
377
|
-
_constIdx: null,
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
this.fnDescriptors.push(desc);
|
|
381
|
-
desc._constIdx = this.constants.addObject(desc); // object entry, no dedup
|
|
382
427
|
return desc;
|
|
383
428
|
}
|
|
384
429
|
|
|
385
|
-
//
|
|
430
|
+
// Emit LOAD_INT instructions that push closure metadata onto the value stack
|
|
431
|
+
// immediately before a MAKE_CLOSURE instruction. The runtime pops these
|
|
432
|
+
// values in MAKE_CLOSURE instead of reading DATA words from bytecode.
|
|
433
|
+
//
|
|
434
|
+
// Stack layout when MAKE_CLOSURE executes (top is rightmost):
|
|
435
|
+
// [isLocal_0, idx_0, ..., isLocal_N-1, idx_N-1, uvCount, localCount, paramCount]
|
|
436
|
+
_emitClosureMetadata(desc: any, node: t.Node, bc: b.Bytecode) {
|
|
437
|
+
// Push each upvalue descriptor in order; runtime pops them in reverse.
|
|
438
|
+
for (const uv of desc.upvalues) {
|
|
439
|
+
this.emit(bc, [this.OP.LOAD_INT, uv.isLocal ? 1 : 0], node);
|
|
440
|
+
this.emit(bc, [this.OP.LOAD_INT, uv.index], node);
|
|
441
|
+
}
|
|
442
|
+
this.emit(bc, [this.OP.LOAD_INT, desc.upvalues.length], node);
|
|
443
|
+
this.emit(bc, [this.OP.LOAD_INT, desc.localCount], node);
|
|
444
|
+
this.emit(bc, [this.OP.LOAD_INT, desc.paramCount], node);
|
|
445
|
+
}
|
|
386
446
|
|
|
387
|
-
|
|
388
|
-
|
|
447
|
+
// Main (top-level)
|
|
448
|
+
_compileMain(body: t.Statement[]) {
|
|
389
449
|
const bc = this.bytecode;
|
|
390
450
|
|
|
391
|
-
// Hoist all FunctionDeclarations: MAKE_CLOSURE
|
|
392
|
-
// (mirrors JS hoisting
|
|
451
|
+
// Hoist all FunctionDeclarations: MAKE_CLOSURE -> STORE_GLOBAL
|
|
452
|
+
// (mirrors JS hoisting -- functions are available before other code)
|
|
393
453
|
for (const node of body) {
|
|
394
454
|
if (node.type !== "FunctionDeclaration") continue;
|
|
395
|
-
const desc = this.fnDescriptors.find(
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
455
|
+
const desc = this.fnDescriptors.find(
|
|
456
|
+
(d) => d._fnIdx === (node as any)._fnIdx,
|
|
457
|
+
);
|
|
458
|
+
const nameRef = b.constantOperand(node.id.name);
|
|
459
|
+
this._emitClosureMetadata(desc, node, bc);
|
|
460
|
+
this.emit(
|
|
461
|
+
bc,
|
|
462
|
+
[this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
|
|
463
|
+
node,
|
|
464
|
+
);
|
|
465
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, nameRef], node);
|
|
399
466
|
}
|
|
400
467
|
|
|
401
468
|
// Compile everything else in order
|
|
402
469
|
for (const node of body) {
|
|
403
470
|
if (node.type === "FunctionDeclaration") continue;
|
|
404
|
-
this._compileStatement(node, null, bc); // null scope
|
|
471
|
+
this._compileStatement(node, null, bc); // null scope -> global context
|
|
405
472
|
}
|
|
406
473
|
|
|
407
|
-
|
|
474
|
+
this.emit(bc, [this.OP.RETURN], null); // end program
|
|
408
475
|
|
|
409
|
-
//
|
|
476
|
+
// Append all function bodies. Each function's entryLabel (already generated
|
|
477
|
+
// in _compileFunctionDecl) points directly to the first body instruction;
|
|
478
|
+
// metadata is pushed onto the stack at each call site, not stored inline.
|
|
410
479
|
for (const descriptor of this.fnDescriptors) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
// Real body starts immediately after the preamble.
|
|
418
|
-
const bodyPc = descriptor.startPc + 2;
|
|
419
|
-
|
|
420
|
-
// Build real body with jump targets resolved from bodyPc as the base.
|
|
421
|
-
const realBodyInstrs = descriptor.bytecode.map((instr) =>
|
|
422
|
-
this._offsetJump(instr, bodyPc),
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
// Pack each instruction into a 32-bit word and store as a constant.
|
|
426
|
-
// The PATCH handler will write these words directly into this.bytecode.
|
|
427
|
-
const realBodyWords =
|
|
428
|
-
this.serializer._serializeBytecode(realBodyInstrs);
|
|
429
|
-
const bodyConstIdx = this.constants.addObject(realBodyWords);
|
|
430
|
-
|
|
431
|
-
// Emit preamble: push destination PC, then PATCH.
|
|
432
|
-
const destPcConstIdx = this.constants.intern(bodyPc);
|
|
433
|
-
this.bytecode.push([OP.LOAD_CONST, destPcConstIdx]);
|
|
434
|
-
this.bytecode.push([OP.PATCH, bodyConstIdx]);
|
|
435
|
-
|
|
436
|
-
// Garbage fill — same length as real body, never executed (PATCH fires first).
|
|
437
|
-
for (let i = 0; i < realBodyInstrs.length; i++) {
|
|
438
|
-
this.bytecode.push([OP.LOAD_CONST, 0]);
|
|
439
|
-
}
|
|
440
|
-
} else {
|
|
441
|
-
for (const instr of descriptor.bytecode) {
|
|
442
|
-
this.bytecode.push(this._offsetJump(instr, descriptor.startPc));
|
|
443
|
-
}
|
|
480
|
+
this.bytecode.push([
|
|
481
|
+
null,
|
|
482
|
+
{ type: "defineLabel", label: descriptor.entryLabel },
|
|
483
|
+
]);
|
|
484
|
+
for (const instr of descriptor.bytecode) {
|
|
485
|
+
this.bytecode.push(instr);
|
|
444
486
|
}
|
|
445
487
|
}
|
|
446
488
|
|
|
@@ -449,23 +491,24 @@ class Compiler {
|
|
|
449
491
|
`Program too large: ${this.bytecode.length} instructions, max 16,777,215`,
|
|
450
492
|
);
|
|
451
493
|
|
|
452
|
-
if (this.constants.items.length > 0xffffff)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
494
|
+
// if (this.constants.items.length > 0xffffff)
|
|
495
|
+
// throw new Error(
|
|
496
|
+
// `Constant pool too large: ${this.constants.items.length} entries, max 16,777,215`,
|
|
497
|
+
// );
|
|
456
498
|
}
|
|
457
499
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
500
|
+
// Statements
|
|
501
|
+
_compileStatement(node: t.Statement, scope: Scope, bc: b.Bytecode) {
|
|
502
|
+
switch (node.type) {
|
|
503
|
+
case "EmptyStatement": {
|
|
504
|
+
// nothing to emit -- bare semicolon is a no-op
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
464
507
|
|
|
465
|
-
|
|
508
|
+
case "DebuggerStatement":
|
|
509
|
+
this.emit(bc, [this.OP.DEBUGGER], node);
|
|
510
|
+
break;
|
|
466
511
|
|
|
467
|
-
_compileStatement(node, scope, bc) {
|
|
468
|
-
switch (node.type) {
|
|
469
512
|
case "BlockStatement": {
|
|
470
513
|
for (const stmt of node.body) {
|
|
471
514
|
this._compileStatement(stmt, scope, bc);
|
|
@@ -474,23 +517,32 @@ class Compiler {
|
|
|
474
517
|
}
|
|
475
518
|
|
|
476
519
|
case "FunctionDeclaration": {
|
|
477
|
-
// Nested function
|
|
520
|
+
// Nested function -- compile it into a descriptor, then emit
|
|
478
521
|
// MAKE_CLOSURE so it's captured as a live closure at runtime.
|
|
479
522
|
// (_compileFunctionDecl pushes/pops _currentCtx internally)
|
|
480
523
|
const desc = this._compileFunctionDecl(node);
|
|
481
|
-
|
|
524
|
+
this._emitClosureMetadata(desc, node, bc);
|
|
525
|
+
this.emit(
|
|
526
|
+
bc,
|
|
527
|
+
[this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
|
|
528
|
+
node,
|
|
529
|
+
);
|
|
482
530
|
if (scope) {
|
|
483
531
|
const slot = scope.define(node.id.name);
|
|
484
|
-
|
|
532
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
485
533
|
} else {
|
|
486
|
-
|
|
534
|
+
this.emit(
|
|
535
|
+
bc,
|
|
536
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(node.id.name)],
|
|
537
|
+
node,
|
|
538
|
+
);
|
|
487
539
|
}
|
|
488
540
|
break;
|
|
489
541
|
}
|
|
490
542
|
|
|
491
543
|
case "ThrowStatement": {
|
|
492
544
|
this._compileExpr(node.argument, scope, bc);
|
|
493
|
-
|
|
545
|
+
this.emit(bc, [this.OP.THROW], node);
|
|
494
546
|
break;
|
|
495
547
|
}
|
|
496
548
|
|
|
@@ -498,15 +550,27 @@ class Compiler {
|
|
|
498
550
|
if (node.argument) {
|
|
499
551
|
this._compileExpr(node.argument, scope, bc);
|
|
500
552
|
} else {
|
|
501
|
-
|
|
553
|
+
this.emit(
|
|
554
|
+
bc,
|
|
555
|
+
[this.OP.LOAD_CONST, b.constantOperand(undefined)],
|
|
556
|
+
node,
|
|
557
|
+
);
|
|
502
558
|
}
|
|
503
|
-
|
|
559
|
+
// Disarm any open try handlers before leaving the function.
|
|
560
|
+
// TRY_END only touches frame._handlerStack, not the value stack,
|
|
561
|
+
// so the return value sitting on top is safe.
|
|
562
|
+
for (let _ri = this._loopStack.length - 1; _ri >= 0; _ri--) {
|
|
563
|
+
if ((this._loopStack[_ri].type as any) === "try") {
|
|
564
|
+
this.emit(bc, [this.OP.TRY_END], node);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
this.emit(bc, [this.OP.RETURN], node);
|
|
504
568
|
break;
|
|
505
569
|
}
|
|
506
570
|
|
|
507
571
|
case "ExpressionStatement": {
|
|
508
572
|
this._compileExpr(node.expression, scope, bc);
|
|
509
|
-
|
|
573
|
+
this.emit(bc, [this.OP.POP], node); // discard return value of statement-level expressions
|
|
510
574
|
break;
|
|
511
575
|
}
|
|
512
576
|
|
|
@@ -516,27 +580,44 @@ class Compiler {
|
|
|
516
580
|
if (decl.init) {
|
|
517
581
|
this._compileExpr(decl.init, scope, bc);
|
|
518
582
|
} else {
|
|
519
|
-
|
|
583
|
+
this.emit(
|
|
584
|
+
bc,
|
|
585
|
+
[this.OP.LOAD_CONST, b.constantOperand(undefined)],
|
|
586
|
+
node,
|
|
587
|
+
);
|
|
520
588
|
}
|
|
589
|
+
|
|
590
|
+
ok(
|
|
591
|
+
decl.id.type === "Identifier",
|
|
592
|
+
"Only simple identifiers can be declared",
|
|
593
|
+
);
|
|
594
|
+
|
|
521
595
|
// Store: local slot if inside a function, global name otherwise
|
|
522
596
|
if (scope) {
|
|
523
597
|
const slot = scope.define(decl.id.name);
|
|
524
|
-
|
|
598
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
525
599
|
} else {
|
|
526
|
-
|
|
600
|
+
this.emit(
|
|
601
|
+
bc,
|
|
602
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(decl.id.name)],
|
|
603
|
+
node,
|
|
604
|
+
);
|
|
527
605
|
}
|
|
528
606
|
}
|
|
529
607
|
break;
|
|
530
608
|
}
|
|
531
609
|
|
|
532
610
|
case "IfStatement": {
|
|
533
|
-
|
|
611
|
+
const elseOrEndLabel = this._makeLabel("if_else");
|
|
612
|
+
// 1. Compile the test expression -> leaves a value on the stack
|
|
534
613
|
this._compileExpr(node.test, scope, bc);
|
|
535
|
-
// 2. Emit JUMP_IF_FALSE
|
|
536
|
-
|
|
537
|
-
|
|
614
|
+
// 2. Emit JUMP_IF_FALSE to the else branch (or end if no else)
|
|
615
|
+
this.emit(
|
|
616
|
+
bc,
|
|
617
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: elseOrEndLabel }],
|
|
618
|
+
node,
|
|
619
|
+
);
|
|
538
620
|
// 3. Compile the consequent block (the "then" branch)
|
|
539
|
-
// Consequent may be a BlockStatement or a bare statement (no braces)
|
|
540
621
|
const consequentBody =
|
|
541
622
|
node.consequent.type === "BlockStatement"
|
|
542
623
|
? node.consequent.body
|
|
@@ -546,23 +627,35 @@ class Compiler {
|
|
|
546
627
|
}
|
|
547
628
|
if (node.alternate) {
|
|
548
629
|
// 4a. Consequent needs to jump OVER the else block when done
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
630
|
+
const endLabel = this._makeLabel("if_end");
|
|
631
|
+
this.emit(
|
|
632
|
+
bc,
|
|
633
|
+
[this.OP.JUMP, { type: "label", label: endLabel }],
|
|
634
|
+
node,
|
|
635
|
+
);
|
|
636
|
+
// Mark start of else
|
|
637
|
+
this.emit(
|
|
638
|
+
bc,
|
|
639
|
+
[null, { type: "defineLabel", label: elseOrEndLabel }],
|
|
640
|
+
node,
|
|
641
|
+
);
|
|
553
642
|
// 5. Compile the alternate (else) block
|
|
554
643
|
const altBody =
|
|
555
644
|
node.alternate.type === "BlockStatement"
|
|
556
645
|
? node.alternate.body
|
|
557
|
-
: [node.alternate]; // handles `else if`
|
|
646
|
+
: [node.alternate]; // handles `else if` -- it's just a nested IfStatement
|
|
558
647
|
for (const stmt of altBody) {
|
|
559
648
|
this._compileStatement(stmt, scope, bc);
|
|
560
649
|
}
|
|
561
|
-
//
|
|
562
|
-
bc[
|
|
650
|
+
// Mark end (consequent's jump lands here)
|
|
651
|
+
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
563
652
|
} else {
|
|
564
|
-
// 4b. No else
|
|
565
|
-
|
|
653
|
+
// 4b. No else -- label lands right after the then block
|
|
654
|
+
this.emit(
|
|
655
|
+
bc,
|
|
656
|
+
[null, { type: "defineLabel", label: elseOrEndLabel }],
|
|
657
|
+
node,
|
|
658
|
+
);
|
|
566
659
|
}
|
|
567
660
|
break;
|
|
568
661
|
}
|
|
@@ -570,30 +663,41 @@ class Compiler {
|
|
|
570
663
|
case "WhileStatement": {
|
|
571
664
|
const _wLabel = this._pendingLabel;
|
|
572
665
|
this._pendingLabel = null;
|
|
666
|
+
|
|
667
|
+
const loopTopLabel = this._makeLabel("while_top");
|
|
668
|
+
const exitLabel = this._makeLabel("while_exit");
|
|
669
|
+
|
|
573
670
|
this._loopStack.push({
|
|
574
671
|
type: "loop",
|
|
575
672
|
label: _wLabel,
|
|
576
|
-
|
|
577
|
-
|
|
673
|
+
breakLabel: exitLabel,
|
|
674
|
+
continueLabel: loopTopLabel, // continue re-evaluates the test
|
|
578
675
|
});
|
|
579
|
-
const loopCtxW = this._loopStack[this._loopStack.length - 1];
|
|
580
676
|
|
|
581
|
-
|
|
677
|
+
this.emit(
|
|
678
|
+
bc,
|
|
679
|
+
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
680
|
+
node,
|
|
681
|
+
);
|
|
582
682
|
this._compileExpr(node.test, scope, bc);
|
|
583
|
-
|
|
584
|
-
|
|
683
|
+
this.emit(
|
|
684
|
+
bc,
|
|
685
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
|
|
686
|
+
node,
|
|
687
|
+
);
|
|
585
688
|
|
|
586
|
-
|
|
689
|
+
const whileBody =
|
|
690
|
+
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
691
|
+
for (const stmt of whileBody) {
|
|
587
692
|
this._compileStatement(stmt, scope, bc);
|
|
588
693
|
}
|
|
589
694
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
bc[
|
|
596
|
-
for (const idx of loopCtxW.breakJumps) bc[idx][1] = exitTargetW;
|
|
695
|
+
this.emit(
|
|
696
|
+
bc,
|
|
697
|
+
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
698
|
+
node,
|
|
699
|
+
);
|
|
700
|
+
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
597
701
|
|
|
598
702
|
this._loopStack.pop();
|
|
599
703
|
break;
|
|
@@ -602,33 +706,49 @@ class Compiler {
|
|
|
602
706
|
case "DoWhileStatement": {
|
|
603
707
|
const _dwLabel = this._pendingLabel;
|
|
604
708
|
this._pendingLabel = null;
|
|
709
|
+
|
|
710
|
+
const loopTopLabel = this._makeLabel("dowhile_top");
|
|
711
|
+
const continueLabel = this._makeLabel("dowhile_cont");
|
|
712
|
+
const exitLabel = this._makeLabel("dowhile_exit");
|
|
713
|
+
|
|
605
714
|
this._loopStack.push({
|
|
606
715
|
type: "loop",
|
|
607
716
|
label: _dwLabel,
|
|
608
|
-
|
|
609
|
-
|
|
717
|
+
breakLabel: exitLabel,
|
|
718
|
+
continueLabel: continueLabel, // continue falls to the test
|
|
610
719
|
});
|
|
611
|
-
const loopCtxDW = this._loopStack[this._loopStack.length - 1];
|
|
612
720
|
|
|
613
|
-
|
|
721
|
+
this.emit(
|
|
722
|
+
bc,
|
|
723
|
+
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
724
|
+
node,
|
|
725
|
+
);
|
|
614
726
|
|
|
615
|
-
|
|
727
|
+
const doWhileBody =
|
|
728
|
+
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
729
|
+
for (const stmt of doWhileBody) {
|
|
616
730
|
this._compileStatement(stmt, scope, bc);
|
|
617
731
|
}
|
|
618
732
|
|
|
619
|
-
// continue
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
733
|
+
// continue -> skip rest of body, fall through to test
|
|
734
|
+
this.emit(
|
|
735
|
+
bc,
|
|
736
|
+
[null, { type: "defineLabel", label: continueLabel }],
|
|
737
|
+
node,
|
|
738
|
+
);
|
|
624
739
|
this._compileExpr(node.test, scope, bc);
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
740
|
+
this.emit(
|
|
741
|
+
bc,
|
|
742
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
|
|
743
|
+
node,
|
|
744
|
+
);
|
|
745
|
+
this.emit(
|
|
746
|
+
bc,
|
|
747
|
+
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
748
|
+
node,
|
|
749
|
+
);
|
|
628
750
|
|
|
629
|
-
|
|
630
|
-
bc[exitJumpIdxDW][1] = exitTargetDW;
|
|
631
|
-
for (const idx of loopCtxDW.breakJumps) bc[idx][1] = exitTargetDW;
|
|
751
|
+
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
632
752
|
|
|
633
753
|
this._loopStack.pop();
|
|
634
754
|
break;
|
|
@@ -637,126 +757,172 @@ class Compiler {
|
|
|
637
757
|
case "ForStatement": {
|
|
638
758
|
const _fLabel = this._pendingLabel;
|
|
639
759
|
this._pendingLabel = null;
|
|
760
|
+
|
|
761
|
+
const loopTopLabel = this._makeLabel("for_top");
|
|
762
|
+
const exitLabel = this._makeLabel("for_exit");
|
|
763
|
+
// continue jumps to the update clause if present, else straight to test
|
|
764
|
+
const updateLabel = node.update
|
|
765
|
+
? this._makeLabel("for_update")
|
|
766
|
+
: loopTopLabel;
|
|
767
|
+
|
|
640
768
|
this._loopStack.push({
|
|
641
769
|
type: "loop",
|
|
642
770
|
label: _fLabel,
|
|
643
|
-
|
|
644
|
-
|
|
771
|
+
breakLabel: exitLabel,
|
|
772
|
+
continueLabel: updateLabel,
|
|
645
773
|
});
|
|
646
|
-
const loopCtxF = this._loopStack[this._loopStack.length - 1];
|
|
647
774
|
|
|
648
775
|
if (node.init) {
|
|
649
776
|
if (node.init.type === "VariableDeclaration") {
|
|
650
777
|
this._compileStatement(node.init, scope, bc);
|
|
651
778
|
} else {
|
|
652
779
|
this._compileExpr(node.init, scope, bc);
|
|
653
|
-
|
|
780
|
+
this.emit(bc, [this.OP.POP], node);
|
|
654
781
|
}
|
|
655
782
|
}
|
|
656
783
|
|
|
657
|
-
|
|
784
|
+
this.emit(
|
|
785
|
+
bc,
|
|
786
|
+
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
787
|
+
node,
|
|
788
|
+
);
|
|
658
789
|
if (node.test) {
|
|
659
790
|
this._compileExpr(node.test, scope, bc);
|
|
660
|
-
|
|
791
|
+
this.emit(
|
|
792
|
+
bc,
|
|
793
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: exitLabel }],
|
|
794
|
+
node,
|
|
795
|
+
);
|
|
661
796
|
}
|
|
662
|
-
const exitJumpIdxF = node.test ? bc.length - 1 : null;
|
|
663
797
|
|
|
664
|
-
|
|
798
|
+
const forBody =
|
|
799
|
+
node.body.type === "BlockStatement" ? node.body.body : [node.body];
|
|
800
|
+
for (const stmt of forBody) {
|
|
665
801
|
this._compileStatement(stmt, scope, bc);
|
|
666
802
|
}
|
|
667
803
|
|
|
668
|
-
// continue
|
|
804
|
+
// continue -> run update (if any) then back to test
|
|
669
805
|
if (node.update) {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
806
|
+
this.emit(
|
|
807
|
+
bc,
|
|
808
|
+
[null, { type: "defineLabel", label: updateLabel }],
|
|
809
|
+
node,
|
|
810
|
+
);
|
|
673
811
|
this._compileExpr(node.update, scope, bc);
|
|
674
|
-
|
|
675
|
-
} else {
|
|
676
|
-
// No update — continue goes straight to the test
|
|
677
|
-
for (const idx of loopCtxF.continueJumps) bc[idx][1] = loopTopF;
|
|
812
|
+
this.emit(bc, [this.OP.POP], node);
|
|
678
813
|
}
|
|
679
814
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
815
|
+
this.emit(
|
|
816
|
+
bc,
|
|
817
|
+
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
818
|
+
node,
|
|
819
|
+
);
|
|
820
|
+
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
685
821
|
|
|
686
822
|
this._loopStack.pop();
|
|
687
823
|
break;
|
|
688
824
|
}
|
|
689
825
|
|
|
690
826
|
case "BreakStatement": {
|
|
691
|
-
|
|
692
|
-
|
|
827
|
+
// Find the jump target in the loop stack.
|
|
828
|
+
let _bTargetIdx = -1;
|
|
693
829
|
if (node.label) {
|
|
694
830
|
const _bLabelName = node.label.name;
|
|
695
|
-
let _bFound = -1;
|
|
696
831
|
for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
|
|
697
832
|
if (this._loopStack[_bi].label === _bLabelName) {
|
|
698
|
-
|
|
833
|
+
_bTargetIdx = _bi;
|
|
699
834
|
break;
|
|
700
835
|
}
|
|
701
836
|
}
|
|
702
|
-
if (
|
|
703
|
-
throw new Error(`Label '${
|
|
704
|
-
this._loopStack[_bFound].breakJumps.push(_bJumpIdx);
|
|
837
|
+
if (_bTargetIdx === -1)
|
|
838
|
+
throw new Error(`Label '${node.label.name}' not found`);
|
|
705
839
|
} else {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
840
|
+
// Find innermost loop/switch/block (skip "try" entries)
|
|
841
|
+
for (let _bi = this._loopStack.length - 1; _bi >= 0; _bi--) {
|
|
842
|
+
if ((this._loopStack[_bi].type as any) !== "try") {
|
|
843
|
+
_bTargetIdx = _bi;
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (_bTargetIdx === -1) throw new Error("break outside loop");
|
|
848
|
+
}
|
|
849
|
+
// Emit TRY_END for every open try block between here and the target.
|
|
850
|
+
for (let _bi = this._loopStack.length - 1; _bi > _bTargetIdx; _bi--) {
|
|
851
|
+
if ((this._loopStack[_bi].type as any) === "try") {
|
|
852
|
+
this.emit(bc, [this.OP.TRY_END], node);
|
|
853
|
+
}
|
|
711
854
|
}
|
|
855
|
+
this.emit(
|
|
856
|
+
bc,
|
|
857
|
+
[
|
|
858
|
+
this.OP.JUMP,
|
|
859
|
+
{ type: "label", label: this._loopStack[_bTargetIdx].breakLabel },
|
|
860
|
+
],
|
|
861
|
+
node,
|
|
862
|
+
);
|
|
712
863
|
break;
|
|
713
864
|
}
|
|
714
865
|
|
|
715
866
|
case "ContinueStatement": {
|
|
716
|
-
|
|
717
|
-
|
|
867
|
+
// Find the target loop in the loop stack.
|
|
868
|
+
let _cTargetIdx = -1;
|
|
718
869
|
if (node.label) {
|
|
719
870
|
const _cLabelName = node.label.name;
|
|
720
|
-
let _cFound = -1;
|
|
721
871
|
for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
|
|
722
872
|
if (
|
|
723
873
|
this._loopStack[_ci].label === _cLabelName &&
|
|
724
874
|
this._loopStack[_ci].type === "loop"
|
|
725
875
|
) {
|
|
726
|
-
|
|
876
|
+
_cTargetIdx = _ci;
|
|
727
877
|
break;
|
|
728
878
|
}
|
|
729
879
|
}
|
|
730
|
-
if (
|
|
731
|
-
throw new Error(
|
|
732
|
-
|
|
880
|
+
if (_cTargetIdx === -1)
|
|
881
|
+
throw new Error(
|
|
882
|
+
`Label '${node.label.name}' not found for continue`,
|
|
883
|
+
);
|
|
733
884
|
} else {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
for (let i = this._loopStack.length - 1; i >= 0; i--) {
|
|
739
|
-
if (this._loopStack[i].type === "loop") {
|
|
740
|
-
loopIdx = i;
|
|
885
|
+
// Find the innermost loop (skip switch, block, and try contexts)
|
|
886
|
+
for (let _ci = this._loopStack.length - 1; _ci >= 0; _ci--) {
|
|
887
|
+
if (this._loopStack[_ci].type === "loop") {
|
|
888
|
+
_cTargetIdx = _ci;
|
|
741
889
|
break;
|
|
742
890
|
}
|
|
743
891
|
}
|
|
744
|
-
if (
|
|
745
|
-
this._loopStack[loopIdx].continueJumps.push(_cJumpIdx);
|
|
892
|
+
if (_cTargetIdx === -1) throw new Error("continue outside loop");
|
|
746
893
|
}
|
|
894
|
+
// Emit TRY_END for every open try block between here and the target loop.
|
|
895
|
+
for (let _ci = this._loopStack.length - 1; _ci > _cTargetIdx; _ci--) {
|
|
896
|
+
if ((this._loopStack[_ci].type as any) === "try") {
|
|
897
|
+
this.emit(bc, [this.OP.TRY_END], node);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
this.emit(
|
|
901
|
+
bc,
|
|
902
|
+
[
|
|
903
|
+
this.OP.JUMP,
|
|
904
|
+
{
|
|
905
|
+
type: "label",
|
|
906
|
+
label: this._loopStack[_cTargetIdx].continueLabel,
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
node,
|
|
910
|
+
);
|
|
747
911
|
break;
|
|
748
912
|
}
|
|
749
913
|
|
|
750
914
|
case "SwitchStatement": {
|
|
751
915
|
const _swLabel = this._pendingLabel;
|
|
752
916
|
this._pendingLabel = null;
|
|
917
|
+
|
|
918
|
+
const switchBreakLabel = this._makeLabel("sw_break");
|
|
919
|
+
|
|
753
920
|
this._loopStack.push({
|
|
754
921
|
type: "switch",
|
|
755
922
|
label: _swLabel,
|
|
756
|
-
|
|
757
|
-
|
|
923
|
+
breakLabel: switchBreakLabel,
|
|
924
|
+
continueLabel: switchBreakLabel, // not used for switch
|
|
758
925
|
});
|
|
759
|
-
const switchCtx = this._loopStack[this._loopStack.length - 1];
|
|
760
926
|
|
|
761
927
|
// Compile the discriminant and leave it on the stack
|
|
762
928
|
this._compileExpr(node.discriminant, scope, bc);
|
|
@@ -764,58 +930,70 @@ class Compiler {
|
|
|
764
930
|
const cases = node.cases;
|
|
765
931
|
const defaultIdx = cases.findIndex((c) => c.test === null);
|
|
766
932
|
|
|
767
|
-
//
|
|
768
|
-
const
|
|
933
|
+
// Pre-allocate a label for each case body so dispatch can reference them
|
|
934
|
+
const caseLabels = cases.map((_, i) => this._makeLabel(`sw_case_${i}`));
|
|
769
935
|
|
|
770
|
-
for
|
|
771
|
-
|
|
936
|
+
// Dispatch section: for each non-default case, check and jump to its body
|
|
937
|
+
for (let i = 0; i < cases.length; i++) {
|
|
938
|
+
const cas = cases[i];
|
|
939
|
+
if (cas.test === null) continue; // skip default in dispatch
|
|
772
940
|
|
|
773
|
-
|
|
774
|
-
|
|
941
|
+
const nextCheckLabel = this._makeLabel("sw_next");
|
|
942
|
+
this.emit(bc, [this.OP.DUP], node);
|
|
775
943
|
this._compileExpr(cas.test, scope, bc);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
944
|
+
this.emit(bc, [this.OP.EQ], node);
|
|
945
|
+
// If not matched, fall through to the next check
|
|
946
|
+
this.emit(
|
|
947
|
+
bc,
|
|
948
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: nextCheckLabel }],
|
|
949
|
+
node,
|
|
950
|
+
);
|
|
951
|
+
// If matched, jump directly to this case's body
|
|
952
|
+
this.emit(
|
|
953
|
+
bc,
|
|
954
|
+
[this.OP.JUMP, { type: "label", label: caseLabels[i] }],
|
|
955
|
+
node,
|
|
956
|
+
);
|
|
957
|
+
this.emit(
|
|
958
|
+
bc,
|
|
959
|
+
[null, { type: "defineLabel", label: nextCheckLabel }],
|
|
960
|
+
node,
|
|
961
|
+
);
|
|
786
962
|
}
|
|
787
963
|
|
|
788
|
-
// No
|
|
789
|
-
|
|
790
|
-
|
|
964
|
+
// No case matched: jump to default body or exit (which pops discriminant)
|
|
965
|
+
this.emit(
|
|
966
|
+
bc,
|
|
967
|
+
[
|
|
968
|
+
this.OP.JUMP,
|
|
969
|
+
{
|
|
970
|
+
type: "label",
|
|
971
|
+
label:
|
|
972
|
+
defaultIdx !== -1 ? caseLabels[defaultIdx] : switchBreakLabel,
|
|
973
|
+
},
|
|
974
|
+
],
|
|
975
|
+
node,
|
|
976
|
+
);
|
|
791
977
|
|
|
792
|
-
// Body section: compile all case bodies in source order
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
978
|
+
// Body section: compile all case bodies in source order (fallthrough intact)
|
|
979
|
+
for (let i = 0; i < cases.length; i++) {
|
|
980
|
+
this.emit(
|
|
981
|
+
bc,
|
|
982
|
+
[null, { type: "defineLabel", label: caseLabels[i] }],
|
|
983
|
+
node,
|
|
984
|
+
);
|
|
985
|
+
for (const stmt of cases[i].consequent) {
|
|
797
986
|
this._compileStatement(stmt, scope, bc);
|
|
798
987
|
}
|
|
799
988
|
}
|
|
800
989
|
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
// Patch all body jumps
|
|
810
|
-
for (const { cas, jumpIdx } of bodyJumps) {
|
|
811
|
-
bc[jumpIdx][1] = bodyStart.get(cas);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Exit: pop the discriminant and patch break jumps
|
|
815
|
-
bc.push([OP.POP]);
|
|
816
|
-
for (const idx of switchCtx.breakJumps) {
|
|
817
|
-
bc[idx][1] = bc.length - 1; // Point to the POP instruction
|
|
818
|
-
}
|
|
990
|
+
// break label lands here; pop the discriminant and continue after switch
|
|
991
|
+
this.emit(
|
|
992
|
+
bc,
|
|
993
|
+
[null, { type: "defineLabel", label: switchBreakLabel }],
|
|
994
|
+
node,
|
|
995
|
+
);
|
|
996
|
+
this.emit(bc, [this.OP.POP], node);
|
|
819
997
|
|
|
820
998
|
this._loopStack.pop();
|
|
821
999
|
break;
|
|
@@ -837,17 +1015,21 @@ class Compiler {
|
|
|
837
1015
|
this._compileStatement(_lBody, scope, bc);
|
|
838
1016
|
this._pendingLabel = null; // safety clear if handler didn't consume it
|
|
839
1017
|
} else {
|
|
840
|
-
// Non-loop labeled statement (e.g. labeled block)
|
|
1018
|
+
// Non-loop labeled statement (e.g. labeled block) -- only break is valid
|
|
1019
|
+
const blockBreakLabel = this._makeLabel("block_break");
|
|
841
1020
|
this._loopStack.push({
|
|
842
1021
|
type: "block",
|
|
843
1022
|
label: _lName,
|
|
844
|
-
|
|
845
|
-
|
|
1023
|
+
breakLabel: blockBreakLabel,
|
|
1024
|
+
continueLabel: blockBreakLabel, // unused
|
|
846
1025
|
});
|
|
847
1026
|
this._compileStatement(_lBody, scope, bc);
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1027
|
+
this._loopStack.pop();
|
|
1028
|
+
this.emit(
|
|
1029
|
+
bc,
|
|
1030
|
+
[null, { type: "defineLabel", label: blockBreakLabel }],
|
|
1031
|
+
node,
|
|
1032
|
+
);
|
|
851
1033
|
}
|
|
852
1034
|
break;
|
|
853
1035
|
}
|
|
@@ -856,10 +1038,10 @@ class Compiler {
|
|
|
856
1038
|
const _fiLabel = this._pendingLabel;
|
|
857
1039
|
this._pendingLabel = null;
|
|
858
1040
|
|
|
859
|
-
// Evaluate the object expression
|
|
1041
|
+
// Evaluate the object expression -> on stack
|
|
860
1042
|
this._compileExpr(node.right, scope, bc);
|
|
861
1043
|
// FOR_IN_SETUP: pops obj, pushes iterator {keys, i}
|
|
862
|
-
|
|
1044
|
+
this.emit(bc, [this.OP.FOR_IN_SETUP], node);
|
|
863
1045
|
|
|
864
1046
|
// Store iterator in a hidden slot so break/continue need no cleanup
|
|
865
1047
|
let emitLoadIter: () => void;
|
|
@@ -867,55 +1049,79 @@ class Compiler {
|
|
|
867
1049
|
if (scope) {
|
|
868
1050
|
// Reserve a hidden local slot (no name mapping needed)
|
|
869
1051
|
const iterSlot = scope._next++;
|
|
870
|
-
emitLoadIter = () =>
|
|
871
|
-
|
|
1052
|
+
emitLoadIter = () =>
|
|
1053
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, iterSlot], node);
|
|
1054
|
+
emitStoreIter = () =>
|
|
1055
|
+
this.emit(bc, [this.OP.STORE_LOCAL, iterSlot], node);
|
|
872
1056
|
} else {
|
|
873
|
-
// Top level
|
|
874
|
-
const iterNameIdx = this.
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1057
|
+
// Top level -- use a synthetic global that won't collide with user code
|
|
1058
|
+
const iterNameIdx = b.constantOperand("__fi" + this._forInCount++);
|
|
1059
|
+
emitLoadIter = () =>
|
|
1060
|
+
this.emit(bc, [this.OP.LOAD_GLOBAL, iterNameIdx], node);
|
|
1061
|
+
emitStoreIter = () =>
|
|
1062
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, iterNameIdx], node);
|
|
879
1063
|
}
|
|
880
1064
|
emitStoreIter();
|
|
881
1065
|
|
|
1066
|
+
const loopTopLabel = this._makeLabel("forin_top");
|
|
1067
|
+
const exitLabel = this._makeLabel("forin_exit");
|
|
1068
|
+
|
|
882
1069
|
this._loopStack.push({
|
|
883
1070
|
type: "loop",
|
|
884
1071
|
label: _fiLabel,
|
|
885
|
-
|
|
886
|
-
|
|
1072
|
+
breakLabel: exitLabel,
|
|
1073
|
+
continueLabel: loopTopLabel, // continue re-checks the iterator
|
|
887
1074
|
});
|
|
888
|
-
const loopCtxFI = this._loopStack[this._loopStack.length - 1];
|
|
889
1075
|
|
|
890
|
-
|
|
1076
|
+
this.emit(
|
|
1077
|
+
bc,
|
|
1078
|
+
[null, { type: "defineLabel", label: loopTopLabel }],
|
|
1079
|
+
node,
|
|
1080
|
+
);
|
|
891
1081
|
|
|
892
1082
|
// Load iterator, attempt to get next key
|
|
893
1083
|
emitLoadIter();
|
|
894
|
-
|
|
895
|
-
|
|
1084
|
+
this.emit(
|
|
1085
|
+
bc,
|
|
1086
|
+
[this.OP.FOR_IN_NEXT, { type: "label", label: exitLabel }],
|
|
1087
|
+
node,
|
|
1088
|
+
);
|
|
896
1089
|
|
|
897
1090
|
// Assign the key (now on top of stack) to the loop variable
|
|
898
1091
|
if (node.left.type === "VariableDeclaration") {
|
|
899
|
-
const
|
|
1092
|
+
const identifier = node.left.declarations[0].id;
|
|
1093
|
+
ok(
|
|
1094
|
+
identifier.type === "Identifier",
|
|
1095
|
+
"Only simple identifiers can be declared in for-in loops",
|
|
1096
|
+
);
|
|
1097
|
+
const name = identifier.name;
|
|
900
1098
|
if (scope) {
|
|
901
1099
|
const slot = scope.define(name);
|
|
902
|
-
|
|
1100
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
903
1101
|
} else {
|
|
904
|
-
|
|
1102
|
+
this.emit(
|
|
1103
|
+
bc,
|
|
1104
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(name)],
|
|
1105
|
+
node,
|
|
1106
|
+
);
|
|
905
1107
|
}
|
|
906
1108
|
} else if (node.left.type === "Identifier") {
|
|
907
1109
|
const res = this._resolve(node.left.name, this._currentCtx);
|
|
908
1110
|
if (res.kind === "local") {
|
|
909
|
-
|
|
1111
|
+
this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
|
|
910
1112
|
} else if (res.kind === "upvalue") {
|
|
911
|
-
|
|
1113
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
912
1114
|
} else {
|
|
913
|
-
|
|
1115
|
+
this.emit(
|
|
1116
|
+
bc,
|
|
1117
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(node.left.name)],
|
|
1118
|
+
node,
|
|
1119
|
+
);
|
|
914
1120
|
}
|
|
915
1121
|
} else {
|
|
916
1122
|
const src = generate(node.left).code;
|
|
917
1123
|
throw new Error(
|
|
918
|
-
`Unsupported for-in left-hand side: ${node.left.type}\n
|
|
1124
|
+
`Unsupported for-in left-hand side: ${node.left.type}\n -> ${src}`,
|
|
919
1125
|
);
|
|
920
1126
|
}
|
|
921
1127
|
|
|
@@ -926,15 +1132,101 @@ class Compiler {
|
|
|
926
1132
|
this._compileStatement(stmt, scope, bc);
|
|
927
1133
|
}
|
|
928
1134
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1135
|
+
this.emit(
|
|
1136
|
+
bc,
|
|
1137
|
+
[this.OP.JUMP, { type: "label", label: loopTopLabel }],
|
|
1138
|
+
node,
|
|
1139
|
+
);
|
|
1140
|
+
this.emit(bc, [null, { type: "defineLabel", label: exitLabel }], node);
|
|
1141
|
+
|
|
1142
|
+
this._loopStack.pop();
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
case "TryStatement": {
|
|
1147
|
+
if (node.finalizer) {
|
|
1148
|
+
throw new Error(
|
|
1149
|
+
"try..finally is not supported. Use a helper function instead",
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
if (!node.handler) {
|
|
1153
|
+
// try without catch requires finally — not supported
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
"try without catch is not supported (requires finally).",
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const catchLabel = this._makeLabel("catch");
|
|
1160
|
+
const afterCatchLabel = this._makeLabel("after_catch");
|
|
1161
|
+
|
|
1162
|
+
// Emit TRY_SETUP with the catch block's label as the handler PC.
|
|
1163
|
+
// At runtime: saves stack depth + frame stack depth, pushes handler.
|
|
1164
|
+
this.emit(
|
|
1165
|
+
bc,
|
|
1166
|
+
[this.OP.TRY_SETUP, { type: "label", label: catchLabel }],
|
|
1167
|
+
node,
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
// Track the open try block so that break/continue/return inside the
|
|
1171
|
+
// try body can emit the matching TRY_END before their jump.
|
|
1172
|
+
this._loopStack.push({
|
|
1173
|
+
type: "try" as any,
|
|
1174
|
+
label: null,
|
|
1175
|
+
breakLabel: "", // unused
|
|
1176
|
+
continueLabel: "", // unused
|
|
1177
|
+
});
|
|
932
1178
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1179
|
+
// Compile try body
|
|
1180
|
+
for (const stmt of node.block.body) {
|
|
1181
|
+
this._compileStatement(stmt, scope, bc);
|
|
1182
|
+
}
|
|
936
1183
|
|
|
1184
|
+
// Done compiling the try body — pop the tracking entry.
|
|
937
1185
|
this._loopStack.pop();
|
|
1186
|
+
|
|
1187
|
+
// Normal exit: disarm the exception handler.
|
|
1188
|
+
this.emit(bc, [this.OP.TRY_END], node);
|
|
1189
|
+
|
|
1190
|
+
// Jump over the catch block on normal path.
|
|
1191
|
+
this.emit(
|
|
1192
|
+
bc,
|
|
1193
|
+
[this.OP.JUMP, { type: "label", label: afterCatchLabel }],
|
|
1194
|
+
node,
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
// Catch block: exception is on top of the stack (pushed by the VM).
|
|
1198
|
+
this.emit(bc, [null, { type: "defineLabel", label: catchLabel }], node);
|
|
1199
|
+
|
|
1200
|
+
const handler = node.handler;
|
|
1201
|
+
if (handler.param) {
|
|
1202
|
+
// Bind the exception value to the catch variable.
|
|
1203
|
+
const name = (handler.param as t.Identifier).name;
|
|
1204
|
+
if (scope) {
|
|
1205
|
+
const slot = scope.define(name);
|
|
1206
|
+
this.emit(bc, [this.OP.STORE_LOCAL, slot], node);
|
|
1207
|
+
} else {
|
|
1208
|
+
this.emit(
|
|
1209
|
+
bc,
|
|
1210
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(name)],
|
|
1211
|
+
node,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
// Optional catch binding (catch without a variable — ES2019+)
|
|
1216
|
+
this.emit(bc, [this.OP.POP], node);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Compile catch body
|
|
1220
|
+
for (const stmt of handler.body.body) {
|
|
1221
|
+
this._compileStatement(stmt, scope, bc);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Normal-path jump lands here (after the catch block).
|
|
1225
|
+
this.emit(
|
|
1226
|
+
bc,
|
|
1227
|
+
[null, { type: "defineLabel", label: afterCatchLabel }],
|
|
1228
|
+
node,
|
|
1229
|
+
);
|
|
938
1230
|
break;
|
|
939
1231
|
}
|
|
940
1232
|
|
|
@@ -942,65 +1234,75 @@ class Compiler {
|
|
|
942
1234
|
// Use @babel/generator to reproduce the source of unsupported nodes
|
|
943
1235
|
// so we can emit a clear error with context.
|
|
944
1236
|
const src = generate(node).code;
|
|
945
|
-
throw new Error(`Unsupported statement: ${node.type}\n
|
|
1237
|
+
throw new Error(`Unsupported statement: ${node.type}\n -> ${src}`);
|
|
946
1238
|
}
|
|
947
1239
|
}
|
|
948
1240
|
}
|
|
949
1241
|
|
|
950
|
-
//
|
|
951
|
-
|
|
1242
|
+
// Expressions
|
|
952
1243
|
_compileExpr(node, scope, bc) {
|
|
953
1244
|
switch (node.type) {
|
|
954
1245
|
case "NumericLiteral":
|
|
955
1246
|
case "StringLiteral": {
|
|
956
|
-
|
|
1247
|
+
this.emit(
|
|
1248
|
+
bc,
|
|
1249
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.value)],
|
|
1250
|
+
node,
|
|
1251
|
+
);
|
|
957
1252
|
break;
|
|
958
1253
|
}
|
|
959
1254
|
|
|
960
1255
|
case "BooleanLiteral": {
|
|
961
|
-
|
|
1256
|
+
this.emit(
|
|
1257
|
+
bc,
|
|
1258
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.value)],
|
|
1259
|
+
node,
|
|
1260
|
+
);
|
|
962
1261
|
break;
|
|
963
1262
|
}
|
|
964
1263
|
|
|
965
1264
|
case "NullLiteral": {
|
|
966
|
-
|
|
1265
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(null)], node);
|
|
967
1266
|
break;
|
|
968
1267
|
}
|
|
969
1268
|
|
|
970
1269
|
case "Identifier": {
|
|
971
|
-
// scope=null means we're at the top-level
|
|
1270
|
+
// scope=null means we're at the top-level -> always global
|
|
972
1271
|
const res = this._resolve(node.name, this._currentCtx);
|
|
973
1272
|
if (res.kind === "local") {
|
|
974
|
-
|
|
1273
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
975
1274
|
} else if (res.kind === "upvalue") {
|
|
976
|
-
|
|
1275
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
977
1276
|
} else {
|
|
978
|
-
|
|
1277
|
+
this.emit(
|
|
1278
|
+
bc,
|
|
1279
|
+
[this.OP.LOAD_GLOBAL, b.constantOperand(node.name)],
|
|
1280
|
+
node,
|
|
1281
|
+
);
|
|
979
1282
|
}
|
|
980
1283
|
break;
|
|
981
1284
|
}
|
|
982
1285
|
|
|
983
1286
|
case "ThisExpression": {
|
|
984
|
-
|
|
1287
|
+
this.emit(bc, [this.OP.LOAD_THIS], node);
|
|
985
1288
|
break;
|
|
986
1289
|
}
|
|
987
1290
|
|
|
988
1291
|
case "NewExpression": {
|
|
989
|
-
// Push callee, then args
|
|
1292
|
+
// Push callee, then args -- identical layout to CALL but uses NEW opcode
|
|
990
1293
|
this._compileExpr(node.callee, scope, bc);
|
|
991
1294
|
for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
|
|
992
|
-
|
|
1295
|
+
this.emit(bc, [this.OP.NEW, node.arguments.length], node);
|
|
993
1296
|
break;
|
|
994
1297
|
}
|
|
995
1298
|
|
|
996
1299
|
case "SequenceExpression": {
|
|
997
|
-
// (a, b, c)
|
|
998
|
-
// Matches CPython's BINARY_OP / POP_TOP pattern for comma expressions.
|
|
1300
|
+
// (a, b, c) -> eval a -> POP, eval b -> POP, eval c -> leave on stack
|
|
999
1301
|
for (let i = 0; i < node.expressions.length - 1; i++) {
|
|
1000
1302
|
this._compileExpr(node.expressions[i], scope, bc);
|
|
1001
|
-
|
|
1303
|
+
this.emit(bc, [this.OP.POP], node); // discard intermediate result
|
|
1002
1304
|
}
|
|
1003
|
-
// Last expression
|
|
1305
|
+
// Last expression -- its value is the result of the whole sequence
|
|
1004
1306
|
this._compileExpr(
|
|
1005
1307
|
node.expressions[node.expressions.length - 1],
|
|
1006
1308
|
scope,
|
|
@@ -1011,45 +1313,55 @@ class Compiler {
|
|
|
1011
1313
|
|
|
1012
1314
|
case "ConditionalExpression": {
|
|
1013
1315
|
// test ? consequent : alternate
|
|
1014
|
-
|
|
1015
|
-
this.
|
|
1316
|
+
const elseLabel = this._makeLabel("ternary_else");
|
|
1317
|
+
const endLabel = this._makeLabel("ternary_end");
|
|
1016
1318
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1319
|
+
this._compileExpr(node.test, scope, bc);
|
|
1320
|
+
this.emit(
|
|
1321
|
+
bc,
|
|
1322
|
+
[this.OP.JUMP_IF_FALSE, { type: "label", label: elseLabel }],
|
|
1323
|
+
node,
|
|
1324
|
+
);
|
|
1019
1325
|
|
|
1020
1326
|
this._compileExpr(node.consequent, scope, bc);
|
|
1327
|
+
this.emit(bc, [this.OP.JUMP, { type: "label", label: endLabel }], node);
|
|
1021
1328
|
|
|
1022
|
-
|
|
1023
|
-
const jumpToEnd = bc.length - 1;
|
|
1024
|
-
|
|
1025
|
-
bc[jumpToElse][1] = bc.length; // patch: false → alternate
|
|
1329
|
+
this.emit(bc, [null, { type: "defineLabel", label: elseLabel }], node);
|
|
1026
1330
|
this._compileExpr(node.alternate, scope, bc);
|
|
1027
1331
|
|
|
1028
|
-
bc[
|
|
1332
|
+
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1029
1333
|
break;
|
|
1030
1334
|
}
|
|
1031
1335
|
|
|
1032
1336
|
case "LogicalExpression": {
|
|
1033
1337
|
// Pattern (CPython-style):
|
|
1034
1338
|
// eval LHS
|
|
1035
|
-
// JUMP_IF_*_OR_POP
|
|
1339
|
+
// JUMP_IF_*_OR_POP -> target (past RHS)
|
|
1036
1340
|
// eval RHS ← only reached if LHS didn't short-circuit
|
|
1037
1341
|
// [target lands here, stack top is the result either way]
|
|
1038
1342
|
|
|
1039
1343
|
this._compileExpr(node.left, scope, bc);
|
|
1040
1344
|
|
|
1041
1345
|
if (node.operator === "||") {
|
|
1042
|
-
// Short-circuit if LHS is TRUTHY
|
|
1043
|
-
|
|
1044
|
-
|
|
1346
|
+
// Short-circuit if LHS is TRUTHY -- keep it, skip RHS
|
|
1347
|
+
const endLabel = this._makeLabel("or_end");
|
|
1348
|
+
this.emit(
|
|
1349
|
+
bc,
|
|
1350
|
+
[this.OP.JUMP_IF_TRUE_OR_POP, { type: "label", label: endLabel }],
|
|
1351
|
+
node,
|
|
1352
|
+
);
|
|
1045
1353
|
this._compileExpr(node.right, scope, bc);
|
|
1046
|
-
bc[
|
|
1354
|
+
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1047
1355
|
} else if (node.operator === "&&") {
|
|
1048
|
-
// Short-circuit if LHS is FALSY
|
|
1049
|
-
|
|
1050
|
-
|
|
1356
|
+
// Short-circuit if LHS is FALSY -- keep it, skip RHS
|
|
1357
|
+
const endLabel = this._makeLabel("and_end");
|
|
1358
|
+
this.emit(
|
|
1359
|
+
bc,
|
|
1360
|
+
[this.OP.JUMP_IF_FALSE_OR_POP, { type: "label", label: endLabel }],
|
|
1361
|
+
node,
|
|
1362
|
+
);
|
|
1051
1363
|
this._compileExpr(node.right, scope, bc);
|
|
1052
|
-
bc[
|
|
1364
|
+
this.emit(bc, [null, { type: "defineLabel", label: endLabel }], node);
|
|
1053
1365
|
} else {
|
|
1054
1366
|
throw new Error(`Unsupported logical operator: ${node.operator}`);
|
|
1055
1367
|
}
|
|
@@ -1060,70 +1372,74 @@ class Compiler {
|
|
|
1060
1372
|
this._compileExpr(node.left, scope, bc);
|
|
1061
1373
|
this._compileExpr(node.right, scope, bc);
|
|
1062
1374
|
const arithOp = {
|
|
1063
|
-
"+": OP.ADD,
|
|
1064
|
-
"-": OP.SUB,
|
|
1065
|
-
"*": OP.MUL,
|
|
1066
|
-
"/": OP.DIV,
|
|
1067
|
-
"%": OP.MOD,
|
|
1068
|
-
"&": OP.BAND,
|
|
1069
|
-
"|": OP.BOR,
|
|
1070
|
-
"^": OP.BXOR,
|
|
1071
|
-
"<<": OP.SHL,
|
|
1072
|
-
">>": OP.SHR,
|
|
1073
|
-
">>>": OP.USHR,
|
|
1375
|
+
"+": this.OP.ADD,
|
|
1376
|
+
"-": this.OP.SUB,
|
|
1377
|
+
"*": this.OP.MUL,
|
|
1378
|
+
"/": this.OP.DIV,
|
|
1379
|
+
"%": this.OP.MOD,
|
|
1380
|
+
"&": this.OP.BAND,
|
|
1381
|
+
"|": this.OP.BOR,
|
|
1382
|
+
"^": this.OP.BXOR,
|
|
1383
|
+
"<<": this.OP.SHL,
|
|
1384
|
+
">>": this.OP.SHR,
|
|
1385
|
+
">>>": this.OP.USHR,
|
|
1074
1386
|
}[node.operator];
|
|
1075
1387
|
|
|
1076
1388
|
const cmpOp = {
|
|
1077
|
-
"<": OP.LT,
|
|
1078
|
-
">": OP.GT,
|
|
1079
|
-
"===": OP.EQ,
|
|
1080
|
-
"==": OP.LOOSE_EQ,
|
|
1081
|
-
"<=": OP.LTE,
|
|
1082
|
-
">=": OP.GTE,
|
|
1083
|
-
"!==": OP.NEQ,
|
|
1084
|
-
"!=": OP.LOOSE_NEQ,
|
|
1085
|
-
in: OP.IN, // ← add
|
|
1086
|
-
instanceof: OP.INSTANCEOF, // ← add
|
|
1389
|
+
"<": this.OP.LT,
|
|
1390
|
+
">": this.OP.GT,
|
|
1391
|
+
"===": this.OP.EQ,
|
|
1392
|
+
"==": this.OP.LOOSE_EQ,
|
|
1393
|
+
"<=": this.OP.LTE,
|
|
1394
|
+
">=": this.OP.GTE,
|
|
1395
|
+
"!==": this.OP.NEQ,
|
|
1396
|
+
"!=": this.OP.LOOSE_NEQ,
|
|
1397
|
+
in: this.OP.IN, // ← add
|
|
1398
|
+
instanceof: this.OP.INSTANCEOF, // ← add
|
|
1087
1399
|
}[node.operator];
|
|
1088
1400
|
const resolvedOp = arithOp ?? cmpOp;
|
|
1089
1401
|
if (resolvedOp === undefined)
|
|
1090
1402
|
throw new Error(`Unsupported operator: ${node.operator}`);
|
|
1091
|
-
|
|
1403
|
+
this.emit(bc, [resolvedOp], node);
|
|
1092
1404
|
|
|
1093
1405
|
break;
|
|
1094
1406
|
}
|
|
1095
1407
|
|
|
1096
1408
|
case "UpdateExpression": {
|
|
1097
1409
|
const res = this._resolve(node.argument.name, this._currentCtx);
|
|
1098
|
-
const bumpOp = node.operator === "++" ? OP.ADD : OP.SUB;
|
|
1099
|
-
const one =
|
|
1410
|
+
const bumpOp = node.operator === "++" ? this.OP.ADD : this.OP.SUB;
|
|
1411
|
+
const one = b.constantOperand(1);
|
|
1100
1412
|
|
|
1101
1413
|
// Helper closures: emit load / store for whichever resolution kind we have
|
|
1102
1414
|
const emitLoad = () => {
|
|
1103
|
-
if (res.kind === "local")
|
|
1415
|
+
if (res.kind === "local")
|
|
1416
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
1104
1417
|
else if (res.kind === "upvalue")
|
|
1105
|
-
|
|
1418
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1106
1419
|
else
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
this.
|
|
1110
|
-
|
|
1420
|
+
this.emit(
|
|
1421
|
+
bc,
|
|
1422
|
+
[this.OP.LOAD_GLOBAL, b.constantOperand(node.argument.name)],
|
|
1423
|
+
node,
|
|
1424
|
+
);
|
|
1111
1425
|
};
|
|
1112
1426
|
const emitStore = () => {
|
|
1113
|
-
if (res.kind === "local")
|
|
1427
|
+
if (res.kind === "local")
|
|
1428
|
+
this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
|
|
1114
1429
|
else if (res.kind === "upvalue")
|
|
1115
|
-
|
|
1430
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
1116
1431
|
else
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
this.
|
|
1120
|
-
|
|
1432
|
+
this.emit(
|
|
1433
|
+
bc,
|
|
1434
|
+
[this.OP.STORE_GLOBAL, b.constantOperand(node.argument.name)],
|
|
1435
|
+
node,
|
|
1436
|
+
);
|
|
1121
1437
|
};
|
|
1122
1438
|
|
|
1123
1439
|
emitLoad();
|
|
1124
|
-
if (!node.prefix)
|
|
1125
|
-
|
|
1126
|
-
|
|
1440
|
+
if (!node.prefix) this.emit(bc, [this.OP.DUP], node); // post: save old value before mutating
|
|
1441
|
+
this.emit(bc, [this.OP.LOAD_CONST, one], node);
|
|
1442
|
+
this.emit(bc, [bumpOp], node);
|
|
1127
1443
|
emitStore();
|
|
1128
1444
|
if (node.prefix) emitLoad(); // pre: reload new value as result
|
|
1129
1445
|
|
|
@@ -1132,17 +1448,17 @@ class Compiler {
|
|
|
1132
1448
|
|
|
1133
1449
|
case "AssignmentExpression": {
|
|
1134
1450
|
const compoundOp = {
|
|
1135
|
-
"+=": OP.ADD,
|
|
1136
|
-
"-=": OP.SUB,
|
|
1137
|
-
"*=": OP.MUL,
|
|
1138
|
-
"/=": OP.DIV,
|
|
1139
|
-
"%=": OP.MOD,
|
|
1140
|
-
"&=": OP.BAND,
|
|
1141
|
-
"|=": OP.BOR,
|
|
1142
|
-
"^=": OP.BXOR,
|
|
1143
|
-
"<<=": OP.SHL,
|
|
1144
|
-
">>=": OP.SHR,
|
|
1145
|
-
">>>=": OP.USHR,
|
|
1451
|
+
"+=": this.OP.ADD,
|
|
1452
|
+
"-=": this.OP.SUB,
|
|
1453
|
+
"*=": this.OP.MUL,
|
|
1454
|
+
"/=": this.OP.DIV,
|
|
1455
|
+
"%=": this.OP.MOD,
|
|
1456
|
+
"&=": this.OP.BAND,
|
|
1457
|
+
"|=": this.OP.BOR,
|
|
1458
|
+
"^=": this.OP.BXOR,
|
|
1459
|
+
"<<=": this.OP.SHL,
|
|
1460
|
+
">>=": this.OP.SHR,
|
|
1461
|
+
">>>=": this.OP.USHR,
|
|
1146
1462
|
}[node.operator];
|
|
1147
1463
|
|
|
1148
1464
|
const isCompound = compoundOp !== undefined;
|
|
@@ -1151,76 +1467,85 @@ class Compiler {
|
|
|
1151
1467
|
throw new Error(`Unsupported assignment operator: ${node.operator}`);
|
|
1152
1468
|
}
|
|
1153
1469
|
|
|
1154
|
-
//
|
|
1470
|
+
// Member assignment: obj.x = val or arr[i] = val
|
|
1155
1471
|
if (node.left.type === "MemberExpression") {
|
|
1156
1472
|
this._compileExpr(node.left.object, scope, bc); // push obj
|
|
1157
1473
|
|
|
1158
1474
|
if (node.left.computed) {
|
|
1159
1475
|
this._compileExpr(node.left.property, scope, bc); // push key (runtime)
|
|
1160
1476
|
} else {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
this.
|
|
1164
|
-
|
|
1477
|
+
this.emit(
|
|
1478
|
+
bc,
|
|
1479
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.left.property.name)],
|
|
1480
|
+
node,
|
|
1481
|
+
);
|
|
1165
1482
|
}
|
|
1166
1483
|
|
|
1167
1484
|
if (isCompound) {
|
|
1168
1485
|
// Duplicate obj+key on the stack so we can read before we write.
|
|
1169
1486
|
// Stack before DUP2: [..., obj, key]
|
|
1170
|
-
// We need: [..., obj, key, obj, key]
|
|
1487
|
+
// We need: [..., obj, key, obj, key] -> GET_PROP_COMPUTED -> [..., obj, key, currentVal]
|
|
1171
1488
|
// Cheapest approach without a DUP opcode: re-compile the member read.
|
|
1172
1489
|
// (emits obj + key again; a future peephole pass could DUP instead)
|
|
1173
1490
|
this._compileExpr(node.left.object, scope, bc);
|
|
1174
1491
|
if (node.left.computed) {
|
|
1175
1492
|
this._compileExpr(node.left.property, scope, bc);
|
|
1176
1493
|
} else {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1494
|
+
this.emit(
|
|
1495
|
+
bc,
|
|
1496
|
+
[
|
|
1497
|
+
this.OP.LOAD_CONST,
|
|
1498
|
+
b.constantOperand(node.left.property.name),
|
|
1499
|
+
],
|
|
1500
|
+
node,
|
|
1501
|
+
);
|
|
1181
1502
|
}
|
|
1182
|
-
|
|
1503
|
+
this.emit(bc, [this.OP.GET_PROP_COMPUTED], node); // [..., obj, key, currentVal]
|
|
1183
1504
|
this._compileExpr(node.right, scope, bc); // [..., obj, key, currentVal, rhs]
|
|
1184
|
-
|
|
1505
|
+
this.emit(bc, [compoundOp], node); // [..., obj, key, newVal]
|
|
1185
1506
|
} else {
|
|
1186
1507
|
this._compileExpr(node.right, scope, bc); // [..., obj, key, val]
|
|
1187
1508
|
}
|
|
1188
1509
|
|
|
1189
|
-
|
|
1510
|
+
this.emit(bc, [this.OP.SET_PROP], node); // obj[key] = val, leaves val on stack
|
|
1190
1511
|
break;
|
|
1191
1512
|
}
|
|
1192
1513
|
|
|
1193
|
-
//
|
|
1514
|
+
// Plain identifier assignment
|
|
1194
1515
|
const res = this._resolve(node.left.name, this._currentCtx);
|
|
1195
1516
|
|
|
1196
1517
|
if (isCompound) {
|
|
1197
1518
|
// Load the current value of the target first
|
|
1198
1519
|
if (res.kind === "local") {
|
|
1199
|
-
|
|
1520
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
1200
1521
|
} else if (res.kind === "upvalue") {
|
|
1201
|
-
|
|
1522
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1202
1523
|
} else {
|
|
1203
|
-
|
|
1524
|
+
this.emit(
|
|
1525
|
+
bc,
|
|
1526
|
+
[this.OP.LOAD_GLOBAL, b.constantOperand(node.left.name)],
|
|
1527
|
+
node,
|
|
1528
|
+
);
|
|
1204
1529
|
}
|
|
1205
1530
|
}
|
|
1206
1531
|
|
|
1207
1532
|
this._compileExpr(node.right, scope, bc); // push RHS
|
|
1208
1533
|
|
|
1209
1534
|
if (isCompound) {
|
|
1210
|
-
|
|
1535
|
+
this.emit(bc, [compoundOp], node); // apply binary op -> leaves newVal on stack
|
|
1211
1536
|
}
|
|
1212
1537
|
|
|
1213
1538
|
// Store & leave value on stack (assignment is an expression)
|
|
1214
1539
|
if (res.kind === "local") {
|
|
1215
|
-
|
|
1216
|
-
|
|
1540
|
+
this.emit(bc, [this.OP.STORE_LOCAL, res.slot], node);
|
|
1541
|
+
this.emit(bc, [this.OP.LOAD_LOCAL, res.slot], node);
|
|
1217
1542
|
} else if (res.kind === "upvalue") {
|
|
1218
|
-
|
|
1219
|
-
|
|
1543
|
+
this.emit(bc, [this.OP.STORE_UPVALUE, res.index], node);
|
|
1544
|
+
this.emit(bc, [this.OP.LOAD_UPVALUE, res.index], node);
|
|
1220
1545
|
} else {
|
|
1221
|
-
const nameIdx =
|
|
1222
|
-
|
|
1223
|
-
|
|
1546
|
+
const nameIdx = b.constantOperand(node.left.name);
|
|
1547
|
+
this.emit(bc, [this.OP.STORE_GLOBAL, nameIdx], node);
|
|
1548
|
+
this.emit(bc, [this.OP.LOAD_GLOBAL, nameIdx], node);
|
|
1224
1549
|
}
|
|
1225
1550
|
break;
|
|
1226
1551
|
}
|
|
@@ -1231,16 +1556,16 @@ class Compiler {
|
|
|
1231
1556
|
// Push receiver first (GET_PROP leaves it; CALL_METHOD pops it as `this`)
|
|
1232
1557
|
this._compileExpr(node.callee.object, scope, bc);
|
|
1233
1558
|
const prop = node.callee.property.name;
|
|
1234
|
-
const propIdx =
|
|
1235
|
-
|
|
1236
|
-
|
|
1559
|
+
const propIdx = b.constantOperand(prop);
|
|
1560
|
+
this.emit(bc, [this.OP.LOAD_CONST, propIdx], node);
|
|
1561
|
+
this.emit(bc, [this.OP.GET_PROP], node);
|
|
1237
1562
|
for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
|
|
1238
|
-
|
|
1563
|
+
this.emit(bc, [this.OP.CALL_METHOD, node.arguments.length], node);
|
|
1239
1564
|
} else {
|
|
1240
1565
|
// ── Plain call: add(5, 10)
|
|
1241
1566
|
this._compileExpr(node.callee, scope, bc);
|
|
1242
1567
|
for (const arg of node.arguments) this._compileExpr(arg, scope, bc);
|
|
1243
|
-
|
|
1568
|
+
this.emit(bc, [this.OP.CALL, node.arguments.length], node);
|
|
1244
1569
|
}
|
|
1245
1570
|
break;
|
|
1246
1571
|
}
|
|
@@ -1252,141 +1577,244 @@ class Compiler {
|
|
|
1252
1577
|
if (node.operator === "typeof" && node.argument.type === "Identifier") {
|
|
1253
1578
|
const res = this._resolve(node.argument.name, this._currentCtx);
|
|
1254
1579
|
if (res.kind === "global") {
|
|
1255
|
-
// Potentially undeclared
|
|
1256
|
-
|
|
1257
|
-
|
|
1580
|
+
// Potentially undeclared -- let VM guard it
|
|
1581
|
+
this.emit(
|
|
1582
|
+
bc,
|
|
1583
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.argument.name)],
|
|
1584
|
+
node,
|
|
1585
|
+
);
|
|
1586
|
+
this.emit(bc, [this.OP.TYPEOF_SAFE], node);
|
|
1258
1587
|
break;
|
|
1259
1588
|
}
|
|
1260
|
-
// Known local or upvalue
|
|
1589
|
+
// Known local or upvalue -- safe to load first, then typeof
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Special case: delete -- argument must NOT be pre-evaluated.
|
|
1593
|
+
if (node.operator === "delete") {
|
|
1594
|
+
const arg = node.argument;
|
|
1595
|
+
if (arg.type === "MemberExpression") {
|
|
1596
|
+
this._compileExpr(arg.object, scope, bc);
|
|
1597
|
+
if (arg.computed) {
|
|
1598
|
+
this._compileExpr(arg.property, scope, bc);
|
|
1599
|
+
} else {
|
|
1600
|
+
this.emit(
|
|
1601
|
+
bc,
|
|
1602
|
+
[this.OP.LOAD_CONST, b.constantOperand(arg.property.name)],
|
|
1603
|
+
node,
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
this.emit(bc, [this.OP.DELETE_PROP], node);
|
|
1607
|
+
} else {
|
|
1608
|
+
// delete x, delete 0, etc. -- always true in non-strict, just push true
|
|
1609
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(true)], node);
|
|
1610
|
+
}
|
|
1611
|
+
break;
|
|
1261
1612
|
}
|
|
1613
|
+
|
|
1262
1614
|
// All other unary ops: compile argument first, then apply operator
|
|
1263
1615
|
this._compileExpr(node.argument, scope, bc);
|
|
1264
1616
|
switch (node.operator) {
|
|
1265
1617
|
case "-":
|
|
1266
|
-
|
|
1618
|
+
this.emit(bc, [this.OP.UNARY_NEG], node);
|
|
1267
1619
|
break;
|
|
1268
1620
|
case "+":
|
|
1269
|
-
|
|
1621
|
+
this.emit(bc, [this.OP.UNARY_POS], node);
|
|
1270
1622
|
break;
|
|
1271
1623
|
case "!":
|
|
1272
|
-
|
|
1624
|
+
this.emit(bc, [this.OP.UNARY_NOT], node);
|
|
1273
1625
|
break;
|
|
1274
1626
|
case "~":
|
|
1275
|
-
|
|
1627
|
+
this.emit(bc, [this.OP.UNARY_BITNOT], node);
|
|
1276
1628
|
break;
|
|
1277
1629
|
case "typeof":
|
|
1278
|
-
|
|
1630
|
+
this.emit(bc, [this.OP.TYPEOF], node);
|
|
1279
1631
|
break;
|
|
1280
1632
|
case "void":
|
|
1281
|
-
|
|
1633
|
+
this.emit(bc, [this.OP.VOID], node);
|
|
1282
1634
|
break;
|
|
1283
1635
|
|
|
1284
|
-
case "delete": {
|
|
1285
|
-
const arg = node.argument;
|
|
1286
|
-
if (arg.type === "MemberExpression") {
|
|
1287
|
-
this._compileExpr(arg.object, scope, bc);
|
|
1288
|
-
if (arg.computed) {
|
|
1289
|
-
this._compileExpr(arg.property, scope, bc);
|
|
1290
|
-
} else {
|
|
1291
|
-
bc.push([
|
|
1292
|
-
OP.LOAD_CONST,
|
|
1293
|
-
this.constants.intern(arg.property.name),
|
|
1294
|
-
]);
|
|
1295
|
-
}
|
|
1296
|
-
bc.push([OP.DELETE_PROP]);
|
|
1297
|
-
} else {
|
|
1298
|
-
// delete x, delete 0, etc. — always true in non-strict, just push true
|
|
1299
|
-
bc.push([OP.LOAD_CONST, this.constants.intern(true)]);
|
|
1300
|
-
}
|
|
1301
|
-
break;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
1636
|
default:
|
|
1305
1637
|
throw new Error(`Unsupported unary operator: ${node.operator}`);
|
|
1306
1638
|
}
|
|
1307
1639
|
break;
|
|
1308
1640
|
}
|
|
1309
1641
|
|
|
1642
|
+
case "RegExpLiteral": {
|
|
1643
|
+
// Emit: new RegExp(pattern, flags)
|
|
1644
|
+
// Fresh object per evaluation -- correct for stateful g/y flags.
|
|
1645
|
+
this.emit(bc, [this.OP.LOAD_GLOBAL, b.constantOperand("RegExp")], node);
|
|
1646
|
+
this.emit(
|
|
1647
|
+
bc,
|
|
1648
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.pattern)],
|
|
1649
|
+
node,
|
|
1650
|
+
);
|
|
1651
|
+
this.emit(
|
|
1652
|
+
bc,
|
|
1653
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.flags)],
|
|
1654
|
+
node,
|
|
1655
|
+
);
|
|
1656
|
+
this.emit(bc, [this.OP.NEW, 2], node);
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1310
1660
|
case "FunctionExpression": {
|
|
1311
1661
|
// Compile into a descriptor exactly like a declaration,
|
|
1312
|
-
// but leave the resulting closure ON THE STACK
|
|
1662
|
+
// but leave the resulting closure ON THE STACK -- no store.
|
|
1313
1663
|
// The surrounding expression (assignment, call arg, return) consumes it.
|
|
1314
1664
|
const desc = this._compileFunctionDecl(node);
|
|
1315
|
-
|
|
1665
|
+
this._emitClosureMetadata(desc, node, bc);
|
|
1666
|
+
this.emit(
|
|
1667
|
+
bc,
|
|
1668
|
+
[this.OP.MAKE_CLOSURE, { type: "label", label: desc.entryLabel }],
|
|
1669
|
+
node,
|
|
1670
|
+
);
|
|
1316
1671
|
break;
|
|
1317
1672
|
}
|
|
1318
1673
|
|
|
1319
1674
|
case "MemberExpression": {
|
|
1320
1675
|
this._compileExpr(node.object, scope, bc);
|
|
1321
1676
|
if (node.computed) {
|
|
1322
|
-
// nums[i]
|
|
1677
|
+
// nums[i] -- key is runtime value
|
|
1323
1678
|
this._compileExpr(node.property, scope, bc);
|
|
1324
1679
|
} else {
|
|
1325
|
-
// point.x
|
|
1326
|
-
|
|
1680
|
+
// point.x -- push key as string, same opcode handles both
|
|
1681
|
+
this.emit(
|
|
1682
|
+
bc,
|
|
1683
|
+
[this.OP.LOAD_CONST, b.constantOperand(node.property.name)],
|
|
1684
|
+
node,
|
|
1685
|
+
);
|
|
1327
1686
|
}
|
|
1328
1687
|
|
|
1329
|
-
// GET_PROP_COMPUTED pops the object
|
|
1688
|
+
// GET_PROP_COMPUTED pops the object -- correct for value access.
|
|
1330
1689
|
// GET_PROP (peek) is only used in CallExpression's method call path
|
|
1331
1690
|
// where the receiver must survive on the stack for CALL_METHOD.
|
|
1332
|
-
|
|
1691
|
+
this.emit(bc, [this.OP.GET_PROP_COMPUTED], node);
|
|
1333
1692
|
break;
|
|
1334
1693
|
}
|
|
1335
1694
|
|
|
1336
1695
|
case "ArrayExpression": {
|
|
1337
|
-
// Compile each element left
|
|
1696
|
+
// Compile each element left->right, then BUILD_ARRAY collapses them.
|
|
1338
1697
|
// Sparse arrays (holes) get explicit undefined per slot.
|
|
1339
1698
|
for (const el of node.elements) {
|
|
1340
1699
|
if (el === null) {
|
|
1341
1700
|
// hole: e.g. [1,,3]
|
|
1342
|
-
|
|
1701
|
+
this.emit(
|
|
1702
|
+
bc,
|
|
1703
|
+
[this.OP.LOAD_CONST, b.constantOperand(undefined)],
|
|
1704
|
+
node,
|
|
1705
|
+
);
|
|
1343
1706
|
} else {
|
|
1344
1707
|
this._compileExpr(el, scope, bc);
|
|
1345
1708
|
}
|
|
1346
1709
|
}
|
|
1347
|
-
|
|
1710
|
+
this.emit(bc, [this.OP.BUILD_ARRAY, node.elements.length], node);
|
|
1348
1711
|
break;
|
|
1349
1712
|
}
|
|
1350
1713
|
case "ObjectExpression": {
|
|
1351
|
-
//
|
|
1352
|
-
|
|
1714
|
+
// Separate regular data properties from ES5 accessor methods (get/set).
|
|
1715
|
+
const regularProps: t.ObjectProperty[] = [];
|
|
1716
|
+
const accessorProps: t.ObjectMethod[] = [];
|
|
1717
|
+
|
|
1353
1718
|
for (const prop of node.properties) {
|
|
1354
1719
|
if (prop.type === "SpreadElement") {
|
|
1355
1720
|
throw new Error("Object spread not supported");
|
|
1356
1721
|
}
|
|
1357
|
-
|
|
1722
|
+
if (prop.type === "ObjectMethod") {
|
|
1723
|
+
if (prop.kind === "get" || prop.kind === "set") {
|
|
1724
|
+
if (prop.computed) {
|
|
1725
|
+
throw new Error(
|
|
1726
|
+
"Computed getter/setter keys are not supported",
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
accessorProps.push(prop);
|
|
1730
|
+
} else {
|
|
1731
|
+
throw new Error(`Shorthand method syntax is not supported`);
|
|
1732
|
+
}
|
|
1733
|
+
} else {
|
|
1734
|
+
regularProps.push(prop as t.ObjectProperty);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Build the base object from data properties.
|
|
1739
|
+
for (const prop of regularProps) {
|
|
1358
1740
|
const key = prop.key;
|
|
1359
|
-
let keyStr;
|
|
1741
|
+
let keyStr: string;
|
|
1360
1742
|
if (key.type === "Identifier") {
|
|
1361
|
-
keyStr = key.name;
|
|
1743
|
+
keyStr = key.name;
|
|
1362
1744
|
} else if (
|
|
1363
1745
|
key.type === "StringLiteral" ||
|
|
1364
1746
|
key.type === "NumericLiteral"
|
|
1365
1747
|
) {
|
|
1366
|
-
keyStr = String(key.value);
|
|
1748
|
+
keyStr = String(key.value);
|
|
1367
1749
|
} else {
|
|
1368
1750
|
throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1369
1751
|
}
|
|
1370
|
-
|
|
1371
|
-
// Value — any expression, including FunctionExpression
|
|
1752
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
|
|
1372
1753
|
this._compileExpr(prop.value, scope, bc);
|
|
1373
1754
|
}
|
|
1374
|
-
|
|
1755
|
+
this.emit(bc, [this.OP.BUILD_OBJECT, regularProps.length], node);
|
|
1756
|
+
|
|
1757
|
+
// Define each accessor on the object that is now on top of the stack.
|
|
1758
|
+
// Stack after BUILD_OBJECT: [..., obj]
|
|
1759
|
+
// For each accessor: DUP obj, push key, compile fn, DEFINE_GETTER/DEFINE_SETTER
|
|
1760
|
+
// DEFINE_GETTER/DEFINE_SETTER pops fn+key+obj, leaving the original obj.
|
|
1761
|
+
for (const prop of accessorProps) {
|
|
1762
|
+
const key = prop.key;
|
|
1763
|
+
let keyStr: string;
|
|
1764
|
+
if (key.type === "Identifier") {
|
|
1765
|
+
keyStr = key.name;
|
|
1766
|
+
} else if (
|
|
1767
|
+
key.type === "StringLiteral" ||
|
|
1768
|
+
key.type === "NumericLiteral"
|
|
1769
|
+
) {
|
|
1770
|
+
keyStr = String(key.value);
|
|
1771
|
+
} else {
|
|
1772
|
+
throw new Error(`Unsupported object key type: ${key.type}`);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
this.emit(bc, [this.OP.DUP], node); // dup so the original obj stays after the define
|
|
1776
|
+
this.emit(bc, [this.OP.LOAD_CONST, b.constantOperand(keyStr)], node);
|
|
1777
|
+
|
|
1778
|
+
// Compile the accessor body as an anonymous function descriptor.
|
|
1779
|
+
const desc = this._compileFunctionDecl(prop as any);
|
|
1780
|
+
this._emitClosureMetadata(desc, prop as any, bc);
|
|
1781
|
+
this.emit(
|
|
1782
|
+
bc,
|
|
1783
|
+
[
|
|
1784
|
+
this.OP.MAKE_CLOSURE,
|
|
1785
|
+
{
|
|
1786
|
+
type: "label",
|
|
1787
|
+
label: desc.entryLabel,
|
|
1788
|
+
},
|
|
1789
|
+
],
|
|
1790
|
+
node,
|
|
1791
|
+
);
|
|
1792
|
+
|
|
1793
|
+
this.emit(
|
|
1794
|
+
bc,
|
|
1795
|
+
[
|
|
1796
|
+
prop.kind === "get"
|
|
1797
|
+
? this.OP.DEFINE_GETTER
|
|
1798
|
+
: this.OP.DEFINE_SETTER,
|
|
1799
|
+
],
|
|
1800
|
+
node,
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1375
1804
|
break;
|
|
1376
1805
|
}
|
|
1377
1806
|
|
|
1378
1807
|
default: {
|
|
1379
|
-
|
|
1380
|
-
throw new Error(`Unsupported expression: ${node.type}\n → ${src}`);
|
|
1808
|
+
throw new Error(`Unsupported expression: ${node.type}`);
|
|
1381
1809
|
}
|
|
1382
1810
|
}
|
|
1383
1811
|
}
|
|
1384
1812
|
}
|
|
1385
1813
|
|
|
1386
|
-
// ─────────────────────────────────────────────────────────────────
|
|
1387
1814
|
// Serializer
|
|
1388
1815
|
// Turns the compiled output into a commented JS source string.
|
|
1389
|
-
//
|
|
1816
|
+
// Expects fully-resolved bytecode (all label refs and constant refs already
|
|
1817
|
+
// converted to plain integers by resolveLabels + resolveConstants passes).
|
|
1390
1818
|
class Serializer {
|
|
1391
1819
|
compiler: Compiler;
|
|
1392
1820
|
|
|
@@ -1394,70 +1822,95 @@ class Serializer {
|
|
|
1394
1822
|
this.compiler = compiler;
|
|
1395
1823
|
}
|
|
1396
1824
|
|
|
1397
|
-
get
|
|
1398
|
-
return this.compiler.
|
|
1825
|
+
get options() {
|
|
1826
|
+
return this.compiler.options;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
get OP() {
|
|
1830
|
+
return this.compiler.OP;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
get OP_NAME() {
|
|
1834
|
+
return this.compiler.OP_NAME;
|
|
1399
1835
|
}
|
|
1400
1836
|
|
|
1401
|
-
get
|
|
1402
|
-
return this.compiler.
|
|
1837
|
+
get JUMP_OPS() {
|
|
1838
|
+
return this.compiler.JUMP_OPS;
|
|
1403
1839
|
}
|
|
1404
1840
|
|
|
1405
1841
|
// Produce a JS literal for a constant pool entry
|
|
1406
1842
|
_serializeConst(val) {
|
|
1407
1843
|
if (val === null) return "null";
|
|
1408
1844
|
if (val === undefined) return "undefined";
|
|
1409
|
-
if (typeof val === "object" && val._fnIdx !== undefined) {
|
|
1410
|
-
return `FN[${val._fnIdx}]`; // fn descriptor → reference by FN index
|
|
1411
|
-
}
|
|
1412
1845
|
return JSON.stringify(val); // number / string / bool
|
|
1413
1846
|
}
|
|
1414
1847
|
|
|
1415
|
-
// One instruction
|
|
1416
|
-
|
|
1417
|
-
|
|
1848
|
+
// One instruction -> "[op, operand] // MNEMONIC description"
|
|
1849
|
+
// Expects a fully-resolved instruction: operand is a plain number or undefined.
|
|
1850
|
+
_serializeInstr(instr: b.Instruction, constants: any[]) {
|
|
1851
|
+
const [op, rawOperand] = instr;
|
|
1852
|
+
|
|
1853
|
+
ok(
|
|
1854
|
+
rawOperand === undefined || typeof rawOperand === "number",
|
|
1855
|
+
"Unresolved operand: " + JSON.stringify(rawOperand),
|
|
1856
|
+
);
|
|
1857
|
+
const operand = rawOperand as number | undefined;
|
|
1418
1858
|
|
|
1419
|
-
const [op
|
|
1420
|
-
const name = OP_NAME[op] || `OP_${op}`;
|
|
1859
|
+
const name = this.OP_NAME[op] || `OP_${op}`;
|
|
1421
1860
|
let comment = name;
|
|
1422
1861
|
|
|
1862
|
+
const sourceNode = instr[SOURCE_NODE_SYM];
|
|
1863
|
+
const sourceLocation = sourceNode
|
|
1864
|
+
? sourceNode.loc.start?.line +
|
|
1865
|
+
":" +
|
|
1866
|
+
sourceNode.loc.start?.column +
|
|
1867
|
+
"-" +
|
|
1868
|
+
(sourceNode.loc.end?.line + ":" + sourceNode.loc.end?.column)
|
|
1869
|
+
: "";
|
|
1870
|
+
|
|
1423
1871
|
// Annotate operand with its meaning
|
|
1424
1872
|
if (operand !== undefined) {
|
|
1425
1873
|
switch (op) {
|
|
1426
|
-
case OP.LOAD_CONST:
|
|
1427
|
-
case OP.MAKE_CLOSURE: {
|
|
1874
|
+
case this.OP.LOAD_CONST: {
|
|
1428
1875
|
const val = constants[operand];
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1876
|
+
comment += ` ${this._serializeConst(val)}`;
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1879
|
+
case this.OP.MAKE_CLOSURE: {
|
|
1880
|
+
// operand is the absolute PC of the function body's first instruction
|
|
1881
|
+
comment += ` PC ${operand}`;
|
|
1434
1882
|
break;
|
|
1435
1883
|
}
|
|
1436
|
-
case OP.
|
|
1437
|
-
|
|
1884
|
+
case this.OP.DATA: {
|
|
1885
|
+
// Inline function header word — value is a raw integer
|
|
1886
|
+
comment += ` ${operand}`;
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
case this.OP.LOAD_LOCAL:
|
|
1890
|
+
case this.OP.STORE_LOCAL:
|
|
1438
1891
|
comment += ` slot[${operand}]`;
|
|
1439
1892
|
break;
|
|
1440
|
-
case OP.LOAD_UPVALUE:
|
|
1441
|
-
case OP.STORE_UPVALUE:
|
|
1893
|
+
case this.OP.LOAD_UPVALUE:
|
|
1894
|
+
case this.OP.STORE_UPVALUE:
|
|
1442
1895
|
comment += ` upvalue[${operand}]`;
|
|
1443
1896
|
break;
|
|
1444
|
-
case OP.LOAD_GLOBAL:
|
|
1445
|
-
case OP.STORE_GLOBAL:
|
|
1897
|
+
case this.OP.LOAD_GLOBAL:
|
|
1898
|
+
case this.OP.STORE_GLOBAL:
|
|
1446
1899
|
comment += ` "${constants[operand]}"`;
|
|
1447
1900
|
break;
|
|
1448
|
-
case OP.CALL:
|
|
1449
|
-
case OP.CALL_METHOD:
|
|
1901
|
+
case this.OP.CALL:
|
|
1902
|
+
case this.OP.CALL_METHOD:
|
|
1450
1903
|
comment += ` (${operand} args)`;
|
|
1451
1904
|
break;
|
|
1452
1905
|
|
|
1453
|
-
case OP.BUILD_ARRAY:
|
|
1906
|
+
case this.OP.BUILD_ARRAY:
|
|
1454
1907
|
comment += ` (${operand} elements)`;
|
|
1455
1908
|
break;
|
|
1456
|
-
case OP.BUILD_OBJECT:
|
|
1909
|
+
case this.OP.BUILD_OBJECT:
|
|
1457
1910
|
comment += ` (${operand} pairs)`;
|
|
1458
1911
|
break;
|
|
1459
1912
|
|
|
1460
|
-
case OP.NEW:
|
|
1913
|
+
case this.OP.NEW:
|
|
1461
1914
|
comment += ` (${operand} args)`;
|
|
1462
1915
|
break;
|
|
1463
1916
|
|
|
@@ -1466,15 +1919,17 @@ class Serializer {
|
|
|
1466
1919
|
}
|
|
1467
1920
|
}
|
|
1468
1921
|
|
|
1922
|
+
comment = comment.padEnd(40) + sourceLocation;
|
|
1923
|
+
|
|
1469
1924
|
// Pack a [op, operand?] instruction pair into a single 32-bit word.
|
|
1470
1925
|
// Shared between the Serializer and the obfuscation path in _compileMain.
|
|
1471
1926
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
|
|
1927
|
+
const instrText = operand !== undefined ? `[${op}, ${operand}]` : `[${op}]`;
|
|
1928
|
+
const text = `${(instrText + ",").padEnd(12)} ${comment}`;
|
|
1475
1929
|
|
|
1930
|
+
if (!this.options.encodeBytecode) {
|
|
1476
1931
|
return {
|
|
1477
|
-
text:
|
|
1932
|
+
text: text,
|
|
1478
1933
|
value: operand !== undefined ? [op, operand] : [op],
|
|
1479
1934
|
};
|
|
1480
1935
|
}
|
|
@@ -1491,47 +1946,38 @@ class Serializer {
|
|
|
1491
1946
|
}
|
|
1492
1947
|
|
|
1493
1948
|
return {
|
|
1494
|
-
text:
|
|
1949
|
+
text: text,
|
|
1495
1950
|
value: packInstr(instr),
|
|
1496
1951
|
};
|
|
1497
1952
|
}
|
|
1498
1953
|
|
|
1499
|
-
// Serialize
|
|
1500
|
-
|
|
1501
|
-
const lines = [
|
|
1502
|
-
` { // FN[${desc._fnIdx}] — ${desc.name}`,
|
|
1503
|
-
` paramCount: ${desc.paramCount},`,
|
|
1504
|
-
` localCount: ${desc.localCount},`,
|
|
1505
|
-
` upvalueDescriptors: ${JSON5.stringify(desc.upvalueDescriptors)},`,
|
|
1506
|
-
` startPc: ${desc.startPc},`,
|
|
1507
|
-
` },`,
|
|
1508
|
-
];
|
|
1509
|
-
return lines.join("\n");
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// Serialize the CONSTANTS array, showing FN[n] references
|
|
1513
|
-
_serializeConstants() {
|
|
1954
|
+
// Serialize the CONSTANTS array
|
|
1955
|
+
_serializeConstants(constants: any[]) {
|
|
1514
1956
|
const lines = ["var CONSTANTS = ["];
|
|
1515
|
-
|
|
1957
|
+
constants.forEach((val, idx) => {
|
|
1516
1958
|
lines.push(` /* ${idx} */ ${this._serializeConst(val)},`);
|
|
1517
1959
|
});
|
|
1518
1960
|
lines.push("];");
|
|
1519
1961
|
return lines.join("\n");
|
|
1520
1962
|
}
|
|
1521
1963
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1964
|
+
// Filter out any remaining null-opcode pseudo-instructions.
|
|
1965
|
+
// (defineLabel pseudo-ops are already stripped by resolveLabels.)
|
|
1966
|
+
_serializeBytecode(bytecode: b.Bytecode): { bytecode: b.Bytecode } {
|
|
1967
|
+
return {
|
|
1968
|
+
bytecode: bytecode.filter((instr) => instr[0] !== null),
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1526
1971
|
|
|
1972
|
+
__serializeBytecode(bytecode: b.Bytecode, constants: any[]) {
|
|
1527
1973
|
let words = [];
|
|
1528
1974
|
|
|
1529
|
-
//
|
|
1975
|
+
// BYTECODE
|
|
1530
1976
|
for (const instr of bytecode) {
|
|
1531
|
-
words.push(this._serializeInstr(instr).value);
|
|
1977
|
+
words.push(this._serializeInstr(instr, constants).value);
|
|
1532
1978
|
}
|
|
1533
1979
|
|
|
1534
|
-
// Convert packed words
|
|
1980
|
+
// Convert packed words -> raw 4-byte little-endian binary -> base64
|
|
1535
1981
|
const buf = new Uint8Array(words.length * 4);
|
|
1536
1982
|
words.forEach((w, i) => {
|
|
1537
1983
|
buf[i * 4] = w & 0xff;
|
|
@@ -1539,77 +1985,80 @@ class Serializer {
|
|
|
1539
1985
|
buf[i * 4 + 2] = (w >>> 16) & 0xff;
|
|
1540
1986
|
buf[i * 4 + 3] = (w >>> 24) & 0xff;
|
|
1541
1987
|
});
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
return b64;
|
|
1988
|
+
return Buffer.from(buf).toString("base64");
|
|
1545
1989
|
}
|
|
1546
1990
|
|
|
1547
|
-
serialize(bytecode,
|
|
1548
|
-
const
|
|
1991
|
+
serialize(bytecode: b.Bytecode, constants: any[], compiler: Compiler) {
|
|
1992
|
+
const mainStartPc = compiler.mainStartPc;
|
|
1993
|
+
let sections = [];
|
|
1994
|
+
|
|
1995
|
+
var textForm = [];
|
|
1996
|
+
var initBody = [];
|
|
1549
1997
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
for (const
|
|
1553
|
-
|
|
1998
|
+
var bytecodeResult = this._serializeBytecode(bytecode);
|
|
1999
|
+
|
|
2000
|
+
for (const instr of bytecodeResult.bytecode) {
|
|
2001
|
+
const serialized = this._serializeInstr(instr, constants);
|
|
2002
|
+
textForm.push(serialized.text);
|
|
1554
2003
|
}
|
|
1555
|
-
fnLines.push("];");
|
|
1556
|
-
sections.push(fnLines.join("\n"));
|
|
1557
2004
|
|
|
1558
|
-
|
|
1559
|
-
sections.push(this._serializeConstants());
|
|
2005
|
+
initBody.push(textForm.map((line) => `// ${line}`).join("\n"));
|
|
1560
2006
|
|
|
1561
|
-
if (
|
|
1562
|
-
sections.push(
|
|
2007
|
+
if (this.options.encodeBytecode) {
|
|
2008
|
+
sections.push(
|
|
2009
|
+
`var BYTECODE = "${this.__serializeBytecode(bytecodeResult.bytecode, constants)}";`,
|
|
2010
|
+
);
|
|
1563
2011
|
} else {
|
|
1564
2012
|
sections.push(
|
|
1565
|
-
`var BYTECODE = [
|
|
2013
|
+
`var BYTECODE = [${bytecodeResult.bytecode.map((v) => "[" + v[0] + ", " + v[1] + "]").join(",")}]`,
|
|
1566
2014
|
);
|
|
1567
2015
|
}
|
|
1568
2016
|
|
|
1569
|
-
//
|
|
2017
|
+
// MAIN_START_PC
|
|
1570
2018
|
sections.push(`var MAIN_START_PC = ${mainStartPc};`);
|
|
2019
|
+
sections.push(`var ENCODE_BYTECODE = ${!!this.options.encodeBytecode};`);
|
|
2020
|
+
sections.push(`var TIMING_CHECKS = ${!!this.options.timingChecks};`);
|
|
2021
|
+
// Opcodes
|
|
2022
|
+
sections.push(`var OP = ${JSON5.stringify(this.OP)};`);
|
|
2023
|
+
|
|
2024
|
+
// Constants must be defined before the bytecode
|
|
2025
|
+
initBody.push(this._serializeConstants(constants));
|
|
1571
2026
|
|
|
1572
|
-
sections
|
|
2027
|
+
sections = [...initBody, ...sections];
|
|
1573
2028
|
|
|
1574
|
-
//
|
|
2029
|
+
// VM runtime
|
|
1575
2030
|
sections.push(VM_RUNTIME);
|
|
1576
2031
|
|
|
1577
2032
|
return sections.join("\n\n");
|
|
1578
2033
|
}
|
|
1579
2034
|
}
|
|
1580
2035
|
|
|
1581
|
-
|
|
1582
|
-
// VM Runtime (emitted verbatim into the output file)
|
|
1583
|
-
// ─────────────────────────────────────────────────────────────────
|
|
1584
|
-
const VM_RUNTIME = `
|
|
1585
|
-
// ── Opcodes ──────────────────────────────────────────────────────
|
|
1586
|
-
var OP = ${JSON5.stringify(OP)};
|
|
1587
|
-
${stripTypeScriptTypes(
|
|
1588
|
-
readFileSync(join(import.meta.dirname, "./runtime.ts"), "utf-8").split(
|
|
1589
|
-
"@START",
|
|
1590
|
-
)[1],
|
|
1591
|
-
)}
|
|
1592
|
-
`;
|
|
1593
|
-
|
|
1594
|
-
interface Options {
|
|
1595
|
-
sourceMap?: boolean;
|
|
1596
|
-
selfModifying?: boolean;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
export function compileAndSerialize(
|
|
2036
|
+
export async function compileAndSerialize(
|
|
1600
2037
|
sourceCode: string,
|
|
1601
|
-
options: Options
|
|
1602
|
-
selfModifying: true,
|
|
1603
|
-
},
|
|
2038
|
+
options: Options,
|
|
1604
2039
|
) {
|
|
1605
2040
|
const compiler = new Compiler(options);
|
|
1606
|
-
|
|
2041
|
+
let bytecode = compiler.compile(sourceCode);
|
|
2042
|
+
|
|
2043
|
+
// User transform passes (operate on unresolved IR with label/constant refs)
|
|
2044
|
+
const passes = [...(options.selfModifying ? [selfModifying] : [])];
|
|
2045
|
+
for (const pass of passes) {
|
|
2046
|
+
const passResult = pass(bytecode, compiler);
|
|
2047
|
+
bytecode = passResult.bytecode;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Assembler phases: resolve IR operands to plain integers before printing
|
|
2051
|
+
const { bytecode: labelResolved } = resolveLabels(bytecode, compiler);
|
|
2052
|
+
const { bytecode: finalBytecode, constants } =
|
|
2053
|
+
resolveConstants(labelResolved);
|
|
2054
|
+
|
|
1607
2055
|
const output = compiler.serializer.serialize(
|
|
1608
|
-
|
|
1609
|
-
|
|
2056
|
+
finalBytecode,
|
|
2057
|
+
constants,
|
|
2058
|
+
compiler,
|
|
1610
2059
|
);
|
|
1611
2060
|
|
|
1612
|
-
const finalOutput = output;
|
|
2061
|
+
const finalOutput = await obfuscateRuntime(output, options);
|
|
1613
2062
|
|
|
1614
2063
|
return {
|
|
1615
2064
|
code: finalOutput,
|