jsguardian 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1480 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KrakCompiler = exports.KrakBailError = void 0;
4
+ // ── Types ─────────────────────────────────────────────────────────────────────
5
+ class KrakBailError extends Error {
6
+ }
7
+ exports.KrakBailError = KrakBailError;
8
+ // ── Helpers ───────────────────────────────────────────────────────────────────
9
+ // 32-bit LCG: full 32-bit state, keystream byte = key >>> 24
10
+ function computeKeyAtAddress(initialKey, address, lcgMul, lcgInc) {
11
+ let key = initialKey >>> 0;
12
+ for (let i = 0; i < address; i++)
13
+ key = ((Math.imul(key, lcgMul) + lcgInc) | 0) >>> 0;
14
+ return key;
15
+ }
16
+ // ── Compiler ──────────────────────────────────────────────────────────────────
17
+ class KrakCompiler {
18
+ constructor(config) {
19
+ this.bytecodeArr = [];
20
+ this.labels = {};
21
+ this.variables = {};
22
+ this.nextVarSlot = 4;
23
+ this.pendingJumps = [];
24
+ this.functions = {};
25
+ this.loopStack = [];
26
+ // Inline-callback return context: ReturnStatement inside map/filter/forEach
27
+ // callback must jump to a per-callback exit label rather than emitting RET
28
+ // (which would exit the whole VM function).
29
+ this.inlineCallbackStack = [];
30
+ // Hardening: randomise temp reg pool
31
+ this.hardeningEnabled = false;
32
+ this.hardeningSalt = 0;
33
+ this.tempRegMap = {};
34
+ this.tempRegUsed = {};
35
+ this.config = config;
36
+ this.opcodes = config.opcodes;
37
+ this.layouts = config.argLayouts;
38
+ }
39
+ // ── Var / reg management ──────────────────────────────────────────────────
40
+ allocVar(name) {
41
+ if (this.variables[name] === undefined)
42
+ this.variables[name] = this.nextVarSlot++;
43
+ return this.variables[name];
44
+ }
45
+ getVarReg(name) {
46
+ const slot = this.variables[name];
47
+ if (slot === undefined)
48
+ throw new KrakBailError("Undefined variable: " + name);
49
+ return slot % 256;
50
+ }
51
+ getTempReg(index) {
52
+ if (!this.hardeningEnabled)
53
+ return 200 + index;
54
+ if (this.tempRegMap[index] !== undefined)
55
+ return this.tempRegMap[index];
56
+ const poolStart = 200, poolSize = 54;
57
+ const start = ((index * 7) + this.hardeningSalt) % poolSize;
58
+ for (let probe = 0; probe < poolSize; probe++) {
59
+ const reg = poolStart + ((start + probe) % poolSize);
60
+ if (!this.tempRegUsed[reg]) {
61
+ this.tempRegUsed[reg] = true;
62
+ this.tempRegMap[index] = reg;
63
+ return reg;
64
+ }
65
+ }
66
+ const fb = poolStart + (index % poolSize);
67
+ this.tempRegMap[index] = fb;
68
+ return fb;
69
+ }
70
+ // ── Bytecode emission ─────────────────────────────────────────────────────
71
+ emit(bytes) {
72
+ for (const b of bytes)
73
+ this.bytecodeArr.push(b & 0xFF);
74
+ }
75
+ currentAddress() { return this.bytecodeArr.length; }
76
+ setLabel(name) { this.labels[name] = this.currentAddress(); }
77
+ // Encode args according to the per-build shuffled layout.
78
+ encodeArgs(layoutName, argValues) {
79
+ if (layoutName === "FUNC") {
80
+ if (argValues["k"] === undefined)
81
+ argValues["k"] = 0;
82
+ if (argValues["n"] === undefined)
83
+ argValues["n"] = 0;
84
+ }
85
+ const layout = this.layouts[layoutName] ?? [];
86
+ const result = [];
87
+ for (const argDef of layout) {
88
+ const value = argValues[argDef.name];
89
+ if (value === undefined)
90
+ throw new KrakBailError(`Missing arg: ${argDef.name} for ${layoutName}`);
91
+ if (argDef.type === "BYTE") {
92
+ result.push(value & 0xFF);
93
+ }
94
+ else if (argDef.type === "INT") {
95
+ result.push(value & 0xFF);
96
+ result.push((value >> 8) & 0xFF);
97
+ result.push((value >> 16) & 0xFF);
98
+ result.push((value >> 24) & 0xFF);
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ // ── Emit helpers ──────────────────────────────────────────────────────────
104
+ emitMovInt(reg, value) {
105
+ if (reg === 1 && value === 0)
106
+ return;
107
+ this.emit([this.opcodes["MOV"]]);
108
+ this.emit(this.encodeArgs("MOV", { r: reg, v: value }));
109
+ }
110
+ emitMovBool(reg, val) {
111
+ this.emit([this.opcodes["BOOL"]]);
112
+ this.emit(this.encodeArgs("BOOL", { d: reg, v: val ? 1 : 0 }));
113
+ }
114
+ emitMovReg(dest, src) {
115
+ if (dest === src)
116
+ return;
117
+ this.emit([this.opcodes["MOVR"]]);
118
+ this.emit(this.encodeArgs("MOVR", { d: dest, s: src }));
119
+ }
120
+ emitBinaryOp(opName, dest, src) {
121
+ this.emit([this.opcodes[opName]]);
122
+ this.emit(this.encodeArgs(opName, { d: dest, s: src }));
123
+ }
124
+ emitPush(reg) {
125
+ this.emit([this.opcodes["PUSH"]]);
126
+ this.emit(this.encodeArgs("PUSH", { r: reg }));
127
+ }
128
+ emitPop(reg) {
129
+ this.emit([this.opcodes["POP"]]);
130
+ this.emit(this.encodeArgs("POP", { r: reg }));
131
+ }
132
+ emitNewStr(destReg, str) {
133
+ this.emit([this.opcodes["STR"]]);
134
+ this.emit(this.encodeArgs("STR", { d: destReg }));
135
+ const len = str.length;
136
+ this.emit([len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF]);
137
+ for (let i = 0; i < str.length; i++)
138
+ this.emit([str.charCodeAt(i) & 0xFF]);
139
+ }
140
+ emitGetGlobal(destReg, nameReg) {
141
+ this.emit([this.opcodes["GGLO"]]);
142
+ this.emit(this.encodeArgs("GGLO", { d: destReg, n: nameReg }));
143
+ }
144
+ emitGetProp(destReg, objReg, propReg) {
145
+ this.emit([this.opcodes["GPRP"]]);
146
+ this.emit(this.encodeArgs("GPRP", { d: destReg, o: objReg, p: propReg }));
147
+ }
148
+ emitSetProp(objReg, propReg, valueReg) {
149
+ this.emit([this.opcodes["SPRP"]]);
150
+ this.emit(this.encodeArgs("SPRP", { o: objReg, p: propReg, v: valueReg }));
151
+ }
152
+ emitCallMethod(destReg, objReg, methodReg, argCount) {
153
+ this.emit([this.opcodes["METH"]]);
154
+ this.emit(this.encodeArgs("METH", { d: destReg, o: objReg, m: methodReg, c: argCount }));
155
+ }
156
+ emitFromNum(destReg, srcReg) {
157
+ this.emit([this.opcodes["FNUM"]]);
158
+ this.emit(this.encodeArgs("FNUM", { d: destReg, s: srcReg }));
159
+ }
160
+ emitToNum(destReg, srcReg) {
161
+ this.emit([this.opcodes["TNUM"]]);
162
+ this.emit(this.encodeArgs("TNUM", { d: destReg, s: srcReg }));
163
+ }
164
+ emitCallAddr(targetLabel) {
165
+ this.emit([this.opcodes["CADR"]]);
166
+ this.pendingJumps.push({ type: "CADR", address: this.currentAddress(), label: targetLabel });
167
+ this.emit(this.encodeArgs("CADR", { k: 0, t: 0 }));
168
+ }
169
+ emitNew(destReg, constructorReg, argCount) {
170
+ this.emit([this.opcodes["NEW"]]);
171
+ this.emit(this.encodeArgs("NEW", { d: destReg, c: constructorReg, n: argCount }));
172
+ }
173
+ emitInstanceOf(destReg, objReg, constrReg) {
174
+ this.emit([this.opcodes["IOF"]]);
175
+ this.emit(this.encodeArgs("IOF", { d: destReg, o: objReg, c: constrReg }));
176
+ }
177
+ emitTypeof(destReg, srcReg) {
178
+ this.emit([this.opcodes["TYP"]]);
179
+ this.emit(this.encodeArgs("TYP", { d: destReg, s: srcReg }));
180
+ }
181
+ emitRet() { this.emit([this.opcodes["RET"]]); }
182
+ emitJz(reg, targetLabel) {
183
+ this.emit([this.opcodes["JZ"]]);
184
+ this.pendingJumps.push({ type: "JZ", address: this.currentAddress(), label: targetLabel, reg });
185
+ this.emit(this.encodeArgs("JZ", { r: reg, k: 0, t: 0 }));
186
+ }
187
+ emitJnz(reg, targetLabel) {
188
+ this.emit([this.opcodes["JNZ"]]);
189
+ this.pendingJumps.push({ type: "JNZ", address: this.currentAddress(), label: targetLabel, reg });
190
+ this.emit(this.encodeArgs("JNZ", { r: reg, k: 0, t: 0 }));
191
+ }
192
+ emitJmp(targetLabel) {
193
+ this.emit([this.opcodes["JMP"]]);
194
+ this.pendingJumps.push({ type: "JMP", address: this.currentAddress(), label: targetLabel });
195
+ this.emit(this.encodeArgs("JMP", { k: 0, t: 0 }));
196
+ }
197
+ emitJgt(targetLabel) {
198
+ this.emit([this.opcodes["JGT"]]);
199
+ this.pendingJumps.push({ type: "JGT", address: this.currentAddress(), label: targetLabel });
200
+ this.emit(this.encodeArgs("JGT", { k: 0, t: 0 }));
201
+ }
202
+ emitJlt(targetLabel) {
203
+ this.emit([this.opcodes["JLT"]]);
204
+ this.pendingJumps.push({ type: "JLT", address: this.currentAddress(), label: targetLabel });
205
+ this.emit(this.encodeArgs("JLT", { k: 0, t: 0 }));
206
+ }
207
+ // emitCallReg: check if fn is internal (number type) or external (function).
208
+ emitCallReg(destReg, funcReg, argCount) {
209
+ const typeReg = this.getTempReg(46); // reg 246 — safe, below flags reg 255
210
+ const numTypeReg = this.getTempReg(47); // reg 247
211
+ this.emitTypeof(typeReg, funcReg);
212
+ this.emitNewStr(numTypeReg, "number");
213
+ this.emit([this.opcodes["CMP"]]);
214
+ this.emit(this.encodeArgs("CMP", { a: typeReg, b: numTypeReg }));
215
+ const externalLabel = `__creg_ext_${this.currentAddress()}`;
216
+ const endLabel = `__creg_end_${this.currentAddress()}`;
217
+ this.emitJnz(255, externalLabel);
218
+ this.emit([this.opcodes["CREGI"]]);
219
+ this.emit(this.encodeArgs("CREGI", { f: funcReg, n: argCount }));
220
+ if (destReg !== 0)
221
+ this.emitMovReg(destReg, 0);
222
+ this.emitJmp(endLabel);
223
+ this.setLabel(externalLabel);
224
+ this.emit([this.opcodes["CREGE"]]);
225
+ this.emit(this.encodeArgs("CREGE", { d: destReg, f: funcReg, n: argCount }));
226
+ this.setLabel(endLabel);
227
+ }
228
+ // ── compileMemberBase ─────────────────────────────────────────────────────
229
+ compileMemberBase(node, objReg, propReg) {
230
+ if (node.object.type === "Identifier" && this.variables[node.object.name] === undefined) {
231
+ this.emitNewStr(this.getTempReg(0), node.object.name);
232
+ this.emitGetGlobal(objReg, this.getTempReg(0));
233
+ }
234
+ else {
235
+ this.compileExpression(node.object, objReg);
236
+ }
237
+ if (node.computed) {
238
+ this.compileExpression(node.property, propReg);
239
+ }
240
+ else {
241
+ const propName = node.property.name ?? node.property.value;
242
+ this.emitNewStr(propReg, String(propName));
243
+ }
244
+ }
245
+ compileMemberExpression(node, targetReg) {
246
+ const objReg = this.getTempReg(20);
247
+ const propReg = this.getTempReg(21);
248
+ this.compileMemberBase(node, objReg, propReg);
249
+ this.emitGetProp(targetReg, objReg, propReg);
250
+ }
251
+ // ── tryCompileConditionExit ───────────────────────────────────────────────
252
+ tryCompileConditionExit(node, exitLabel) {
253
+ if (node.type !== "BinaryExpression")
254
+ return false;
255
+ const op = node.operator;
256
+ if (!["<", "<=", ">", ">=", "==", "===", "!=", "!=="].includes(op))
257
+ return false;
258
+ let leftReg;
259
+ if (node.left.type === "Identifier" && this.variables[node.left.name] !== undefined) {
260
+ leftReg = this.getVarReg(node.left.name);
261
+ }
262
+ else {
263
+ leftReg = this.getTempReg(48); // reg 248 — was 254 (too close to flags)
264
+ this.compileExpression(node.left, leftReg);
265
+ }
266
+ let rightReg;
267
+ if (node.right.type === "Identifier" && this.variables[node.right.name] !== undefined) {
268
+ rightReg = this.getVarReg(node.right.name);
269
+ }
270
+ else if (node.right.type === "NumericLiteral" && node.right.value === 0) {
271
+ rightReg = 1;
272
+ }
273
+ else {
274
+ rightReg = this.getTempReg(49); // reg 249 — was 255 (CMP flags reg, conflicting)
275
+ this.compileExpression(node.right, rightReg);
276
+ }
277
+ this.emit([this.opcodes["CMP"]]);
278
+ this.emit(this.encodeArgs("CMP", { a: leftReg, b: rightReg }));
279
+ switch (op) {
280
+ case "<":
281
+ this.emitJgt(exitLabel);
282
+ this.emitJz(255, exitLabel);
283
+ break;
284
+ case "<=":
285
+ this.emitJgt(exitLabel);
286
+ break;
287
+ case ">":
288
+ this.emitJlt(exitLabel);
289
+ this.emitJz(255, exitLabel);
290
+ break;
291
+ case ">=":
292
+ this.emitJlt(exitLabel);
293
+ break;
294
+ case "==":
295
+ case "===":
296
+ this.emitJlt(exitLabel);
297
+ this.emitJgt(exitLabel);
298
+ break;
299
+ case "!=":
300
+ case "!==":
301
+ this.emitJz(255, exitLabel);
302
+ break;
303
+ }
304
+ return true;
305
+ }
306
+ // ── compileCallExpression ─────────────────────────────────────────────────
307
+ compileCallExpression(node, targetReg) {
308
+ // Handle array methods with inline callbacks
309
+ if (node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.name) {
310
+ const methodName = node.callee.property.name;
311
+ const isCbMethod = ["map", "filter", "forEach", "reduce"].includes(methodName);
312
+ if (isCbMethod) {
313
+ if (node.arguments.length < 1)
314
+ throw new KrakBailError(`Array.${methodName} requires a callback`);
315
+ const callbackArg = node.arguments[0];
316
+ const cbType = callbackArg.type;
317
+ const isInlineCallback = cbType === "ArrowFunctionExpression" || cbType === "FunctionExpression";
318
+ // ── reduce ── compile as: acc = init; for (i=0; i<len; i++) acc = cb(acc, arr[i], i, arr)
319
+ if (methodName === "reduce") {
320
+ const uid = this.currentAddress();
321
+ const colReg = this.allocVar(`__rd_col_${uid}`) % 256;
322
+ const lenReg = this.allocVar(`__rd_len_${uid}`) % 256;
323
+ const idxReg = this.allocVar(`__rd_idx_${uid}`) % 256;
324
+ const accReg = this.allocVar(`__rd_acc_${uid}`) % 256;
325
+ const elemReg = this.allocVar(`__rd_el_${uid}`) % 256;
326
+ this.compileExpression(node.callee.object, colReg);
327
+ this.emitNewStr(this.getTempReg(4), "length");
328
+ this.emitGetProp(lenReg, colReg, this.getTempReg(4));
329
+ this.emitToNum(lenReg, lenReg);
330
+ // initial accumulator and start index
331
+ let startIdx = 0;
332
+ if (node.arguments.length >= 2) {
333
+ this.compileExpression(node.arguments[1], accReg);
334
+ startIdx = 0;
335
+ }
336
+ else {
337
+ // no initial value: acc = arr[0], start loop at 1
338
+ this.emitMovInt(this.getTempReg(4), 0);
339
+ this.emitFromNum(this.getTempReg(4), this.getTempReg(4));
340
+ this.emitGetProp(accReg, colReg, this.getTempReg(4));
341
+ startIdx = 1;
342
+ }
343
+ this.emitMovInt(idxReg, startIdx);
344
+ const loopStart = `__rd_loop_${uid}`, loopEnd = `__rd_end_${uid}`, loopBody = `__rd_body_${uid}`;
345
+ this.setLabel(loopStart);
346
+ this.emit([this.opcodes["CMP"]]);
347
+ this.emit(this.encodeArgs("CMP", { a: idxReg, b: lenReg }));
348
+ this.emit([this.opcodes["JLT"]]);
349
+ this.pendingJumps.push({ type: "JLT", address: this.currentAddress(), label: loopBody });
350
+ this.emit(this.encodeArgs("JLT", { k: 0, t: 0 }));
351
+ this.emitJmp(loopEnd);
352
+ this.setLabel(loopBody);
353
+ this.emitFromNum(this.getTempReg(4), idxReg);
354
+ this.emitGetProp(elemReg, colReg, this.getTempReg(4));
355
+ if (isInlineCallback) {
356
+ const callbackArity = callbackArg.params.length;
357
+ const savedVars = this.variables;
358
+ const savedNext = this.nextVarSlot;
359
+ this.variables = Object.assign({}, this.variables);
360
+ if (callbackArity >= 1) {
361
+ const p0 = this.allocVar(callbackArg.params[0].name) % 256;
362
+ this.emitMovReg(p0, accReg);
363
+ }
364
+ if (callbackArity >= 2) {
365
+ const p1 = this.allocVar(callbackArg.params[1].name) % 256;
366
+ this.emitMovReg(p1, elemReg);
367
+ }
368
+ if (callbackArity >= 3) {
369
+ const p2 = this.allocVar(callbackArg.params[2].name) % 256;
370
+ this.emitMovReg(p2, idxReg);
371
+ }
372
+ if (callbackArity >= 4) {
373
+ const p3 = this.allocVar(callbackArg.params[3].name) % 256;
374
+ this.emitMovReg(p3, colReg);
375
+ }
376
+ if (callbackArg.body.type === "BlockStatement") {
377
+ const cbRetLabel = `__rd_ret_${uid}_${this.currentAddress()}`;
378
+ this.inlineCallbackStack.push({ cbResReg: accReg, returnLabel: cbRetLabel });
379
+ this.compileStatement(callbackArg.body);
380
+ this.inlineCallbackStack.pop();
381
+ this.setLabel(cbRetLabel);
382
+ }
383
+ else {
384
+ this.compileExpression(callbackArg.body, accReg);
385
+ }
386
+ this.variables = savedVars;
387
+ this.nextVarSlot = savedNext;
388
+ }
389
+ else if (cbType === "Identifier") {
390
+ const callReg = this.getVarReg(callbackArg.name);
391
+ this.emitPush(accReg);
392
+ this.emitPush(elemReg);
393
+ this.emitPush(idxReg);
394
+ this.emitPush(colReg);
395
+ this.emitCallReg(accReg, callReg, 4);
396
+ }
397
+ else {
398
+ throw new KrakBailError("unsupported reduce callback type: " + cbType);
399
+ }
400
+ this.emit([this.opcodes["INC"]]);
401
+ this.emit(this.encodeArgs("INC", { r: idxReg }));
402
+ this.emitJmp(loopStart);
403
+ this.setLabel(loopEnd);
404
+ if (targetReg !== accReg)
405
+ this.emitMovReg(targetReg, accReg);
406
+ return;
407
+ }
408
+ // ── map / filter / forEach ────────────────────────────────────────────
409
+ const uid = this.currentAddress();
410
+ const colReg = this.allocVar(`__cb_col_${uid}`) % 256;
411
+ const lenReg = this.allocVar(`__cb_len_${uid}`) % 256;
412
+ const idxReg = this.allocVar(`__cb_idx_${uid}`) % 256;
413
+ const resReg = this.allocVar(`__cb_res_${uid}`) % 256;
414
+ const elemReg = this.allocVar(`__cb_el_${uid}`) % 256;
415
+ const cbResReg = this.allocVar(`__cb_val_${uid}`) % 256;
416
+ this.compileExpression(node.callee.object, colReg);
417
+ this.emitNewStr(this.getTempReg(4), "length");
418
+ this.emitGetProp(lenReg, colReg, this.getTempReg(4));
419
+ this.emitToNum(lenReg, lenReg);
420
+ if (methodName === "map" || methodName === "filter") {
421
+ const arrayConstReg = this.getTempReg(36);
422
+ this.emitNewStr(this.getTempReg(37), "Array");
423
+ this.emitGetGlobal(arrayConstReg, this.getTempReg(37));
424
+ this.emitNew(resReg, arrayConstReg, 0);
425
+ }
426
+ this.emitMovInt(idxReg, 0);
427
+ const loopStart = `__cb_loop_${uid}`, loopEnd = `__cb_end_${uid}`, loopBody = `__cb_body_${uid}`;
428
+ this.setLabel(loopStart);
429
+ this.emit([this.opcodes["CMP"]]);
430
+ this.emit(this.encodeArgs("CMP", { a: idxReg, b: lenReg }));
431
+ this.emit([this.opcodes["JLT"]]);
432
+ this.pendingJumps.push({ type: "JLT", address: this.currentAddress(), label: loopBody });
433
+ this.emit(this.encodeArgs("JLT", { k: 0, t: 0 }));
434
+ this.emitJmp(loopEnd);
435
+ this.setLabel(loopBody);
436
+ this.emitFromNum(this.getTempReg(4), idxReg);
437
+ this.emitGetProp(elemReg, colReg, this.getTempReg(4));
438
+ if (isInlineCallback) {
439
+ const callbackArity = callbackArg.params.length;
440
+ const savedVars = this.variables;
441
+ const savedNext = this.nextVarSlot;
442
+ this.variables = Object.assign({}, this.variables);
443
+ if (callbackArity >= 1) {
444
+ const p0 = this.allocVar(callbackArg.params[0].name) % 256;
445
+ this.emitMovReg(p0, elemReg);
446
+ }
447
+ if (callbackArity >= 2) {
448
+ const p1 = this.allocVar(callbackArg.params[1].name) % 256;
449
+ this.emitMovReg(p1, idxReg);
450
+ }
451
+ if (callbackArity >= 3) {
452
+ const p2 = this.allocVar(callbackArg.params[2].name) % 256;
453
+ this.emitMovReg(p2, colReg);
454
+ }
455
+ if (callbackArg.body.type === "BlockStatement") {
456
+ // Each `return` in the block must NOT emit RET (that exits the whole
457
+ // VM function). Instead we push a context so ReturnStatement jumps
458
+ // to cbRetLabel, writing the value into cbResReg.
459
+ const cbRetLabel = `__cb_ret_${uid}_${this.currentAddress()}`;
460
+ this.inlineCallbackStack.push({ cbResReg, returnLabel: cbRetLabel });
461
+ this.compileStatement(callbackArg.body);
462
+ this.inlineCallbackStack.pop();
463
+ this.emitMovInt(cbResReg, 0); // fallthrough: callback body had no return
464
+ this.setLabel(cbRetLabel); // all `return expr` paths jump here
465
+ }
466
+ else
467
+ this.compileExpression(callbackArg.body, cbResReg);
468
+ this.variables = savedVars;
469
+ this.nextVarSlot = savedNext;
470
+ }
471
+ else if (cbType === "Identifier") {
472
+ const callReg = this.getVarReg(callbackArg.name);
473
+ this.emitPush(elemReg);
474
+ this.emitPush(idxReg);
475
+ this.emitPush(colReg);
476
+ this.emitCallReg(cbResReg, callReg, 3);
477
+ }
478
+ else {
479
+ throw new KrakBailError("unsupported callback type: " + cbType);
480
+ }
481
+ if (methodName === "map") {
482
+ this.emitPush(cbResReg);
483
+ this.emitNewStr(this.getTempReg(38), "push");
484
+ this.emitCallMethod(this.getTempReg(39), resReg, this.getTempReg(38), 1);
485
+ }
486
+ else if (methodName === "filter") {
487
+ const skipPush = `__cb_skip_${uid}_${this.currentAddress()}`;
488
+ this.emitJz(cbResReg, skipPush);
489
+ this.emitPush(elemReg);
490
+ this.emitNewStr(this.getTempReg(38), "push");
491
+ this.emitCallMethod(this.getTempReg(39), resReg, this.getTempReg(38), 1);
492
+ this.setLabel(skipPush);
493
+ }
494
+ this.emit([this.opcodes["INC"]]);
495
+ this.emit(this.encodeArgs("INC", { r: idxReg }));
496
+ this.emitJmp(loopStart);
497
+ this.setLabel(loopEnd);
498
+ if (methodName === "map" || methodName === "filter") {
499
+ if (targetReg !== resReg)
500
+ this.emitMovReg(targetReg, resReg);
501
+ }
502
+ else {
503
+ this.emitMovInt(targetReg, 0);
504
+ }
505
+ return;
506
+ }
507
+ }
508
+ // Named function call (direct)
509
+ if (node.callee.type === "Identifier" && this.functions[node.callee.name]) {
510
+ for (let i = 0; i < node.arguments.length; i++) {
511
+ const argReg = this.getTempReg(40 + i);
512
+ this.compileExpression(node.arguments[i], argReg);
513
+ this.emitPush(argReg);
514
+ }
515
+ const thisPlaceholder = this.getTempReg(0);
516
+ this.emitMovInt(thisPlaceholder, 0);
517
+ this.emitPush(thisPlaceholder);
518
+ this.emitCallAddr(`__func_${node.callee.name}`);
519
+ if (targetReg !== 0)
520
+ this.emitMovReg(targetReg, 0);
521
+ return;
522
+ }
523
+ // Identifier call
524
+ if (node.callee.type === "Identifier") {
525
+ for (let i = 0; i < node.arguments.length; i++) {
526
+ const argReg = this.getTempReg(40 + i);
527
+ this.compileExpression(node.arguments[i], argReg);
528
+ this.emitPush(argReg);
529
+ }
530
+ let funcReg;
531
+ if (this.variables[node.callee.name] !== undefined) {
532
+ funcReg = this.getVarReg(node.callee.name);
533
+ }
534
+ else {
535
+ funcReg = this.getTempReg(8);
536
+ this.emitNewStr(this.getTempReg(7), node.callee.name);
537
+ this.emitGetGlobal(funcReg, this.getTempReg(7));
538
+ }
539
+ this.emitCallReg(targetReg, funcReg, node.arguments.length);
540
+ return;
541
+ }
542
+ // General call
543
+ const argCount = node.arguments.length;
544
+ for (let i = 0; i < argCount; i++) {
545
+ const argReg = this.getTempReg(40 + i);
546
+ this.compileExpression(node.arguments[i], argReg);
547
+ this.emitPush(argReg);
548
+ }
549
+ if (node.callee.type === "MemberExpression") {
550
+ const objReg = this.getTempReg(30);
551
+ const methodReg = this.getTempReg(31);
552
+ this.compileMemberBase(node.callee, objReg, methodReg);
553
+ this.emitCallMethod(targetReg, objReg, methodReg, argCount);
554
+ }
555
+ else {
556
+ const funcReg = this.getTempReg(8);
557
+ this.compileExpression(node.callee, funcReg);
558
+ this.emitCallReg(targetReg, funcReg, argCount);
559
+ }
560
+ }
561
+ // ── compileExpression (Babel nodes) ──────────────────────────────────────
562
+ compileExpression(node, targetReg) {
563
+ switch (node.type) {
564
+ case "NumericLiteral": {
565
+ const v = node.value;
566
+ if (Number.isInteger(v) && v >= -2147483648 && v <= 2147483647) {
567
+ this.emitMovInt(targetReg, v | 0);
568
+ }
569
+ else {
570
+ this.emitNewStr(targetReg, String(v));
571
+ this.emitToNum(targetReg, targetReg);
572
+ }
573
+ break;
574
+ }
575
+ case "StringLiteral":
576
+ this.emitNewStr(targetReg, node.value);
577
+ break;
578
+ case "BooleanLiteral":
579
+ this.emitMovBool(targetReg, node.value);
580
+ break;
581
+ case "NullLiteral":
582
+ this.emitMovInt(targetReg, 0);
583
+ break;
584
+ case "Identifier":
585
+ if (node.name === "undefined") {
586
+ this.emitMovInt(targetReg, 0);
587
+ break;
588
+ }
589
+ if (this.variables[node.name] !== undefined) {
590
+ const srcReg = this.getVarReg(node.name);
591
+ if (srcReg !== targetReg)
592
+ this.emitMovReg(targetReg, srcReg);
593
+ }
594
+ else {
595
+ this.emitNewStr(this.getTempReg(2), node.name);
596
+ this.emitGetGlobal(targetReg, this.getTempReg(2));
597
+ }
598
+ break;
599
+ case "LogicalExpression": {
600
+ const endL = `__log_end_${this.currentAddress()}`;
601
+ this.compileExpression(node.left, targetReg);
602
+ if (node.operator === "||")
603
+ this.emitJnz(targetReg, endL);
604
+ else if (node.operator === "&&")
605
+ this.emitJz(targetReg, endL);
606
+ else if (node.operator === "??")
607
+ this.emitJnz(targetReg, endL);
608
+ else
609
+ throw new KrakBailError("unsupported logical: " + node.operator);
610
+ this.compileExpression(node.right, targetReg);
611
+ this.setLabel(endL);
612
+ break;
613
+ }
614
+ case "BinaryExpression": {
615
+ this.compileExpression(node.left, targetReg);
616
+ let tempReg;
617
+ if (node.right.type === "Identifier" && this.variables[node.right.name] !== undefined) {
618
+ tempReg = this.getVarReg(node.right.name);
619
+ }
620
+ else if (node.right.type === "NumericLiteral" && node.right.value === 0) {
621
+ tempReg = 1;
622
+ }
623
+ else {
624
+ tempReg = (targetReg === this.getTempReg(0)) ? this.getTempReg(1) : this.getTempReg(0);
625
+ if (node.right.type === "NumericLiteral" || node.right.type === "StringLiteral" || node.right.type === "BooleanLiteral") {
626
+ this.compileExpression(node.right, tempReg);
627
+ }
628
+ else {
629
+ this.emitPush(targetReg);
630
+ this.compileExpression(node.right, tempReg);
631
+ this.emitPop(targetReg);
632
+ }
633
+ }
634
+ const opMap = {
635
+ "+": "ADD", "-": "SUB", "*": "MUL", "/": "DIV", "%": "MOD",
636
+ "&": "AND", "|": "OR", "^": "XOR", "<<": "SHL", ">>": "SAR", ">>>": "SHR",
637
+ };
638
+ const opName = opMap[node.operator];
639
+ if (opName) {
640
+ this.emitBinaryOp(opName, targetReg, tempReg);
641
+ }
642
+ else {
643
+ this.emit([this.opcodes["CMP"]]);
644
+ this.emit(this.encodeArgs("CMP", { a: targetReg, b: tempReg }));
645
+ const skipL = `__cmp_skip_${this.currentAddress()}`;
646
+ const skip2L = `__cmp_skip2_${this.currentAddress()}`;
647
+ switch (node.operator) {
648
+ case "==":
649
+ case "===":
650
+ this.emitMovBool(targetReg, false);
651
+ this.emitJnz(255, skipL);
652
+ this.emitMovBool(targetReg, true);
653
+ this.setLabel(skipL);
654
+ break;
655
+ case "!=":
656
+ case "!==":
657
+ this.emitMovBool(targetReg, false);
658
+ this.emitJz(255, skipL);
659
+ this.emitMovBool(targetReg, true);
660
+ this.setLabel(skipL);
661
+ break;
662
+ case "<":
663
+ this.emitMovBool(targetReg, false);
664
+ this.emitJgt(skipL);
665
+ this.emitJz(255, skip2L);
666
+ this.emitMovBool(targetReg, true);
667
+ this.setLabel(skipL);
668
+ this.setLabel(skip2L);
669
+ break;
670
+ case "<=":
671
+ this.emitMovBool(targetReg, false);
672
+ this.emitJgt(skipL);
673
+ this.emitMovBool(targetReg, true);
674
+ this.setLabel(skipL);
675
+ break;
676
+ case ">":
677
+ this.emitMovBool(targetReg, false);
678
+ this.emitJlt(skipL);
679
+ this.emitJz(255, skip2L);
680
+ this.emitMovBool(targetReg, true);
681
+ this.setLabel(skipL);
682
+ this.setLabel(skip2L);
683
+ break;
684
+ case ">=":
685
+ this.emitMovBool(targetReg, false);
686
+ this.emitJlt(skipL);
687
+ this.emitMovBool(targetReg, true);
688
+ this.setLabel(skipL);
689
+ break;
690
+ case "instanceof":
691
+ this.emitInstanceOf(targetReg, targetReg, tempReg);
692
+ break;
693
+ default:
694
+ throw new KrakBailError("unsupported binary: " + node.operator);
695
+ }
696
+ }
697
+ break;
698
+ }
699
+ case "UnaryExpression":
700
+ if (node.operator === "typeof") {
701
+ const srcReg = this.getTempReg(0);
702
+ this.compileExpression(node.argument, srcReg);
703
+ this.emitTypeof(targetReg, srcReg);
704
+ }
705
+ else if (node.operator === "!") {
706
+ const testReg = (targetReg === this.getTempReg(0)) ? this.getTempReg(1) : this.getTempReg(0);
707
+ this.compileExpression(node.argument, testReg);
708
+ const trueL = `__not_true_${this.currentAddress()}`, endL = `__not_end_${this.currentAddress()}`;
709
+ this.emitJz(testReg, trueL);
710
+ this.emitMovBool(targetReg, false);
711
+ this.emitJmp(endL);
712
+ this.setLabel(trueL);
713
+ this.emitMovBool(targetReg, true);
714
+ this.setLabel(endL);
715
+ }
716
+ else if (node.operator === "-") {
717
+ const valReg = (targetReg === this.getTempReg(0)) ? this.getTempReg(1) : this.getTempReg(0);
718
+ this.compileExpression(node.argument, valReg);
719
+ this.emitMovInt(targetReg, 0);
720
+ this.emitBinaryOp("SUB", targetReg, valReg);
721
+ }
722
+ else if (node.operator === "+") {
723
+ this.compileExpression(node.argument, targetReg);
724
+ }
725
+ else if (node.operator === "void") {
726
+ this.compileExpression(node.argument, this.getTempReg(0));
727
+ this.emitMovInt(targetReg, 0);
728
+ }
729
+ else if (node.operator === "~") {
730
+ this.compileExpression(node.argument, targetReg);
731
+ this.emit([this.opcodes["NOT"]]);
732
+ this.emit(this.encodeArgs("NOT", { r: targetReg }));
733
+ }
734
+ else {
735
+ throw new KrakBailError("unsupported unary: " + node.operator);
736
+ }
737
+ break;
738
+ case "AssignmentExpression": {
739
+ if (node.operator === "=") {
740
+ this.compileExpression(node.right, targetReg);
741
+ if (node.left.type === "Identifier") {
742
+ const destReg = this.allocVar(node.left.name) % 256;
743
+ if (destReg !== targetReg)
744
+ this.emitMovReg(destReg, targetReg);
745
+ }
746
+ else if (node.left.type === "MemberExpression") {
747
+ this.emitPush(targetReg);
748
+ const objR = this.getTempReg(10), propR = this.getTempReg(11);
749
+ this.compileMemberBase(node.left, objR, propR);
750
+ const valR = this.getTempReg(12);
751
+ this.emitPop(valR);
752
+ this.emitSetProp(objR, propR, valR);
753
+ if (targetReg !== valR)
754
+ this.emitMovReg(targetReg, valR);
755
+ }
756
+ }
757
+ else {
758
+ const op = node.operator.slice(0, -1);
759
+ const opMap = { "+": "ADD", "-": "SUB", "*": "MUL", "/": "DIV", "%": "MOD", "&": "AND", "|": "OR", "^": "XOR", "<<": "SHL", ">>": "SAR", ">>>": "SHR" };
760
+ const opName = opMap[op];
761
+ if (!opName)
762
+ throw new KrakBailError("unsupported compound assignment: " + node.operator);
763
+ if (node.left.type === "Identifier") {
764
+ const destReg = this.allocVar(node.left.name) % 256;
765
+ const tempR = this.getTempReg(0);
766
+ this.compileExpression(node.right, tempR);
767
+ this.emitBinaryOp(opName, destReg, tempR);
768
+ if (targetReg !== destReg)
769
+ this.emitMovReg(targetReg, destReg);
770
+ }
771
+ else if (node.left.type === "MemberExpression") {
772
+ const objR = this.getTempReg(20), propR = this.getTempReg(21);
773
+ this.compileMemberBase(node.left, objR, propR);
774
+ const curR = this.getTempReg(19);
775
+ this.emitGetProp(curR, objR, propR);
776
+ const rightR = this.getTempReg(0);
777
+ this.compileExpression(node.right, rightR);
778
+ this.emitBinaryOp(opName, curR, rightR);
779
+ this.emitSetProp(objR, propR, curR);
780
+ if (targetReg !== 0)
781
+ this.emitMovReg(targetReg, curR);
782
+ }
783
+ }
784
+ break;
785
+ }
786
+ case "UpdateExpression": {
787
+ if (node.argument.type === "Identifier") {
788
+ const varReg = this.getVarReg(node.argument.name);
789
+ const op = node.operator === "++" ? "INC" : "DEC";
790
+ if (node.prefix) {
791
+ this.emit([this.opcodes[op]]);
792
+ this.emit(this.encodeArgs(op, { r: varReg }));
793
+ if (targetReg !== varReg)
794
+ this.emitMovReg(targetReg, varReg);
795
+ }
796
+ else {
797
+ if (targetReg !== varReg)
798
+ this.emitMovReg(targetReg, varReg);
799
+ this.emit([this.opcodes[op]]);
800
+ this.emit(this.encodeArgs(op, { r: varReg }));
801
+ }
802
+ }
803
+ else if (node.argument.type === "MemberExpression") {
804
+ const objR = this.getTempReg(20), propR = this.getTempReg(21);
805
+ this.compileMemberBase(node.argument, objR, propR);
806
+ const valR = this.getTempReg(19);
807
+ this.emitGetProp(valR, objR, propR);
808
+ if (!node.prefix)
809
+ this.emitMovReg(targetReg, valR);
810
+ const oneReg = this.getTempReg(0);
811
+ this.emitMovInt(oneReg, 1);
812
+ if (node.operator === "++")
813
+ this.emitBinaryOp("ADD", valR, oneReg);
814
+ else
815
+ this.emitBinaryOp("SUB", valR, oneReg);
816
+ if (node.prefix)
817
+ this.emitMovReg(targetReg, valR);
818
+ this.emitSetProp(objR, propR, valR);
819
+ }
820
+ break;
821
+ }
822
+ case "ConditionalExpression": {
823
+ const elseL = `__cond_else_${this.currentAddress()}`, endL = `__cond_end_${this.currentAddress()}`;
824
+ const testR = (targetReg === this.getTempReg(0)) ? this.getTempReg(1) : this.getTempReg(0);
825
+ this.compileExpression(node.test, testR);
826
+ this.emitJz(testR, elseL);
827
+ this.compileExpression(node.consequent, targetReg);
828
+ this.emitJmp(endL);
829
+ this.setLabel(elseL);
830
+ this.compileExpression(node.alternate, targetReg);
831
+ this.setLabel(endL);
832
+ break;
833
+ }
834
+ case "MemberExpression":
835
+ this.compileMemberExpression(node, targetReg);
836
+ break;
837
+ case "CallExpression":
838
+ this.compileCallExpression(node, targetReg);
839
+ break;
840
+ case "NewExpression": {
841
+ for (let i = 0; i < node.arguments.length; i++) {
842
+ const argR = this.getTempReg(1);
843
+ this.compileExpression(node.arguments[i], argR);
844
+ this.emitPush(argR);
845
+ }
846
+ const conR = this.getTempReg(0);
847
+ this.compileExpression(node.callee, conR);
848
+ this.emitNew(targetReg, conR, node.arguments.length);
849
+ break;
850
+ }
851
+ case "ArrayExpression": {
852
+ // Build array in a fresh var-slot register so that sub-expression
853
+ // compilation (which reuses getTempReg slots for inner calls) cannot
854
+ // overwrite the array reference.
855
+ const arrVarSlot = this.nextVarSlot++ % 256; // guaranteed unique var slot
856
+ const arrNR = this.getTempReg(32); // "Array" string (high temp, safe)
857
+ const arrCR = this.getTempReg(33); // Array constructor ref
858
+ const pushNR = this.getTempReg(34); // "push" string — set once before loop
859
+ const callTR = this.getTempReg(35); // discard return of push()
860
+ this.emitNewStr(arrNR, "Array");
861
+ this.emitGetGlobal(arrCR, arrNR);
862
+ this.emitNew(arrVarSlot, arrCR, 0); // array lives in stable var slot
863
+ this.emitNewStr(pushNR, "push"); // set once before loop
864
+ for (let i = 0; i < node.elements.length; i++) {
865
+ if (!node.elements[i])
866
+ continue;
867
+ // Compile element into a fresh temp — must not be arrVarSlot
868
+ const elTR = this.getTempReg(36);
869
+ this.compileExpression(node.elements[i], elTR);
870
+ this.emitPush(elTR);
871
+ // Re-emit pushNR in case element sub-expression clobbered it
872
+ this.emitNewStr(pushNR, "push");
873
+ this.emitCallMethod(callTR, arrVarSlot, pushNR, 1);
874
+ }
875
+ if (targetReg !== arrVarSlot)
876
+ this.emitMovReg(targetReg, arrVarSlot);
877
+ break;
878
+ }
879
+ case "ObjectExpression": {
880
+ // Use fresh var-slot for the object so sub-expressions don't clobber it
881
+ const objVarSlot = this.nextVarSlot++ % 256;
882
+ const objCR = this.getTempReg(40);
883
+ this.emitNewStr(this.getTempReg(41), "Object");
884
+ this.emitGetGlobal(objCR, this.getTempReg(41));
885
+ this.emitNew(objVarSlot, objCR, 0);
886
+ for (const prop of node.properties) {
887
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property")
888
+ throw new KrakBailError("spread");
889
+ const keyName = prop.key.name ?? prop.key.value;
890
+ // propR must survive the compileExpression call, so use stable high regs
891
+ const propR = this.getTempReg(42);
892
+ const valR = this.getTempReg(43);
893
+ this.emitNewStr(propR, String(keyName));
894
+ this.compileExpression(prop.value, valR);
895
+ // Re-emit propR key in case sub-expression smashed it
896
+ this.emitNewStr(propR, String(keyName));
897
+ this.emitSetProp(objVarSlot, propR, valR);
898
+ }
899
+ if (targetReg !== objVarSlot)
900
+ this.emitMovReg(targetReg, objVarSlot);
901
+ break;
902
+ }
903
+ case "ArrowFunctionExpression":
904
+ case "FunctionExpression": {
905
+ const labelName = `anon_${this.currentAddress()}`;
906
+ const startL = `__func_${labelName}`, endL = `__func_end_${labelName}`;
907
+ this.emitJmp(endL);
908
+ this.setLabel(startL);
909
+ const savedVars = this.variables, savedNext = this.nextVarSlot;
910
+ this.variables = Object.assign({}, this.variables);
911
+ this.allocVar("this");
912
+ for (const p of node.params)
913
+ this.allocVar(p.name);
914
+ const thisR = this.getVarReg("this");
915
+ this.emitPop(thisR);
916
+ for (let p = node.params.length - 1; p >= 0; p--) {
917
+ const paramR = this.getVarReg(node.params[p].name);
918
+ this.emitPop(paramR);
919
+ }
920
+ if (node.body.type === "BlockStatement") {
921
+ this.compileStatement(node.body);
922
+ this.emitMovInt(0, 0);
923
+ this.emitRet();
924
+ }
925
+ else {
926
+ this.compileExpression(node.body, 0);
927
+ this.emitRet();
928
+ }
929
+ this.variables = savedVars;
930
+ this.nextVarSlot = savedNext;
931
+ this.setLabel(endL);
932
+ this.emit([this.opcodes["FUNC"]]);
933
+ this.pendingJumps.push({ type: "FUNC", address: this.currentAddress(), label: startL });
934
+ this.emit(this.encodeArgs("FUNC", { d: targetReg, a: 0, k: 0, n: node.params.length }));
935
+ break;
936
+ }
937
+ case "SequenceExpression":
938
+ for (const expr of node.expressions)
939
+ this.compileExpression(expr, targetReg);
940
+ break;
941
+ case "TemplateLiteral":
942
+ if (node.quasis.length === 1 && node.expressions.length === 0) {
943
+ this.emitNewStr(targetReg, node.quasis[0].value.cooked);
944
+ }
945
+ else {
946
+ this.emitNewStr(targetReg, node.quasis[0].value.cooked);
947
+ for (let i = 0; i < node.expressions.length; i++) {
948
+ this.emitPush(targetReg);
949
+ const exprR = this.getTempReg(0);
950
+ this.compileExpression(node.expressions[i], exprR);
951
+ this.emitPop(targetReg);
952
+ this.emitBinaryOp("ADD", targetReg, exprR);
953
+ const nextQ = node.quasis[i + 1]?.value.cooked ?? "";
954
+ if (nextQ.length > 0) {
955
+ this.emitPush(targetReg);
956
+ this.emitNewStr(this.getTempReg(0), nextQ);
957
+ this.emitPop(targetReg);
958
+ this.emitBinaryOp("ADD", targetReg, this.getTempReg(0));
959
+ }
960
+ }
961
+ }
962
+ break;
963
+ case "ThisExpression":
964
+ if (this.variables["this"] !== undefined)
965
+ this.emitMovReg(targetReg, this.getVarReg("this"));
966
+ else
967
+ this.emitMovInt(targetReg, 0);
968
+ break;
969
+ default:
970
+ throw new KrakBailError("unsupported expression: " + node.type);
971
+ }
972
+ }
973
+ // ── compileStatement ──────────────────────────────────────────────────────
974
+ compileStatement(node) {
975
+ switch (node.type) {
976
+ case "VariableDeclaration":
977
+ for (const decl of node.declarations) {
978
+ if (decl.id.type === "Identifier") {
979
+ const reg = this.allocVar(decl.id.name) % 256;
980
+ if (decl.init)
981
+ this.compileExpression(decl.init, reg);
982
+ else
983
+ this.emitMovInt(reg, 0);
984
+ }
985
+ else if (decl.id.type === "ArrayPattern") {
986
+ // const [a, b, c] = expr → tmp = expr; a = tmp[0]; b = tmp[1]; ...
987
+ const tmpR = this.getTempReg(10);
988
+ if (decl.init)
989
+ this.compileExpression(decl.init, tmpR);
990
+ else
991
+ this.emitMovInt(tmpR, 0);
992
+ for (let _ai = 0; _ai < decl.id.elements.length; _ai++) {
993
+ const elem = decl.id.elements[_ai];
994
+ if (!elem)
995
+ continue; // sparse hole: skip
996
+ if (elem.type !== "Identifier")
997
+ throw new KrakBailError("nested destructuring");
998
+ const idxNumR = this.getTempReg(11);
999
+ const idxStrR = this.getTempReg(12);
1000
+ const elR = this.allocVar(elem.name) % 256;
1001
+ this.emitMovInt(idxNumR, _ai);
1002
+ this.emitFromNum(idxStrR, idxNumR); // convert int→JS value for GPRP
1003
+ this.emitGetProp(elR, tmpR, idxStrR);
1004
+ }
1005
+ }
1006
+ else if (decl.id.type === "ObjectPattern") {
1007
+ // const { a, b } = expr → tmp = expr; a = tmp.a; b = tmp.b;
1008
+ const tmpR = this.getTempReg(10);
1009
+ if (decl.init)
1010
+ this.compileExpression(decl.init, tmpR);
1011
+ else
1012
+ this.emitMovInt(tmpR, 0);
1013
+ for (const prop of decl.id.properties) {
1014
+ if (prop.type !== "ObjectProperty")
1015
+ throw new KrakBailError("rest destructuring");
1016
+ const keyName = prop.key.name ?? prop.key.value;
1017
+ const valNode = prop.value;
1018
+ if (valNode.type !== "Identifier")
1019
+ throw new KrakBailError("nested obj destructuring");
1020
+ const propR = this.allocVar(valNode.name) % 256;
1021
+ const kR = this.getTempReg(11);
1022
+ this.emitNewStr(kR, String(keyName));
1023
+ this.emitGetProp(propR, tmpR, kR);
1024
+ }
1025
+ }
1026
+ else {
1027
+ throw new KrakBailError("destructuring: " + decl.id.type);
1028
+ }
1029
+ }
1030
+ break;
1031
+ case "ExpressionStatement":
1032
+ this.compileExpression(node.expression, this.getTempReg(3));
1033
+ break;
1034
+ case "IfStatement": {
1035
+ const endL = `__if_end_${this.currentAddress()}`;
1036
+ const elseL = `__if_else_${this.currentAddress()}`;
1037
+ if (node.alternate) {
1038
+ if (!this.tryCompileConditionExit(node.test, elseL)) {
1039
+ this.compileExpression(node.test, 254);
1040
+ this.emitJz(254, elseL);
1041
+ }
1042
+ this.compileStatement(node.consequent);
1043
+ this.emitJmp(endL);
1044
+ this.setLabel(elseL);
1045
+ this.compileStatement(node.alternate);
1046
+ }
1047
+ else {
1048
+ if (!this.tryCompileConditionExit(node.test, endL)) {
1049
+ this.compileExpression(node.test, 254);
1050
+ this.emitJz(254, endL);
1051
+ }
1052
+ this.compileStatement(node.consequent);
1053
+ }
1054
+ this.setLabel(endL);
1055
+ break;
1056
+ }
1057
+ case "WhileStatement": {
1058
+ const loopStart = `__while_start_${this.currentAddress()}`;
1059
+ const loopEnd = `__while_end_${this.currentAddress()}`;
1060
+ this.setLabel(loopStart);
1061
+ if (!this.tryCompileConditionExit(node.test, loopEnd)) {
1062
+ this.compileExpression(node.test, 254);
1063
+ this.emitJz(254, loopEnd);
1064
+ }
1065
+ this.loopStack.push({ continueLabel: loopStart, breakLabel: loopEnd });
1066
+ this.compileStatement(node.body);
1067
+ this.loopStack.pop();
1068
+ this.emitJmp(loopStart);
1069
+ this.setLabel(loopEnd);
1070
+ break;
1071
+ }
1072
+ case "DoWhileStatement": {
1073
+ const doStart = `__do_start_${this.currentAddress()}`;
1074
+ const doEnd = `__do_end_${this.currentAddress()}`;
1075
+ const doCond = `__do_cond_${this.currentAddress()}`;
1076
+ this.setLabel(doStart);
1077
+ this.loopStack.push({ continueLabel: doCond, breakLabel: doEnd });
1078
+ this.compileStatement(node.body);
1079
+ this.loopStack.pop();
1080
+ this.setLabel(doCond);
1081
+ this.compileExpression(node.test, 254);
1082
+ this.emitJnz(254, doStart);
1083
+ this.setLabel(doEnd);
1084
+ break;
1085
+ }
1086
+ case "ForStatement": {
1087
+ const forStart = `__for_start_${this.currentAddress()}`;
1088
+ const forEnd = `__for_end_${this.currentAddress()}`;
1089
+ const forUpdate = `__for_update_${this.currentAddress()}`;
1090
+ if (node.init) {
1091
+ if (node.init.type === "VariableDeclaration")
1092
+ this.compileStatement(node.init);
1093
+ else
1094
+ this.compileExpression(node.init, this.getTempReg(3));
1095
+ }
1096
+ this.setLabel(forStart);
1097
+ if (node.test) {
1098
+ if (!this.tryCompileConditionExit(node.test, forEnd)) {
1099
+ this.compileExpression(node.test, 254);
1100
+ this.emitJz(254, forEnd);
1101
+ }
1102
+ }
1103
+ this.loopStack.push({ continueLabel: forUpdate, breakLabel: forEnd });
1104
+ this.compileStatement(node.body);
1105
+ this.loopStack.pop();
1106
+ this.setLabel(forUpdate);
1107
+ if (node.update)
1108
+ this.compileExpression(node.update, this.getTempReg(3));
1109
+ this.emitJmp(forStart);
1110
+ this.setLabel(forEnd);
1111
+ break;
1112
+ }
1113
+ case "BlockStatement":
1114
+ for (const s of node.body)
1115
+ this.compileStatement(s);
1116
+ break;
1117
+ case "FunctionDeclaration": {
1118
+ if (this.functions[node.id.name])
1119
+ break;
1120
+ const funcVarReg = this.allocVar(node.id.name) % 256;
1121
+ this.compileExpression({
1122
+ type: "FunctionExpression",
1123
+ params: node.params,
1124
+ body: node.body,
1125
+ }, funcVarReg);
1126
+ break;
1127
+ }
1128
+ case "ReturnStatement": {
1129
+ const cbCtx = this.inlineCallbackStack.length > 0
1130
+ ? this.inlineCallbackStack[this.inlineCallbackStack.length - 1]
1131
+ : null;
1132
+ if (cbCtx) {
1133
+ // Inside an inline callback — write return value into cbResReg and
1134
+ // jump to the callback exit label (do NOT emit RET here).
1135
+ if (node.argument)
1136
+ this.compileExpression(node.argument, cbCtx.cbResReg);
1137
+ else
1138
+ this.emitMovInt(cbCtx.cbResReg, 0);
1139
+ this.emitJmp(cbCtx.returnLabel);
1140
+ }
1141
+ else {
1142
+ if (node.argument)
1143
+ this.compileExpression(node.argument, 0);
1144
+ else
1145
+ this.emitMovInt(0, 0);
1146
+ this.emitRet();
1147
+ }
1148
+ break;
1149
+ }
1150
+ case "BreakStatement":
1151
+ if (!this.loopStack.length)
1152
+ throw new KrakBailError("break outside loop");
1153
+ this.emitJmp(this.loopStack[this.loopStack.length - 1].breakLabel);
1154
+ break;
1155
+ case "ContinueStatement":
1156
+ if (!this.loopStack.length)
1157
+ throw new KrakBailError("continue outside loop");
1158
+ {
1159
+ const cl = this.loopStack[this.loopStack.length - 1].continueLabel;
1160
+ if (!cl)
1161
+ throw new KrakBailError("continue in switch");
1162
+ this.emitJmp(cl);
1163
+ }
1164
+ break;
1165
+ case "EmptyStatement":
1166
+ break;
1167
+ case "ThrowStatement": {
1168
+ const errR = this.getTempReg(0);
1169
+ this.compileExpression(node.argument, errR);
1170
+ this.emitPush(errR);
1171
+ this.emitNewStr(this.getTempReg(1), "__krak_throw");
1172
+ this.emitGetGlobal(this.getTempReg(2), this.getTempReg(1));
1173
+ this.emitCallReg(this.getTempReg(3), this.getTempReg(2), 1);
1174
+ break;
1175
+ }
1176
+ case "TryStatement":
1177
+ this.compileStatement(node.block);
1178
+ break;
1179
+ case "SwitchStatement": {
1180
+ const discR = this.getTempReg(14);
1181
+ this.compileExpression(node.discriminant, discR);
1182
+ const switchEndL = `__switch_end_${this.currentAddress()}`;
1183
+ const caseLabels = [];
1184
+ let defaultLabel = null;
1185
+ for (let i = 0; i < node.cases.length; i++) {
1186
+ const c = node.cases[i];
1187
+ const cLabel = `__switch_case_${this.currentAddress()}_${i}`;
1188
+ caseLabels.push(cLabel);
1189
+ if (c.test === null) {
1190
+ defaultLabel = cLabel;
1191
+ }
1192
+ else {
1193
+ const testR = this.getTempReg(15);
1194
+ this.compileExpression(c.test, testR);
1195
+ this.emit([this.opcodes["CMP"]]);
1196
+ this.emit(this.encodeArgs("CMP", { a: discR, b: testR }));
1197
+ this.emitJz(255, cLabel);
1198
+ }
1199
+ }
1200
+ if (defaultLabel)
1201
+ this.emitJmp(defaultLabel);
1202
+ else
1203
+ this.emitJmp(switchEndL);
1204
+ this.loopStack.push({ breakLabel: switchEndL, continueLabel: null });
1205
+ for (let i = 0; i < node.cases.length; i++) {
1206
+ this.setLabel(caseLabels[i]);
1207
+ for (const stmt of node.cases[i].consequent)
1208
+ this.compileStatement(stmt);
1209
+ }
1210
+ this.loopStack.pop();
1211
+ this.setLabel(switchEndL);
1212
+ break;
1213
+ }
1214
+ case "ForOfStatement": {
1215
+ // for (const x of iterable) { body }
1216
+ // Lowered to: idx=0; len=arr.length; while(idx<len){ x=arr[idx]; body; idx++ }
1217
+ // Covers arrays and array-likes only. True iterators bail.
1218
+ if (node.left.type !== "VariableDeclaration")
1219
+ throw new KrakBailError("for-of non-var");
1220
+ const foDecl = node.left.declarations[0];
1221
+ if (!foDecl || foDecl.id.type !== "Identifier")
1222
+ throw new KrakBailError("for-of destructuring");
1223
+ const foVarReg = this.allocVar(foDecl.id.name) % 256;
1224
+ const foArrR = this.getTempReg(6);
1225
+ const foIdxR = this.allocVar(`__fo_idx_${this.currentAddress()}`) % 256;
1226
+ const foLenR = this.getTempReg(7);
1227
+ const foLenKey = this.getTempReg(8);
1228
+ const foIdxStrR = this.getTempReg(13); // idx converted to JS value for GPRP
1229
+ this.compileExpression(node.right, foArrR);
1230
+ this.emitMovInt(foIdxR, 0);
1231
+ this.emitNewStr(foLenKey, "length");
1232
+ this.emitGetProp(foLenR, foArrR, foLenKey);
1233
+ this.emitToNum(foLenR, foLenR); // ensure numeric
1234
+ const foStart = `__fo_start_${this.currentAddress()}`;
1235
+ const foEnd = `__fo_end_${this.currentAddress()}`;
1236
+ this.setLabel(foStart);
1237
+ // CMP reg[255]: 0=equal, 1=a>b, -1=a<b; jump if idx >= len (equal OR greater)
1238
+ this.emit([this.opcodes["CMP"]]);
1239
+ this.emit(this.encodeArgs("CMP", { a: foIdxR, b: foLenR }));
1240
+ this.emitJgt(foEnd); // idx > len (reg[255] = 1)
1241
+ this.emitJz(255, foEnd); // idx == len (reg[255] = 0)
1242
+ // x = arr[idx] (GPRP needs JS-value key, so convert int via FNUM)
1243
+ this.emitFromNum(foIdxStrR, foIdxR);
1244
+ this.emitGetProp(foVarReg, foArrR, foIdxStrR);
1245
+ this.loopStack.push({ continueLabel: foStart, breakLabel: foEnd });
1246
+ this.compileStatement(node.body);
1247
+ this.loopStack.pop();
1248
+ this.emit([this.opcodes["INC"]]);
1249
+ this.emit(this.encodeArgs("INC", { r: foIdxR }));
1250
+ this.emitJmp(foStart);
1251
+ this.setLabel(foEnd);
1252
+ break;
1253
+ }
1254
+ case "ForInStatement": {
1255
+ // for (const k in obj) { body } — lowered to Object.keys(obj) array loop
1256
+ if (node.left.type !== "VariableDeclaration")
1257
+ throw new KrakBailError("for-in non-var");
1258
+ const fiDecl = node.left.declarations[0];
1259
+ if (!fiDecl || fiDecl.id.type !== "Identifier")
1260
+ throw new KrakBailError("for-in destructuring");
1261
+ const fiVarReg = this.allocVar(fiDecl.id.name) % 256;
1262
+ const fiKeysR = this.getTempReg(6);
1263
+ const fiObjNR = this.getTempReg(9); // name reg for "Object" global
1264
+ const fiObjR = this.getTempReg(5); // the right-hand object
1265
+ const fiIdxR = this.allocVar(`__fi_idx_${this.currentAddress()}`) % 256;
1266
+ const fiLenR = this.getTempReg(7);
1267
+ const fiLenKeyR = this.getTempReg(8);
1268
+ const fiIdxStrR = this.getTempReg(13);
1269
+ // fiKeysR = Object.keys(right)
1270
+ this.emitNewStr(fiObjNR, "Object");
1271
+ this.emitGetGlobal(fiObjR, fiObjNR); // fiObjR = globalThis.Object
1272
+ this.compileExpression(node.right, fiObjNR);
1273
+ this.emitPush(fiObjNR);
1274
+ this.emitNewStr(fiLenKeyR, "keys");
1275
+ this.emitCallMethod(fiKeysR, fiObjR, fiLenKeyR, 1);
1276
+ this.emitMovInt(fiIdxR, 0);
1277
+ this.emitNewStr(fiLenKeyR, "length");
1278
+ this.emitGetProp(fiLenR, fiKeysR, fiLenKeyR);
1279
+ this.emitToNum(fiLenR, fiLenR);
1280
+ const fiStart = `__fi_start_${this.currentAddress()}`;
1281
+ const fiEnd = `__fi_end_${this.currentAddress()}`;
1282
+ this.setLabel(fiStart);
1283
+ this.emit([this.opcodes["CMP"]]);
1284
+ this.emit(this.encodeArgs("CMP", { a: fiIdxR, b: fiLenR }));
1285
+ this.emitJgt(fiEnd);
1286
+ this.emitJz(255, fiEnd);
1287
+ this.emitFromNum(fiIdxStrR, fiIdxR);
1288
+ this.emitGetProp(fiVarReg, fiKeysR, fiIdxStrR);
1289
+ this.loopStack.push({ continueLabel: fiStart, breakLabel: fiEnd });
1290
+ this.compileStatement(node.body);
1291
+ this.loopStack.pop();
1292
+ this.emit([this.opcodes["INC"]]);
1293
+ this.emit(this.encodeArgs("INC", { r: fiIdxR }));
1294
+ this.emitJmp(fiStart);
1295
+ this.setLabel(fiEnd);
1296
+ break;
1297
+ }
1298
+ default:
1299
+ throw new KrakBailError("unsupported statement: " + node.type);
1300
+ }
1301
+ }
1302
+ // ── resolveJumps ─────────────────────────────────────────────────────────
1303
+ resolveJumps() {
1304
+ const { lcgMul, lcgInc, initialKey } = this.config;
1305
+ for (const jump of this.pendingJumps) {
1306
+ const targetAddr = this.labels[jump.label];
1307
+ if (targetAddr === undefined)
1308
+ throw new KrakBailError("Unknown label: " + jump.label);
1309
+ const resetKey = computeKeyAtAddress(initialKey, targetAddr, lcgMul, lcgInc);
1310
+ const layout = this.layouts[jump.type] ?? [];
1311
+ let addr = jump.address;
1312
+ for (const argDef of layout) {
1313
+ if (["t", "addr", "a"].includes(argDef.name)) {
1314
+ this.bytecodeArr[addr] = targetAddr & 0xFF;
1315
+ this.bytecodeArr[addr + 1] = (targetAddr >> 8) & 0xFF;
1316
+ this.bytecodeArr[addr + 2] = (targetAddr >> 16) & 0xFF;
1317
+ this.bytecodeArr[addr + 3] = (targetAddr >> 24) & 0xFF;
1318
+ addr += 4;
1319
+ }
1320
+ else if (["k", "key"].includes(argDef.name)) {
1321
+ // k is now a 32-bit LCG checkpoint — patch 4 bytes little-endian
1322
+ this.bytecodeArr[addr] = resetKey & 0xFF;
1323
+ this.bytecodeArr[addr + 1] = (resetKey >> 8) & 0xFF;
1324
+ this.bytecodeArr[addr + 2] = (resetKey >> 16) & 0xFF;
1325
+ this.bytecodeArr[addr + 3] = (resetKey >> 24) & 0xFF;
1326
+ addr += 4;
1327
+ }
1328
+ else if (["r", "reg"].includes(argDef.name)) {
1329
+ addr += 1;
1330
+ }
1331
+ else if (argDef.type === "INT") {
1332
+ addr += 4;
1333
+ }
1334
+ else {
1335
+ addr += 1;
1336
+ }
1337
+ }
1338
+ }
1339
+ }
1340
+ // ── encrypt ───────────────────────────────────────────────────────────────
1341
+ encryptBytecode() {
1342
+ const { initialKey, lcgMul, lcgInc } = this.config;
1343
+ const encrypted = [];
1344
+ let key = initialKey >>> 0;
1345
+ for (const b of this.bytecodeArr) {
1346
+ encrypted.push((b ^ (key >>> 24)) & 0xFF);
1347
+ key = ((Math.imul(key, lcgMul) + lcgInc) | 0) >>> 0;
1348
+ }
1349
+ return encrypted;
1350
+ }
1351
+ // ── compile: top-level entry ──────────────────────────────────────────────
1352
+ /**
1353
+ * Compile a Babel Program AST body (array of statements) as the main script.
1354
+ * All function declarations in the body are pre-collected and emitted before main.
1355
+ */
1356
+ compileProgram(stmts) {
1357
+ // Pre-collect top-level vars + functions
1358
+ for (const node of stmts) {
1359
+ if (node.type === "VariableDeclaration") {
1360
+ for (const d of node.declarations) {
1361
+ if (d.id.type === "Identifier")
1362
+ this.allocVar(d.id.name);
1363
+ }
1364
+ }
1365
+ }
1366
+ this.collectFunctions(stmts);
1367
+ const globalVars = Object.assign({}, this.variables);
1368
+ const globalNextSlot = this.nextVarSlot;
1369
+ const hasFunctions = Object.keys(this.functions).length > 0;
1370
+ const mainLabel = "__main_start";
1371
+ if (hasFunctions)
1372
+ this.emitJmp(mainLabel);
1373
+ // Emit functions
1374
+ let runningSlot = globalNextSlot;
1375
+ for (const [funcName, funcInfo] of Object.entries(this.functions)) {
1376
+ this.setLabel(`__func_${funcName}`);
1377
+ const savedVars = this.variables, savedNext = this.nextVarSlot;
1378
+ this.variables = Object.assign({}, globalVars);
1379
+ this.nextVarSlot = runningSlot;
1380
+ this.variables["this"] = this.nextVarSlot++;
1381
+ for (const p of funcInfo.params)
1382
+ this.variables[p] = this.nextVarSlot++;
1383
+ const thisR = this.getVarReg("this");
1384
+ this.emitPop(thisR);
1385
+ for (let p = funcInfo.params.length - 1; p >= 0; p--) {
1386
+ this.emitPop(this.getVarReg(funcInfo.params[p]));
1387
+ }
1388
+ this.compileStatement(funcInfo.body);
1389
+ this.emitMovInt(0, 0);
1390
+ this.emitRet();
1391
+ runningSlot = this.nextVarSlot;
1392
+ this.variables = savedVars;
1393
+ this.nextVarSlot = savedNext;
1394
+ }
1395
+ if (hasFunctions)
1396
+ this.setLabel(mainLabel);
1397
+ for (const stmt of stmts)
1398
+ this.compileStatement(stmt);
1399
+ this.emit([this.opcodes["HALT"]]);
1400
+ this.resolveJumps();
1401
+ const encrypted = this.encryptBytecode();
1402
+ const base64 = Buffer.from(encrypted).toString("base64");
1403
+ return { bytecode: encrypted, base64 };
1404
+ }
1405
+ /**
1406
+ * Compile a single Babel FunctionDeclaration node.
1407
+ * Used by transform-vm.ts to virtualise individual functions.
1408
+ */
1409
+ compileFunction(fnNode) {
1410
+ if (!fnNode.id)
1411
+ throw new KrakBailError("anonymous function");
1412
+ // Pre-collect params + body vars
1413
+ for (const p of fnNode.params) {
1414
+ if (p.type !== "Identifier")
1415
+ throw new KrakBailError("param-pattern");
1416
+ this.allocVar(p.name);
1417
+ }
1418
+ this.collectFunctions(fnNode.body.body);
1419
+ const mainLabel = "__main_start";
1420
+ const globalVars = Object.assign({}, this.variables);
1421
+ const hasFunctions = Object.keys(this.functions).length > 0;
1422
+ if (hasFunctions)
1423
+ this.emitJmp(mainLabel);
1424
+ // Sub-functions declared inside this function
1425
+ let runningSlot = this.nextVarSlot;
1426
+ for (const [fname, finfo] of Object.entries(this.functions)) {
1427
+ this.setLabel(`__func_${fname}`);
1428
+ const savedVars = this.variables, savedNext = this.nextVarSlot;
1429
+ this.variables = Object.assign({}, globalVars);
1430
+ this.nextVarSlot = runningSlot;
1431
+ this.variables["this"] = this.nextVarSlot++;
1432
+ for (const p of finfo.params)
1433
+ this.variables[p] = this.nextVarSlot++;
1434
+ const thisR = this.getVarReg("this");
1435
+ this.emitPop(thisR);
1436
+ for (let p = finfo.params.length - 1; p >= 0; p--) {
1437
+ this.emitPop(this.getVarReg(finfo.params[p]));
1438
+ }
1439
+ this.compileStatement(finfo.body);
1440
+ this.emitMovInt(0, 0);
1441
+ this.emitRet();
1442
+ runningSlot = this.nextVarSlot;
1443
+ this.variables = savedVars;
1444
+ this.nextVarSlot = savedNext;
1445
+ }
1446
+ if (hasFunctions)
1447
+ this.setLabel(mainLabel);
1448
+ // Pop params in reverse order (caller pushed them in order)
1449
+ const thisR2 = this.allocVar("this") % 256;
1450
+ this.emitPop(thisR2);
1451
+ for (let p = fnNode.params.length - 1; p >= 0; p--) {
1452
+ this.emitPop(this.getVarReg(fnNode.params[p].name));
1453
+ }
1454
+ this.compileStatement(fnNode.body);
1455
+ this.emitMovInt(0, 0);
1456
+ this.emitRet();
1457
+ this.resolveJumps();
1458
+ const encrypted = this.encryptBytecode();
1459
+ const base64 = Buffer.from(encrypted).toString("base64");
1460
+ return { bytecode: encrypted, base64 };
1461
+ }
1462
+ collectFunctions(stmts) {
1463
+ for (const node of stmts) {
1464
+ if (!node)
1465
+ continue;
1466
+ if (node.type === "FunctionDeclaration") {
1467
+ this.allocVar(node.id.name);
1468
+ this.functions[node.id.name] = {
1469
+ params: node.params.map((p) => p.name),
1470
+ body: node.body,
1471
+ };
1472
+ this.collectFunctions(node.body.body);
1473
+ }
1474
+ else if (node.type === "BlockStatement") {
1475
+ this.collectFunctions(node.body);
1476
+ }
1477
+ }
1478
+ }
1479
+ }
1480
+ exports.KrakCompiler = KrakCompiler;