@tsrx/core 0.1.4 → 0.1.7

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.
@@ -1,22 +1,27 @@
1
1
  /** @import * as AST from 'estree' */
2
2
 
3
+ import * as b from '../utils/builders.js';
4
+ import { is_function_or_component_node } from '../utils/ast.js';
5
+
3
6
  /**
4
7
  * Lazy destructuring transform — framework-agnostic.
5
8
  *
6
- * Shared between `@tsrx/react` and `@tsrx/solid`. Walks an AST and rewrites
7
- * references to names introduced by `&{ ... }` / `&[ ... ]` destructuring
8
- * patterns into member-expression accesses on a generated source identifier.
9
+ * Shared between `@tsrx/react`, `@tsrx/preact`, `@tsrx/solid`, and `@tsrx/vue`.
10
+ * Walks an AST and rewrites references to names introduced by `&{ ... }` /
11
+ * `&[ ... ]` destructuring patterns into member-expression accesses on a
12
+ * generated source identifier.
9
13
  *
10
14
  * Usage:
11
15
  * 1. Create a context with `createLazyContext()` (or provide any object with
12
16
  * a `lazy_next_id: number` field).
13
17
  * 2. Run `preallocateLazyIds(root, context)` once over the full program to
14
- * assign stable `metadata.lazy_id` values to every lazy pattern.
15
- * 3. For each function/component scope, collect bindings with
16
- * `collectLazyBindingsFromComponent(params, body, context)` and pass the
17
- * resulting map into `applyLazyTransforms(body, map)`.
18
- * 4. If a component declares lazy params, pass its params through
19
- * `replaceLazyParams(params)` before emitting.
18
+ * assign stable `metadata.lazy_id` values to every lazy pattern and to
19
+ * flag function-like nodes whose subtree contains any lazy pattern via
20
+ * `metadata.has_lazy_descendants`.
21
+ * 3. After converting components to functions, call `applyLazyTransforms(fn,
22
+ * new Map())` on each top-level function. The function-handler walks the
23
+ * whole subtree, collects param + body bindings, replaces lazy patterns
24
+ * with their generated identifiers, and rewrites every reference.
20
25
  *
21
26
  * The transform is purely AST-to-AST and has no framework-specific knowledge.
22
27
  */
