swf-toolkit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -2
  3. package/README.tr.md +126 -0
  4. package/dist/abc/abc-file.d.ts +168 -0
  5. package/dist/abc/abc-file.js +656 -0
  6. package/dist/abc/analysis.d.ts +36 -0
  7. package/dist/abc/analysis.js +213 -0
  8. package/dist/abc/code.d.ts +59 -0
  9. package/dist/abc/code.js +379 -0
  10. package/dist/abc/constants.d.ts +80 -0
  11. package/dist/abc/constants.js +93 -0
  12. package/dist/abc/interner.d.ts +46 -0
  13. package/dist/abc/interner.js +168 -0
  14. package/dist/abc/model.d.ts +40 -0
  15. package/dist/abc/model.js +146 -0
  16. package/dist/abc/opcodes.d.ts +39 -0
  17. package/dist/abc/opcodes.js +196 -0
  18. package/dist/abc/pcode/assembler.d.ts +42 -0
  19. package/dist/abc/pcode/assembler.js +635 -0
  20. package/dist/abc/pcode/disassembler.d.ts +17 -0
  21. package/dist/abc/pcode/disassembler.js +147 -0
  22. package/dist/abc/pcode/format.d.ts +23 -0
  23. package/dist/abc/pcode/format.js +131 -0
  24. package/dist/abc/pcode/lexer.d.ts +34 -0
  25. package/dist/abc/pcode/lexer.js +183 -0
  26. package/dist/assets/assets.d.ts +26 -0
  27. package/dist/assets/assets.js +151 -0
  28. package/dist/assets/images.d.ts +31 -0
  29. package/dist/assets/images.js +238 -0
  30. package/dist/assets/png.d.ts +12 -0
  31. package/dist/assets/png.js +196 -0
  32. package/dist/assets/sounds.d.ts +37 -0
  33. package/dist/assets/sounds.js +236 -0
  34. package/dist/avm1/avm1.d.ts +55 -0
  35. package/dist/avm1/avm1.js +642 -0
  36. package/dist/avm1/project.d.ts +16 -0
  37. package/dist/avm1/project.js +81 -0
  38. package/dist/cjs/abc/abc-file.d.ts +168 -0
  39. package/dist/cjs/abc/abc-file.js +663 -0
  40. package/dist/cjs/abc/analysis.d.ts +36 -0
  41. package/dist/cjs/abc/analysis.js +221 -0
  42. package/dist/cjs/abc/code.d.ts +59 -0
  43. package/dist/cjs/abc/code.js +387 -0
  44. package/dist/cjs/abc/constants.d.ts +80 -0
  45. package/dist/cjs/abc/constants.js +97 -0
  46. package/dist/cjs/abc/interner.d.ts +46 -0
  47. package/dist/cjs/abc/interner.js +172 -0
  48. package/dist/cjs/abc/model.d.ts +40 -0
  49. package/dist/cjs/abc/model.js +155 -0
  50. package/dist/cjs/abc/opcodes.d.ts +39 -0
  51. package/dist/cjs/abc/opcodes.js +201 -0
  52. package/dist/cjs/abc/pcode/assembler.d.ts +42 -0
  53. package/dist/cjs/abc/pcode/assembler.js +640 -0
  54. package/dist/cjs/abc/pcode/disassembler.d.ts +17 -0
  55. package/dist/cjs/abc/pcode/disassembler.js +152 -0
  56. package/dist/cjs/abc/pcode/format.d.ts +23 -0
  57. package/dist/cjs/abc/pcode/format.js +141 -0
  58. package/dist/cjs/abc/pcode/lexer.d.ts +34 -0
  59. package/dist/cjs/abc/pcode/lexer.js +188 -0
  60. package/dist/cjs/assets/assets.d.ts +26 -0
  61. package/dist/cjs/assets/assets.js +159 -0
  62. package/dist/cjs/assets/images.d.ts +31 -0
  63. package/dist/cjs/assets/images.js +246 -0
  64. package/dist/cjs/assets/png.d.ts +12 -0
  65. package/dist/cjs/assets/png.js +202 -0
  66. package/dist/cjs/assets/sounds.d.ts +37 -0
  67. package/dist/cjs/assets/sounds.js +246 -0
  68. package/dist/cjs/avm1/avm1.d.ts +55 -0
  69. package/dist/cjs/avm1/avm1.js +649 -0
  70. package/dist/cjs/avm1/project.d.ts +16 -0
  71. package/dist/cjs/avm1/project.js +88 -0
  72. package/dist/cjs/compiler/ast.d.ts +285 -0
  73. package/dist/cjs/compiler/ast.js +3 -0
  74. package/dist/cjs/compiler/class-compiler.d.ts +34 -0
  75. package/dist/cjs/compiler/class-compiler.js +440 -0
  76. package/dist/cjs/compiler/codegen.d.ts +144 -0
  77. package/dist/cjs/compiler/codegen.js +1589 -0
  78. package/dist/cjs/compiler/lexer.d.ts +27 -0
  79. package/dist/cjs/compiler/lexer.js +254 -0
  80. package/dist/cjs/compiler/parser.d.ts +58 -0
  81. package/dist/cjs/compiler/parser.js +989 -0
  82. package/dist/cjs/compiler/program.d.ts +29 -0
  83. package/dist/cjs/compiler/program.js +36 -0
  84. package/dist/cjs/compiler/scope.d.ts +22 -0
  85. package/dist/cjs/compiler/scope.js +242 -0
  86. package/dist/cjs/compression/lzma.d.ts +37 -0
  87. package/dist/cjs/compression/lzma.js +575 -0
  88. package/dist/cjs/compression/swf-container.d.ts +19 -0
  89. package/dist/cjs/compression/swf-container.js +92 -0
  90. package/dist/cjs/decompiler/ast.d.ts +228 -0
  91. package/dist/cjs/decompiler/ast.js +455 -0
  92. package/dist/cjs/decompiler/cfg.d.ts +40 -0
  93. package/dist/cjs/decompiler/cfg.js +236 -0
  94. package/dist/cjs/decompiler/class-printer.d.ts +9 -0
  95. package/dist/cjs/decompiler/class-printer.js +173 -0
  96. package/dist/cjs/decompiler/method.d.ts +41 -0
  97. package/dist/cjs/decompiler/method.js +436 -0
  98. package/dist/cjs/decompiler/simulate.d.ts +55 -0
  99. package/dist/cjs/decompiler/simulate.js +591 -0
  100. package/dist/cjs/decompiler/structure.d.ts +48 -0
  101. package/dist/cjs/decompiler/structure.js +764 -0
  102. package/dist/cjs/errors.d.ts +11 -0
  103. package/dist/cjs/errors.js +25 -0
  104. package/dist/cjs/index.d.ts +41 -0
  105. package/dist/cjs/index.js +82 -0
  106. package/dist/cjs/io/reader.d.ts +48 -0
  107. package/dist/cjs/io/reader.js +180 -0
  108. package/dist/cjs/io/writer.d.ts +37 -0
  109. package/dist/cjs/io/writer.js +175 -0
  110. package/dist/cjs/package.json +1 -0
  111. package/dist/cjs/project/pcode-project.d.ts +32 -0
  112. package/dist/cjs/project/pcode-project.js +210 -0
  113. package/dist/cjs/project/source-export.d.ts +12 -0
  114. package/dist/cjs/project/source-export.js +51 -0
  115. package/dist/cjs/project/source-import.d.ts +13 -0
  116. package/dist/cjs/project/source-import.js +73 -0
  117. package/dist/cjs/swf/swf.d.ts +63 -0
  118. package/dist/cjs/swf/swf.js +147 -0
  119. package/dist/cjs/swf/tag-codes.d.ts +77 -0
  120. package/dist/cjs/swf/tag-codes.js +109 -0
  121. package/dist/cjs/swf/tags.d.ts +118 -0
  122. package/dist/cjs/swf/tags.js +330 -0
  123. package/dist/cli.d.ts +2 -0
  124. package/dist/cli.js +296 -0
  125. package/dist/compiler/ast.d.ts +285 -0
  126. package/dist/compiler/ast.js +2 -0
  127. package/dist/compiler/class-compiler.d.ts +34 -0
  128. package/dist/compiler/class-compiler.js +437 -0
  129. package/dist/compiler/codegen.d.ts +144 -0
  130. package/dist/compiler/codegen.js +1585 -0
  131. package/dist/compiler/lexer.d.ts +27 -0
  132. package/dist/compiler/lexer.js +249 -0
  133. package/dist/compiler/parser.d.ts +58 -0
  134. package/dist/compiler/parser.js +984 -0
  135. package/dist/compiler/program.d.ts +29 -0
  136. package/dist/compiler/program.js +32 -0
  137. package/dist/compiler/scope.d.ts +22 -0
  138. package/dist/compiler/scope.js +238 -0
  139. package/dist/compression/lzma.d.ts +37 -0
  140. package/dist/compression/lzma.js +569 -0
  141. package/dist/compression/swf-container.d.ts +19 -0
  142. package/dist/compression/swf-container.js +87 -0
  143. package/dist/decompiler/ast.d.ts +228 -0
  144. package/dist/decompiler/ast.js +441 -0
  145. package/dist/decompiler/cfg.d.ts +40 -0
  146. package/dist/decompiler/cfg.js +231 -0
  147. package/dist/decompiler/class-printer.d.ts +9 -0
  148. package/dist/decompiler/class-printer.js +170 -0
  149. package/dist/decompiler/method.d.ts +41 -0
  150. package/dist/decompiler/method.js +427 -0
  151. package/dist/decompiler/simulate.d.ts +55 -0
  152. package/dist/decompiler/simulate.js +586 -0
  153. package/dist/decompiler/structure.d.ts +48 -0
  154. package/dist/decompiler/structure.js +760 -0
  155. package/dist/errors.d.ts +11 -0
  156. package/dist/errors.js +20 -0
  157. package/dist/index.d.ts +41 -0
  158. package/dist/index.js +49 -0
  159. package/dist/io/reader.d.ts +48 -0
  160. package/dist/io/reader.js +176 -0
  161. package/dist/io/writer.d.ts +37 -0
  162. package/dist/io/writer.js +169 -0
  163. package/dist/project/pcode-project.d.ts +32 -0
  164. package/dist/project/pcode-project.js +203 -0
  165. package/dist/project/source-export.d.ts +12 -0
  166. package/dist/project/source-export.js +47 -0
  167. package/dist/project/source-import.d.ts +13 -0
  168. package/dist/project/source-import.js +70 -0
  169. package/dist/swf/swf.d.ts +63 -0
  170. package/dist/swf/swf.js +143 -0
  171. package/dist/swf/tag-codes.d.ts +77 -0
  172. package/dist/swf/tag-codes.js +105 -0
  173. package/dist/swf/tags.d.ts +118 -0
  174. package/dist/swf/tags.js +316 -0
  175. package/package.json +71 -4
