optipack 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/BENCHMARKING.md +127 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -2
  4. package/bin/optipack.js +12 -0
  5. package/example/manifest.json +27 -0
  6. package/example/scripts/drops.js +24 -0
  7. package/example/scripts/main.js +67 -0
  8. package/example/scripts/util.js +30 -0
  9. package/package.json +34 -4
  10. package/src/bench.js +231 -0
  11. package/src/cli.js +245 -0
  12. package/src/config.js +141 -0
  13. package/src/diff.js +108 -0
  14. package/src/harness/driver.js +216 -0
  15. package/src/harness/loader-hooks.mjs +63 -0
  16. package/src/harness/mock/@minecraft/common.mjs +17 -0
  17. package/src/harness/mock/@minecraft/debug-utilities.mjs +10 -0
  18. package/src/harness/mock/@minecraft/diagnostics.mjs +14 -0
  19. package/src/harness/mock/@minecraft/server-admin.mjs +14 -0
  20. package/src/harness/mock/@minecraft/server-editor.mjs +39 -0
  21. package/src/harness/mock/@minecraft/server-extras.mjs +538 -0
  22. package/src/harness/mock/@minecraft/server-gametest.mjs +22 -0
  23. package/src/harness/mock/@minecraft/server-graphics.mjs +21 -0
  24. package/src/harness/mock/@minecraft/server-net.mjs +15 -0
  25. package/src/harness/mock/@minecraft/server-ui.mjs +125 -0
  26. package/src/harness/mock/@minecraft/server.mjs +394 -0
  27. package/src/harness/mock/permissive.mjs +71 -0
  28. package/src/hotpath.js +158 -0
  29. package/src/lint.js +150 -0
  30. package/src/pack-walker.js +50 -0
  31. package/src/pass-utils.js +419 -0
  32. package/src/passes/b1-cache-getter-chains.js +406 -0
  33. package/src/passes/b2-closure-lift.js +316 -0
  34. package/src/passes/b3-string-build.js +167 -0
  35. package/src/passes/b4-dynprop-hoist.js +177 -0
  36. package/src/passes/b5-null-chain-flatten.js +137 -0
  37. package/src/passes/index.js +43 -0
  38. package/src/passes/r1-cross-tick-cache.js +192 -0
  39. package/src/passes/r2-json-fold.js +201 -0
  40. package/src/passes/r3-try-catch-strip.js +61 -0
  41. package/src/passes/r4-getter-cache-cross-barrier.js +29 -0
  42. package/src/passes/s1-const-fold.js +130 -0
  43. package/src/passes/s2-dead-code.js +183 -0
  44. package/src/passes/s3-bool-simplify.js +82 -0
  45. package/src/passes/s4-for-of-to-indexed.js +140 -0
  46. package/src/passes/s5-array-methods-to-loops.js +357 -0
  47. package/src/passes/s6-destructure-to-direct.js +186 -0
  48. package/src/passes/s7-hoist-invariant-members.js +327 -0
  49. package/src/pipeline.js +186 -0
  50. package/src/pragma.js +115 -0
  51. package/src/report.js +98 -0
  52. package/test/array-methods-to-loops/expected.js +58 -0
  53. package/test/array-methods-to-loops/expects.json +6 -0
  54. package/test/array-methods-to-loops/input.js +23 -0
  55. package/test/array-methods-to-loops/scenario.json +22 -0
  56. package/test/array-methods-to-loops-bail/expected.js +10 -0
  57. package/test/array-methods-to-loops-bail/input.js +24 -0
  58. package/test/bool-simplify/expected.js +14 -0
  59. package/test/bool-simplify/expects.json +11 -0
  60. package/test/bool-simplify/input.js +18 -0
  61. package/test/cache-getter-chains/expected.js +20 -0
  62. package/test/cache-getter-chains/expects.json +11 -0
  63. package/test/cache-getter-chains/input.js +20 -0
  64. package/test/cache-getter-chains/scenario.json +35 -0
  65. package/test/closure-lift/expected.js +58 -0
  66. package/test/closure-lift/expects.json +1 -0
  67. package/test/closure-lift/input.js +50 -0
  68. package/test/closure-lift/scenario.json +9 -0
  69. package/test/const-fold/expected.js +12 -0
  70. package/test/const-fold/expects.json +1 -0
  71. package/test/const-fold/input.js +12 -0
  72. package/test/const-fold-bail/expected.js +7 -0
  73. package/test/const-fold-bail/expects.json +3 -0
  74. package/test/const-fold-bail/input.js +7 -0
  75. package/test/cross-tick-cache/config.json +1 -0
  76. package/test/cross-tick-cache/expected.js +38 -0
  77. package/test/cross-tick-cache/expects.json +6 -0
  78. package/test/cross-tick-cache/input.js +13 -0
  79. package/test/cross-tick-cache/scenario.json +33 -0
  80. package/test/dead-code/expected.js +14 -0
  81. package/test/dead-code/expects.json +5 -0
  82. package/test/dead-code/input.js +23 -0
  83. package/test/destructure-to-direct/expected.js +37 -0
  84. package/test/destructure-to-direct/expects.json +11 -0
  85. package/test/destructure-to-direct/input.js +28 -0
  86. package/test/destructure-to-direct/scenario.json +22 -0
  87. package/test/destructure-to-direct-bail/expected.js +36 -0
  88. package/test/destructure-to-direct-bail/expects.json +11 -0
  89. package/test/destructure-to-direct-bail/input.js +25 -0
  90. package/test/differential.test.mjs +267 -0
  91. package/test/dynprop-hoist/expected.js +24 -0
  92. package/test/dynprop-hoist/expects.json +11 -0
  93. package/test/dynprop-hoist/input.js +25 -0
  94. package/test/dynprop-hoist/scenario.json +24 -0
  95. package/test/example-report-snapshot.json +107 -0
  96. package/test/example.test.mjs +115 -0
  97. package/test/fetch-corpus.mjs +92 -0
  98. package/test/for-of-to-indexed/expected.js +59 -0
  99. package/test/for-of-to-indexed/expects.json +11 -0
  100. package/test/for-of-to-indexed/input.js +43 -0
  101. package/test/for-of-to-indexed-bail/expected.js +25 -0
  102. package/test/for-of-to-indexed-bail/expects.json +11 -0
  103. package/test/for-of-to-indexed-bail/input.js +29 -0
  104. package/test/gen-expected.mjs +33 -0
  105. package/test/getter-cache-cross-barrier/config.json +1 -0
  106. package/test/getter-cache-cross-barrier/expected.js +10 -0
  107. package/test/getter-cache-cross-barrier/expects.json +6 -0
  108. package/test/getter-cache-cross-barrier/input.js +11 -0
  109. package/test/getter-cache-cross-barrier/scenario.json +28 -0
  110. package/test/harness/fixture.js +117 -0
  111. package/test/hoist-invariant-members/expected.js +37 -0
  112. package/test/hoist-invariant-members/expects.json +11 -0
  113. package/test/hoist-invariant-members/input.js +34 -0
  114. package/test/hoist-invariant-members/scenario.json +35 -0
  115. package/test/hoist-invariant-members-bail/expected.js +19 -0
  116. package/test/hoist-invariant-members-bail/expects.json +5 -0
  117. package/test/hoist-invariant-members-bail/input.js +22 -0
  118. package/test/infra.test.mjs +270 -0
  119. package/test/json-fold/config.json +1 -0
  120. package/test/json-fold/expected.js +14 -0
  121. package/test/json-fold/expects.json +12 -0
  122. package/test/json-fold/input.js +17 -0
  123. package/test/json-fold-bail/config.json +1 -0
  124. package/test/json-fold-bail/expected.js +10 -0
  125. package/test/json-fold-bail/expects.json +5 -0
  126. package/test/json-fold-bail/input.js +14 -0
  127. package/test/null-chain-flatten/expected.js +22 -0
  128. package/test/null-chain-flatten/expects.json +5 -0
  129. package/test/null-chain-flatten/input.js +22 -0
  130. package/test/null-chain-flatten/scenario.json +24 -0
  131. package/test/server-ui-forms/expected.js +15 -0
  132. package/test/server-ui-forms/expects.json +1 -0
  133. package/test/server-ui-forms/input.js +25 -0
  134. package/test/server-ui-forms/scenario.json +1 -0
  135. package/test/string-build/expected.js +29 -0
  136. package/test/string-build/expects.json +5 -0
  137. package/test/string-build/input.js +29 -0
  138. package/test/string-build/scenario.json +27 -0
  139. package/test/string-build-bail/expected.js +10 -0
  140. package/test/string-build-bail/input.js +21 -0
  141. package/test/transforms.test.mjs +43 -0
  142. package/test/try-catch-strip/config.json +1 -0
  143. package/test/try-catch-strip/expected.js +57 -0
  144. package/test/try-catch-strip/expects.json +1 -0
  145. package/test/try-catch-strip/input.js +34 -0
  146. package/test/try-catch-strip/scenario.json +28 -0
  147. package/test/update-example-snapshot.mjs +19 -0
  148. package/test/update-mock-stubs.mjs +91 -0
