optipack 0.0.0-stage → 1.0.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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -2
  3. package/bin/optipack.js +12 -0
  4. package/example/manifest.json +27 -0
  5. package/example/scripts/drops.js +24 -0
  6. package/example/scripts/main.js +67 -0
  7. package/example/scripts/util.js +30 -0
  8. package/package.json +34 -6
  9. package/src/cli.js +180 -0
  10. package/src/config.js +141 -0
  11. package/src/diff.js +108 -0
  12. package/src/hotpath.js +158 -0
  13. package/src/lint.js +150 -0
  14. package/src/pack-walker.js +50 -0
  15. package/src/pass-utils.js +419 -0
  16. package/src/passes/b1-cache-getter-chains.js +406 -0
  17. package/src/passes/b2-closure-lift.js +316 -0
  18. package/src/passes/b3-string-build.js +167 -0
  19. package/src/passes/b4-dynprop-hoist.js +177 -0
  20. package/src/passes/b5-null-chain-flatten.js +137 -0
  21. package/src/passes/index.js +43 -0
  22. package/src/passes/r1-cross-tick-cache.js +192 -0
  23. package/src/passes/r2-json-fold.js +201 -0
  24. package/src/passes/r3-try-catch-strip.js +61 -0
  25. package/src/passes/r4-getter-cache-cross-barrier.js +29 -0
  26. package/src/passes/s1-const-fold.js +130 -0
  27. package/src/passes/s2-dead-code.js +183 -0
  28. package/src/passes/s3-bool-simplify.js +82 -0
  29. package/src/passes/s4-for-of-to-indexed.js +140 -0
  30. package/src/passes/s5-array-methods-to-loops.js +357 -0
  31. package/src/passes/s6-destructure-to-direct.js +186 -0
  32. package/src/passes/s7-hoist-invariant-members.js +327 -0
  33. package/src/pipeline.js +186 -0
  34. package/src/pragma.js +115 -0
  35. package/src/report.js +98 -0
  36. package/test/array-methods-to-loops/expected.js +58 -0
  37. package/test/array-methods-to-loops/expects.json +6 -0
  38. package/test/array-methods-to-loops/input.js +23 -0
  39. package/test/array-methods-to-loops/scenario.json +22 -0
  40. package/test/array-methods-to-loops-bail/expected.js +10 -0
  41. package/test/array-methods-to-loops-bail/input.js +24 -0
  42. package/test/bool-simplify/expected.js +14 -0
  43. package/test/bool-simplify/expects.json +11 -0
  44. package/test/bool-simplify/input.js +18 -0
  45. package/test/cache-getter-chains/expected.js +20 -0
  46. package/test/cache-getter-chains/expects.json +11 -0
  47. package/test/cache-getter-chains/input.js +20 -0
  48. package/test/cache-getter-chains/scenario.json +35 -0
  49. package/test/closure-lift/expected.js +58 -0
  50. package/test/closure-lift/expects.json +1 -0
  51. package/test/closure-lift/input.js +50 -0
  52. package/test/closure-lift/scenario.json +9 -0
  53. package/test/const-fold/expected.js +12 -0
  54. package/test/const-fold/expects.json +1 -0
  55. package/test/const-fold/input.js +12 -0
  56. package/test/const-fold-bail/expected.js +7 -0
  57. package/test/const-fold-bail/expects.json +3 -0
  58. package/test/const-fold-bail/input.js +7 -0
  59. package/test/cross-tick-cache/config.json +1 -0
  60. package/test/cross-tick-cache/expected.js +38 -0
  61. package/test/cross-tick-cache/expects.json +6 -0
  62. package/test/cross-tick-cache/input.js +13 -0
  63. package/test/cross-tick-cache/scenario.json +33 -0
  64. package/test/dead-code/expected.js +14 -0
  65. package/test/dead-code/expects.json +5 -0
  66. package/test/dead-code/input.js +23 -0
  67. package/test/destructure-to-direct/expected.js +37 -0
  68. package/test/destructure-to-direct/expects.json +11 -0
  69. package/test/destructure-to-direct/input.js +28 -0
  70. package/test/destructure-to-direct/scenario.json +22 -0
  71. package/test/destructure-to-direct-bail/expected.js +36 -0
  72. package/test/destructure-to-direct-bail/expects.json +11 -0
  73. package/test/destructure-to-direct-bail/input.js +25 -0
  74. package/test/differential.test.mjs +267 -0
  75. package/test/dynprop-hoist/expected.js +24 -0
  76. package/test/dynprop-hoist/expects.json +11 -0
  77. package/test/dynprop-hoist/input.js +25 -0
  78. package/test/dynprop-hoist/scenario.json +24 -0
  79. package/test/example-report-snapshot.json +107 -0
  80. package/test/example.test.mjs +115 -0
  81. package/test/for-of-to-indexed/expected.js +59 -0
  82. package/test/for-of-to-indexed/expects.json +11 -0
  83. package/test/for-of-to-indexed/input.js +43 -0
  84. package/test/for-of-to-indexed-bail/expected.js +25 -0
  85. package/test/for-of-to-indexed-bail/expects.json +11 -0
  86. package/test/for-of-to-indexed-bail/input.js +29 -0
  87. package/test/gen-expected.mjs +33 -0
  88. package/test/getter-cache-cross-barrier/config.json +1 -0
  89. package/test/getter-cache-cross-barrier/expected.js +10 -0
  90. package/test/getter-cache-cross-barrier/expects.json +6 -0
  91. package/test/getter-cache-cross-barrier/input.js +11 -0
  92. package/test/getter-cache-cross-barrier/scenario.json +28 -0
  93. package/test/harness/fixture.js +117 -0
  94. package/test/harness/setup.js +198 -0
  95. package/test/hoist-invariant-members/expected.js +37 -0
  96. package/test/hoist-invariant-members/expects.json +11 -0
  97. package/test/hoist-invariant-members/input.js +34 -0
  98. package/test/hoist-invariant-members/scenario.json +35 -0
  99. package/test/hoist-invariant-members-bail/expected.js +19 -0
  100. package/test/hoist-invariant-members-bail/expects.json +5 -0
  101. package/test/hoist-invariant-members-bail/input.js +22 -0
  102. package/test/infra.test.mjs +260 -0
  103. package/test/json-fold/config.json +1 -0
  104. package/test/json-fold/expected.js +14 -0
  105. package/test/json-fold/expects.json +12 -0
  106. package/test/json-fold/input.js +17 -0
  107. package/test/json-fold-bail/config.json +1 -0
  108. package/test/json-fold-bail/expected.js +10 -0
  109. package/test/json-fold-bail/expects.json +5 -0
  110. package/test/json-fold-bail/input.js +14 -0
  111. package/test/loader-hooks.mjs +38 -0
  112. package/test/mock/@minecraft/server.mjs +274 -0
  113. package/test/null-chain-flatten/expected.js +22 -0
  114. package/test/null-chain-flatten/expects.json +5 -0
  115. package/test/null-chain-flatten/input.js +22 -0
  116. package/test/null-chain-flatten/scenario.json +24 -0
  117. package/test/string-build/expected.js +29 -0
  118. package/test/string-build/expects.json +5 -0
  119. package/test/string-build/input.js +29 -0
  120. package/test/string-build/scenario.json +27 -0
  121. package/test/string-build-bail/expected.js +10 -0
  122. package/test/string-build-bail/input.js +21 -0
  123. package/test/transforms.test.mjs +43 -0
  124. package/test/try-catch-strip/config.json +1 -0
  125. package/test/try-catch-strip/expected.js +57 -0
  126. package/test/try-catch-strip/expects.json +1 -0
  127. package/test/try-catch-strip/input.js +34 -0
  128. package/test/try-catch-strip/scenario.json +28 -0
  129. package/test/update-example-snapshot.mjs +19 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * S5 array-methods-to-loops — hot-path only.
