[compiler] Remove redundant InferMutableContextVariables (#32097)

This removes special casing for `PropertyStore` mutability inference
within FunctionExpressions.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32097).
* #32287
* #32104
* #32098
* __->__ #32097
This commit is contained in:
mofeiZ
2025-02-18 09:37:21 -07:00
committed by GitHub
parent d99f8bba2e
commit a92acdb188
5 changed files with 55 additions and 175 deletions
@@ -10,7 +10,6 @@ import {
Effect,
HIRFunction,
Identifier,
IdentifierId,
LoweredFunction,
isRefOrRefValue,
makeInstructionId,
@@ -18,27 +17,9 @@ import {
import {deadCodeElimination} from '../Optimization';
import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
import {inferMutableContextVariables} from './InferMutableContextVariables';
import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
// Helper class to track indirections such as LoadLocal and PropertyLoad.
export class IdentifierState {
properties: Map<IdentifierId, Identifier> = new Map();
resolve(identifier: Identifier): Identifier {
const resolved = this.properties.get(identifier.id);
if (resolved !== undefined) {
return resolved;
}
return identifier;
}
alias(lvalue: Identifier, value: Identifier): void {
this.properties.set(lvalue.id, this.properties.get(value.id) ?? value);
}
}
export default function analyseFunctions(func: HIRFunction): void {
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
@@ -78,7 +59,6 @@ function lower(func: HIRFunction): void {
}
function infer(loweredFunc: LoweredFunction): void {
const knownMutated = inferMutableContextVariables(loweredFunc.func);
for (const operand of loweredFunc.func.context) {
const identifier = operand.identifier;
CompilerError.invariant(operand.effect === Effect.Unknown, {
@@ -95,10 +75,11 @@ function infer(loweredFunc: LoweredFunction): void {
* render
*/
operand.effect = Effect.Capture;
} else if (knownMutated.has(operand)) {
operand.effect = Effect.Mutate;
} else if (isMutatedOrReassigned(identifier)) {
// Note that this also reflects if identifier is ConditionallyMutated
/**
* Reflects direct reassignments, PropertyStores, and ConditionallyMutate
* (directly or through maybe-aliases)
*/
operand.effect = Effect.Capture;
} else {
operand.effect = Effect.Read;
@@ -1,105 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Effect, HIRFunction, Identifier, Place} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {IdentifierState} from './AnalyseFunctions';
/*
* This pass infers which of the given function's context (free) variables
* are definitively mutated by the function. This analysis is *partial*,
* and only annotates provable mutations, and may miss mutations via indirections.
* The intent of this pass is to drive validations, rejecting known-bad code
* while avoiding false negatives, and the inference should *not* be used to
* drive changes in output.
*
* Note that a complete analysis is possible but would have too many false negatives.
* The approach would be to run LeaveSSA and InferReactiveScopeVariables in order to
* find all possible aliases of a context variable which may be mutated. However, this
* can lead to false negatives:
*
* ```
* const [x, setX] = useState(null); // x is frozen
* const fn = () => { // context=[x]
* const z = {}; // z is mutable
* foo(z, x); // potentially mutate z and x
* z.a = true; // definitively mutate z
* }
* fn();
* ```
*
* When we analyze function expressions we assume that context variables are mutable,
* so we assume that `x` is mutable. We infer that `foo(z, x)` could be mutating the
* two variables to alias each other, such that `z.a = true` could be mutating `x`,
* and we would infer that `x` is definitively mutated. Then when we run
* InferReferenceEffects on the outer code we'd reject it, since there is a definitive
* mutation of a frozen value.
*
* Thus the actual implementation looks at only basic aliasing. The above example would
* pass, since z does not directly alias `x`. However, mutations through trivial aliases
* are detected:
*
* ```
* const [x, setX] = useState(null); // x is frozen
* const fn = () => { // context=[x]
* const z = x;
* z.a = true; // ERROR: mutates x
* }
* fn();
* ```
*/
export function inferMutableContextVariables(fn: HIRFunction): Set<Place> {
const state = new IdentifierState();
const knownMutatedIdentifiers = new Set<Identifier>();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'PropertyLoad':
case 'ComputedLoad': {
state.alias(instr.lvalue.identifier, instr.value.object.identifier);
break;
}
case 'LoadLocal':
case 'LoadContext': {
if (instr.lvalue.identifier.name === null) {
state.alias(instr.lvalue.identifier, instr.value.place.identifier);
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(instr.value)) {
visitOperand(state, knownMutatedIdentifiers, operand);
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visitOperand(state, knownMutatedIdentifiers, operand);
}
}
const results = new Set<Place>();
for (const operand of fn.context) {
if (knownMutatedIdentifiers.has(operand.identifier)) {
results.add(operand);
}
}
return results;
}
function visitOperand(
state: IdentifierState,
knownMutatedIdentifiers: Set<Identifier>,
operand: Place,
): void {
const resolved = state.resolve(operand.identifier);
if (operand.effect === Effect.Mutate || operand.effect === Effect.Store) {
knownMutatedIdentifiers.add(resolved);
}
}