@@ -103,6 +103,7 @@ export function inferMutationAliasingRanges(
103103 kind : MutationKind ;
104104 place : Place ;
105105 reason : MutationReason | null ;
106+ noBackwardRangeExtension : boolean ;
106107 } > = [ ] ;
107108 const renders : Array < { index : number ; place : Place } > = [ ] ;
108109
@@ -180,6 +181,9 @@ export function inferMutationAliasingRanges(
180181 : MutationKind . Conditional ,
181182 reason : null ,
182183 place : effect . value ,
184+ noBackwardRangeExtension :
185+ effect . kind === 'MutateTransitiveConditionally' &&
186+ ( effect . noBackwardRangeExtension ?? false ) ,
183187 } ) ;
184188 } else if (
185189 effect . kind === 'Mutate' ||
@@ -195,6 +199,7 @@ export function inferMutationAliasingRanges(
195199 : MutationKind . Conditional ,
196200 reason : effect . kind === 'Mutate' ? ( effect . reason ?? null ) : null ,
197201 place : effect . value ,
202+ noBackwardRangeExtension : false ,
198203 } ) ;
199204 } else if (
200205 effect . kind === 'MutateFrozen' ||
@@ -249,6 +254,7 @@ export function inferMutationAliasingRanges(
249254 mutation . place . loc ,
250255 mutation . reason ,
251256 shouldRecordErrors ? fn . env : null ,
257+ mutation . noBackwardRangeExtension ,
252258 ) ;
253259 }
254260 for ( const render of renders ) {
@@ -711,16 +717,85 @@ class AliasingState {
711717 loc : SourceLocation ,
712718 reason : MutationReason | null ,
713719 env : Environment | null ,
720+ noBackwardRangeExtension : boolean = false ,
714721 ) : void {
722+ /*
723+ * Only skip backward-alias range extension when ALL of these hold:
724+ * 1. The effect requested noBackwardRangeExtension (MethodCall receiver)
725+ * 2. The start node is not a PropertyLoad result (no createdFrom edges),
726+ * since PropertyLoad receivers need backward range extension for
727+ * correctness (e.g. `x.y.push(val)` must extend x and y).
728+ * 3. At least one of the start's backward alias targets was produced by
729+ * a function call (indicated by having maybeAliases from the default
730+ * Apply handler). This distinguishes `expensiveProcessing(data).map(fn)`
731+ * from `localArray.push(val)` — only the former should skip, since the
732+ * call result represents a separately-memoizable value.
733+ */
734+ const startNode = this . nodes . get ( start ) ;
735+ let effectiveSkipAliases = false ;
736+ if (
737+ noBackwardRangeExtension &&
738+ startNode != null &&
739+ startNode . createdFrom . size === 0
740+ ) {
741+ /*
742+ * Walk backward through aliases from the start node to check if
743+ * any value in the chain was produced by a function call (Apply).
744+ * Apply results are identifiable by having maybeAliases, since the
745+ * default Apply handler records MaybeAlias from each argument to
746+ * the result. Only walk through alias edges (not maybeAliases or
747+ * captures) to avoid matching catch parameters and similar patterns
748+ * where range extension is required for correctness.
749+ */
750+ const visited = new Set < Identifier > ( ) ;
751+ const aliasQueue : Array < Identifier > = [ start ] ;
752+ while ( aliasQueue . length > 0 ) {
753+ const id = aliasQueue . pop ( ) ! ;
754+ if ( visited . has ( id ) ) {
755+ continue ;
756+ }
757+ visited . add ( id ) ;
758+ const node = this . nodes . get ( id ) ;
759+ if ( node == null ) {
760+ continue ;
761+ }
762+ if ( node . maybeAliases . size > 0 ) {
763+ effectiveSkipAliases = true ;
764+ break ;
765+ }
766+ for ( const [ nextAliasId ] of node . aliases ) {
767+ aliasQueue . push ( nextAliasId ) ;
768+ }
769+ }
770+ }
715771 const seen = new Map < Identifier , MutationKind > ( ) ;
716772 const queue : Array < {
717773 place : Identifier ;
718774 transitive : boolean ;
719775 direction : 'backwards' | 'forwards' ;
720776 kind : MutationKind ;
721- } > = [ { place : start , transitive, direction : 'backwards' , kind : startKind } ] ;
777+ /*
778+ * When true, skip extending the mutable range for this node. This
779+ * is set for nodes reached via backward alias/maybeAlias edges when
780+ * effectiveSkipAliases is enabled, so that the receiver's backward
781+ * aliases are not pulled into the same reactive scope as the method
782+ * call. Nodes reached via capture edges reset this flag, since
783+ * captures represent structural containment (e.g. function closures)
784+ * where range extension is required for correctness.
785+ */
786+ skipRangeExtension : boolean ;
787+ } > = [
788+ {
789+ place : start ,
790+ transitive,
791+ direction : 'backwards' ,
792+ kind : startKind ,
793+ skipRangeExtension : false ,
794+ } ,
795+ ] ;
722796 while ( queue . length !== 0 ) {
723- const { place : current , transitive, direction, kind} = queue . pop ( ) ! ;
797+ const { place : current , transitive, direction, kind, skipRangeExtension} =
798+ queue . pop ( ) ! ;
724799 const previousKind = seen . get ( current ) ;
725800 if ( previousKind != null && previousKind >= kind ) {
726801 continue ;
@@ -732,7 +807,7 @@ class AliasingState {
732807 }
733808 node . mutationReason ??= reason ;
734809 node . lastMutated = Math . max ( node . lastMutated , index ) ;
735- if ( end != null ) {
810+ if ( end != null && ! skipRangeExtension ) {
736811 node . id . mutableRange . end = makeInstructionId (
737812 Math . max ( node . id . mutableRange . end , end ) ,
738813 ) ;
@@ -768,6 +843,7 @@ class AliasingState {
768843 direction : 'forwards' ,
769844 // Traversing a maybeAlias edge always downgrades to conditional mutation
770845 kind : edge . kind === 'maybeAlias' ? MutationKind . Conditional : kind ,
846+ skipRangeExtension : false ,
771847 } ) ;
772848 }
773849 for ( const [ alias , when ] of node . createdFrom ) {
@@ -779,6 +855,8 @@ class AliasingState {
779855 transitive : true ,
780856 direction : 'backwards' ,
781857 kind,
858+ // Propagate parent's skipRangeExtension but don't initiate it
859+ skipRangeExtension,
782860 } ) ;
783861 }
784862 if ( direction === 'backwards' || node . value . kind !== 'Phi' ) {
@@ -801,6 +879,16 @@ class AliasingState {
801879 transitive,
802880 direction : 'backwards' ,
803881 kind,
882+ /*
883+ * Skip range extension when traversing backward through alias
884+ * edges from a conditional method-call mutation. This prevents
885+ * the receiver's creation-site values from having their ranges
886+ * extended to cover the method call instruction, which would
887+ * otherwise cause them to be merged into the same scope.
888+ */
889+ skipRangeExtension :
890+ skipRangeExtension ||
891+ ( effectiveSkipAliases && current === start ) ,
804892 } ) ;
805893 }
806894 /**
@@ -819,6 +907,9 @@ class AliasingState {
819907 transitive,
820908 direction : 'backwards' ,
821909 kind : MutationKind . Conditional ,
910+ skipRangeExtension :
911+ skipRangeExtension ||
912+ ( effectiveSkipAliases && current === start ) ,
822913 } ) ;
823914 }
824915 }
@@ -835,6 +926,13 @@ class AliasingState {
835926 transitive,
836927 direction : 'backwards' ,
837928 kind,
929+ /*
930+ * Reset skipRangeExtension for capture edges. Captures
931+ * represent structural containment (e.g. a function closing
932+ * over a value), and the captured value's range must be
933+ * extended to cover the mutation point for correctness.
934+ */
935+ skipRangeExtension : false ,
838936 } ) ;
839937 }
840938 }
0 commit comments