3
+ *
4
+ * In hot functions, inline-arrow `.forEach`, `.map`, `.filter`, `.some`,
5
+ * `.every`, `.find` calls become plain indexed loops. Every method-call
6
+ * allocation (iterator + callback invocation machinery) disappears.
7
+ *
8
+ * Bail conditions:
9
+ * - callback is not an inline arrow/function expression
10
+ * - callback uses `this` or `arguments`, is async, is a generator, or
11
+ * contains await/yield
12
+ * - callback parameters are not plain identifiers
13
+ * - receiver is optionally chained (either via `?.` in the receiver or an
14
+ * optional call `arr?.map(...)`)
15
+ * - the call is not unconditionally evaluated within its statement
16
+ * (conditional expressions, logical right-hand sides, spread, concise
17
+ * arrow bodies, loop test/update positions)
18
+ * - receiver provenance may include holes (sparse arrays change
19
+ * forEach/map/some/every/find/filter semantics)
20
+ *
21
+ * Note: the receiver is always evaluated exactly once (hoisted to a `const`
22
+ * before the loop), so a call-containing receiver never duplicates side
23
+ * effects — this supersedes the "receiver referenced more than once" bail.
24
+ */
25
+ import traverse from '@babel/traverse';
26
+ import * as t from '@babel/types';
27
+ import { walk, provablyArray } from '../pass-utils.js';
28
+
29
+ const _traverse = traverse.default || traverse;
30
+
31
+ export const ID = 'array-methods-to-loops';
32
+ export const CODE = 'S5';
33
+
34
+ const METHODS = new Set(['forEach', 'map', 'filter', 'some', 'every', 'find']);
35
+
36
+ // Nodes that make the call conditionally evaluated relative to its statement.
37
+ function unconditionalWalk(callPath, stmtPath) {
38
+ let p = callPath.parentPath;
39
+ while (p && p !== stmtPath) {
40
+ const n = p.node;
41
+ if (t.isConditionalExpression(n)) return false;
42
+ if (t.isLogicalExpression(n)) return false;
43
+ if (t.isOptionalCallExpression(n) || t.isOptionalMemberExpression(n)) return false;
44
+ if (t.isSpreadElement(n)) return false;
45
+ if (t.isFunctionExpression(n) || t.isArrowFunctionExpression(n)) return false; // concise body crossing
46
+ if (t.isSwitchStatement(n) || t.isSwitchCase(n)) return false;
47
+ p = p.parentPath;
48
+ }
49
+ if (!p) return false;
50
+ const sn = stmtPath.node;
51
+ if (t.isForStatement(sn) || t.isWhileStatement(sn) || t.isDoWhileStatement(sn)) {
52
+ // The call must live inside the loop body.
53
+ let q = callPath;
54
+ let inBody = false;
55
+ while (q && q.node !== sn) {
56
+ if (q.parentPath && q.parentPath.node === sn && q.parentPath.node.body === q.node) { inBody = true; break; }
57
+ if (q.parentPath && q.parentPath.node === sn && q.key === 'body') { inBody = true; break; }
58
+ q = q.parentPath;
59
+ }
60
+ if (!inBody) return false;
61
+ }
62
+ return true;
63
+ }
64
+
65
+ function callbackIssues(cb) {
66
+ const problems = [];
67
+ if (cb.async) problems.push('async callback');
68
+ if (cb.generator) problems.push('generator callback');
69
+ let sawThis = false;
70
+ let sawArguments = false;
71
+ let sawAwaitYield = false;
72
+ walk(cb.body, (n) => {
73
+ if (t.isThisExpression(n)) { sawThis = true; return false; }
74
+ if (t.isIdentifier(n) && n.name === 'arguments' && !t.isMemberExpression(n)) {
75
+ // crude: any bare `arguments` reference
76
+ sawArguments = true;
77
+ return false;
78
+ }
79
+ if (t.isAwaitExpression(n) || t.isYieldExpression(n)) { sawAwaitYield = true; return false; }
80
+ return true;
81
+ });
82
+ // `arguments` references only count when they resolve outside the callback
83
+ // params — a param named arguments is pathological; treat as a bail.
84
+ if (sawThis) problems.push('callback uses this');
85
+ if (sawArguments) problems.push('callback uses arguments');
86
+ if (sawAwaitYield) problems.push('callback contains await/yield');
87
+ for (const p of cb.params) {
88
+ if (!t.isIdentifier(p)) problems.push('callback parameter is not a plain identifier');
89
+ }
90
+ if (cb.params.length > 3) problems.push('callback has more than 3 parameters');
91
+ return problems;
92
+ }
93
+
94
+ export default function arrayMethodsToLoops(programPath, ctx) {
95
+ const decided = new WeakSet();
96
+ const bailed = new WeakSet();
97
+
98
+ const recordBailOnce = (node, line, reason) => {
99
+ if (bailed.has(node)) return;
100
+ bailed.add(node);
101
+ ctx.bail(line, CODE, reason);
102
+ };
103
+
104
+ programPath.traverse({
105
+ CallExpression(path) {
106
+ const node = path.node;
107
+ if (decided.has(node)) return;
108
+ decided.add(node);
109
+
110
+ const callee = node.callee;
111
+ if (!t.isMemberExpression(callee) || callee.computed) return;
112
+ const prop = callee.property;
113
+ if (!t.isIdentifier(prop) || !METHODS.has(prop.name)) return;
114
+ const line = node.loc ? node.loc.start.line : 0;
115
+
116
+ // Hot functions only — silently skip elsewhere.
117
+ const fnParent = path.getFunctionParent();
118
+ if (!fnParent || !ctx.isHot(fnParent.node)) return;
119
+ if (ctx.isDisabled(path, ID, CODE)) return;
120
+
121
+ const fail = (reason) => {
122
+ recordBailOnce(node, line, reason);
123
+ };
124
+
125
+ if (node.arguments.length !== 1) return fail('expected exactly one callback argument');
126
+ const cb = node.arguments[0];
127
+ if (!t.isArrowFunctionExpression(cb) && !t.isFunctionExpression(cb)) {
128
+ return fail('callback is not an inline function expression');
129
+ }
130
+ const issues = callbackIssues(cb);
131
+ if (issues.length > 0) return fail(issues[0]);
132
+
133
+ // Optional chaining anywhere in the receiver or callee → bail.
134
+ let optional = false;
135
+ walk(callee.object, (n) => {
136
+ if (t.isOptionalMemberExpression(n) || t.isOptionalCallExpression(n)) { optional = true; return false; }
137
+ return true;
138
+ });
139
+ if (optional) return fail('receiver is optionally chained');
140
+
141
+ // Sparse-array provenance → bail.
142
+ const prov = provablyArray(callee.object, path);
143
+ if (prov.array && prov.holey) return fail('receiver may be a sparse array');
144
+
145
+ // Nearest statement ancestor (never crossing a function boundary).
146
+ let stmtPath = path;
147
+ while (stmtPath && !stmtPath.isStatement()) stmtPath = stmtPath.parentPath;
148
+ if (!stmtPath) return fail('no statement ancestor in the same function');
149
+ if (!unconditionalWalk(path, stmtPath)) {
150
+ return fail('call is conditionally evaluated within its statement');
151
+ }
152
+
153
+ const method = prop.name;
154
+ const used = !(t.isExpressionStatement(stmtPath.node) && stmtPath.node.expression === node);
155
+ const needsValue = used && method !== 'forEach';
156
+
157
+ const scope = stmtPath.scope;
158
+ const arrName = scope.generateUid('opArr');
159
+ const idxName = scope.generateUid('opI');
160
+ const accName = scope.generateUid('opAcc');
161
+ const resName = scope.generateUid('opRes');
162
+
163
+ // ---- translate callback body ----
164
+ const p0 = cb.params[0] || null;
165
+ const p1 = cb.params[1] || null;
166
+ const p2 = cb.params[2] || null;
167
+ const elemName = p0 ? p0.name : scope.generateUid('opV');
168
+ const elemKind = p0 && p0.name && scopeHasViolations(scope, p0.name, cb) ? 'let' : 'const';
169
+
170
+ const body = t.cloneNode(cb.body, true);
171
+ const bodyStmts = t.isBlockStatement(body) ? body.body : [t.returnStatement(body)];
172
+
173
+ const translate = (stmts) => {
174
+ const out = [];
175
+ for (const s of stmts) {
176
+ if (t.isReturnStatement(s)) {
177
+ const arg = s.argument;
178
+ switch (method) {
179
+ case 'forEach':
180
+ if (arg) out.push(t.expressionStatement(arg));
181
+ out.push(t.continueStatement());
182
+ break;
183
+ case 'map':
184
+ if (!used) {
185
+ if (arg) out.push(t.expressionStatement(arg));
186
+ } else {
187
+ out.push(t.expressionStatement(
188
+ t.callExpression(t.memberExpression(t.identifier(accName), t.identifier('push')), [arg || t.identifier('undefined')])
189
+ ));
190
+ }
191
+ out.push(t.continueStatement());
192
+ break;
193
+ case 'filter':
194
+ if (arg && used) {
195
+ out.push(t.ifStatement(
196
+ arg,
197
+ t.expressionStatement(
198
+ t.callExpression(t.memberExpression(t.identifier(accName), t.identifier('push')), [t.identifier(elemName)])
199
+ )
200
+ ));
201
+ }
202
+ out.push(t.continueStatement());
203
+ break;
204
+ case 'some':
205
+ if (arg) {
206
+ out.push(t.ifStatement(
207
+ arg,
208
+ t.blockStatement([
209
+ t.expressionStatement(t.assignmentExpression('=', t.identifier(resName), t.booleanLiteral(true))),
210
+ t.breakStatement(),
211
+ ])
212
+ ));
213
+ }
214
+ out.push(t.continueStatement());
215
+ break;
216
+ case 'every':
217
+ if (arg) {
218
+ out.push(t.ifStatement(
219
+ t.unaryExpression('!', arg),
220
+ t.blockStatement([
221
+ t.expressionStatement(t.assignmentExpression('=', t.identifier(resName), t.booleanLiteral(false))),
222
+ t.breakStatement(),
223
+ ])
224
+ ));
225
+ }
226
+ out.push(t.continueStatement());
227
+ break;
228
+ case 'find':
229
+ if (arg) {
230
+ out.push(t.ifStatement(
231
+ arg,
232
+ t.blockStatement([
233
+ t.expressionStatement(t.assignmentExpression('=', t.identifier(resName), t.identifier(elemName))),
234
+ t.breakStatement(),
235
+ ])
236
+ ));
237
+ }
238
+ out.push(t.continueStatement());
239
+ break;
240
+ default:
241
+ out.push(s);
242
+ }
243
+ continue;
244
+ }
245
+ if (t.isBlockStatement(s)) {
246
+ s.body = translate(s.body);
247
+ out.push(s);
248
+ continue;
249
+ }
250
+ if (t.isIfStatement(s)) {
251
+ if (s.consequent && t.isBlockStatement(s.consequent)) s.consequent.body = translate(s.consequent.body);
252
+ if (s.alternate && t.isBlockStatement(s.alternate)) s.alternate.body = translate(s.alternate.body);
253
+ out.push(s);
254
+ continue;
255
+ }
256
+ if (t.isForStatement(s) || t.isForOfStatement(s) || t.isForInStatement(s) ||
257
+ t.isWhileStatement(s) || t.isDoWhileStatement(s)) {
258
+ if (s.body && t.isBlockStatement(s.body)) s.body.body = translate(s.body.body);
259
+ out.push(s);
260
+ continue;
261
+ }
262
+ if (t.isTryStatement(s)) {
263
+ if (s.block) s.block.body = translate(s.block.body);
264
+ out.push(s);
265
+ continue;
266
+ }
267
+ if (t.isLabeledStatement(s)) {
268
+ if (s.body && t.isBlockStatement(s.body)) s.body.body = translate(s.body.body);
269
+ out.push(s);
270
+ continue;
271
+ }
272
+ out.push(s);
273
+ }
274
+ return out;
275
+ };
276
+
277
+ const translated = translate(bodyStmts);
278
+ // A trailing `continue` translated from a final `return` is redundant
279
+ // *when fall-through behaves identically* — true for every method except
280
+ // map, whose fall-through pushes `undefined`. For map, a trailing
281
+ // continue means the callback never falls through, so the implicit
282
+ // push is dropped along with the continue.
283
+ let endsWithReturn = false;
284
+ if (translated.length > 0 && t.isContinueStatement(translated[translated.length - 1])) {
285
+ translated.pop();
286
+ endsWithReturn = true;
287
+ }
288
+ if (method === 'map' && used && !endsWithReturn) {
289
+ translated.push(t.expressionStatement(
290
+ t.callExpression(t.memberExpression(t.identifier(accName), t.identifier('push')), [t.identifier('undefined')])
291
+ ));
292
+ }
293
+
294
+ // ---- assemble loop ----
295
+ const bindDecls = [];
296
+ bindDecls.push(t.variableDeclaration(elemKind, [
297
+ t.variableDeclarator(t.identifier(elemName),
298
+ t.memberExpression(t.identifier(arrName), t.identifier(idxName), true)),
299
+ ]));
300
+ if (p1) {
301
+ bindDecls.push(t.variableDeclaration('const', [t.variableDeclarator(p1, t.identifier(idxName))]));
302
+ }
303
+ if (p2) {
304
+ bindDecls.push(t.variableDeclaration('const', [t.variableDeclarator(p2, t.identifier(arrName))]));
305
+ }
306
+
307
+ const loop = t.forStatement(
308
+ t.variableDeclaration('let', [t.variableDeclarator(t.identifier(idxName), t.numericLiteral(0))]),
309
+ t.binaryExpression('<', t.identifier(idxName), t.memberExpression(t.identifier(arrName), t.identifier('length'))),
310
+ t.updateExpression('++', t.identifier(idxName)),
311
+ t.blockStatement([...bindDecls, ...translated])
312
+ );
313
+
314
+ const decls = [t.variableDeclaration('const', [t.variableDeclarator(t.identifier(arrName), callee.object)])];
315
+ if ((method === 'map' || method === 'filter') && used) {
316
+ decls.push(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(accName), t.arrayExpression([]))]));
317
+ }
318
+ if (method === 'some') {
319
+ decls.push(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(resName), t.booleanLiteral(false))]));
320
+ } else if (method === 'every') {
321
+ decls.push(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(resName), t.booleanLiteral(true))]));
322
+ } else if (method === 'find') {
323
+ decls.push(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(resName))]));
324
+ }
325
+
326
+ // ---- splice into the statement list ----
327
+ if (!needsValue) {
328
+ // Value-discarding form: drop the call from its statement.
329
+ if (t.isExpressionStatement(stmtPath.node) && stmtPath.node.expression === node) {
330
+ stmtPath.replaceWithMultiple([...decls, loop]);
331
+ } else {
332
+ path.replaceWith(t.identifier('undefined'));
333
+ stmtPath.insertBefore([...decls, loop]);
334
+ }
335
+ } else {
336
+ const resultName = (method === 'map' || method === 'filter') ? accName : resName;
337
+ path.replaceWith(t.identifier(resultName));
338
+ const stmtClone = t.cloneNode(stmtPath.node, true);
339
+ stmtPath.replaceWithMultiple([...decls, loop, stmtClone]);
340
+ }
341
+ ctx.applied(ID);
342
+ },
343
+ });
344
+ }
345
+
346
+ function scopeHasViolations(scope, name, cb) {
347
+ // Whether the callback parameter is reassigned inside the callback.
348
+ let violations = 0;
349
+ walk(cb.body, (n) => {
350
+ if ((t.isAssignmentExpression(n) || t.isUpdateExpression(n))) {
351
+ const target = t.isAssignmentExpression(n) ? n.left : n.argument;
352
+ if (t.isIdentifier(target) && target.name === name) violations++;
353
+ }
354
+ return true;
355
+ });
356
+ return violations > 0;
357
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * S6 destructure-to-direct — hot functions only.
3
+ *
4
+ * `const { x, y } = obj` (obj a simple never-reassigned identifier, <= 3 total
5
+ * uses of the pattern variables) becomes direct `obj.x` reads at use sites.
6
+ * Default values are hoisted to an `if` at the declaration site so they are
7
+ * never re-evaluated per use.
8
+ *
9
+ * Bail conditions: nested patterns, rest elements, computed keys, non-identifier
10
+ * object, reassigned object, > 3 uses, any call/assignment/await between the
11
+ * declaration and the last use (the object's properties could change), or
12
+ * for-of/for-in heads.
13
+ */
14
+ import traverse from '@babel/traverse';
15
+ import * as t from '@babel/types';
16
+ import { containsBarrier, corridorSafe } from '../pass-utils.js';
17
+
18
+ const _traverse = traverse.default || traverse;
19
+
20
+ export const ID = 'destructure-to-direct';
21
+ export const CODE = 'S6';
22
+
23
+ /** Root identifier name of a member chain, or the identifier itself. */
24
+ function memberRootIs(node, name) {
25
+ let cur = node;
26
+ while (t.isMemberExpression(cur)) cur = cur.object;
27
+ return t.isIdentifier(cur) && cur.name === name;
28
+ }
29
+
30
+ export default function destructureToDirect(programPath, ctx) {
31
+ programPath.traverse({
32
+ VariableDeclaration(path) {
33
+ const node = path.node;
34
+ const line = node.loc ? node.loc.start.line : 0;
35
+ const fnParent = path.getFunctionParent();
36
+ if (!fnParent || !ctx.isHot(fnParent.node)) return;
37
+ if (ctx.isDisabled(path, ID, CODE)) return;
38
+
39
+ const fail = (reason) => ctx.bail(line, CODE, reason);
40
+
41
+ if (node.kind !== 'const') {
42
+ if (node.declarations.length === 1 && t.isObjectPattern(node.declarations[0].id) && line > 0) {
43
+ return fail('declaration is not const');
44
+ }
45
+ return;
46
+ }
47
+ const parent = path.parentPath;
48
+ if (parent.isForOfStatement() || parent.isForInStatement() || parent.isForStatement()) {
49
+ return line > 0 ? fail('declaration is a loop head') : undefined;
50
+ }
51
+ if (node.declarations.length !== 1) return line > 0 ? fail('multiple declarators') : undefined;
52
+ const declarator = node.declarations[0];
53
+ const pattern = declarator.id;
54
+ if (!t.isObjectPattern(pattern)) return; // not an opportunity
55
+ const obj = declarator.init;
56
+ if (!obj || !t.isIdentifier(obj)) {
57
+ return line > 0 ? fail('object is not a simple identifier (member chains are B1 territory)') : undefined;
58
+ }
59
+
60
+ const objBinding = path.scope.getBinding(obj.name);
61
+ if (!objBinding) return fail('object binding not found');
62
+ if (objBinding.constantViolations.length > 0) return fail('object identifier is reassigned');
63
+
64
+ // Validate pattern shape.
65
+ const props = [];
66
+ for (const p of pattern.properties) {
67
+ if (t.isRestElement(p)) return fail('rest element');
68
+ if (!t.isObjectProperty(p)) return fail('unsupported pattern element');
69
+ if (p.computed) return fail('computed key');
70
+ let keyName = null;
71
+ if (t.isIdentifier(p.key)) keyName = p.key.name;
72
+ else if (t.isStringLiteral(p.key) && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(p.key.value)) keyName = p.key.value;
73
+ else return fail('non-identifier key');
74
+ if (!t.isIdentifier(p.value) && !t.isAssignmentPattern(p.value)) return fail('nested pattern or non-identifier value');
75
+ if (t.isAssignmentPattern(p.value)) {
76
+ if (!t.isIdentifier(p.value.left)) return fail('nested pattern in default target');
77
+ props.push({ key: keyName, local: p.value.left.name, hasDefault: true, deflt: p.value.right });
78
+ } else {
79
+ props.push({ key: keyName, local: p.value.name, hasDefault: false, deflt: null });
80
+ }
81
+ }
82
+ if (props.length === 0) return fail('empty pattern');
83
+
84
+ // Uses.
85
+ let totalUses = 0;
86
+ const refsByLocal = new Map();
87
+ for (const p of props) {
88
+ const binding = path.scope.getBinding(p.local);
89
+ if (!binding) return fail(`binding ${p.local} not found`);
90
+ if (binding.constantViolations.length > 0) return fail(`binding ${p.local} is reassigned`);
91
+ refsByLocal.set(p.local, binding);
92
+ totalUses += binding.references;
93
+ }
94
+ if (totalUses > 3) return fail(`total uses ${totalUses} > 3`);
95
+
96
+ // Barrier check between declaration and each use: any statement strictly
97
+ // between the declaration and the use's statement must be free of calls
98
+ // and assignments; within the use's own statement, an evaluation-order
99
+ // corridor check ensures no call executes before the read.
100
+ const fnBodyPath = fnParent.get('body');
101
+ const declStart = declarator.start ?? 0;
102
+ const declEnd = declarator.end ?? 0;
103
+ const useNodes = [];
104
+ for (const [, binding] of refsByLocal) {
105
+ for (const ref of binding.referencePaths) useNodes.push(ref.node);
106
+ }
107
+ const lastRefPos = useNodes.reduce((m, u) => Math.max(m, u.end ?? 0), declEnd);
108
+ let barrierProblem = null;
109
+ fnBodyPath.traverse({
110
+ Statement(sPath) {
111
+ if (barrierProblem) return false;
112
+ const n = sPath.node;
113
+ if (n === node) return;
114
+ if (sPath.isFunctionDeclaration()) return;
115
+ const s = n.start ?? 0;
116
+ const e = n.end ?? 0;
117
+ const own = useNodes.filter((u) => u.start >= s && u.end <= e);
118
+ if (own.length > 0) {
119
+ // Corridor check inside the use's own statement (sorted by position).
120
+ own.sort((a, b) => a.start - b.start);
121
+ if (!corridorSafe(n, own)) {
122
+ barrierProblem = 'a call inside the use statement executes before the read';
123
+ }
124
+ return;
125
+ }
126
+ const between = e > declStart && s < lastRefPos;
127
+ if (between && containsBarrier(n, { loops: true })) {
128
+ barrierProblem = 'calls or assignments between declaration and use could mutate the object';
129
+ }
130
+ },
131
+ });
132
+ if (!barrierProblem) {
133
+ // Property writes to the object anywhere in the function are mutation
134
+ // risks (`cfg = ...` reassignment is covered by constantViolations).
135
+ fnBodyPath.traverse({
136
+ AssignmentExpression(aPath) {
137
+ if (memberRootIs(aPath.node.left, obj.name)) {
138
+ barrierProblem = `property of "${obj.name}" is written`;
139
+ }
140
+ },
141
+ UpdateExpression(aPath) {
142
+ if (memberRootIs(aPath.node.argument, obj.name)) barrierProblem = `property of "${obj.name}" is updated`;
143
+ },
144
+ UnaryExpression(aPath) {
145
+ if (aPath.node.operator === 'delete' && memberRootIs(aPath.node.argument, obj.name)) {
146
+ barrierProblem = `property of "${obj.name}" is deleted`;
147
+ }
148
+ },
149
+ });
150
+ }
151
+ if (barrierProblem) return fail(barrierProblem);
152
+
153
+ // Rewrite.
154
+ const objExpr = t.identifier(obj.name);
155
+ let hasDefaults = false;
156
+ for (const p of props) {
157
+ if (p.hasDefault) { hasDefaults = true; continue; }
158
+ const binding = refsByLocal.get(p.local);
159
+ for (const ref of binding.referencePaths) {
160
+ if (ctx.isDisabled(ref, ID, CODE)) continue;
161
+ ref.replaceWith(t.memberExpression(t.cloneNode(objExpr), t.identifier(p.key)));
162
+ }
163
+ }
164
+
165
+ if (hasDefaults) {
166
+ // Keep defaulted locals: hoist the default to an if.
167
+ const stmts = [];
168
+ for (const p of props) {
169
+ if (!p.hasDefault) continue;
170
+ stmts.push(t.variableDeclaration('let', [
171
+ t.variableDeclarator(t.identifier(p.local),
172
+ t.memberExpression(t.cloneNode(objExpr), t.identifier(p.key))),
173
+ ]));
174
+ stmts.push(t.ifStatement(
175
+ t.binaryExpression('===', t.identifier(p.local), t.identifier('undefined')),
176
+ t.expressionStatement(t.assignmentExpression('=', t.identifier(p.local), p.deflt))
177
+ ));
178
+ }
179
+ path.replaceWithMultiple(stmts);
180
+ } else {
181
+ path.remove();
182
+ }
183
+ ctx.applied(ID);
184
+ },
185
+ });
186
+ }