77 Set_union ,
88 getOrInsertDefault ,
99} from '../Utils/utils' ;
10+ import { collectOptionalChainSidemap } from './CollectOptionalChainDependencies' ;
1011import {
1112 BasicBlock ,
1213 BlockId ,
@@ -15,10 +16,12 @@ import {
1516 HIRFunction ,
1617 Identifier ,
1718 IdentifierId ,
19+ InstructionId ,
1820 InstructionValue ,
1921 ReactiveScopeDependency ,
2022 ScopeId ,
2123} from './HIR' ;
24+ import { collectTemporariesSidemap } from './PropagateScopeDependenciesHIR' ;
2225
2326/**
2427 * Helper function for `PropagateScopeDependencies`. Uses control flow graph
@@ -83,28 +86,57 @@ export function collectHoistablePropertyLoads(
8386 fn : HIRFunction ,
8487 temporaries : ReadonlyMap < IdentifierId , ReactiveScopeDependency > ,
8588 hoistableFromOptionals : ReadonlyMap < BlockId , ReactiveScopeDependency > ,
86- ) : ReadonlyMap < ScopeId , BlockInfo > {
89+ nestedFnImmutableContext : ReadonlySet < IdentifierId > | null ,
90+ ) : ReadonlyMap < BlockId , BlockInfo > {
8791 const registry = new PropertyPathRegistry ( ) ;
8892
89- const nodes = collectNonNullsInBlocks (
90- fn ,
91- temporaries ,
93+ const functionExpressionLoads = collectFunctionExpressionFakeLoads ( fn ) ;
94+ const actuallyEvaluatedTemporaries = new Map (
95+ [ ...temporaries ] . filter ( ( [ id ] ) => ! functionExpressionLoads . has ( id ) ) ,
96+ ) ;
97+
98+ /**
99+ * Due to current limitations of mutable range inference, there are edge cases in
100+ * which we infer known-immutable values (e.g. props or hook params) to have a
101+ * mutable range and scope.
102+ * (see `destructure-array-declaration-to-context-var` fixture)
103+ * We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
104+ * is being rewritten to HIR).
105+ */
106+ const knownImmutableIdentifiers = new Set < IdentifierId > ( ) ;
107+ if ( fn . fnType === 'Component' || fn . fnType === 'Hook' ) {
108+ for ( const p of fn . params ) {
109+ if ( p . kind === 'Identifier' ) {
110+ knownImmutableIdentifiers . add ( p . identifier . id ) ;
111+ }
112+ }
113+ }
114+ const nodes = collectNonNullsInBlocks ( fn , {
115+ temporaries : actuallyEvaluatedTemporaries ,
116+ knownImmutableIdentifiers,
92117 hoistableFromOptionals,
93118 registry,
94- ) ;
119+ nestedFnImmutableContext,
120+ } ) ;
95121 propagateNonNull ( fn , nodes , registry ) ;
96122
97- const nodesKeyedByScopeId = new Map < ScopeId , BlockInfo > ( ) ;
123+ return nodes ;
124+ }
125+
126+ export function keyByScopeId < T > (
127+ fn : HIRFunction ,
128+ source : ReadonlyMap < BlockId , T > ,
129+ ) : ReadonlyMap < ScopeId , T > {
130+ const keyedByScopeId = new Map < ScopeId , T > ( ) ;
98131 for ( const [ _ , block ] of fn . body . blocks ) {
99132 if ( block . terminal . kind === 'scope' ) {
100- nodesKeyedByScopeId . set (
133+ keyedByScopeId . set (
101134 block . terminal . scope . id ,
102- nodes . get ( block . terminal . block ) ! ,
135+ source . get ( block . terminal . block ) ! ,
103136 ) ;
104137 }
105138 }
106-
107- return nodesKeyedByScopeId ;
139+ return keyedByScopeId ;
108140}
109141
110142export type BlockInfo = {
@@ -211,45 +243,75 @@ class PropertyPathRegistry {
211243
212244function getMaybeNonNullInInstruction (
213245 instr : InstructionValue ,
214- temporaries : ReadonlyMap < IdentifierId , ReactiveScopeDependency > ,
215- registry : PropertyPathRegistry ,
246+ context : CollectNonNullsInBlocksContext ,
216247) : PropertyPathNode | null {
217248 let path = null ;
218249 if ( instr . kind === 'PropertyLoad' ) {
219- path = temporaries . get ( instr . object . identifier . id ) ?? {
250+ path = context . temporaries . get ( instr . object . identifier . id ) ?? {
220251 identifier : instr . object . identifier ,
221252 path : [ ] ,
222253 } ;
223254 } else if ( instr . kind === 'Destructure' ) {
224- path = temporaries . get ( instr . value . identifier . id ) ?? null ;
255+ path = context . temporaries . get ( instr . value . identifier . id ) ?? null ;
225256 } else if ( instr . kind === 'ComputedLoad' ) {
226- path = temporaries . get ( instr . object . identifier . id ) ?? null ;
257+ path = context . temporaries . get ( instr . object . identifier . id ) ?? null ;
258+ }
259+ return path != null ? context . registry . getOrCreateProperty ( path ) : null ;
260+ }
261+
262+ function isImmutableAtInstr (
263+ identifier : Identifier ,
264+ instr : InstructionId ,
265+ context : CollectNonNullsInBlocksContext ,
266+ ) : boolean {
267+ if ( context . nestedFnImmutableContext != null ) {
268+ /**
269+ * Comparing instructions ids across inner-outer function bodies is not valid, as they are numbered
270+ */
271+ return context . nestedFnImmutableContext . has ( identifier . id ) ;
272+ } else {
273+ /**
274+ * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
275+ * are not valid with respect to current instruction id numbering.
276+ * We use attached reactive scope ranges as a proxy for mutable range, but this
277+ * is an overestimate as (1) scope ranges merge and align to form valid program
278+ * blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
279+ * non-mutable identifiers.
280+ *
281+ * See comment in exported function for why we track known immutable identifiers.
282+ */
283+ const mutableAtInstr =
284+ identifier . mutableRange . end > identifier . mutableRange . start + 1 &&
285+ identifier . scope != null &&
286+ inRange (
287+ {
288+ id : instr ,
289+ } ,
290+ identifier . scope . range ,
291+ ) ;
292+ return (
293+ ! mutableAtInstr || context . knownImmutableIdentifiers . has ( identifier . id )
294+ ) ;
227295 }
228- return path != null ? registry . getOrCreateProperty ( path ) : null ;
229296}
230297
298+ type CollectNonNullsInBlocksContext = {
299+ temporaries : ReadonlyMap < IdentifierId , ReactiveScopeDependency > ;
300+ knownImmutableIdentifiers : ReadonlySet < IdentifierId > ;
301+ hoistableFromOptionals : ReadonlyMap < BlockId , ReactiveScopeDependency > ;
302+ registry : PropertyPathRegistry ;
303+ /**
304+ * (For nested / inner function declarations)
305+ * Context variables (i.e. captured from an outer scope) that are immutable.
306+ * Note that this technically could be merged into `knownImmutableIdentifiers`,
307+ * but are currently kept separate for readability.
308+ */
309+ nestedFnImmutableContext : ReadonlySet < IdentifierId > | null ;
310+ } ;
231311function collectNonNullsInBlocks (
232312 fn : HIRFunction ,
233- temporaries : ReadonlyMap < IdentifierId , ReactiveScopeDependency > ,
234- hoistableFromOptionals : ReadonlyMap < BlockId , ReactiveScopeDependency > ,
235- registry : PropertyPathRegistry ,
313+ context : CollectNonNullsInBlocksContext ,
236314) : ReadonlyMap < BlockId , BlockInfo > {
237- /**
238- * Due to current limitations of mutable range inference, there are edge cases in
239- * which we infer known-immutable values (e.g. props or hook params) to have a
240- * mutable range and scope.
241- * (see `destructure-array-declaration-to-context-var` fixture)
242- * We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
243- * is being rewritten to HIR).
244- */
245- const knownImmutableIdentifiers = new Set < IdentifierId > ( ) ;
246- if ( fn . fnType === 'Component' || fn . fnType === 'Hook' ) {
247- for ( const p of fn . params ) {
248- if ( p . kind === 'Identifier' ) {
249- knownImmutableIdentifiers . add ( p . identifier . id ) ;
250- }
251- }
252- }
253315 /**
254316 * Known non-null objects such as functional component props can be safely
255317 * read from any block.
@@ -261,53 +323,58 @@ function collectNonNullsInBlocks(
261323 fn . params [ 0 ] . kind === 'Identifier'
262324 ) {
263325 const identifier = fn . params [ 0 ] . identifier ;
264- knownNonNullIdentifiers . add ( registry . getOrCreateIdentifier ( identifier ) ) ;
326+ knownNonNullIdentifiers . add (
327+ context . registry . getOrCreateIdentifier ( identifier ) ,
328+ ) ;
265329 }
266330 const nodes = new Map < BlockId , BlockInfo > ( ) ;
267331 for ( const [ _ , block ] of fn . body . blocks ) {
268332 const assumedNonNullObjects = new Set < PropertyPathNode > (
269333 knownNonNullIdentifiers ,
270334 ) ;
271335
272- const maybeOptionalChain = hoistableFromOptionals . get ( block . id ) ;
336+ const maybeOptionalChain = context . hoistableFromOptionals . get ( block . id ) ;
273337 if ( maybeOptionalChain != null ) {
274338 assumedNonNullObjects . add (
275- registry . getOrCreateProperty ( maybeOptionalChain ) ,
339+ context . registry . getOrCreateProperty ( maybeOptionalChain ) ,
276340 ) ;
277341 }
278342 for ( const instr of block . instructions ) {
279- const maybeNonNull = getMaybeNonNullInInstruction (
280- instr . value ,
281- temporaries ,
282- registry ,
283- ) ;
284- if ( maybeNonNull != null ) {
285- const baseIdentifier = maybeNonNull . fullPath . identifier ;
286- /**
287- * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
288- * are not valid with respect to current instruction id numbering.
289- * We use attached reactive scope ranges as a proxy for mutable range, but this
290- * is an overestimate as (1) scope ranges merge and align to form valid program
291- * blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
292- * non-mutable identifiers.
293- *
294- * See comment at top of function for why we track known immutable identifiers.
295- */
296- const isMutableAtInstr =
297- baseIdentifier . mutableRange . end >
298- baseIdentifier . mutableRange . start + 1 &&
299- baseIdentifier . scope != null &&
300- inRange (
301- {
302- id : instr . id ,
303- } ,
304- baseIdentifier . scope . range ,
305- ) ;
306- if (
307- ! isMutableAtInstr ||
308- knownImmutableIdentifiers . has ( baseIdentifier . id )
309- ) {
310- assumedNonNullObjects . add ( maybeNonNull ) ;
343+ const maybeNonNull = getMaybeNonNullInInstruction ( instr . value , context ) ;
344+ if (
345+ maybeNonNull != null &&
346+ isImmutableAtInstr ( maybeNonNull . fullPath . identifier , instr . id , context )
347+ ) {
348+ assumedNonNullObjects . add ( maybeNonNull ) ;
349+ }
350+ if (
351+ instr . value . kind === 'FunctionExpression' &&
352+ ! fn . env . config . enableTreatFunctionDepsAsConditional
353+ ) {
354+ const innerFn = instr . value . loweredFunc ;
355+ const innerTemporaries = collectTemporariesSidemap (
356+ innerFn . func ,
357+ new Set ( ) ,
358+ ) ;
359+ const innerOptionals = collectOptionalChainSidemap ( innerFn . func ) ;
360+ const innerHoistableMap = collectHoistablePropertyLoads (
361+ innerFn . func ,
362+ innerTemporaries ,
363+ innerOptionals . hoistableObjects ,
364+ context . nestedFnImmutableContext ??
365+ new Set (
366+ innerFn . func . context
367+ . filter ( place =>
368+ isImmutableAtInstr ( place . identifier , instr . id , context ) ,
369+ )
370+ . map ( place => place . identifier . id ) ,
371+ ) ,
372+ ) ;
373+ const innerHoistables = assertNonNull (
374+ innerHoistableMap . get ( innerFn . func . body . entry ) ,
375+ ) ;
376+ for ( const entry of innerHoistables . assumedNonNullObjects ) {
377+ assumedNonNullObjects . add ( entry ) ;
311378 }
312379 }
313380 }
@@ -515,3 +582,27 @@ function reduceMaybeOptionalChains(
515582 }
516583 } while ( changed ) ;
517584}
585+
586+ function collectFunctionExpressionFakeLoads (
587+ fn : HIRFunction ,
588+ ) : Set < IdentifierId > {
589+ const sources = new Map < IdentifierId , IdentifierId > ( ) ;
590+ const functionExpressionReferences = new Set < IdentifierId > ( ) ;
591+
592+ for ( const [ _ , block ] of fn . body . blocks ) {
593+ for ( const { lvalue, value} of block . instructions ) {
594+ if ( value . kind === 'FunctionExpression' ) {
595+ for ( const reference of value . loweredFunc . dependencies ) {
596+ let curr : IdentifierId | undefined = reference . identifier . id ;
597+ while ( curr != null ) {
598+ functionExpressionReferences . add ( curr ) ;
599+ curr = sources . get ( curr ) ;
600+ }
601+ }
602+ } else if ( value . kind === 'PropertyLoad' ) {
603+ sources . set ( lvalue . identifier . id , value . object . identifier . id ) ;
604+ }
605+ }
606+ }
607+ return functionExpressionReferences ;
608+ }
0 commit comments