1
1
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
2
- /** @import { AST, ExpressionMetadata } from '#compiler' */
2
+ /** @import { AST, Binding, ExpressionMetadata } from '#compiler' */
3
3
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
4
4
import { walk } from 'zimmerframe' ;
5
5
import { object } from '../../../../../utils/ast.js' ;
@@ -10,6 +10,42 @@ import is_reference from 'is-reference';
10
10
import { dev , is_ignored , locator , component_name } from '../../../../../state.js' ;
11
11
import { build_getter } from '../../utils.js' ;
12
12
13
+ /**
14
+ * @param {import('estree').Node | null | undefined } node
15
+ * @returns {node is import('estree').Function }
16
+ */
17
+ function is_function ( node ) {
18
+ return Boolean (
19
+ node &&
20
+ ( node . type === 'ArrowFunctionExpression' ||
21
+ node . type === 'FunctionExpression' ||
22
+ node . type === 'FunctionDeclaration' )
23
+ ) ;
24
+ }
25
+
26
+ /**
27
+ * Determines whether repeated reads of a binding within a single expression
28
+ * should be memoized to avoid inconsistent results.
29
+ * @param {Binding | null } binding
30
+ * @returns {binding is Binding }
31
+ */
32
+ function should_memoize_binding ( binding ) {
33
+ if ( binding === null ) return false ;
34
+
35
+ switch ( binding . kind ) {
36
+ case 'state' :
37
+ case 'raw_state' :
38
+ case 'derived' :
39
+ case 'legacy_reactive' :
40
+ case 'template' :
41
+ return true ;
42
+ default :
43
+ return false ;
44
+ }
45
+ }
46
+ /** @type {WeakMap<any, Map<string, { id: Identifier; getter: (node: Identifier) => Expression }>> } */
47
+ const memoized_reads_by_scope = new WeakMap ( ) ;
48
+
13
49
/**
14
50
* A utility for extracting complex expressions (such as call expressions)
15
51
* from templates and replacing them with `$0`, `$1` etc
@@ -391,7 +427,103 @@ export function validate_mutation(node, context, expression) {
391
427
* @param {ExpressionMetadata } metadata
392
428
*/
393
429
export function build_expression ( context , expression , metadata , state = context . state ) {
394
- const value = /** @type {Expression } */ ( context . visit ( expression , state ) ) ;
430
+ /** @type {import('../../types.js').ComponentClientTransformState } */
431
+ let child_state = state ;
432
+ /** @type {import('estree').Statement[] } */
433
+ const assignments = [ ] ;
434
+ /** @type {Set<string> | null } */
435
+ let memoized_ids = null ;
436
+
437
+ if ( state . analysis . runes && ! metadata . has_await ) {
438
+ const component_state = /** @type {ComponentClientTransformState } */ ( context . state ) ;
439
+ /** @type {Map<Binding, number> } */
440
+ const counts = new Map ( ) ;
441
+
442
+ walk ( expression , null , {
443
+ Identifier ( node , { path } ) {
444
+ const parent = /** @type {Expression } */ ( path . at ( - 1 ) ) ;
445
+ if ( ! is_reference ( node , parent ) ) return ;
446
+
447
+ // avoid memoizing reads that occur within nested functions, since those
448
+ // must re-evaluate when the function executes later
449
+ if ( path . some ( ( ancestor , i ) => i < path . length - 1 && is_function ( ancestor ) ) ) {
450
+ return ;
451
+ }
452
+
453
+ const binding = state . scope . get ( node . name ) ;
454
+ if ( ! should_memoize_binding ( binding ) ) return ;
455
+
456
+ counts . set ( binding , ( counts . get ( binding ) ?? 0 ) + 1 ) ;
457
+ }
458
+ } ) ;
459
+
460
+ memoized_ids = new Set ( ) ;
461
+
462
+ for ( const [ binding , count ] of counts ) {
463
+ if ( count <= 1 ) continue ;
464
+ const name = binding . node ?. name ;
465
+ if ( ! name ) continue ;
466
+
467
+ const original = state . transform [ name ] ;
468
+ if ( ! original ?. read ) continue ;
469
+
470
+ let scope_records = memoized_reads_by_scope . get ( binding . scope ) ;
471
+ if ( ! scope_records ) {
472
+ scope_records = new Map ( ) ;
473
+ memoized_reads_by_scope . set ( binding . scope , scope_records ) ;
474
+ }
475
+
476
+ if ( child_state === state ) {
477
+ child_state = {
478
+ ...state ,
479
+ transform : { ...state . transform }
480
+ } ;
481
+ }
482
+
483
+ let record = scope_records . get ( name ) ;
484
+
485
+ if ( ! record ) {
486
+ const memo_id = b . id ( state . scope . generate ( `${ name } _value` ) ) ;
487
+ record = {
488
+ id : memo_id ,
489
+ getter : original . read
490
+ } ;
491
+ scope_records . set ( name , record ) ;
492
+ component_state . init . push ( b . let ( memo_id ) ) ;
493
+ }
494
+ memoized_ids . add ( record . id . name ) ;
495
+
496
+ const previous = child_state . transform [ name ] ;
497
+ child_state . transform [ name ] = {
498
+ ...previous ,
499
+ read ( ) {
500
+ return record . id ;
501
+ } ,
502
+ assign : previous ?. assign ,
503
+ mutate : previous ?. mutate ,
504
+ update : previous ?. update
505
+ } ;
506
+
507
+ assignments . push ( b . stmt ( b . assignment ( '=' , record . id , record . getter ( b . id ( name ) ) ) ) ) ;
508
+ }
509
+ }
510
+
511
+ let value = /** @type {Expression } */ ( context . visit ( expression , child_state ) ) ;
512
+
513
+ if ( memoized_ids !== null && memoized_ids . size > 0 ) {
514
+ const memoized = memoized_ids ;
515
+ walk ( value , null , {
516
+ MemberExpression ( node ) {
517
+ if ( node . object . type === 'Identifier' && memoized . has ( node . object . name ) ) {
518
+ node . optional = true ;
519
+ }
520
+ }
521
+ } ) ;
522
+ }
523
+
524
+ if ( assignments . length > 0 ) {
525
+ value = b . call ( b . arrow ( [ ] , b . block ( [ ...assignments , b . return ( value ) ] ) ) ) ;
526
+ }
395
527
396
528
// Components not explicitly in legacy mode might be expected to be in runes mode (especially since we didn't
397
529
// adjust this behavior until recently, which broke people's existing components), so we also bail in this case.
0 commit comments