@@ -0,0 +1,406 @@
1
+ /**
2
+ * B1 cache-getter-chains — balanced level.
3
+ *
4
+ * Within a hot function, repeated identical member chains rooted at a host
5
+ * object are replaced by one hoisted `const`, removing repeated host-boundary
6
+ * crossings and repeated allocations (`p.location.x` twice = 2 allocations +
7
+ * 2 crossings).
8
+ *
9
+ * Barrier model (semantics-preserving):
10
+ * - Statement runs with NO calls/assignments/await/yield anywhere ("spans")
11
+ * group chains freely across their top-level statements.
12
+ * - A statement containing calls is processed with an evaluation-order
13
+ * "corridor" rule: uses of a chain may group only when no call executes
14
+ * between them (a call executes before a use when it does not contain the
15
+ * use and starts earlier; argument/object sub-expressions evaluate before
16
+ * the call itself). The hoisted const is inserted before the statement,
17
+ * which is safe exactly when no call executes before the first use.
18
+ * - await/yield always end a region. Loops and for-of/for-in are barriers.
19
+ *
20
+ * Host-object heuristic: root is a parameter of the hot function, `this`, a
21
+ * previously hoisted synthetic const, or the chain contains a marker property
22
+ * (`world`, `location`, `getComponent`, `getEntities`, `getAllPlayers`,
23
+ * `dimension`, ...).
24
+ *
25
+ * Chains overlapping a property write are never hoisted; roots reassigned in
26
+ * scope are never hoisted. Both are recorded as bails, as are repeated chains
27
+ * that could not group because a call barrier sits between their uses.
28
+ */
29
+ import traverse from '@babel/traverse';
30
+ import * as t from '@babel/types';
31
+ import { walkSameFunction, walkWithParents, corridorSafe, ChainAnalyzer, HOST_MARKERS } from '../pass-utils.js';
32
+
33
+ const _traverse = traverse.default || traverse;
34
+
35
+ export const ID = 'cache-getter-chains';
36
+ export const CODE = 'B1';
37
+
38
+ function hasBarrierSubtree(node) {
39
+ let found = false;
40
+ walkSameFunction(node, (n) => {
41
+ if (found) return false;
42
+ if (t.isCallExpression(n) || t.isNewExpression(n) || t.isTaggedTemplateExpression(n) ||
43
+ t.isOptionalCallExpression(n) || t.isAwaitExpression(n) || t.isYieldExpression(n) ||
44
+ t.isAssignmentExpression(n) || t.isUpdateExpression(n) ||
45
+ t.isForOfStatement(n) || t.isForInStatement(n) ||
46
+ t.isForStatement(n) || t.isWhileStatement(n) || t.isDoWhileStatement(n)) {
47
+ found = true;
48
+ return false;
49
+ }
50
+ if (t.isUnaryExpression(n) && n.operator === 'delete') { found = true; return false; }
51
+ return true;
52
+ });
53
+ return found;
54
+ }
55
+
56
+ /** R4 mode: only await/yield are uncrossable. */
57
+ function hasSuspensionSubtree(node) {
58
+ let found = false;
59
+ walkSameFunction(node, (n) => {
60
+ if (found) return false;
61
+ if (t.isAwaitExpression(n) || t.isYieldExpression(n)) { found = true; return false; }
62
+ return true;
63
+ });
64
+ return found;
65
+ }
66
+
67
+ function describeChain(memberNode) {
68
+ const links = [];
69
+ let cur = memberNode;
70
+ let root = null;
71
+ let isThis = false;
72
+ while (true) {
73
+ if (!t.isMemberExpression(cur)) break;
74
+ if (cur.computed || !t.isIdentifier(cur.property)) return null;
75
+ links.unshift(cur.property.name);
76
+ const obj = cur.object;
77
+ if (t.isMemberExpression(obj)) { cur = obj; continue; }
78
+ if (t.isIdentifier(obj)) root = obj;
79
+ else if (t.isThisExpression(obj)) { isThis = true; }
80
+ else return null;
81
+ break;
82
+ }
83
+ if (!root && !isThis) return null;
84
+ return { root, isThis, links };
85
+ }
86
+
87
+ /** Replace oldNode with newNode anywhere in the tree rooted at rootNode. */
88
+ function replaceIn(rootNode, oldNode, newNode) {
89
+ const stack = [rootNode];
90
+ while (stack.length > 0) {
91
+ const cur = stack.pop();
92
+ if (!cur || typeof cur !== 'object') continue;
93
+ for (const key of Object.keys(cur)) {
94
+ if (key === 'loc' || key === 'leadingComments' || key === 'trailingComments' || key === 'innerComments' || key === 'extra') continue;
95
+ const v = cur[key];
96
+ if (v === oldNode) { cur[key] = newNode; return true; }
97
+ if (Array.isArray(v)) {
98
+ const idx = v.indexOf(oldNode);
99
+ if (idx >= 0) { v[idx] = newNode; return true; }
100
+ for (const item of v) if (item && typeof item === 'object' && typeof item.type === 'string') stack.push(item);
101
+ } else if (v && typeof v === 'object' && typeof v.type === 'string') {
102
+ stack.push(v);
103
+ }
104
+ }
105
+ }
106
+ return false;
107
+ }
108
+
109
+ const pos = (n) => (n.start !== undefined ? n.start : (n.loc ? n.loc.start.index ?? 0 : 0));
110
+ void pos;
111
+
112
+ export default function cacheGetterChains(programPath, ctx, opts = {}) {
113
+ const crossBarriers = !!opts.crossBarriers;
114
+ const passId = opts.id || ID;
115
+ const passCode = opts.code || CODE;
116
+ const onApply = opts.onApply || null;
117
+ const analyzer = new ChainAnalyzer();
118
+
119
+ // Collect unconditionally-evaluated chain uses from a statement subtree.
120
+ const collectFromNode = (n, stmt, uses) => {
121
+ walkWithParents(n, (m, parent) => {
122
+ if (t.isFunctionExpression(m) || t.isArrowFunctionExpression(m)) return false;
123
+ if (!t.isMemberExpression(m) || m.computed) return true;
124
+ // Never hoist a chain used as a call callee — extracting the method
125
+ // would detach `this`.
126
+ if (parent && t.isCallExpression(parent) && parent.callee === m) return true;
127
+ const chain = describeChain(m);
128
+ if (chain && chain.links.length >= 1) {
129
+ uses.push({ node: m, stmt, ...chain });
130
+ }
131
+ return true;
132
+ });
133
+ };
134
+ const collectFromStmt = (stmt, uses) => {
135
+ if (t.isBlockStatement(stmt)) { for (const s of stmt.body) collectFromStmt(s, uses); return; }
136
+ if (t.isLabeledStatement(stmt)) { collectFromStmt(stmt.body, uses); return; }
137
+ if (t.isIfStatement(stmt)) { collectFromNode(stmt.test, stmt, uses); return; }
138
+ if (t.isSwitchStatement(stmt)) { collectFromNode(stmt.discriminant, stmt, uses); return; }
139
+ if (t.isWhileStatement(stmt) || t.isDoWhileStatement(stmt) || t.isForStatement(stmt) ||
140
+ t.isForOfStatement(stmt) || t.isForInStatement(stmt) || t.isTryStatement(stmt)) return;
141
+ // Conditional sub-expressions are not unconditionally evaluated.
142
+ const walkNoCond = (n, cb) => {
143
+ walkWithParents(n, (m, parent) => {
144
+ if (t.isConditionalExpression(m)) {
145
+ walkNoCond(m.test, cb);
146
+ return false;
147
+ }
148
+ if (t.isLogicalExpression(m)) {
149
+ walkNoCond(m.left, cb);
150
+ return false;
151
+ }
152
+ return cb(m, parent);
153
+ });
154
+ };
155
+ walkNoCond(stmt, (m, parent) => {
156
+ if (!t.isMemberExpression(m) || m.computed) return true;
157
+ // Never cache a chain used as a call callee — extracting the method
158
+ // would detach `this`.
159
+ if (parent && t.isCallExpression(parent) && parent.callee === m) return true;
160
+ const chain = describeChain(m);
161
+ if (chain && chain.links.length >= 1) uses.push({ node: m, stmt, ...chain });
162
+ return true;
163
+ });
164
+ };
165
+
166
+ /** All call-family nodes in a statement (excluding nested functions). */
167
+ const contains = (outer, inner) => outer.start <= inner.start && inner.end <= outer.end;
168
+
169
+ const processStatementList = (stmts, ownerPath, fnNode, isCaseList, depth) => {
170
+ if (depth > 6 || !stmts || stmts.length === 0) return;
171
+
172
+ // Partition into units: maximal barrier-free spans and standalone barrier
173
+ // statements.
174
+ const units = [];
175
+ let span = [];
176
+ for (const stmt of stmts) {
177
+ const barrier = crossBarriers ? hasSuspensionSubtree(stmt) : hasBarrierSubtree(stmt);
178
+ if (barrier) {
179
+ if (span.length > 0) { units.push({ kind: 'span', stmts: span }); span = []; }
180
+ units.push({ kind: 'single', stmts: [stmt] });
181
+ } else {
182
+ span.push(stmt);
183
+ }
184
+ }
185
+ if (span.length > 0) units.push({ kind: 'span', stmts: span });
186
+
187
+ // Global chain census for bail reporting.
188
+ const allUsesByStmt = new Map();
189
+ for (const stmt of stmts) {
190
+ const uses = [];
191
+ collectFromStmt(stmt, uses);
192
+ allUsesByStmt.set(stmt, uses);
193
+ }
194
+
195
+ const bindingId = (name) => {
196
+ const binding = ownerPath.scope.getBinding(name);
197
+ return binding ? analyzer.bindingId(binding) : 'global:' + name;
198
+ };
199
+ const isParamOfFn = (name) => {
200
+ const params = fnNode.params || [];
201
+ return params.some((p) => t.isIdentifier(p) && p.name === name);
202
+ };
203
+ const neverReassigned = (name) => {
204
+ const binding = ownerPath.scope.getBinding(name);
205
+ if (!binding) return true;
206
+ return binding.constantViolations.length === 0;
207
+ };
208
+ const writeChainsFor = (stmts2) => {
209
+ const out = [];
210
+ for (const stmt of stmts2) {
211
+ walkSameFunction(stmt, (n) => {
212
+ let target = null;
213
+ if (t.isAssignmentExpression(n) && t.isMemberExpression(n.left)) target = n.left;
214
+ else if (t.isUpdateExpression(n) && t.isMemberExpression(n.argument)) target = n.argument;
215
+ else if (t.isUnaryExpression(n) && n.operator === 'delete' && t.isMemberExpression(n.argument)) target = n.argument;
216
+ if (target) {
217
+ const c = describeChain(target);
218
+ if (c) out.push(c);
219
+ }
220
+ return true;
221
+ });
222
+ }
223
+ return out;
224
+ };
225
+
226
+ const groupKeyOf = (u) => (u.isThis ? '@this' : bindingId(u.root.name)) + '#' + u.links.join('.');
227
+ const qualify = (g, writes) => {
228
+ const linksStr = g.links.join('.');
229
+ if (!g.isThis) {
230
+ const overlap = writes.some((w) => {
231
+ if (w.isThis !== g.isThis) return false;
232
+ if (!w.isThis && bindingId(w.root.name) !== bindingId(g.root.name)) return false;
233
+ const ws = w.links.join('.');
234
+ return ws === linksStr || ws.startsWith(linksStr + '.') || linksStr.startsWith(ws + '.');
235
+ });
236
+ if (overlap) return `chain .${linksStr} overlaps a property write`;
237
+ if (g.root && !neverReassigned(g.root.name)) return `root "${g.root.name}" is reassigned in scope`;
238
+ }
239
+ const synthetic = !g.isThis && g.root && analyzer.syntheticRoots.has(g.root.name);
240
+ const hasMarker = g.links.some((l) => HOST_MARKERS.has(l));
241
+ const isParam = !g.isThis && g.root && isParamOfFn(g.root.name);
242
+ if (!g.isThis && !synthetic && !hasMarker && !isParam) {
243
+ return `chain .${linksStr} has no recognizable host root`;
244
+ }
245
+ return null;
246
+ };
247
+
248
+ const hoistedKeys = new Set();
249
+ const groupUseTotals = new Map(); // key -> total uses (for bail reporting)
250
+ for (const [, uses] of allUsesByStmt) {
251
+ for (const u of uses) {
252
+ const k = groupKeyOf(u);
253
+ groupUseTotals.set(k, (groupUseTotals.get(k) || 0) + 1);
254
+ }
255
+ }
256
+
257
+ const tryHoist = (uses, writes, insertBeforeStmt, unitStmts) => {
258
+ // Group by chain key.
259
+ const groups = new Map();
260
+ for (const u of uses) {
261
+ const k = groupKeyOf(u);
262
+ if (!groups.has(k)) groups.set(k, { root: u.root, isThis: u.isThis, links: u.links, uses: [] });
263
+ groups.get(k).uses.push(u);
264
+ }
265
+ const entries = [...groups.entries()].sort((a, b) => {
266
+ const d = b[1].links.length - a[1].links.length;
267
+ return d || (a[0] < b[0] ? -1 : 1);
268
+ });
269
+ const consumed = globalConsumed;
270
+ const decls = [];
271
+ const allUses = [...uses];
272
+ for (const [, g] of entries) {
273
+ const live = g.uses.filter((u) => !consumed.has(u.node));
274
+ if (live.length < 2) continue;
275
+ const problem = qualify(g, writes);
276
+ if (problem) {
277
+ if (!hoistedKeys.has(groupKeyOf(g))) {
278
+ const line = live[0].node.loc ? live[0].node.loc.start.line : 0;
279
+ ctx.bail(line, passCode, problem);
280
+ }
281
+ continue;
282
+ }
283
+ const name = ownerPath.scope.generateUid('opC');
284
+ let expr = g.isThis ? t.thisExpression() : t.identifier(g.root.name);
285
+ for (const link of g.links) expr = t.memberExpression(expr, t.identifier(link));
286
+ decls.push(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(name), expr)]));
287
+ if (!g.isThis) analyzer.markSynthetic(name);
288
+ hoistedKeys.add(groupKeyOf(g));
289
+ ctx.applied(passId);
290
+ if (onApply) {
291
+ const line = live[0].node.loc ? live[0].node.loc.start.line : 0;
292
+ onApply(line, g.links.join('.'));
293
+ }
294
+ for (const u of live) {
295
+ consumed.add(u.node);
296
+ replaceIn(u.stmt, u.node, t.identifier(name));
297
+ for (const u2 of allUses) {
298
+ if (consumed.has(u2.node)) continue;
299
+ if (u2.isThis !== g.isThis) continue;
300
+ if (!u2.isThis && bindingId(u2.root.name) !== bindingId(g.root.name)) continue;
301
+ if (u2.links.length < g.links.length &&
302
+ g.links.slice(0, u2.links.length).join('.') === u2.links.join('.')) {
303
+ consumed.add(u2.node);
304
+ }
305
+ }
306
+ }
307
+ }
308
+ if (decls.length > 0 && insertBeforeStmt) {
309
+ if (isCaseList) {
310
+ const idx = ownerPath.node.consequent.indexOf(insertBeforeStmt);
311
+ ownerPath.node.consequent.splice(idx < 0 ? 0 : idx, 0, ...decls);
312
+ } else {
313
+ let stmtPath = null;
314
+ ownerPath.traverse({
315
+ Statement(p) { if (p.node === insertBeforeStmt && !stmtPath) { stmtPath = p; p.stop(); } },
316
+ });
317
+ if (stmtPath) stmtPath.insertBefore(decls);
318
+ else {
319
+ const arr = ownerPath.node.body;
320
+ const idx = arr.indexOf(insertBeforeStmt);
321
+ if (idx >= 0) arr.splice(idx, 0, ...decls);
322
+ }
323
+ }
324
+ }
325
+ return decls.length > 0;
326
+ };
327
+
328
+ const globalConsumed = new Set();
329
+ let anyApplied = false;
330
+ for (const unit of units) {
331
+ if (unit.kind === 'span') {
332
+ const uses = [];
333
+ for (const stmt of unit.stmts) collectFromStmt(stmt, uses);
334
+ if (uses.length === 0) continue;
335
+ // First use's statement is the insertion point.
336
+ uses.sort((a, b) => pos(a.node) - pos(b.node));
337
+ const firstStmt = uses[0].stmt;
338
+ anyApplied = tryHoist(uses, writeChainsFor(unit.stmts), firstStmt, unit.stmts) || anyApplied;
339
+ } else {
340
+ const stmt = unit.stmts[0];
341
+ const uses = [];
342
+ collectFromStmt(stmt, uses);
343
+ if (uses.length === 0) continue;
344
+ uses.sort((a, b) => pos(a.node) - pos(b.node));
345
+ // Group uses by chain, then corridor-check each group independently.
346
+ const byChain = new Map();
347
+ for (const u of uses) {
348
+ const k = groupKeyOf(u);
349
+ if (!byChain.has(k)) byChain.set(k, []);
350
+ byChain.get(k).push(u);
351
+ }
352
+ const eligible = [];
353
+ for (const [, list] of byChain) {
354
+ if (corridorSafe(stmt, list.map((u) => u.node))) eligible.push(...list);
355
+ }
356
+ if (eligible.length === 0) continue;
357
+ anyApplied = tryHoist(eligible, writeChainsFor([stmt]), stmt, [stmt]) || anyApplied;
358
+ }
359
+ }
360
+
361
+ // Bail reporting: repeated chains (>= 2 live uses anywhere in this list)
362
+ // that never got hoisted. Prefix groups fully consumed by longer hoists
363
+ // are not reported.
364
+ for (const [key, total] of groupUseTotals) {
365
+ if (total < 2 || hoistedKeys.has(key)) continue;
366
+ let live = 0;
367
+ for (const [, uses] of allUsesByStmt) {
368
+ live += uses.filter((u) => groupKeyOf(u) === key && !globalConsumed.has(u.node)).length;
369
+ }
370
+ if (live < 2) continue;
371
+ // Find one representative use line.
372
+ for (const [, uses] of allUsesByStmt) {
373
+ const rep = uses.find((u) => groupKeyOf(u) === key && !globalConsumed.has(u.node) && u.node.loc);
374
+ if (rep) {
375
+ const linksStr = key.split('#')[1];
376
+ ctx.bail(rep.node.loc.start.line, passCode,
377
+ `chain .${linksStr} repeats but uses are split by call/assignment barriers`);
378
+ break;
379
+ }
380
+ }
381
+ }
382
+
383
+ // Recurse into nested blocks / switch cases.
384
+ ownerPath.traverse({
385
+ BlockStatement(bp) {
386
+ if (bp.node === ownerPath.node) return;
387
+ processStatementList(bp.node.body, bp, fnNode, false, depth + 1);
388
+ },
389
+ SwitchCase(cp) {
390
+ processStatementList(cp.node.consequent, cp, fnNode, true, depth + 1);
391
+ },
392
+ });
393
+ };
394
+
395
+ programPath.traverse({
396
+ 'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression'(path) {
397
+ const fn = path.node;
398
+ if (!ctx.isHot(fn)) return;
399
+ if (ctx.isDisabled(path, passId, passCode)) return;
400
+ if (t.isBlockStatement(fn.body)) {
401
+ const bodyPath = path.get('body');
402
+ processStatementList(fn.body.body, bodyPath, fn, false, 0);
403
+ }
404
+ },
405
+ });
406
+ }