@@ -102,6 +102,7 @@ export function inferMutationAliasingRanges(
102102 kind : MutationKind ;
103103 place : Place ;
104104 reason : MutationReason | null ;
105+ noBackwardRangeExtension : boolean ;
105106 } > = [ ] ;
106107 const renders : Array < { index : number ; place : Place } > = [ ] ;
107108
@@ -179,6 +180,9 @@ export function inferMutationAliasingRanges(
179180 : MutationKind . Conditional ,
180181 reason : null ,
181182 place : effect . value ,
183+ noBackwardRangeExtension :
184+ effect . kind === 'MutateTransitiveConditionally' &&
185+ ( effect . noBackwardRangeExtension ?? false ) ,
182186 } ) ;
183187 } else if (
184188 effect . kind === 'Mutate' ||
@@ -194,6 +198,7 @@ export function inferMutationAliasingRanges(
194198 : MutationKind . Conditional ,
195199 reason : effect . kind === 'Mutate' ? ( effect . reason ?? null ) : null ,
196200 place : effect . value ,
201+ noBackwardRangeExtension : false ,
197202 } ) ;
198203 } else if (
199204 effect . kind === 'MutateFrozen' ||
@@ -246,6 +251,7 @@ export function inferMutationAliasingRanges(
246251 mutation . place . loc ,
247252 mutation . reason ,
248253 errors ,
254+ mutation . noBackwardRangeExtension ,
249255 ) ;
250256 }
251257 for ( const render of renders ) {
@@ -707,16 +713,85 @@ class AliasingState {
707713 loc : SourceLocation ,
708714 reason : MutationReason | null ,
709715 errors : CompilerError ,
716+ noBackwardRangeExtension : boolean = false ,
710717 ) : void {
718+ /*
719+ * Only skip backward-alias range extension when ALL of these hold:
720+ * 1. The effect requested noBackwardRangeExtension (MethodCall receiver)
721+ * 2. The start node is not a PropertyLoad result (no createdFrom edges),
722+ * since PropertyLoad receivers need backward range extension for
723+ * correctness (e.g. `x.y.push(val)` must extend x and y).
724+ * 3. At least one of the start's backward alias targets was produced by
725+ * a function call (indicated by having maybeAliases from the default
726+ * Apply handler). This distinguishes `expensiveProcessing(data).map(fn)`
727+ * from `localArray.push(val)` — only the former should skip, since the
728+ * call result represents a separately-memoizable value.
729+ */
730+ const startNode = this . nodes . get ( start ) ;
731+ let effectiveSkipAliases = false ;
732+ if (
733+ noBackwardRangeExtension &&
734+ startNode != null &&
735+ startNode . createdFrom . size === 0
736+ ) {
737+ /*
738+ * Walk backward through aliases from the start node to check if
739+ * any value in the chain was produced by a function call (Apply).
740+ * Apply results are identifiable by having maybeAliases, since the
741+ * default Apply handler records MaybeAlias from each argument to
742+ * the result. Only walk through alias edges (not maybeAliases or
743+ * captures) to avoid matching catch parameters and similar patterns
744+ * where range extension is required for correctness.
745+ */
746+ const visited = new Set < Identifier > ( ) ;
747+ const aliasQueue : Array < Identifier > = [ start ] ;
748+ while ( aliasQueue . length > 0 ) {
749+ const id = aliasQueue . pop ( ) ! ;
750+ if ( visited . has ( id ) ) {
751+ continue ;
752+ }
753+ visited . add ( id ) ;
754+ const node = this . nodes . get ( id ) ;
755+ if ( node == null ) {
756+ continue ;
757+ }
758+ if ( node . maybeAliases . size > 0 ) {
759+ effectiveSkipAliases = true ;
760+ break ;
761+ }
762+ for ( const [ nextAliasId ] of node . aliases ) {
763+ aliasQueue . push ( nextAliasId ) ;
764+ }
765+ }
766+ }
711767 const seen = new Map < Identifier , MutationKind > ( ) ;
712768 const queue : Array < {
713769 place : Identifier ;
714770 transitive : boolean ;
715771 direction : 'backwards' | 'forwards' ;
716772 kind : MutationKind ;
717- } > = [ { place : start , transitive, direction : 'backwards' , kind : startKind } ] ;
773+ /*
774+ * When true, skip extending the mutable range for this node. This
775+ * is set for nodes reached via backward alias/maybeAlias edges when
776+ * effectiveSkipAliases is enabled, so that the receiver's backward
777+ * aliases are not pulled into the same reactive scope as the method
778+ * call. Nodes reached via capture edges reset this flag, since
779+ * captures represent structural containment (e.g. function closures)
780+ * where range extension is required for correctness.
781+ */
782+ skipRangeExtension : boolean ;
783+ } > = [
784+ {
785+ place : start ,
786+ transitive,
787+ direction : 'backwards' ,
788+ kind : startKind ,
789+ skipRangeExtension : false ,
790+ } ,
791+ ] ;
718792 while ( queue . length !== 0 ) {
719- const { place : current , transitive, direction, kind} = queue . pop ( ) ! ;
793+ const { place : current , transitive, direction, kind, skipRangeExtension} =
794+ queue . pop ( ) ! ;
720795 const previousKind = seen . get ( current ) ;
721796 if ( previousKind != null && previousKind >= kind ) {
722797 continue ;
@@ -728,7 +803,7 @@ class AliasingState {
728803 }
729804 node . mutationReason ??= reason ;
730805 node . lastMutated = Math . max ( node . lastMutated , index ) ;
731- if ( end != null ) {
806+ if ( end != null && ! skipRangeExtension ) {
732807 node . id . mutableRange . end = makeInstructionId (
733808 Math . max ( node . id . mutableRange . end , end ) ,
734809 ) ;
@@ -764,6 +839,7 @@ class AliasingState {
764839 direction : 'forwards' ,
765840 // Traversing a maybeAlias edge always downgrades to conditional mutation
766841 kind : edge . kind === 'maybeAlias' ? MutationKind . Conditional : kind ,
842+ skipRangeExtension : false ,
767843 } ) ;
768844 }
769845 for ( const [ alias , when ] of node . createdFrom ) {
@@ -775,6 +851,8 @@ class AliasingState {
775851 transitive : true ,
776852 direction : 'backwards' ,
777853 kind,
854+ // Propagate parent's skipRangeExtension but don't initiate it
855+ skipRangeExtension,
778856 } ) ;
779857 }
780858 if ( direction === 'backwards' || node . value . kind !== 'Phi' ) {
@@ -797,6 +875,16 @@ class AliasingState {
797875 transitive,
798876 direction : 'backwards' ,
799877 kind,
878+ /*
879+ * Skip range extension when traversing backward through alias
880+ * edges from a conditional method-call mutation. This prevents
881+ * the receiver's creation-site values from having their ranges
882+ * extended to cover the method call instruction, which would
883+ * otherwise cause them to be merged into the same scope.
884+ */
885+ skipRangeExtension :
886+ skipRangeExtension ||
887+ ( effectiveSkipAliases && current === start ) ,
800888 } ) ;
801889 }
802890 /**
@@ -815,6 +903,9 @@ class AliasingState {
815903 transitive,
816904 direction : 'backwards' ,
817905 kind : MutationKind . Conditional ,
906+ skipRangeExtension :
907+ skipRangeExtension ||
908+ ( effectiveSkipAliases && current === start ) ,
818909 } ) ;
819910 }
820911 }
@@ -831,6 +922,13 @@ class AliasingState {
831922 transitive,
832923 direction : 'backwards' ,
833924 kind,
925+ /*
926+ * Reset skipRangeExtension for capture edges. Captures
927+ * represent structural containment (e.g. a function closing
928+ * over a value), and the captured value's range must be
929+ * extended to cover the mutation point for correctness.
930+ */
931+ skipRangeExtension : false ,
834932 } ) ;
835933 }
836934 }
0 commit comments