@@ -0,0 +1,764 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Structurer = void 0;
4
+ const ast_js_1 = require("./ast.js");
5
+ const cfg_js_1 = require("./cfg.js");
6
+ const simulate_js_1 = require("./simulate.js");
7
+ const opcodes_js_1 = require("../abc/opcodes.js");
8
+ class Structurer {
9
+ ctx;
10
+ cfg;
11
+ visited = new Set();
12
+ loops;
13
+ labelCounter = 0;
14
+ tries = [];
15
+ /** Labels of loop statements that need one, keyed by LoopCtx. */
16
+ loopStmts = new Map();
17
+ constructor(ctx, cfg) {
18
+ this.ctx = ctx;
19
+ this.cfg = cfg;
20
+ this.loops = (0, cfg_js_1.findLoops)(cfg);
21
+ const groups = new Map();
22
+ cfg.exceptions.forEach((e, index) => {
23
+ const fromBlock = cfg.labelBlock.get(e.from);
24
+ const toBlock = cfg.labelBlock.get(e.to);
25
+ const handler = cfg.labelBlock.get(e.target);
26
+ if (fromBlock === undefined || toBlock === undefined || handler === undefined)
27
+ return;
28
+ const key = `${fromBlock}:${toBlock}`;
29
+ let g = groups.get(key);
30
+ if (!g) {
31
+ g = { key, fromBlock, toBlock, handlers: [] };
32
+ groups.set(key, g);
33
+ }
34
+ g.handlers.push({ index, block: handler });
35
+ });
36
+ // Outer (larger) try ranges first.
37
+ this.tries = [...groups.values()].sort((a, b) => a.fromBlock - b.fromBlock || b.toBlock - a.toBlock);
38
+ }
39
+ run() {
40
+ return this.seq(0, { follows: new Set(), loops: [], activeLoops: new Set(), activeTries: new Set() }, []);
41
+ }
42
+ /**
43
+ * If block `id` only evaluates a condition on top of `stack` (no statements,
44
+ * stack unchanged afterwards), returns that condition.
45
+ */
46
+ blockIsEmptyCondition(id, stack = []) {
47
+ const b = this.cfg.blocks[id];
48
+ if (b.kind !== 'cond' || b.entryHeight !== stack.length || this.visited.has(id) || this.loops.has(id))
49
+ return null;
50
+ if (this.tries.some((t) => t.fromBlock === id))
51
+ return null;
52
+ const stmts = [];
53
+ try {
54
+ const r = (0, simulate_js_1.simulate)(this.ctx, b.insns, stack, stmts);
55
+ if (stmts.length || r.term.k !== 'cond' || r.stack.length !== stack.length)
56
+ return null;
57
+ if (r.stack.some((v, i) => v !== stack[i]))
58
+ return null;
59
+ return { c: r.term.c };
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ /** Collapses short-circuit conditions (`a && b`, `a || b`) starting at a conditional block. */
66
+ collapse(block, c, region, stack = []) {
67
+ const group = new Set([block]);
68
+ let [t, f] = this.cfg.blocks[block].succs;
69
+ for (let changed = true; changed;) {
70
+ changed = false;
71
+ for (const x of [t, f]) {
72
+ if (group.has(x) || (region && !region.has(x)))
73
+ continue;
74
+ const preds = this.cfg.blocks[x].preds;
75
+ if (!preds.every((p) => group.has(p)))
76
+ continue;
77
+ const inner = this.blockIsEmptyCondition(x, stack);
78
+ if (!inner)
79
+ continue;
80
+ const [tx, fx] = this.cfg.blocks[x].succs;
81
+ let ok = true;
82
+ if (x === t && fx === f) {
83
+ c = { k: 'binary', op: '&&', a: c, b: inner.c };
84
+ t = tx;
85
+ }
86
+ else if (x === t && tx === f) {
87
+ c = { k: 'binary', op: '&&', a: c, b: (0, ast_js_1.not)(inner.c) };
88
+ t = fx;
89
+ }
90
+ else if (x === f && tx === t) {
91
+ c = { k: 'binary', op: '||', a: c, b: inner.c };
92
+ f = fx;
93
+ }
94
+ else if (x === f && fx === t) {
95
+ c = { k: 'binary', op: '||', a: c, b: (0, ast_js_1.not)(inner.c) };
96
+ f = tx;
97
+ }
98
+ else
99
+ ok = false;
100
+ if (ok) {
101
+ group.add(x);
102
+ this.visited.add(x);
103
+ changed = true;
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ return { c, t, f, group };
109
+ }
110
+ mergeOf(block, env) {
111
+ const m = this.cfg.ipdom[block];
112
+ if (m >= 0) {
113
+ if (env.region && !env.region.has(m))
114
+ return -1;
115
+ return m;
116
+ }
117
+ // No post-dominator (some paths return/throw): use the earliest block
118
+ // reachable from at least two successors inside the current region.
119
+ const succs = [...new Set(this.cfg.blocks[block].succs)];
120
+ if (succs.length < 2)
121
+ return -1;
122
+ const stop = new Set([block, ...env.follows]);
123
+ for (const l of env.loops) {
124
+ stop.add(l.header);
125
+ stop.add(l.breakTarget);
126
+ stop.add(l.continueTarget);
127
+ }
128
+ const reach = (from) => {
129
+ const seen = new Set();
130
+ const work = [from];
131
+ while (work.length) {
132
+ const x = work.pop();
133
+ if (x < 0 || seen.has(x))
134
+ continue;
135
+ if (env.region && !env.region.has(x))
136
+ continue;
137
+ seen.add(x);
138
+ if (stop.has(x) && x !== from)
139
+ continue;
140
+ for (const s of this.cfg.blocks[x].succs)
141
+ if (s !== block)
142
+ work.push(s);
143
+ }
144
+ return seen;
145
+ };
146
+ const counts = new Map();
147
+ for (const s of succs)
148
+ for (const x of reach(s))
149
+ counts.set(x, (counts.get(x) ?? 0) + 1);
150
+ const targets = new Set(succs);
151
+ let best = -1;
152
+ let bestCount = 0;
153
+ for (const [x, n] of counts) {
154
+ if (n < 2 || (succs.length > 2 && targets.has(x)))
155
+ continue;
156
+ if (stop.has(x) && !env.follows.has(x))
157
+ continue;
158
+ const better = n > bestCount || (n === bestCount && this.cfg.order[x] < this.cfg.order[best]);
159
+ if (best < 0 || better) {
160
+ best = x;
161
+ bestCount = n;
162
+ }
163
+ }
164
+ if (best >= 0 && env.loops.some((l) => l.header === best || l.continueTarget === best || l.breakTarget === best))
165
+ return -1;
166
+ return best;
167
+ }
168
+ /** Follows blocks that only contain `label` / `jump` / `nop` to their destination. */
169
+ resolveTrivial(id) {
170
+ for (let guard = 0; guard < 64; guard++) {
171
+ const b = this.cfg.blocks[id];
172
+ if (!b || (b.kind !== 'jump' && b.kind !== 'fall') || b.succs.length !== 1)
173
+ return id;
174
+ if (!b.insns.every((i) => i.op === opcodes_js_1.Op.label || i.op === opcodes_js_1.Op.jump || i.op === opcodes_js_1.Op.nop))
175
+ return id;
176
+ if (this.tries.some((t) => t.fromBlock === id) || this.loops.has(id))
177
+ return id;
178
+ id = b.succs[0];
179
+ }
180
+ return id;
181
+ }
182
+ /** Statement for jumping to `target` from inside the current constructs, or null. */
183
+ jumpStmt(target, env) {
184
+ const resolved = this.resolveTrivial(target);
185
+ for (let i = env.loops.length - 1; i >= 0; i--) {
186
+ const l = env.loops[i];
187
+ const innermost = i === env.loops.length - 1 && env.switchBreak === undefined;
188
+ if (target === l.breakTarget || resolved === l.breakTarget)
189
+ return innermost ? { k: 'break' } : { k: 'break', label: this.loopLabel(l) };
190
+ if ((target === l.continueTarget || resolved === l.continueTarget) && l.latch === undefined) {
191
+ return i === env.loops.length - 1 ? { k: 'continue' } : { k: 'continue', label: this.loopLabel(l) };
192
+ }
193
+ }
194
+ if (env.switchBreak !== undefined && (target === env.switchBreak || resolved === env.switchBreak))
195
+ return { k: 'break' };
196
+ return null;
197
+ }
198
+ loopLabel(l) {
199
+ if (!l.label) {
200
+ l.label = `loop${this.labelCounter++}`;
201
+ const s = this.loopStmts.get(l);
202
+ if (s && 'label' in s)
203
+ s.label = l.label;
204
+ }
205
+ return l.label;
206
+ }
207
+ seq(start, env, entryStack, allowStartTarget = false) {
208
+ const out = [];
209
+ let cur = start;
210
+ let stack = entryStack;
211
+ let first = true;
212
+ while (cur >= 0) {
213
+ if (!(first && allowStartTarget)) {
214
+ const latchLoop = env.loops[env.loops.length - 1];
215
+ if (latchLoop?.latch === cur && !this.visited.has(cur)) {
216
+ // do-while latch: its statements end the body, its condition is the loop test.
217
+ this.visited.add(cur);
218
+ const stmts = [];
219
+ const r = (0, simulate_js_1.simulate)(this.ctx, this.cfg.blocks[cur].insns, stack, stmts);
220
+ if (r.term.k !== 'cond')
221
+ throw new simulate_js_1.SimulationError('do-while latch without condition');
222
+ const [t] = this.cfg.blocks[cur].succs;
223
+ env.latchResult = { stmts, c: t === latchLoop.header ? r.term.c : (0, ast_js_1.not)(r.term.c), latch: cur };
224
+ out.push(...stmts);
225
+ break;
226
+ }
227
+ const j = this.jumpStmt(cur, env);
228
+ if (j) {
229
+ out.push(j);
230
+ break;
231
+ }
232
+ if (env.follows.has(cur) || env.follows.has(this.resolveTrivial(cur)))
233
+ break;
234
+ if (env.region && !env.region.has(cur)) {
235
+ env.exits?.push(this.resolveTrivial(cur));
236
+ break;
237
+ }
238
+ }
239
+ first = false;
240
+ if (this.visited.has(cur)) {
241
+ out.push({ k: 'comment', text: `goto block ${cur} (unstructured jump)` });
242
+ break;
243
+ }
244
+ const tryGroup = this.tries.find((t) => t.fromBlock === cur && !env.activeTries.has(t.key));
245
+ if (tryGroup) {
246
+ const r = this.emitTry(tryGroup, env, stack);
247
+ out.push(r.stmt);
248
+ cur = r.next;
249
+ continue;
250
+ }
251
+ const loop = this.loops.get(cur);
252
+ if (loop && !env.activeLoops.has(cur)) {
253
+ const r = this.emitLoop(loop, env, stack);
254
+ out.push(...r.stmts);
255
+ cur = r.next;
256
+ continue;
257
+ }
258
+ this.visited.add(cur);
259
+ const b = this.cfg.blocks[cur];
260
+ if (b.kind === 'raw') {
261
+ out.push({ k: 'comment', text: 'undecodable bytecode' });
262
+ break;
263
+ }
264
+ const r = (0, simulate_js_1.simulate)(this.ctx, b.insns, stack, out);
265
+ stack = r.stack;
266
+ const term = r.term;
267
+ switch (term.k) {
268
+ case 'fall':
269
+ case 'jump':
270
+ cur = b.succs[0] ?? -1;
271
+ if (cur < 0 && b.kind !== 'end')
272
+ out.push({ k: 'comment', text: 'end of code' });
273
+ break;
274
+ case 'end':
275
+ cur = -1;
276
+ break;
277
+ case 'return':
278
+ out.push(term.e ? { k: 'return', e: term.e } : { k: 'return' });
279
+ cur = -1;
280
+ break;
281
+ case 'throw':
282
+ out.push({ k: 'throw', e: term.e });
283
+ cur = -1;
284
+ break;
285
+ case 'cond': {
286
+ const merge = this.mergeOf(cur, env);
287
+ if (merge >= 0 && this.cfg.blocks[merge].entryHeight > 0) {
288
+ stack = this.evalCond(cur, term.c, merge, stack);
289
+ cur = merge;
290
+ break;
291
+ }
292
+ const { c, t, f } = this.collapse(cur, term.c, env.region, stack);
293
+ const follows = merge >= 0 ? new Set([...env.follows, merge]) : env.follows;
294
+ // Compilers emit `if(c) A else B` as `iffalse Lelse; A; ...`, so the
295
+ // fall-through successor is the source's "then" branch.
296
+ const inner = { ...env, follows, latchResult: undefined };
297
+ const thenBody = this.seq(f, inner, stack);
298
+ const elseEnv = { ...env, follows, latchResult: undefined };
299
+ const elseBody = this.seq(t, elseEnv, stack);
300
+ if (inner.latchResult || elseEnv.latchResult)
301
+ env.latchResult = inner.latchResult ?? elseEnv.latchResult;
302
+ out.push(makeIf((0, ast_js_1.not)(c), thenBody, elseBody));
303
+ cur = merge;
304
+ break;
305
+ }
306
+ case 'switch': {
307
+ const r2 = this.emitSwitch(cur, term.e, env, stack);
308
+ out.push(r2.stmt);
309
+ cur = r2.next;
310
+ break;
311
+ }
312
+ }
313
+ if (env.latchResult)
314
+ break;
315
+ }
316
+ return out;
317
+ }
318
+ // -------------------------------------------------------------------------
319
+ // Value merges (ternaries, && / || used as values)
320
+ // -------------------------------------------------------------------------
321
+ evalCond(block, c0, merge, stack) {
322
+ // `(a && b) ? x : y` compiles to two conditional jumps sharing a target.
323
+ const { c, t, f } = this.collapse(block, c0, undefined, stack);
324
+ const st = this.evalPath(t, merge, stack);
325
+ const sf = this.evalPath(f, merge, stack);
326
+ if (st.length !== sf.length)
327
+ throw new simulate_js_1.SimulationError('Stack height mismatch at value merge');
328
+ const negOf = (cond, v) => (cond.k === 'unary' && cond.op === '!' && cond.e === v) || (v.k === 'unary' && v.op === '!' && v.e === cond);
329
+ return st.map((x, i) => {
330
+ const y = sf[i];
331
+ if (x === y)
332
+ return x;
333
+ // x ? x : y → x || y
334
+ if (x === c)
335
+ return { k: 'binary', op: '||', a: c, b: y };
336
+ // c ? x : c → c && x
337
+ if (y === c)
338
+ return { k: 'binary', op: '&&', a: c, b: x };
339
+ // !x ? x : y → x && y
340
+ if (negOf(c, x))
341
+ return { k: 'binary', op: '&&', a: x, b: y };
342
+ // !y ? x : y → y || x
343
+ if (negOf(c, y))
344
+ return { k: 'binary', op: '||', a: y, b: x };
345
+ if (c.k === 'unary' && c.op === '!')
346
+ return { k: 'cond', c: c.e, t: y, f: x };
347
+ return { k: 'cond', c, t: x, f: y };
348
+ });
349
+ }
350
+ evalPath(start, merge, stack) {
351
+ let cur = start;
352
+ let st = stack;
353
+ for (let guard = 0; guard < 10000; guard++) {
354
+ if (cur === merge)
355
+ return st;
356
+ if (cur < 0 || this.visited.has(cur))
357
+ throw new simulate_js_1.SimulationError('Unsupported control flow inside expression');
358
+ this.visited.add(cur);
359
+ const b = this.cfg.blocks[cur];
360
+ const stmts = [];
361
+ const r = (0, simulate_js_1.simulate)(this.ctx, b.insns, st, stmts);
362
+ if (stmts.length) {
363
+ // Side effects inside an expression (e.g. `(a = b) > 0`): fold them
364
+ // into a comma expression on the value this path produced.
365
+ if (!stmts.every((s) => s.k === 'expr'))
366
+ throw new simulate_js_1.SimulationError('Statement inside expression');
367
+ const idx = r.stack.findIndex((v, i) => v !== st[i]);
368
+ if (idx < 0)
369
+ throw new simulate_js_1.SimulationError('Statement inside expression');
370
+ let seqExpr = r.stack[idx];
371
+ for (let k = stmts.length - 1; k >= 0; k--)
372
+ seqExpr = { k: 'binary', op: ',', a: stmts[k].e, b: seqExpr };
373
+ r.stack[idx] = seqExpr;
374
+ }
375
+ st = r.stack;
376
+ if (r.term.k === 'fall' || r.term.k === 'jump') {
377
+ cur = b.succs[0] ?? -1;
378
+ continue;
379
+ }
380
+ if (r.term.k === 'cond') {
381
+ const inner = this.cfg.ipdom[cur];
382
+ if (inner >= 0 && inner !== merge && this.cfg.blocks[inner].entryHeight > 0) {
383
+ st = this.evalCond(cur, r.term.c, inner, st);
384
+ cur = inner;
385
+ continue;
386
+ }
387
+ return this.evalCond(cur, r.term.c, merge, st);
388
+ }
389
+ throw new simulate_js_1.SimulationError('Unsupported terminator inside expression');
390
+ }
391
+ throw new simulate_js_1.SimulationError('Expression too complex');
392
+ }
393
+ // -------------------------------------------------------------------------
394
+ // Loops
395
+ // -------------------------------------------------------------------------
396
+ emitLoop(loop, env, stack) {
397
+ const h = loop.header;
398
+ const hb = this.cfg.blocks[h];
399
+ const outside = (x) => !loop.body.has(x);
400
+ // Exit selection.
401
+ let exit = -1;
402
+ let form = 'forever';
403
+ let latch;
404
+ const headerCond = hb.kind === 'cond' ? this.blockIsEmptyCondition(h) ?? this.pureHeaderCondition(h) : null;
405
+ if (headerCond && hb.succs.some(outside) && hb.succs.some((s) => !outside(s))) {
406
+ form = 'while';
407
+ }
408
+ else {
409
+ const condLatch = loop.latches.find((l) => {
410
+ const lb = this.cfg.blocks[l];
411
+ return lb.kind === 'cond' && lb.succs.includes(h) && lb.succs.some(outside);
412
+ });
413
+ if (condLatch !== undefined && condLatch === h && loop.body.size === 1) {
414
+ // Single-block do-while: body and test in the header block.
415
+ this.visited.add(h);
416
+ const stmts = [];
417
+ const r = (0, simulate_js_1.simulate)(this.ctx, hb.insns, stack, stmts);
418
+ if (r.term.k === 'cond') {
419
+ const [t] = hb.succs;
420
+ const next = hb.succs.find(outside);
421
+ return { stmts: [{ k: 'dowhile', c: t === h ? r.term.c : (0, ast_js_1.not)(r.term.c), body: stmts }], next };
422
+ }
423
+ this.visited.delete(h);
424
+ }
425
+ else if (condLatch !== undefined && loop.latches.length === 1) {
426
+ form = 'dowhile';
427
+ latch = condLatch;
428
+ exit = this.cfg.blocks[condLatch].succs.find(outside);
429
+ }
430
+ }
431
+ if (form === 'while') {
432
+ const activeLoops = new Set([...env.activeLoops, h]);
433
+ this.visited.add(h);
434
+ const { c, t, f, group } = this.collapse(h, headerCond.c, loop.body);
435
+ const inBody = !outside(t) ? t : f;
436
+ exit = inBody === t ? f : t;
437
+ const cond = inBody === t ? c : (0, ast_js_1.not)(c);
438
+ for (const g of group)
439
+ this.visited.add(g);
440
+ // for-loop update block: the single latch, reached by several paths (continue / merges)
441
+ // or holding only increments of locals.
442
+ let update;
443
+ let updateBlock = -1;
444
+ if (loop.latches.length === 1) {
445
+ const l = loop.latches[0];
446
+ const lb = this.cfg.blocks[l];
447
+ if (l !== h && l !== inBody && (lb.kind === 'fall' || lb.kind === 'jump') && lb.succs[0] === h && !this.loops.has(l)) {
448
+ const stmts = [];
449
+ try {
450
+ const r = (0, simulate_js_1.simulate)(this.ctx, lb.insns, [], stmts);
451
+ const simple = stmts.length > 0 && stmts.every((s) => s.k === 'expr' && (s.e.k === 'postfix' || (s.e.k === 'assign' && s.e.target.k === 'local')));
452
+ if (r.stack.length === 0 && stmts.every((s) => s.k === 'expr') && (lb.preds.length >= 2 || simple)) {
453
+ update = stmts;
454
+ updateBlock = l;
455
+ }
456
+ }
457
+ catch {
458
+ /* not an update block */
459
+ }
460
+ }
461
+ }
462
+ const lc = { header: h, continueTarget: updateBlock >= 0 ? updateBlock : h, breakTarget: exit };
463
+ const stmt = update
464
+ ? { k: 'for', c: cond, update: commaOf(update.map((s) => s.e)), body: [] }
465
+ : { k: 'while', c: cond, body: [] };
466
+ this.loopStmts.set(lc, stmt);
467
+ const bodyEnv = { ...env, follows: new Set(), loops: [...env.loops, lc], switchBreak: undefined, region: intersect(this.loopRegion(loop, exit), env.region), exits: [], activeLoops };
468
+ stmt.body = this.seq(inBody, bodyEnv, stack);
469
+ if (updateBlock >= 0)
470
+ this.visited.add(updateBlock);
471
+ if (lc.label)
472
+ stmt.label = lc.label;
473
+ return { stmts: [stmt], next: exit };
474
+ }
475
+ if (exit < 0) {
476
+ const candidates = new Map();
477
+ for (const x of loop.body) {
478
+ for (const s0 of this.cfg.blocks[x].succs) {
479
+ if (!outside(s0))
480
+ continue;
481
+ const s = this.resolveTrivial(s0);
482
+ candidates.set(s, (candidates.get(s) ?? 0) + 1);
483
+ }
484
+ }
485
+ let best = -1;
486
+ for (const [s, n] of candidates)
487
+ if (best < 0 || n > candidates.get(best) || (n === candidates.get(best) && s < best))
488
+ best = s;
489
+ exit = best;
490
+ }
491
+ const activeLoops = new Set([...env.activeLoops, h]);
492
+ const lc = { header: h, continueTarget: latch ?? h, breakTarget: exit, latch };
493
+ const bodyEnv = { ...env, follows: new Set(), loops: [...env.loops, lc], switchBreak: undefined, region: intersect(this.loopRegion(loop, exit), env.region), exits: [], activeLoops, latchResult: undefined };
494
+ if (form === 'dowhile') {
495
+ const stmt = { k: 'dowhile', c: (0, ast_js_1.lit)(true), body: [] };
496
+ this.loopStmts.set(lc, stmt);
497
+ stmt.body = this.seq(h, bodyEnv, stack, true);
498
+ if (bodyEnv.latchResult)
499
+ stmt.c = bodyEnv.latchResult.c;
500
+ if (lc.label)
501
+ stmt.label = lc.label;
502
+ return { stmts: [stmt], next: exit };
503
+ }
504
+ const stmt = { k: 'while', c: (0, ast_js_1.lit)(true), body: [] };
505
+ this.loopStmts.set(lc, stmt);
506
+ stmt.body = this.seq(h, bodyEnv, stack, true);
507
+ if (lc.label)
508
+ stmt.label = lc.label;
509
+ return { stmts: [stmt], next: exit };
510
+ }
511
+ /**
512
+ * Natural loop body plus dead-end blocks (return/throw paths) dominated by
513
+ * the header: those are syntactically inside the loop even though they
514
+ * never branch back.
515
+ */
516
+ loopRegion(loop, exit) {
517
+ const region = new Set(loop.body);
518
+ const canLeave = new Map();
519
+ const leaves = (x, seen) => {
520
+ const c = canLeave.get(x);
521
+ if (c !== undefined)
522
+ return c;
523
+ if (seen.has(x))
524
+ return false;
525
+ seen.add(x);
526
+ let r = false;
527
+ for (const s of this.cfg.blocks[x].succs) {
528
+ if (loop.body.has(s))
529
+ continue;
530
+ if (!(0, cfg_js_1.dominates)(this.cfg, loop.header, s)) {
531
+ r = true;
532
+ break;
533
+ }
534
+ if (leaves(s, seen)) {
535
+ r = true;
536
+ break;
537
+ }
538
+ }
539
+ canLeave.set(x, r);
540
+ return r;
541
+ };
542
+ for (const x of loop.body) {
543
+ for (const s of this.cfg.blocks[x].succs) {
544
+ if (region.has(s) || !(0, cfg_js_1.dominates)(this.cfg, loop.header, s))
545
+ continue;
546
+ const stack = [s];
547
+ const group = [];
548
+ let deadEnd = true;
549
+ const seen = new Set();
550
+ while (stack.length) {
551
+ const y = stack.pop();
552
+ if (seen.has(y) || region.has(y))
553
+ continue;
554
+ if (y === exit) {
555
+ deadEnd = false;
556
+ break;
557
+ }
558
+ seen.add(y);
559
+ group.push(y);
560
+ const yb = this.cfg.blocks[y];
561
+ if (yb.kind !== 'return' && yb.kind !== 'throw' && yb.succs.length === 0)
562
+ deadEnd = false;
563
+ for (const z of yb.succs) {
564
+ if (loop.body.has(z))
565
+ continue;
566
+ if (!(0, cfg_js_1.dominates)(this.cfg, loop.header, z))
567
+ deadEnd = false;
568
+ else
569
+ stack.push(z);
570
+ }
571
+ }
572
+ if (deadEnd && group.length <= 64 && !group.some((g) => leaves(g, new Set())))
573
+ for (const g of group)
574
+ region.add(g);
575
+ }
576
+ }
577
+ return region;
578
+ }
579
+ /** Header condition for blocks that only compute a condition from scratch (e.g. hasnext2). */
580
+ pureHeaderCondition(id) {
581
+ const b = this.cfg.blocks[id];
582
+ const stmts = [];
583
+ try {
584
+ const r = (0, simulate_js_1.simulate)(this.ctx, b.insns, [], stmts);
585
+ if (stmts.length === 0 && r.stack.length === 0 && r.term.k === 'cond')
586
+ return { c: r.term.c };
587
+ }
588
+ catch {
589
+ /* not a pure condition */
590
+ }
591
+ return null;
592
+ }
593
+ // -------------------------------------------------------------------------
594
+ // switch
595
+ // -------------------------------------------------------------------------
596
+ switchFollow(block, env) {
597
+ const m = this.cfg.ipdom[block];
598
+ if (m >= 0)
599
+ return env.region && !env.region.has(m) ? -1 : m;
600
+ // Most common target of `break`-like jumps out of the case bodies.
601
+ const targets = new Set(this.cfg.blocks[block].succs);
602
+ const counts = new Map();
603
+ for (const b of this.cfg.blocks) {
604
+ if (b.kind !== 'jump' || b.id === block)
605
+ continue;
606
+ if (!(0, cfg_js_1.dominates)(this.cfg, block, b.id))
607
+ continue;
608
+ const t = this.resolveTrivial(b.succs[0]);
609
+ if (targets.has(t) || t === block)
610
+ continue;
611
+ if (env.region && !env.region.has(t))
612
+ continue;
613
+ if (!(0, cfg_js_1.dominates)(this.cfg, block, t) && !env.follows.has(t))
614
+ continue;
615
+ counts.set(t, (counts.get(t) ?? 0) + 1);
616
+ }
617
+ let best = -1;
618
+ for (const [t, n] of counts)
619
+ if (best < 0 || n > counts.get(best) || (n === counts.get(best) && t > best))
620
+ best = t;
621
+ // A "follow" that is only reached from inside one case is part of that case.
622
+ if (best >= 0 && counts.get(best) < 2) {
623
+ const reachedFrom = [...targets].filter((t) => this.reaches(t, best, block));
624
+ if (reachedFrom.length < 2)
625
+ return -1;
626
+ }
627
+ return best;
628
+ }
629
+ reaches(from, to, avoid) {
630
+ const seen = new Set();
631
+ const work = [from];
632
+ while (work.length) {
633
+ const x = work.pop();
634
+ if (x === to)
635
+ return true;
636
+ if (seen.has(x) || x === avoid)
637
+ continue;
638
+ seen.add(x);
639
+ work.push(...this.cfg.blocks[x].succs);
640
+ }
641
+ return false;
642
+ }
643
+ emitSwitch(block, index, env, stack) {
644
+ const b = this.cfg.blocks[block];
645
+ const follow = this.switchFollow(block, env);
646
+ const { disc, values } = recoverSwitch(index);
647
+ const targets = b.succs;
648
+ const order = [...new Set(targets)].sort((x, y) => x - y);
649
+ const cases = [];
650
+ for (let i = 0; i < order.length; i++) {
651
+ const t = order[i];
652
+ const next = order[i + 1];
653
+ const follows = new Set([...env.follows, ...(follow >= 0 ? [follow] : []), ...(next !== undefined ? [next] : [])]);
654
+ const caseEnv = { ...env, follows, switchBreak: follow >= 0 ? follow : undefined };
655
+ const body = t === follow ? [{ k: 'break' }] : this.seq(t, caseEnv, stack);
656
+ const labels = [];
657
+ targets.forEach((tt, k) => {
658
+ if (tt !== t)
659
+ return;
660
+ if (k === 0)
661
+ labels.push(null);
662
+ else if (!values)
663
+ labels.push((0, ast_js_1.lit)(k - 1));
664
+ else if (values.has(k - 1))
665
+ labels.push(values.get(k - 1));
666
+ // Indices without a case value are the dispatcher's "no match" index (= default).
667
+ });
668
+ if (labels.length === 0)
669
+ labels.push(null);
670
+ // Default last among labels sharing a body, as compilers emit it.
671
+ labels.sort((x, y) => (x === null ? 1 : 0) - (y === null ? 1 : 0));
672
+ labels.forEach((l, k) => cases.push({ test: l, body: k === labels.length - 1 ? body : [] }));
673
+ }
674
+ // Drop "default: break;" when default jumps straight to the end.
675
+ const last = cases[cases.length - 1];
676
+ if (last && last.test === null && last.body.length === 1 && last.body[0].k === 'break' && targets[0] === follow)
677
+ cases.pop();
678
+ return { stmt: { k: 'switch', disc: disc ?? index, cases }, next: follow };
679
+ }
680
+ // -------------------------------------------------------------------------
681
+ // try / catch
682
+ // -------------------------------------------------------------------------
683
+ emitTry(g, env, stack) {
684
+ const savedScope = [...this.ctx.scope];
685
+ const region = new Set();
686
+ for (let i = g.fromBlock; i < g.toBlock; i++)
687
+ region.add(i);
688
+ const activeTries = new Set([...env.activeTries, g.key]);
689
+ const exits = [];
690
+ const bodyEnv = { ...env, region: intersect(region, env.region), exits, activeTries };
691
+ const body = this.seq(g.fromBlock, bodyEnv, stack, true);
692
+ const counts = new Map();
693
+ for (const x of exits)
694
+ counts.set(x, (counts.get(x) ?? 0) + 1);
695
+ let follow = -1;
696
+ for (const [x, n] of counts)
697
+ if (follow < 0 || n > counts.get(follow))
698
+ follow = x;
699
+ const catches = g.handlers.map(({ index, block }) => {
700
+ const e = this.cfg.exceptions[index];
701
+ const name = e.varName ? this.ctx.abc.multinameName(e.varName) : `e${index}`;
702
+ this.ctx.catchNames.set(index, name);
703
+ const type = e.excType ? this.ctx.typeName(e.excType) : '*';
704
+ const follows = new Set([...env.follows, ...(follow >= 0 ? [follow] : [])]);
705
+ const handlerEnv = { ...env, follows, activeTries };
706
+ // Handlers start with an empty scope stack (relative to init_scope_depth).
707
+ this.ctx.scope.length = 0;
708
+ const hbody = this.visited.has(block) ? [] : this.seq(block, handlerEnv, [{ k: 'name', name }]);
709
+ return { name, type, body: hbody };
710
+ });
711
+ this.ctx.scope.length = 0;
712
+ this.ctx.scope.push(...savedScope);
713
+ return { stmt: { k: 'try', body, catches }, next: follow };
714
+ }
715
+ }
716
+ exports.Structurer = Structurer;
717
+ function intersect(a, b) {
718
+ if (!b)
719
+ return a;
720
+ return new Set([...a].filter((x) => b.has(x)));
721
+ }
722
+ function commaOf(list) {
723
+ if (list.length === 0)
724
+ return undefined;
725
+ return list.reduceRight((acc, e) => (acc ? { k: 'binary', op: ',', a: e, b: acc } : e), undefined);
726
+ }
727
+ function makeIf(c, thenBody, elseBody) {
728
+ if (thenBody.length === 0 && elseBody.length > 0)
729
+ return { k: 'if', c: (0, ast_js_1.not)(c), then: elseBody };
730
+ return elseBody.length ? { k: 'if', c, then: thenBody, else: elseBody } : { k: 'if', c, then: thenBody };
731
+ }
732
+ /**
733
+ * Compilers lower `switch(x)` to a chain of `x === v ? index : ...` feeding
734
+ * lookupswitch. Recovers the discriminant and index → case value mapping.
735
+ */
736
+ function recoverSwitch(index) {
737
+ const values = new Map();
738
+ let disc;
739
+ let e = index;
740
+ for (let guard = 0; guard < 10000 && e.k === 'cond'; guard++) {
741
+ let c = e.c;
742
+ let hit = e.t;
743
+ let miss = e.f;
744
+ if (c.k === 'binary' && c.op === '!==') {
745
+ c = { ...c, op: '===' };
746
+ [hit, miss] = [miss, hit];
747
+ }
748
+ if (c.k !== 'binary' || c.op !== '===' || hit.k !== 'lit' || typeof hit.v !== 'number')
749
+ return {};
750
+ const [x, v] = isRegLike(c.b) ? [c.b, c.a] : [c.a, c.b];
751
+ if (disc && JSON.stringify(disc) !== JSON.stringify(x))
752
+ return {};
753
+ disc = x;
754
+ values.set(hit.v, v);
755
+ e = miss;
756
+ }
757
+ if (!disc)
758
+ return {};
759
+ return { disc, values };
760
+ }
761
+ function isRegLike(e) {
762
+ const x = e.k === 'coerce' ? e.e : e;
763
+ return x.k === 'local';
764
+ }