@@ -19,6 +19,7 @@ import {
1919 Environment ,
2020 FunctionExpression ,
2121 GeneratedSource ,
22+ getHookKind ,
2223 HIRFunction ,
2324 Hole ,
2425 IdentifierId ,
@@ -198,6 +199,7 @@ export function inferMutationAliasingEffects(
198199 isFunctionExpression ,
199200 fn ,
200201 hoistedContextDeclarations ,
202+ findNonMutatedDestructureSpreads ( fn ) ,
201203 ) ;
202204
203205 let iterationCount = 0 ;
@@ -287,15 +289,18 @@ class Context {
287289 isFuctionExpression : boolean ;
288290 fn : HIRFunction ;
289291 hoistedContextDeclarations : Map < DeclarationId , Place | null > ;
292+ nonMutatingSpreads : Set < IdentifierId > ;
290293
291294 constructor (
292295 isFunctionExpression : boolean ,
293296 fn : HIRFunction ,
294297 hoistedContextDeclarations : Map < DeclarationId , Place | null > ,
298+ nonMutatingSpreads : Set < IdentifierId > ,
295299 ) {
296300 this . isFuctionExpression = isFunctionExpression ;
297301 this . fn = fn ;
298302 this . hoistedContextDeclarations = hoistedContextDeclarations ;
303+ this . nonMutatingSpreads = nonMutatingSpreads ;
299304 }
300305
301306 cacheApplySignature (
@@ -322,6 +327,161 @@ class Context {
322327 }
323328}
324329
330+ /**
331+ * Finds objects created via ObjectPattern spread destructuring
332+ * (`const {x, ...spread} = ...`) where a) the rvalue is known frozen and
333+ * b) the spread value cannot possibly be directly mutated. The idea is that
334+ * for this set of values, we can treat the spread object as frozen.
335+ *
336+ * The primary use case for this is props spreading:
337+ *
338+ * ```
339+ * function Component({prop, ...otherProps}) {
340+ * const transformedProp = transform(prop, otherProps.foo);
341+ * // pass `otherProps` down:
342+ * return <Foo {...otherProps} prop={transformedProp} />;
343+ * }
344+ * ```
345+ *
346+ * Here we know that since `otherProps` cannot be mutated, we don't have to treat
347+ * it as mutable: `otherProps.foo` only reads a value that must be frozen, so it
348+ * can be treated as frozen too.
349+ */
350+ function findNonMutatedDestructureSpreads ( fn : HIRFunction ) : Set < IdentifierId > {
351+ const knownFrozen = new Set < IdentifierId > ( ) ;
352+ if ( fn . fnType === 'Component' ) {
353+ const [ props ] = fn . params ;
354+ if ( props != null && props . kind === 'Identifier' ) {
355+ knownFrozen . add ( props . identifier . id ) ;
356+ }
357+ } else {
358+ for ( const param of fn . params ) {
359+ if ( param . kind === 'Identifier' ) {
360+ knownFrozen . add ( param . identifier . id ) ;
361+ }
362+ }
363+ }
364+
365+ // Map of temporaries to identifiers for spread objects
366+ const candidateNonMutatingSpreads = new Map < IdentifierId , IdentifierId > ( ) ;
367+ for ( const block of fn . body . blocks . values ( ) ) {
368+ if ( candidateNonMutatingSpreads . size !== 0 ) {
369+ for ( const phi of block . phis ) {
370+ for ( const operand of phi . operands . values ( ) ) {
371+ const spread = candidateNonMutatingSpreads . get ( operand . identifier . id ) ;
372+ if ( spread != null ) {
373+ candidateNonMutatingSpreads . delete ( spread ) ;
374+ }
375+ }
376+ }
377+ }
378+ for ( const instr of block . instructions ) {
379+ const { lvalue, value} = instr ;
380+ switch ( value . kind ) {
381+ case 'Destructure' : {
382+ if (
383+ ! knownFrozen . has ( value . value . identifier . id ) ||
384+ ! (
385+ value . lvalue . kind === InstructionKind . Let ||
386+ value . lvalue . kind === InstructionKind . Const
387+ ) ||
388+ value . lvalue . pattern . kind !== 'ObjectPattern'
389+ ) {
390+ continue ;
391+ }
392+ for ( const item of value . lvalue . pattern . properties ) {
393+ if ( item . kind !== 'Spread' ) {
394+ continue ;
395+ }
396+ candidateNonMutatingSpreads . set (
397+ item . place . identifier . id ,
398+ item . place . identifier . id ,
399+ ) ;
400+ }
401+ break ;
402+ }
403+ case 'LoadLocal' : {
404+ const spread = candidateNonMutatingSpreads . get (
405+ value . place . identifier . id ,
406+ ) ;
407+ if ( spread != null ) {
408+ candidateNonMutatingSpreads . set ( lvalue . identifier . id , spread ) ;
409+ }
410+ break ;
411+ }
412+ case 'StoreLocal' : {
413+ const spread = candidateNonMutatingSpreads . get (
414+ value . value . identifier . id ,
415+ ) ;
416+ if ( spread != null ) {
417+ candidateNonMutatingSpreads . set ( lvalue . identifier . id , spread ) ;
418+ candidateNonMutatingSpreads . set (
419+ value . lvalue . place . identifier . id ,
420+ spread ,
421+ ) ;
422+ }
423+ break ;
424+ }
425+ case 'JsxFragment' :
426+ case 'JsxExpression' : {
427+ // Passing objects created with spread to jsx can't mutate them
428+ break ;
429+ }
430+ case 'PropertyLoad' : {
431+ // Properties must be frozen since the original value was frozen
432+ break ;
433+ }
434+ case 'CallExpression' :
435+ case 'MethodCall' : {
436+ const callee =
437+ value . kind === 'CallExpression' ? value . callee : value . property ;
438+ if ( getHookKind ( fn . env , callee . identifier ) != null ) {
439+ // Hook calls have frozen arguments, and non-ref returns are frozen
440+ if ( ! isRefOrRefValue ( lvalue . identifier ) ) {
441+ knownFrozen . add ( lvalue . identifier . id ) ;
442+ }
443+ } else {
444+ // Non-hook calls check their operands, since they are potentially mutable
445+ if ( candidateNonMutatingSpreads . size !== 0 ) {
446+ // Otherwise any reference to the spread object itself may mutate
447+ for ( const operand of eachInstructionValueOperand ( value ) ) {
448+ const spread = candidateNonMutatingSpreads . get (
449+ operand . identifier . id ,
450+ ) ;
451+ if ( spread != null ) {
452+ candidateNonMutatingSpreads . delete ( spread ) ;
453+ }
454+ }
455+ }
456+ }
457+ break ;
458+ }
459+ default : {
460+ if ( candidateNonMutatingSpreads . size !== 0 ) {
461+ // Otherwise any reference to the spread object itself may mutate
462+ for ( const operand of eachInstructionValueOperand ( value ) ) {
463+ const spread = candidateNonMutatingSpreads . get (
464+ operand . identifier . id ,
465+ ) ;
466+ if ( spread != null ) {
467+ candidateNonMutatingSpreads . delete ( spread ) ;
468+ }
469+ }
470+ }
471+ }
472+ }
473+ }
474+ }
475+
476+ const nonMutatingSpreads = new Set < IdentifierId > ( ) ;
477+ for ( const [ key , value ] of candidateNonMutatingSpreads ) {
478+ if ( key === value ) {
479+ nonMutatingSpreads . add ( key ) ;
480+ }
481+ }
482+ return nonMutatingSpreads ;
483+ }
484+
325485function inferParam (
326486 param : Place | SpreadPattern ,
327487 initialState : InferenceState ,
@@ -2054,7 +2214,9 @@ function computeSignatureForInstruction(
20542214 kind : 'Create' ,
20552215 into : place ,
20562216 reason : ValueReason . Other ,
2057- value : ValueKind . Mutable ,
2217+ value : context . nonMutatingSpreads . has ( place . identifier . id )
2218+ ? ValueKind . Frozen
2219+ : ValueKind . Mutable ,
20582220 } ) ;
20592221 effects . push ( {
20602222 kind : 'Capture' ,
0 commit comments