@@ -68,7 +73,7 @@ function set_source_location(node, loc_info) {
68
73
  * @returns {any}
69
74
  */
70
75
  function create_generated_identifier(name, loc_info, source_name, source_length) {
71
- const id = /** @type {any} */ ({ type: 'Identifier', name, metadata: { path: [] } });
76
+ const id = b.id(name);
72
77
  if (source_name && source_name !== name) id.metadata.source_name = source_name;
73
78
  if (source_length != null) id.metadata.source_length = source_length;
74
79
  return set_source_location(id, loc_info);
@@ -111,40 +116,19 @@ function create_lazy_object_type_annotation(pattern) {
111
116
  const key = prop.key;
112
117
  if (key.type !== 'Identifier' && key.type !== 'Literal') continue;
113
118
 
114
- members.push({
115
- type: 'TSPropertySignature',
116
- key:
117
- key.type === 'Identifier'
118
- ? create_generated_identifier(key.name, key)
119
- : set_source_location({ ...key, metadata: { path: [] } }, key),
120
- computed: false,
121
- optional: false,
122
- readonly: false,
123
- static: false,
124
- kind: 'init',
125
- typeAnnotation: {
126
- type: 'TSTypeAnnotation',
127
- typeAnnotation: {
128
- type: 'TSAnyKeyword',
129
- metadata: { path: [] },
130
- },
131
- metadata: { path: [] },
132
- },
133
- metadata: { path: [] },
134
- });
119
+ const member_key =
120
+ key.type === 'Identifier'
121
+ ? create_generated_identifier(key.name, key)
122
+ : set_source_location({ ...key, metadata: { path: [] } }, key);
123
+
124
+ members.push(
125
+ b.ts_property_signature(member_key, b.ts_type_annotation(b.ts_keyword_type('any'))),
126
+ );
135
127
  }
136
128
 
137
129
  if (members.length === 0) return null;
138
130
 
139
- return {
140
- type: 'TSTypeAnnotation',
141
- typeAnnotation: {
142
- type: 'TSTypeLiteral',
143
- members,
144
- metadata: { path: [] },
145
- },
146
- metadata: { path: [] },
147
- };
131
+ return b.ts_type_annotation(b.ts_type_literal(members));
148
132
  }
149
133
 
150
134
  /**
@@ -215,36 +199,60 @@ function set_lazy_param_binding_mappings(lazy_id, pattern) {
215
199
  * Collect lazy bindings from a destructuring pattern.
216
200
  *
217
201
  * For `&{ name, age }` on source `S`, maps `name` → `S.name`, `age` → `S.age`.
218
- * For `&[a, b]` on source `S`, maps `a` → `S[0]`, `b` → `S[1]`. Handles nested
219
- * `AssignmentPattern` (default values); skips `RestElement`.
202
+ * For `&[a, b]` on source `S`, maps `a` → `S[0]`, `b` → `S[1]`. Recurses into
203
+ * nested `ObjectPattern` / `ArrayPattern` values so that `&{ outer: &{ inner } }`
204
+ * on source `S` maps `inner` → `S.outer.inner`, and `&{ pair: &[first, second] }`
205
+ * maps `first` → `S.pair[0]`. Handles `AssignmentPattern` (default values lost,
206
+ * but the binding still resolves to the member chain). Skips `RestElement`.
220
207
  *
221
208
  * @param {any} pattern
222
209
  * @param {string} source_name
223
210
  * @param {Map<string, LazyBinding>} lazy_bindings
224
211
  */
225
212
  export function collect_lazy_bindings(pattern, source_name, lazy_bindings) {
213
+ collect_lazy_bindings_at(
214
+ pattern,
215
+ source_name,
216
+ () => create_generated_identifier(source_name),
217
+ lazy_bindings,
218
+ );
219
+ }
220
+
221
+ /**
222
+ * Walk a destructure pattern and register a `LazyBinding` for each leaf
223
+ * `Identifier`, where `build_parent` produces the AST expression that reaches
224
+ * this pattern's value from the synthesized source identifier. Each nested
225
+ * level composes its accessor onto `build_parent`, so leaves get the full
226
+ * member chain (e.g. `source.outer.inner` for `&{ outer: &{ inner } }`).
227
+ *
228
+ * @param {any} pattern
229
+ * @param {string} source_name
230
+ * @param {(reference?: any) => any} build_parent
231
+ * @param {Map<string, LazyBinding>} lazy_bindings
232
+ */
233
+ function collect_lazy_bindings_at(pattern, source_name, build_parent, lazy_bindings) {
226
234
  if (pattern.type === 'ObjectPattern') {
227
235
  for (const prop of pattern.properties || []) {
228
236
  if (prop.type === 'RestElement') continue;
229
237
  const value = prop.value;
230
238
  const actual = value.type === 'AssignmentPattern' ? value.left : value;
239
+ const key = prop.key;
240
+ const computed = prop.computed || key.type !== 'Identifier';
241
+
242
+ /** @type {(reference?: any) => any} */
243
+ const build_self = (reference) =>
244
+ b.member(
245
+ build_parent(),
246
+ computed || key.type !== 'Identifier'
247
+ ? { ...key }
248
+ : create_generated_identifier(key.name, reference, reference?.name),
249
+ computed,
250
+ );
251
+
231
252
  if (actual.type === 'Identifier') {
232
- const key = prop.key;
233
- const computed = prop.computed || key.type !== 'Identifier';
234
- lazy_bindings.set(actual.name, {
235
- source_name,
236
- read: (reference) => ({
237
- type: 'MemberExpression',
238
- object: create_generated_identifier(source_name),
239
- property:
240
- computed || key.type !== 'Identifier'
241
- ? { ...key }
242
- : create_generated_identifier(key.name, reference, reference?.name),
243
- computed,
244
- optional: false,
245
- metadata: { path: [] },
246
- }),
247
- });
253
+ lazy_bindings.set(actual.name, { source_name, read: build_self });
254
+ } else if (actual.type === 'ObjectPattern' || actual.type === 'ArrayPattern') {
255
+ collect_lazy_bindings_at(actual, source_name, build_self, lazy_bindings);
248
256
  }
249
257
  }
250
258
  } else if (pattern.type === 'ArrayPattern') {
@@ -253,55 +261,18 @@ export function collect_lazy_bindings(pattern, source_name, lazy_bindings) {
253
261
  if (!element) continue;
254
262
  if (element.type === 'RestElement') continue;
255
263
  const actual = element.type === 'AssignmentPattern' ? element.left : element;
256
- if (actual.type === 'Identifier') {
257
- const index = i;
258
- lazy_bindings.set(actual.name, {
259
- source_name,
260
- read: () => ({
261
- type: 'MemberExpression',
262
- object: create_generated_identifier(source_name),
263
- property: { type: 'Literal', value: index, raw: String(index), metadata: { path: [] } },
264
- computed: true,
265
- optional: false,
266
- metadata: { path: [] },
267
- }),
268
- });
269
- }
270
- }
271
- }
272
- }
264
+ const index = i;
273
265
 
274
- /**
275
- * Collect lazy bindings from a component's params and top-level body declarations.
276
- * Mutates each lazy pattern's `metadata.lazy_id` in place (idempotent if already set).
277
- *
278
- * @param {any[]} params
279
- * @param {any[]} body
280
- * @param {LazyContext} context
281
- * @returns {Map<string, LazyBinding>}
282
- */
283
- export function collect_lazy_bindings_from_component(params, body, context) {
284
- /** @type {Map<string, LazyBinding>} */
285
- const lazy_bindings = new Map();
286
-
287
- for (const param of params) {
288
- const pattern = param.type === 'AssignmentPattern' ? param.left : param;
289
- if ((pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') && pattern.lazy) {
290
- const lazy_name = pattern.metadata?.lazy_id || generate_lazy_id(context);
291
- if (!pattern.metadata?.lazy_id) {
292
- pattern.metadata = { ...pattern.metadata, lazy_id: lazy_name };
266
+ /** @type {() => any} */
267
+ const build_self = () => b.member(build_parent(), b.literal(index), true);
268
+
269
+ if (actual.type === 'Identifier') {
270
+ lazy_bindings.set(actual.name, { source_name, read: build_self });
271
+ } else if (actual.type === 'ObjectPattern' || actual.type === 'ArrayPattern') {
272
+ collect_lazy_bindings_at(actual, source_name, build_self, lazy_bindings);
293
273
  }
294
- collect_lazy_bindings(pattern, lazy_name, lazy_bindings);
295
274
  }
296
275
  }
297
-
298
- // VariableDeclaration lazy patterns already have their `lazy_id` assigned
299
- // by `preallocate_lazy_ids` (run once over the whole program by the target
300
- // transforms), so `collect_lazy_bindings_from_statements` handles them
301
- // alongside the expression-statement assignment form.
302
- collect_lazy_bindings_from_statements(body, lazy_bindings);
303
-
304
- return lazy_bindings;
305
276
  }
306
277
 
307
278
  /**
@@ -317,61 +288,164 @@ export function collect_lazy_bindings_from_statements(statements, lazy_bindings)
317
288
  for (const stmt of statements || []) {
318
289
  if (stmt.type === 'VariableDeclaration') {
319
290
  for (const declarator of stmt.declarations || []) {
320
- const pattern = declarator.id;
321
- if (
322
- (pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
323
- pattern.lazy &&
324
- pattern.metadata?.lazy_id &&
325
- !lazy_bindings_contains(lazy_bindings, pattern)
326
- ) {
327
- collect_lazy_bindings(pattern, pattern.metadata.lazy_id, lazy_bindings);
328
- }
291
+ visit_topmost_lazy_patterns(declarator.id, (lazy) => {
292
+ if (!lazy.metadata?.lazy_id) return;
293
+ collect_lazy_bindings(lazy, lazy.metadata.lazy_id, lazy_bindings);
294
+ });
329
295
  }
330
296
  } else if (
331
297
  stmt.type === 'ExpressionStatement' &&
332
298
  stmt.expression?.type === 'AssignmentExpression' &&
333
- stmt.expression.operator === '=' &&
334
- (stmt.expression.left?.type === 'ObjectPattern' ||
335
- stmt.expression.left?.type === 'ArrayPattern') &&
336
- stmt.expression.left.lazy &&
337
- stmt.expression.left.metadata?.lazy_id
299
+ stmt.expression.operator === '='
338
300
  ) {
339
- collect_lazy_bindings(
340
- stmt.expression.left,
341
- stmt.expression.left.metadata.lazy_id,
342
- lazy_bindings,
343
- );
301
+ visit_topmost_lazy_patterns(stmt.expression.left, (lazy) => {
302
+ if (!lazy.metadata?.lazy_id) return;
303
+ collect_lazy_bindings(lazy, lazy.metadata.lazy_id, lazy_bindings);
304
+ });
344
305
  }
345
306
  }
346
307
  }
347
308
 
348
309
  /**
349
- * @param {Map<string, LazyBinding>} lazy_bindings
310
+ * Walk a destructure pattern tree, calling `visit` on every *topmost-lazy*
311
+ * descendant — a lazy `ObjectPattern` / `ArrayPattern` with no lazy ancestor
312
+ * within the same pattern tree. Descends through `AssignmentPattern`,
313
+ * `RestElement`, and non-lazy `ObjectPattern` / `ArrayPattern`. Stops at lazy
314
+ * patterns: their inner leaves are reached via accessor chains rooted at the
315
+ * lazy pattern's synthesized id, not by further descent here.
316
+ *
350
317
  * @param {any} pattern
351
- * @returns {boolean}
318
+ * @param {(node: any) => void} visit
352
319
  */
353
- function lazy_bindings_contains(lazy_bindings, pattern) {
320
+ function visit_topmost_lazy_patterns(pattern, visit) {
321
+ if (!pattern || typeof pattern !== 'object') return;
322
+ if (pattern.type === 'AssignmentPattern') {
323
+ visit_topmost_lazy_patterns(pattern.left, visit);
324
+ return;
325
+ }
326
+ if (pattern.type === 'RestElement') {
327
+ visit_topmost_lazy_patterns(pattern.argument, visit);
328
+ return;
329
+ }
330
+ if (pattern.type !== 'ObjectPattern' && pattern.type !== 'ArrayPattern') return;
331
+
332
+ if (pattern.lazy) {
333
+ visit(pattern);
334
+ return;
335
+ }
336
+
354
337
  if (pattern.type === 'ObjectPattern') {
355
338
  for (const prop of pattern.properties || []) {
356
- if (prop.type === 'RestElement') continue;
357
- const value = prop.value;
358
- const actual = value?.type === 'AssignmentPattern' ? value.left : value;
359
- if (actual?.type === 'Identifier' && lazy_bindings.has(actual.name)) return true;
339
+ if (prop.type === 'RestElement') visit_topmost_lazy_patterns(prop.argument, visit);
340
+ else visit_topmost_lazy_patterns(prop.value, visit);
360
341
  }
361
- } else if (pattern.type === 'ArrayPattern') {
342
+ } else {
362
343
  for (const element of pattern.elements || []) {
363
- if (!element || element.type === 'RestElement') continue;
364
- const actual = element.type === 'AssignmentPattern' ? element.left : element;
365
- if (actual?.type === 'Identifier' && lazy_bindings.has(actual.name)) return true;
344
+ if (element) visit_topmost_lazy_patterns(element, visit);
366
345
  }
367
346
  }
368
- return false;
347
+ }
348
+
349
+ /**
350
+ * Build the replacement identifier for a lazy pattern. When `is_top` is true
351
+ * (the pattern is itself a function parameter) we attach the original
352
+ * `typeAnnotation`, synthesize an object-shaped annotation for untyped object
353
+ * params so TypeScript sees prop names, and register source-mapping info.
354
+ * Nested replacements (inside a non-lazy outer destructure) can't carry an
355
+ * inline type annotation — that's not valid syntax — so they get a plain
356
+ * identifier with just source-range info.
357
+ *
358
+ * @param {any} pattern
359
+ * @param {boolean} is_top
360
+ * @returns {any}
361
+ */
362
+ function build_lazy_id_for_pattern(pattern, is_top) {
363
+ const pattern_range = get_lazy_pattern_mapping_range(pattern);
364
+ const lazy_id = pattern_range
365
+ ? create_generated_identifier(
366
+ pattern.metadata.lazy_id,
367
+ pattern_range,
368
+ undefined,
369
+ pattern_range.source_length,
370
+ )
371
+ : create_generated_identifier(pattern.metadata.lazy_id);
372
+ if (!is_top) return lazy_id;
373
+ if (pattern.typeAnnotation) {
374
+ lazy_id.typeAnnotation = pattern.typeAnnotation;
375
+ } else {
376
+ const type_annotation = create_lazy_object_type_annotation(pattern);
377
+ if (type_annotation) lazy_id.typeAnnotation = type_annotation;
378
+ }
379
+ set_lazy_param_binding_mappings(lazy_id, pattern);
380
+ return lazy_id;
381
+ }
382
+
383
+ /**
384
+ * Walk a destructure pattern tree and replace every topmost-lazy pattern with
385
+ * its synthesized id identifier. Non-lazy outer patterns are preserved so a
386
+ * source like `{ pair: &[a, b] }` becomes `{ pair: __lazy0 }`. The `is_top`
387
+ * flag is true only when the caller is invoking on a position that itself
388
+ * binds a single param (so a directly-lazy pattern can carry param-level type
389
+ * info); recursive descent into child patterns passes `false`.
390
+ *
391
+ * @param {any} pattern
392
+ * @param {boolean} [is_top]
393
+ * @returns {any}
394
+ */
395
+ function replace_lazy_in_pattern(pattern, is_top = true) {
396
+ if (!pattern || typeof pattern !== 'object') return pattern;
397
+
398
+ if (pattern.type === 'AssignmentPattern') {
399
+ const new_left = replace_lazy_in_pattern(pattern.left, is_top);
400
+ return new_left === pattern.left ? pattern : { ...pattern, left: new_left };
401
+ }
402
+ if (pattern.type === 'RestElement') {
403
+ const new_arg = replace_lazy_in_pattern(pattern.argument, false);
404
+ return new_arg === pattern.argument ? pattern : { ...pattern, argument: new_arg };
405
+ }
406
+ if (pattern.type !== 'ObjectPattern' && pattern.type !== 'ArrayPattern') return pattern;
407
+
408
+ if (pattern.lazy && pattern.metadata?.lazy_id) {
409
+ return build_lazy_id_for_pattern(pattern, is_top);
410
+ }
411
+
412
+ if (pattern.type === 'ObjectPattern') {
413
+ let changed = false;
414
+ const new_properties = (pattern.properties || []).map((/** @type {any} */ prop) => {
415
+ if (prop.type === 'RestElement') {
416
+ const new_arg = replace_lazy_in_pattern(prop.argument, false);
417
+ if (new_arg === prop.argument) return prop;
418
+ changed = true;
419
+ return { ...prop, argument: new_arg };
420
+ }
421
+ const new_value = replace_lazy_in_pattern(prop.value, false);
422
+ if (new_value === prop.value) return prop;
423
+ changed = true;
424
+ return { ...prop, value: new_value };
425
+ });
426
+ return changed ? { ...pattern, properties: new_properties } : pattern;
427
+ }
428
+
429
+ let changed = false;
430
+ const new_elements = (pattern.elements || []).map((/** @type {any} */ element) => {
431
+ if (!element) return element;
432
+ const new_element = replace_lazy_in_pattern(element, false);
433
+ if (new_element !== element) changed = true;
434
+ return new_element;
435
+ });
436
+ return changed ? { ...pattern, elements: new_elements } : pattern;
369
437
  }
370
438
 
371
439
  /**
372
440
  * Walk the AST and pre-allocate `lazy_id` metadata on every lazy destructuring
373
441
  * pattern: function/component params, variable declarator ids, and statement-level
374
- * assignment LHS. Idempotent: skips patterns that already have a `lazy_id`.
442
+ * assignment LHS. Walks into non-lazy outer patterns to find nested lazy ones,
443
+ * e.g. `{ pair: &[a, b] }` allocates an id for the inner `&[a, b]`. Idempotent:
444
+ * skips patterns that already have a `lazy_id`.
445
+ *
446
+ * Also stamps `metadata.has_lazy_descendants = true` on every function-like
447
+ * node whose subtree contains any lazy pattern, so `apply_lazy_transforms`
448
+ * can take a constant-time early-return path for purely non-lazy functions.
375
449
  *
376
450
  * @param {any} root
377
451
  * @param {LazyContext} context
@@ -379,32 +453,29 @@ function lazy_bindings_contains(lazy_bindings, pattern) {
379
453
  export function preallocate_lazy_ids(root, context) {
380
454
  /** @param {any} pattern */
381
455
  const assign_id = (pattern) => {
382
- if (
383
- (pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
384
- pattern.lazy &&
385
- !pattern.metadata?.lazy_id
386
- ) {
387
- pattern.metadata = {
388
- ...pattern.metadata,
389
- lazy_id: generate_lazy_id(context),
390
- };
391
- }
456
+ visit_topmost_lazy_patterns(pattern, (lazy) => {
457
+ if (lazy.metadata?.lazy_id) return;
458
+ lazy.metadata = { ...lazy.metadata, lazy_id: generate_lazy_id(context) };
459
+ });
392
460
  };
393
461
 
394
- /** @param {any} node */
462
+ /**
463
+ * @param {any} node
464
+ * @returns {boolean} true if `node`'s subtree contains any lazy pattern.
465
+ */
395
466
  const visit = (node) => {
396
- if (!node || typeof node !== 'object') return;
467
+ if (!node || typeof node !== 'object') return false;
397
468
  if (Array.isArray(node)) {
398
- for (const child of node) visit(child);
399
- return;
469
+ let found = false;
470
+ for (const child of node) {
471
+ if (visit(child)) found = true;
472
+ }
473
+ return found;
400
474
  }
401
475
 
402
- if (
403
- node.type === 'FunctionDeclaration' ||
404
- node.type === 'FunctionExpression' ||
405
- node.type === 'ArrowFunctionExpression' ||
406
- node.type === 'Component'
407
- ) {
476
+ const is_function_like = is_function_or_component_node(node);
477
+
478
+ if (is_function_like) {
408
479
  for (const param of node.params || []) {
409
480
  assign_id(param?.type === 'AssignmentPattern' ? param.left : param);
410
481
  }
@@ -422,10 +493,19 @@ export function preallocate_lazy_ids(root, context) {
422
493
  assign_id(node.expression.left);
423
494
  }
424
495
 
496
+ let found =
497
+ (node.type === 'ObjectPattern' || node.type === 'ArrayPattern') && node.lazy === true;
498
+
425
499
  for (const key of Object.keys(node)) {
426
500
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
427
- visit(node[key]);
501
+ if (visit(node[key])) found = true;
502
+ }
503
+
504
+ if (is_function_like && found) {
505
+ node.metadata = { ...node.metadata, has_lazy_descendants: true };
428
506
  }
507
+
508
+ return found;
429
509
  };
430
510
 
431
511
  visit(root);
@@ -468,15 +548,11 @@ export function apply_lazy_transforms(node, lazy_bindings) {
468
548
  const own_bindings = new Map();
469
549
  let had_lazy_param = false;
470
550
  for (const param of node.params || []) {
471
- const pattern = param?.type === 'AssignmentPattern' ? param.left : param;
472
- if (
473
- (pattern?.type === 'ObjectPattern' || pattern?.type === 'ArrayPattern') &&
474
- pattern.lazy &&
475
- pattern.metadata?.lazy_id
476
- ) {
551
+ visit_topmost_lazy_patterns(param, (lazy) => {
552
+ if (!lazy.metadata?.lazy_id) return;
477
553
  had_lazy_param = true;
478
- collect_lazy_bindings(pattern, pattern.metadata.lazy_id, own_bindings);
479
- }
554
+ collect_lazy_bindings(lazy, lazy.metadata.lazy_id, own_bindings);
555
+ });
480
556
  }
481
557
 
482
558
  // Own bindings override any outer binding with the same name.
@@ -485,10 +561,20 @@ export function apply_lazy_transforms(node, lazy_bindings) {
485
561
  ? new Map([...outer_minus_shadow, ...own_bindings])
486
562
  : outer_minus_shadow;
487
563
 
488
- if (inner_bindings.size === 0 && !params_changed && !had_lazy_param) return node;
564
+ if (
565
+ inner_bindings.size === 0 &&
566
+ !params_changed &&
567
+ !had_lazy_param &&
568
+ !node.metadata?.has_lazy_descendants
569
+ ) {
570
+ return node;
571
+ }
489
572
 
490
- const new_body =
491
- inner_bindings.size > 0 ? apply_lazy_transforms(node.body, inner_bindings) : node.body;
573
+ // Past the early-return: either we have active lazy bindings, lazy
574
+ // params to replace, defaults referencing outer lazy, or the body
575
+ // contains lazy descendants the BlockStatement handler will collect.
576
+ // In every case the body needs to be walked.
577
+ const new_body = apply_lazy_transforms(node.body, inner_bindings);
492
578
 
493
579
  const final_params_src = params_changed ? new_params : node.params;
494
580
  const final_params = had_lazy_param ? replace_lazy_params(final_params_src) : final_params_src;
@@ -625,19 +711,31 @@ export function apply_lazy_transforms(node, lazy_bindings) {
625
711
  const lazy_id = create_generated_identifier(pattern.metadata.lazy_id);
626
712
  if (pattern.typeAnnotation) lazy_id.typeAnnotation = pattern.typeAnnotation;
627
713
  const init = apply_lazy_transforms(node.expression.right, lazy_bindings);
628
- return /** @type {any} */ ({
629
- type: 'VariableDeclaration',
630
- kind: 'const',
631
- declarations: [
632
- {
633
- type: 'VariableDeclarator',
634
- id: lazy_id,
635
- init,
636
- metadata: { path: [] },
714
+ return b.const(lazy_id, init);
715
+ }
716
+
717
+ // Non-lazy outer assignment whose LHS contains nested lazy patterns:
718
+ // `{ pair: &[a, b] } = obj` → `{ pair: __lazy0 } = obj`. JS reference
719
+ // semantics carry writes from `__lazy0[0] = x` back into `obj.pair[0]`.
720
+ if (
721
+ node.type === 'ExpressionStatement' &&
722
+ node.expression?.type === 'AssignmentExpression' &&
723
+ node.expression.operator === '=' &&
724
+ (node.expression.left?.type === 'ObjectPattern' ||
725
+ node.expression.left?.type === 'ArrayPattern') &&
726
+ !node.expression.left.lazy
727
+ ) {
728
+ const new_left = replace_lazy_in_pattern(node.expression.left);
729
+ if (new_left !== node.expression.left) {
730
+ return {
731
+ ...node,
732
+ expression: {
733
+ ...node.expression,
734
+ left: new_left,
735
+ right: apply_lazy_transforms(node.expression.right, lazy_bindings),
637
736
  },
638
- ],
639
- metadata: { path: [] },
640
- });
737
+ };
738
+ }
641
739
  }
642
740
 
643
741
  // AssignmentExpression / UpdateExpression whose target is a lazy identifier.
@@ -674,6 +772,23 @@ export function apply_lazy_transforms(node, lazy_bindings) {
674
772
  };
675
773
  }
676
774
 
775
+ // Non-lazy outer declarator whose id contains nested lazy patterns:
776
+ // `let { pair: &[a, b] } = data` → `let { pair: __lazy0 } = data`.
777
+ if (
778
+ node.type === 'VariableDeclarator' &&
779
+ (node.id?.type === 'ObjectPattern' || node.id?.type === 'ArrayPattern') &&
780
+ !node.id.lazy
781
+ ) {
782
+ const new_id = replace_lazy_in_pattern(node.id);
783
+ if (new_id !== node.id) {
784
+ return {
785
+ ...node,
786
+ id: new_id,
787
+ init: apply_lazy_transforms(node.init, lazy_bindings),
788
+ };
789
+ }
790
+ }
791
+
677
792
  // Shorthand object properties `{ name }` → `{ name: __lazy0.name }`.
678
793
  if (node.type === 'Property' && node.shorthand && node.value?.type === 'Identifier') {
679
794
  const binding = lazy_bindings.get(node.value.name);
@@ -799,38 +914,19 @@ function remove_shadowed(lazy_bindings, shadowed) {
799
914
 
800
915
  /**
801
916
  * Replace any lazy `&{}` / `&[]` patterns in a parameter list with their
802
- * generated lazy identifiers. Leaves non-lazy params untouched.
917
+ * generated lazy identifiers, including patterns nested inside non-lazy outer
918
+ * patterns. For `({ pair: &[a, b] })` returns `({ pair: __lazy0 })`. Leaves
919
+ * params without any lazy descendants untouched.
803
920
  *
804
921
  * @param {any[]} params
805
922
  * @returns {any[]}
806
923
  */
807
924
  export function replace_lazy_params(params) {
808
925
  return params.map((param) => {
809
- const pattern = param.type === 'AssignmentPattern' ? param.left : param;
810
- if (
811
- (pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') &&
812
- pattern.lazy &&
813
- pattern.metadata?.lazy_id
814
- ) {
815
- const pattern_range = get_lazy_pattern_mapping_range(pattern);
816
- const lazy_id = pattern_range
817
- ? create_generated_identifier(
818
- pattern.metadata.lazy_id,
819
- pattern_range,
820
- undefined,
821
- pattern_range.source_length,
822
- )
823
- : create_generated_identifier(pattern.metadata.lazy_id);
824
- if (pattern.typeAnnotation) {
825
- lazy_id.typeAnnotation = pattern.typeAnnotation;
826
- } else {
827
- const type_annotation = create_lazy_object_type_annotation(pattern);
828
- if (type_annotation) lazy_id.typeAnnotation = type_annotation;
829
- }
830
- set_lazy_param_binding_mappings(lazy_id, pattern);
831
- if (param.type === 'AssignmentPattern') return { ...param, left: lazy_id };
832
- return lazy_id;
926
+ if (param.type === 'AssignmentPattern') {
927
+ const new_left = replace_lazy_in_pattern(param.left);
928
+ return new_left === param.left ? param : { ...param, left: new_left };
833
929
  }
834
- return param;
930
+ return replace_lazy_in_pattern(param);
835
931
  });
836
932
  }