react/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts
Joseph Savona a39da6c61f
[compiler] Use new diagnostics for core inference errors (#33760)
Uses the new diagnostic type for errors created during mutation/aliasing
inference, such as errors for mutating immutable values like props or
state, reassigning globals, etc.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33760).
* #33981
* #33777
* #33767
* #33765
* __->__ #33760
2025-07-24 15:43:08 -07:00

245 lines
7.7 KiB
TypeScript

/**
* 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 {CompilerDiagnostic} from '../CompilerError';
import {
FunctionExpression,
GeneratedSource,
Hole,
IdentifierId,
ObjectMethod,
Place,
SourceLocation,
SpreadPattern,
ValueKind,
ValueReason,
} from '../HIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {printSourceLocation} from '../HIR/PrintHIR';
/**
* `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
* more values in a program. These effects include mutation of values, freezing values,
* tracking data flow between values, and other specialized cases.
*/
export type AliasingEffect =
/**
* Marks the given value and its direct aliases as frozen.
*
* Captured values are *not* considered frozen, because we cannot be sure that a previously
* captured value will still be captured at the point of the freeze.
*
* For example:
* const x = {};
* const y = [x];
* y.pop(); // y dosn't contain x anymore!
* freeze(y);
* mutate(x); // safe to mutate!
*
* The exception to this is FunctionExpressions - since it is impossible to change which
* value a function closes over[1] we can transitively freeze functions and their captures.
*
* [1] Except for `let` values that are reassigned and closed over by a function, but we
* handle this explicitly with StoreContext/LoadContext.
*/
| {kind: 'Freeze'; value: Place; reason: ValueReason}
/**
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
*/
| {kind: 'Mutate'; value: Place}
/**
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
* This should be rare.
*
* TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more
* correct for iterators of unknown types.
*/
| {kind: 'MutateConditionally'; value: Place}
/**
* Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable.
*/
| {kind: 'MutateTransitive'; value: Place}
/**
* Mutates any of the value, its direct aliases, and its transitive captures that are mutable.
*/
| {kind: 'MutateTransitiveConditionally'; value: Place}
/**
* Records information flow from `from` to `into` in cases where local mutation of the destination
* will *not* mutate the source:
*
* - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a)
* - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a)
*
* Example: `array.push(item)`. Information from item is captured into array, but there is not a
* direct aliasing, and local mutations of array will not modify item.
*/
| {kind: 'Capture'; from: Place; into: Place}
/**
* Records information flow from `from` to `into` in cases where local mutation of the destination
* *will* mutate the source:
*
* - Alias a -> b and Mutate(b) => (does imply) Mutate(a)
* - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a)
*
* Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign.
* But we have to assume that it _could_ be returning its input, such that a local mutation of
* c could be mutating a.
*/
| {kind: 'Alias'; from: Place; into: Place}
/**
* Records direct assignment: `into = from`.
*/
| {kind: 'Assign'; from: Place; into: Place}
/**
* Creates a value of the given type at the given place
*/
| {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason}
/**
* Creates a new value with the same kind as the starting value.
*/
| {kind: 'CreateFrom'; from: Place; into: Place}
/**
* Immutable data flow, used for escape analysis. Does not influence mutable range analysis:
*/
| {kind: 'ImmutableCapture'; from: Place; into: Place}
/**
* Calls the function at the given place with the given arguments either captured or aliased,
* and captures/aliases the result into the given place.
*/
| {
kind: 'Apply';
receiver: Place;
function: Place;
mutatesFunction: boolean;
args: Array<Place | SpreadPattern | Hole>;
into: Place;
signature: FunctionSignature | null;
loc: SourceLocation;
}
/**
* Constructs a function value with the given captures. The mutability of the function
* will be determined by the mutability of the capture values when evaluated.
*/
| {
kind: 'CreateFunction';
captures: Array<Place>;
function: FunctionExpression | ObjectMethod;
into: Place;
}
/**
* Mutation of a value known to be immutable
*/
| {kind: 'MutateFrozen'; place: Place; error: CompilerDiagnostic}
/**
* Mutation of a global
*/
| {
kind: 'MutateGlobal';
place: Place;
error: CompilerDiagnostic;
}
/**
* Indicates a side-effect that is not safe during render
*/
| {kind: 'Impure'; place: Place; error: CompilerDiagnostic}
/**
* Indicates that a given place is accessed during render. Used to distingush
* hook arguments that are known to be called immediately vs those used for
* event handlers/effects, and for JSX values known to be called during render
* (tags, children) vs those that may be events/effect (other props).
*/
| {
kind: 'Render';
place: Place;
};
export function hashEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Apply': {
return [
effect.kind,
effect.receiver.identifier.id,
effect.function.identifier.id,
effect.mutatesFunction,
effect.args
.map(a => {
if (a.kind === 'Hole') {
return '';
} else if (a.kind === 'Identifier') {
return a.identifier.id;
} else {
return `...${a.place.identifier.id}`;
}
})
.join(','),
effect.into.identifier.id,
].join(':');
}
case 'CreateFrom':
case 'ImmutableCapture':
case 'Assign':
case 'Alias':
case 'Capture': {
return [
effect.kind,
effect.from.identifier.id,
effect.into.identifier.id,
].join(':');
}
case 'Create': {
return [
effect.kind,
effect.into.identifier.id,
effect.value,
effect.reason,
].join(':');
}
case 'Freeze': {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'Impure':
case 'Render': {
return [effect.kind, effect.place.identifier.id].join(':');
}
case 'MutateFrozen':
case 'MutateGlobal': {
return [
effect.kind,
effect.place.identifier.id,
effect.error.severity,
effect.error.category,
effect.error.description,
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
].join(':');
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return [effect.kind, effect.value.identifier.id].join(':');
}
case 'CreateFunction': {
return [
effect.kind,
effect.into.identifier.id,
// return places are a unique way to identify functions themselves
effect.function.loweredFunc.func.returns.identifier.id,
effect.captures.map(p => p.identifier.id).join(','),
].join(':');
}
}
}
export type AliasingSignature = {
receiver: IdentifierId;
params: Array<IdentifierId>;
rest: IdentifierId | null;
returns: IdentifierId;
effects: Array<AliasingEffect>;
temporaries: Array<Place>;
};