import { CompilerError, ErrorSeverity } from "../CompilerError";
import { Environment } from "../HIR";
import {
AbstractValue,
BasicBlock,
BlockId,
CallExpression,
Effect,
FunctionEffect,
GeneratedSource,
HIRFunction,
IdentifierId,
InstructionKind,
InstructionValue,
MethodCall,
Phi,
Place,
SpreadPattern,
Type,
ValueKind,
ValueReason,
isArrayType,
isMutableEffect,
isObjectType,
isRefValueType,
isUseRefType,
} from "../HIR/HIR";
import { FunctionSignature } from "../HIR/ObjectShape";
import {
printIdentifier,
printMixedHIR,
printPlace,
printSourceLocation,
} from "../HIR/PrintHIR";
import {
eachCallArgument,
eachInstructionOperand,
eachInstructionValueOperand,
eachPatternOperand,
eachTerminalOperand,
eachTerminalSuccessor,
} from "../HIR/visitors";
import { assertExhaustive } from "../Utils/utils";
import { isEffectHook } from "../Validation/ValidateMemoizedEffectDependencies";
const UndefinedValue: InstructionValue = {
kind: "Primitive",
loc: GeneratedSource,
value: undefined,
};
export default function inferReferenceEffects(
fn: HIRFunction,
options: { isFunctionExpression: boolean } = { isFunctionExpression: false }
): void {
const initialState = InferenceState.empty(fn.env);
const value: InstructionValue = {
kind: "Primitive",
loc: fn.loc,
value: undefined,
};
initialState.initialize(value, {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
for (const ref of fn.context) {
const value: InstructionValue = {
kind: "ObjectExpression",
properties: [],
loc: ref.loc,
};
initialState.initialize(value, {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set([ref]),
});
initialState.define(ref, value);
}
const paramKind: AbstractValue = options.isFunctionExpression
? {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
}
: {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.ReactiveFunctionArgument]),
context: new Set(),
};
if (fn.fnType === "Component") {
CompilerError.invariant(fn.params.length <= 2, {
reason:
"Expected React component to have not more than two parameters: one for props and for ref",
description: null,
loc: fn.loc,
suggestions: null,
});
const [props, ref] = fn.params;
let value: InstructionValue;
let place: Place;
if (props) {
inferParam(props, initialState, paramKind);
}
if (ref) {
if (ref.kind === "Identifier") {
place = ref;
value = {
kind: "ObjectExpression",
properties: [],
loc: ref.loc,
};
} else {
place = ref.place;
value = {
kind: "ObjectExpression",
properties: [],
loc: ref.place.loc,
};
}
initialState.initialize(value, {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
initialState.define(place, value);
}
} else {
for (const param of fn.params) {
inferParam(param, initialState, paramKind);
}
}
const statesByBlock: Map<BlockId, InferenceState> = new Map();
const queuedStates: Map<BlockId, InferenceState> = new Map();
function queue(blockId: BlockId, state: InferenceState): void {
let queuedState = queuedStates.get(blockId);
if (queuedState != null) {
state = queuedState.merge(state) ?? queuedState;
queuedStates.set(blockId, state);
} else {
const prevState = statesByBlock.get(blockId);
const nextState = prevState != null ? prevState.merge(state) : state;
if (nextState != null) {
queuedStates.set(blockId, nextState);
}
}
}
queue(fn.body.entry, initialState);
const functionEffects: Array<FunctionEffect> = fn.effects ?? [];
while (queuedStates.size !== 0) {
for (const [blockId, block] of fn.body.blocks) {
const incomingState = queuedStates.get(blockId);
queuedStates.delete(blockId);
if (incomingState == null) {
continue;
}
statesByBlock.set(blockId, incomingState);
const state = incomingState.clone();
inferBlock(fn.env, functionEffects, state, block);
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
queue(nextBlockId, state);
}
}
}
if (!options.isFunctionExpression) {
functionEffects.forEach((eff) => {
switch (eff.kind) {
case "ReactMutation":
case "GlobalMutation": {
CompilerError.throw(eff.error);
}
case "ContextMutation": {
CompilerError.throw({
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
});
}
default:
assertExhaustive(
eff,
`Unexpected function effect kind \`${(eff as any).kind}\``
);
}
});
} else {
fn.effects = functionEffects;
}
}
class InferenceState {
#env: Environment;
#values: Map<InstructionValue, AbstractValue>;
#variables: Map<IdentifierId, Set<InstructionValue>>;
constructor(
env: Environment,
values: Map<InstructionValue, AbstractValue>,
variables: Map<IdentifierId, Set<InstructionValue>>
) {
this.#env = env;
this.#values = values;
this.#variables = variables;
}
static empty(env: Environment): InferenceState {
return new InferenceState(env, new Map(), new Map());
}
initialize(value: InstructionValue, kind: AbstractValue): void {
CompilerError.invariant(value.kind !== "LoadLocal", {
reason:
"Expected all top-level identifiers to be defined as variables, not values",
description: null,
loc: value.loc,
suggestions: null,
});
this.#values.set(value, kind);
}
values(place: Place): Array<InstructionValue> {
const values = this.#variables.get(place.identifier.id);
CompilerError.invariant(values != null, {
reason: `[hoisting] Expected value kind to be initialized`,
description: `${printPlace(place)}`,
loc: place.loc,
suggestions: null,
});
return Array.from(values);
}
kind(place: Place): AbstractValue {
const values = this.#variables.get(place.identifier.id);
CompilerError.invariant(values != null, {
reason: `[hoisting] Expected value kind to be initialized`,
description: `${printPlace(place)}`,
loc: place.loc,
suggestions: null,
});
let mergedKind: AbstractValue | null = null;
for (const value of values) {
const kind = this.#values.get(value)!;
mergedKind =
mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind;
}
CompilerError.invariant(mergedKind !== null, {
reason: `InferReferenceEffects::kind: Expected at least one value`,
description: `No value found at \`${printPlace(place)}\``,
loc: place.loc,
suggestions: null,
});
return mergedKind;
}
alias(place: Place, value: Place): void {
const values = this.#variables.get(value.identifier.id);
CompilerError.invariant(values != null, {
reason: `[hoisting] Expected value for identifier to be initialized`,
description: `${printIdentifier(value.identifier)}`,
loc: value.loc,
suggestions: null,
});
this.#variables.set(place.identifier.id, new Set(values));
}
define(place: Place, value: InstructionValue): void {
CompilerError.invariant(this.#values.has(value), {
reason: `Expected value to be initialized at '${printSourceLocation(
value.loc
)}'`,
description: null,
loc: value.loc,
suggestions: null,
});
this.#variables.set(place.identifier.id, new Set([value]));
}
isDefined(place: Place): boolean {
return this.#variables.has(place.identifier.id);
}
referenceAndRecordEffects(
place: Place,
effectKind: Effect,
reason: ValueReason,
functionEffects: Array<FunctionEffect>
): void {
const values = this.#variables.get(place.identifier.id);
if (values === undefined) {
CompilerError.invariant(effectKind !== Effect.Store, {
reason: "[InferReferenceEffects] Unhandled store reference effect",
description: null,
loc: place.loc,
suggestions: null,
});
place.effect =
effectKind === Effect.ConditionallyMutate
? Effect.ConditionallyMutate
: Effect.Read;
return;
}
for (const value of values) {
if (
(value.kind === "FunctionExpression" ||
value.kind === "ObjectMethod") &&
value.loweredFunc.func.effects != null
) {
for (const effect of value.loweredFunc.func.effects) {
if (
effect.kind === "GlobalMutation" ||
effect.kind === "ReactMutation"
) {
functionEffects.push(effect);
} else {
for (const place of effect.places) {
if (this.isDefined(place)) {
const replayedEffect = this.reference(
{ ...place, loc: effect.loc },
effect.effect,
reason
);
if (replayedEffect != null) {
if (replayedEffect.kind === "ContextMutation") {
functionEffects.push(effect);
} else {
functionEffects.push(replayedEffect);
}
}
}
}
}
}
}
}
const functionEffect = this.reference(place, effectKind, reason);
if (functionEffect !== null) {
functionEffects.push(functionEffect);
}
}
reference(
place: Place,
effectKind: Effect,
reason: ValueReason
): FunctionEffect | null {
const values = this.#variables.get(place.identifier.id);
CompilerError.invariant(values !== undefined, {
reason: "[InferReferenceEffects] Expected value to be initialized",
description: null,
loc: place.loc,
suggestions: null,
});
let valueKind: AbstractValue | null = this.kind(place);
let effect: Effect | null = null;
let functionEffect: FunctionEffect | null = null;
switch (effectKind) {
case Effect.Freeze: {
if (
valueKind.kind === ValueKind.Mutable ||
valueKind.kind === ValueKind.Context ||
valueKind.kind === ValueKind.MaybeFrozen
) {
const reasonSet = new Set([reason]);
effect = Effect.Freeze;
valueKind = {
kind: ValueKind.Frozen,
reason: reasonSet,
context: new Set(),
};
values.forEach((value) => {
this.#values.set(value, {
kind: ValueKind.Frozen,
reason: reasonSet,
context: new Set(),
});
if (
this.#env.config.enablePreserveExistingMemoizationGuarantees ||
this.#env.config.enableTransitivelyFreezeFunctionExpressions
) {
if (value.kind === "FunctionExpression") {
for (const operand of eachInstructionValueOperand(value)) {
this.referenceAndRecordEffects(
operand,
Effect.Freeze,
ValueReason.Other,
[]
);
}
}
}
});
} else {
effect = Effect.Read;
}
break;
}
case Effect.ConditionallyMutate: {
if (
valueKind.kind === ValueKind.Mutable ||
valueKind.kind === ValueKind.Context
) {
effect = Effect.ConditionallyMutate;
} else {
effect = Effect.Read;
}
break;
}
case Effect.Mutate: {
if (
isRefValueType(place.identifier) ||
isUseRefType(place.identifier)
) {
} else if (valueKind.kind === ValueKind.Context) {
functionEffect = {
kind: "ContextMutation",
loc: place.loc,
effect: effectKind,
places:
valueKind.context.size === 0
? new Set([place])
: valueKind.context,
};
} else if (
valueKind.kind !== ValueKind.Mutable &&
valueKind.kind !== ValueKind.Primitive
) {
let reason = getWriteErrorReason(valueKind);
functionEffect = {
kind:
valueKind.reason.size === 1 &&
valueKind.reason.has(ValueReason.Global)
? "GlobalMutation"
: "ReactMutation",
error: {
reason,
description:
place.identifier.name !== null &&
place.identifier.name.kind === "named"
? `Found mutation of \`${place.identifier.name.value}\``
: null,
loc: place.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
};
}
effect = Effect.Mutate;
break;
}
case Effect.Store: {
if (
isRefValueType(place.identifier) ||
isUseRefType(place.identifier)
) {
} else if (valueKind.kind === ValueKind.Context) {
functionEffect = {
kind: "ContextMutation",
loc: place.loc,
effect: effectKind,
places:
valueKind.context.size === 0
? new Set([place])
: valueKind.context,
};
} else if (
valueKind.kind !== ValueKind.Mutable &&
valueKind.kind !== ValueKind.Primitive
) {
let reason = getWriteErrorReason(valueKind);
functionEffect = {
kind:
valueKind.reason.size === 1 &&
valueKind.reason.has(ValueReason.Global)
? "GlobalMutation"
: "ReactMutation",
error: {
reason,
description:
place.identifier.name !== null &&
place.identifier.name.kind === "named"
? `Found mutation of \`${place.identifier.name.value}\``
: null,
loc: place.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
};
}
effect = isObjectType(place.identifier) ? Effect.Store : Effect.Mutate;
break;
}
case Effect.Capture: {
if (
valueKind.kind === ValueKind.Primitive ||
valueKind.kind === ValueKind.Global ||
valueKind.kind === ValueKind.Frozen ||
valueKind.kind === ValueKind.MaybeFrozen
) {
effect = Effect.Read;
} else {
effect = Effect.Capture;
}
break;
}
case Effect.Read: {
effect = Effect.Read;
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason:
"Unexpected unknown effect, expected to infer a precise effect kind",
description: null,
loc: place.loc,
suggestions: null,
});
}
default: {
assertExhaustive(
effectKind,
`Unexpected reference kind \`${effectKind as any as string}\``
);
}
}
CompilerError.invariant(effect !== null, {
reason: "Expected effect to be set",
description: null,
loc: place.loc,
suggestions: null,
});
place.effect = effect;
return functionEffect;
}
merge(other: InferenceState): InferenceState | null {
let nextValues: Map<InstructionValue, AbstractValue> | null = null;
let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
for (const [id, thisValue] of this.#values) {
const otherValue = other.#values.get(id);
if (otherValue !== undefined) {
const mergedValue = mergeAbstractValues(thisValue, otherValue);
if (mergedValue !== thisValue) {
nextValues = nextValues ?? new Map(this.#values);
nextValues.set(id, mergedValue);
}
}
}
for (const [id, otherValue] of other.#values) {
if (this.#values.has(id)) {
continue;
}
nextValues = nextValues ?? new Map(this.#values);
nextValues.set(id, otherValue);
}
for (const [id, thisValues] of this.#variables) {
const otherValues = other.#variables.get(id);
if (otherValues !== undefined) {
let mergedValues: Set<InstructionValue> | null = null;
for (const otherValue of otherValues) {
if (!thisValues.has(otherValue)) {
mergedValues = mergedValues ?? new Set(thisValues);
mergedValues.add(otherValue);
}
}
if (mergedValues !== null) {
nextVariables = nextVariables ?? new Map(this.#variables);
nextVariables.set(id, mergedValues);
}
}
}
for (const [id, otherValues] of other.#variables) {
if (this.#variables.has(id)) {
continue;
}
nextVariables = nextVariables ?? new Map(this.#variables);
nextVariables.set(id, new Set(otherValues));
}
if (nextVariables === null && nextValues === null) {
return null;
} else {
return new InferenceState(
this.#env,
nextValues ?? new Map(this.#values),
nextVariables ?? new Map(this.#variables)
);
}
}
clone(): InferenceState {
return new InferenceState(
this.#env,
new Map(this.#values),
new Map(this.#variables)
);
}
debug(): any {
const result: any = { values: {}, variables: {} };
const objects: Map<InstructionValue, number> = new Map();
function identify(value: InstructionValue): number {
let id = objects.get(value);
if (id == null) {
id = objects.size;
objects.set(value, id);
}
return id;
}
for (const [value, kind] of this.#values) {
const id = identify(value);
result.values[id] = { kind, value: printMixedHIR(value) };
}
for (const [variable, values] of this.#variables) {
result.variables[`$${variable}`] = [...values].map(identify);
}
return result;
}
inferPhi(phi: Phi): void {
const values: Set<InstructionValue> = new Set();
for (const [_, operand] of phi.operands) {
const operandValues = this.#variables.get(operand.id);
if (operandValues === undefined) continue;
for (const v of operandValues) {
values.add(v);
}
}
if (values.size > 0) {
this.#variables.set(phi.id.id, values);
}
}
}
function inferParam(
param: Place | SpreadPattern,
initialState: InferenceState,
paramKind: AbstractValue
): void {
let value: InstructionValue;
let place: Place;
if (param.kind === "Identifier") {
place = param;
value = {
kind: "Primitive",
loc: param.loc,
value: undefined,
};
} else {
place = param.place;
value = {
kind: "Primitive",
loc: param.place.loc,
value: undefined,
};
}
initialState.initialize(value, paramKind);
initialState.define(place, value);
}
function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
if (a === b) {
return a;
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
return ValueKind.MaybeFrozen;
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.MaybeFrozen;
} else if (a === ValueKind.Context || b === ValueKind.Context) {
return ValueKind.Context;
} else {
return ValueKind.Mutable;
}
} else if (a === ValueKind.Context || b === ValueKind.Context) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.MaybeFrozen;
} else {
return ValueKind.Context;
}
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.Frozen;
} else if (a === ValueKind.Global || b === ValueKind.Global) {
return ValueKind.Global;
} else {
CompilerError.invariant(
a === ValueKind.Primitive && b == ValueKind.Primitive,
{
reason: `Unexpected value kind in mergeValues()`,
description: `Found kinds ${a} and ${b}`,
loc: GeneratedSource,
}
);
return ValueKind.Primitive;
}
}
function isSuperset<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
for (const v of b) {
if (!a.has(v)) {
return false;
}
}
return true;
}
function mergeAbstractValues(
a: AbstractValue,
b: AbstractValue
): AbstractValue {
const kind = mergeValues(a.kind, b.kind);
if (
kind === a.kind &&
kind === b.kind &&
isSuperset(a.reason, b.reason) &&
isSuperset(a.context, b.context)
) {
return a;
}
const reason = new Set(a.reason);
for (const r of b.reason) {
reason.add(r);
}
const context = new Set(a.context);
for (const c of b.context) {
context.add(c);
}
return { kind, reason, context };
}
function inferBlock(
env: Environment,
functionEffects: Array<FunctionEffect>,
state: InferenceState,
block: BasicBlock
): void {
for (const phi of block.phis) {
state.inferPhi(phi);
}
for (const instr of block.instructions) {
const instrValue = instr.value;
let effect: { kind: Effect; reason: ValueReason } | null = null;
let lvalueEffect = Effect.ConditionallyMutate;
let valueKind: AbstractValue;
switch (instrValue.kind) {
case "BinaryExpression": {
valueKind = {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = {
kind: Effect.Read,
reason: ValueReason.Other,
};
break;
}
case "ArrayExpression": {
valueKind = hasContextRefOperand(state, instrValue)
? {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set(),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = { kind: Effect.Capture, reason: ValueReason.Other };
lvalueEffect = Effect.Store;
break;
}
case "NewExpression": {
valueKind = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
state.referenceAndRecordEffects(
instrValue.callee,
Effect.Read,
ValueReason.Other,
functionEffects
);
for (const operand of eachCallArgument(instrValue.args)) {
state.referenceAndRecordEffects(
operand,
Effect.ConditionallyMutate,
ValueReason.Other,
functionEffects
);
}
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = lvalueEffect;
continue;
}
case "ObjectExpression": {
valueKind = hasContextRefOperand(state, instrValue)
? {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set(),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
for (const property of instrValue.properties) {
switch (property.kind) {
case "ObjectProperty": {
if (property.key.kind === "computed") {
state.referenceAndRecordEffects(
property.key.name,
Effect.Freeze,
ValueReason.Other,
functionEffects
);
}
state.referenceAndRecordEffects(
property.place,
Effect.Capture,
ValueReason.Other,
functionEffects
);
break;
}
case "Spread": {
state.referenceAndRecordEffects(
property.place,
Effect.Capture,
ValueReason.Other,
functionEffects
);
break;
}
default: {
assertExhaustive(
property,
`Unexpected property kind \`${(property as any).kind}\``
);
}
}
}
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.Store;
continue;
}
case "UnaryExpression": {
valueKind = {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = { kind: Effect.Read, reason: ValueReason.Other };
break;
}
case "UnsupportedNode": {
valueKind = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
break;
}
case "JsxExpression": {
if (instrValue.tag.kind === "Identifier") {
state.referenceAndRecordEffects(
instrValue.tag,
Effect.Freeze,
ValueReason.JsxCaptured,
functionEffects
);
}
if (instrValue.children !== null) {
for (const child of instrValue.children) {
state.referenceAndRecordEffects(
child,
Effect.Freeze,
ValueReason.JsxCaptured,
functionEffects
);
}
}
for (const attr of instrValue.props) {
if (attr.kind === "JsxSpreadAttribute") {
state.referenceAndRecordEffects(
attr.argument,
Effect.Freeze,
ValueReason.JsxCaptured,
functionEffects
);
} else {
const propEffects: Array<FunctionEffect> = [];
state.referenceAndRecordEffects(
attr.place,
Effect.Freeze,
ValueReason.JsxCaptured,
propEffects
);
functionEffects.push(
...propEffects.filter(
(propEffect) => propEffect.kind !== "GlobalMutation"
)
);
}
}
state.initialize(instrValue, {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.ConditionallyMutate;
continue;
}
case "JsxFragment": {
valueKind = {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = {
kind: Effect.Freeze,
reason: ValueReason.Other,
};
break;
}
case "TaggedTemplateExpression": {
valueKind = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = {
kind: Effect.ConditionallyMutate,
reason: ValueReason.Other,
};
break;
}
case "TemplateLiteral": {
valueKind = {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = { kind: Effect.Read, reason: ValueReason.Other };
break;
}
case "RegExpLiteral": {
valueKind = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = {
kind: Effect.ConditionallyMutate,
reason: ValueReason.Other,
};
break;
}
case "MetaProperty": {
if (instrValue.meta !== "import" || instrValue.property !== "meta") {
continue;
}
valueKind = {
kind: ValueKind.Global,
reason: new Set([ValueReason.Global]),
context: new Set(),
};
break;
}
case "LoadGlobal":
valueKind = {
kind: ValueKind.Global,
reason: new Set([ValueReason.Global]),
context: new Set(),
};
break;
case "Debugger":
case "JSXText":
case "Primitive": {
valueKind = {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
break;
}
case "ObjectMethod":
case "FunctionExpression": {
let hasMutableOperand = false;
for (const operand of eachInstructionOperand(instr)) {
state.referenceAndRecordEffects(
operand,
operand.effect === Effect.Unknown ? Effect.Read : operand.effect,
ValueReason.Other,
[]
);
hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
const values = state.values(operand);
for (const value of values) {
if (
(value.kind === "ObjectMethod" ||
value.kind === "FunctionExpression") &&
value.loweredFunc.func.effects !== null
) {
instrValue.loweredFunc.func.effects ??= [];
instrValue.loweredFunc.func.effects.push(
...value.loweredFunc.func.effects
);
}
}
}
state.initialize(instrValue, {
kind: hasMutableOperand ? ValueKind.Mutable : ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.Store;
continue;
}
case "CallExpression": {
const signature = getFunctionCallSignature(
env,
instrValue.callee.identifier.type
);
const effects =
signature !== null ? getFunctionEffects(instrValue, signature) : null;
const returnValueKind: AbstractValue =
signature !== null
? {
kind: signature.returnValueKind,
reason: new Set([
signature.returnValueReason ??
ValueReason.KnownReturnSignature,
]),
context: new Set(),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
let hasCaptureArgument = false;
let isUseEffect = isEffectHook(instrValue.callee.identifier);
for (let i = 0; i < instrValue.args.length; i++) {
const argumentEffects: Array<FunctionEffect> = [];
const arg = instrValue.args[i];
const place = arg.kind === "Identifier" ? arg : arg.place;
if (effects !== null) {
state.referenceAndRecordEffects(
place,
effects[i],
ValueReason.Other,
argumentEffects
);
} else {
state.referenceAndRecordEffects(
place,
Effect.ConditionallyMutate,
ValueReason.Other,
argumentEffects
);
}
functionEffects.push(
...argumentEffects.filter(
(argEffect) =>
!isUseEffect || i !== 0 || argEffect.kind !== "GlobalMutation"
)
);
hasCaptureArgument ||= place.effect === Effect.Capture;
}
if (signature !== null) {
state.referenceAndRecordEffects(
instrValue.callee,
signature.calleeEffect,
ValueReason.Other,
functionEffects
);
} else {
state.referenceAndRecordEffects(
instrValue.callee,
Effect.ConditionallyMutate,
ValueReason.Other,
functionEffects
);
}
hasCaptureArgument ||= instrValue.callee.effect === Effect.Capture;
state.initialize(instrValue, returnValueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = hasCaptureArgument
? Effect.Store
: Effect.ConditionallyMutate;
continue;
}
case "MethodCall": {
CompilerError.invariant(state.isDefined(instrValue.receiver), {
reason:
"[InferReferenceEffects] Internal error: receiver of PropertyCall should have been defined by corresponding PropertyLoad",
description: null,
loc: instrValue.loc,
suggestions: null,
});
state.referenceAndRecordEffects(
instrValue.property,
Effect.Read,
ValueReason.Other,
functionEffects
);
const signature = getFunctionCallSignature(
env,
instrValue.property.identifier.type
);
const returnValueKind: AbstractValue =
signature !== null
? {
kind: signature.returnValueKind,
reason: new Set([ValueReason.Other]),
context: new Set(),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
if (
signature !== null &&
signature.mutableOnlyIfOperandsAreMutable &&
areArgumentsImmutableAndNonMutating(state, instrValue.args)
) {
for (const arg of instrValue.args) {
const place = arg.kind === "Identifier" ? arg : arg.place;
state.referenceAndRecordEffects(
place,
Effect.Read,
ValueReason.Other,
functionEffects
);
}
state.referenceAndRecordEffects(
instrValue.receiver,
Effect.Capture,
ValueReason.Other,
functionEffects
);
state.initialize(instrValue, returnValueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect =
instrValue.receiver.effect === Effect.Capture
? Effect.Store
: Effect.ConditionallyMutate;
continue;
}
const effects =
signature !== null ? getFunctionEffects(instrValue, signature) : null;
let hasCaptureArgument = false;
let isUseEffect = isEffectHook(instrValue.property.identifier);
for (let i = 0; i < instrValue.args.length; i++) {
const argumentEffects: Array<FunctionEffect> = [];
const arg = instrValue.args[i];
const place = arg.kind === "Identifier" ? arg : arg.place;
if (effects !== null) {
state.referenceAndRecordEffects(
place,
effects[i],
ValueReason.Other,
argumentEffects
);
} else {
state.referenceAndRecordEffects(
place,
Effect.ConditionallyMutate,
ValueReason.Other,
argumentEffects
);
}
functionEffects.push(
...argumentEffects.filter(
(argEffect) =>
!isUseEffect || i !== 0 || argEffect.kind !== "GlobalMutation"
)
);
hasCaptureArgument ||= place.effect === Effect.Capture;
}
if (signature !== null) {
state.referenceAndRecordEffects(
instrValue.receiver,
signature.calleeEffect,
ValueReason.Other,
functionEffects
);
} else {
state.referenceAndRecordEffects(
instrValue.receiver,
Effect.ConditionallyMutate,
ValueReason.Other,
functionEffects
);
}
hasCaptureArgument ||= instrValue.receiver.effect === Effect.Capture;
state.initialize(instrValue, returnValueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = hasCaptureArgument
? Effect.Store
: Effect.ConditionallyMutate;
continue;
}
case "PropertyStore": {
const effect =
state.kind(instrValue.object).kind === ValueKind.Context
? Effect.ConditionallyMutate
: Effect.Capture;
state.referenceAndRecordEffects(
instrValue.value,
effect,
ValueReason.Other,
functionEffects
);
state.referenceAndRecordEffects(
instrValue.object,
Effect.Store,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
continue;
}
case "PropertyDelete": {
valueKind = {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
effect = { kind: Effect.Mutate, reason: ValueReason.Other };
break;
}
case "PropertyLoad": {
state.referenceAndRecordEffects(
instrValue.object,
Effect.Read,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.initialize(instrValue, state.kind(instrValue.object));
state.define(lvalue, instrValue);
continue;
}
case "ComputedStore": {
const effect =
state.kind(instrValue.object).kind === ValueKind.Context
? Effect.ConditionallyMutate
: Effect.Capture;
state.referenceAndRecordEffects(
instrValue.value,
effect,
ValueReason.Other,
functionEffects
);
state.referenceAndRecordEffects(
instrValue.property,
Effect.Capture,
ValueReason.Other,
functionEffects
);
state.referenceAndRecordEffects(
instrValue.object,
Effect.Store,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
continue;
}
case "ComputedDelete": {
state.referenceAndRecordEffects(
instrValue.object,
Effect.Mutate,
ValueReason.Other,
functionEffects
);
state.referenceAndRecordEffects(
instrValue.property,
Effect.Read,
ValueReason.Other,
functionEffects
);
state.initialize(instrValue, {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.Mutate;
continue;
}
case "ComputedLoad": {
state.referenceAndRecordEffects(
instrValue.object,
Effect.Read,
ValueReason.Other,
functionEffects
);
state.referenceAndRecordEffects(
instrValue.property,
Effect.Read,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.initialize(instrValue, state.kind(instrValue.object));
state.define(lvalue, instrValue);
continue;
}
case "Await": {
state.initialize(instrValue, state.kind(instrValue.value));
state.referenceAndRecordEffects(
instrValue.value,
Effect.ConditionallyMutate,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.alias(lvalue, instrValue.value);
continue;
}
case "TypeCastExpression": {
state.initialize(instrValue, state.kind(instrValue.value));
state.referenceAndRecordEffects(
instrValue.value,
Effect.Read,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.alias(lvalue, instrValue.value);
continue;
}
case "StartMemoize":
case "FinishMemoize": {
for (const val of eachInstructionValueOperand(instrValue)) {
if (env.config.enablePreserveExistingMemoizationGuarantees) {
state.referenceAndRecordEffects(
val,
Effect.Freeze,
ValueReason.Other,
[]
);
} else {
state.referenceAndRecordEffects(
val,
Effect.Read,
ValueReason.Other,
[]
);
}
}
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
state.initialize(instrValue, {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
state.define(lvalue, instrValue);
continue;
}
case "LoadLocal": {
const lvalue = instr.lvalue;
const effect =
state.isDefined(lvalue) &&
state.kind(lvalue).kind === ValueKind.Context
? Effect.ConditionallyMutate
: Effect.Capture;
state.referenceAndRecordEffects(
instrValue.place,
effect,
ValueReason.Other,
[]
);
lvalue.effect = Effect.ConditionallyMutate;
state.alias(lvalue, instrValue.place);
continue;
}
case "LoadContext": {
state.referenceAndRecordEffects(
instrValue.place,
Effect.Capture,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
const valueKind = state.kind(instrValue.place);
state.initialize(instrValue, valueKind);
state.define(lvalue, instrValue);
continue;
}
case "DeclareLocal": {
const value = UndefinedValue;
state.initialize(
value,
instrValue.lvalue.kind === InstructionKind.Catch
? {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
}
: {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
}
);
state.define(instrValue.lvalue.place, value);
continue;
}
case "DeclareContext": {
state.initialize(instrValue, {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
state.define(instrValue.lvalue.place, instrValue);
continue;
}
case "PostfixUpdate":
case "PrefixUpdate": {
const effect =
state.isDefined(instrValue.lvalue) &&
state.kind(instrValue.lvalue).kind === ValueKind.Context
? Effect.ConditionallyMutate
: Effect.Capture;
state.referenceAndRecordEffects(
instrValue.value,
effect,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
state.alias(instrValue.lvalue, instrValue.value);
instrValue.lvalue.effect = Effect.Store;
continue;
}
case "StoreLocal": {
const effect =
state.isDefined(instrValue.lvalue.place) &&
state.kind(instrValue.lvalue.place).kind === ValueKind.Context
? Effect.ConditionallyMutate
: Effect.Capture;
state.referenceAndRecordEffects(
instrValue.value,
effect,
ValueReason.Other,
[]
);
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
state.alias(instrValue.lvalue.place, instrValue.value);
instrValue.lvalue.place.effect = Effect.Store;
continue;
}
case "StoreContext": {
state.referenceAndRecordEffects(
instrValue.value,
Effect.ConditionallyMutate,
ValueReason.Other,
functionEffects
);
state.referenceAndRecordEffects(
instrValue.lvalue.place,
Effect.Mutate,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
continue;
}
case "StoreGlobal": {
state.referenceAndRecordEffects(
instrValue.value,
Effect.Capture,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.Store;
functionEffects.push({
kind: "GlobalMutation",
error: {
reason:
"Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)",
loc: instr.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
});
continue;
}
case "Destructure": {
let effect: Effect = Effect.Capture;
for (const place of eachPatternOperand(instrValue.lvalue.pattern)) {
if (
state.isDefined(place) &&
state.kind(place).kind === ValueKind.Context
) {
effect = Effect.ConditionallyMutate;
break;
}
}
state.referenceAndRecordEffects(
instrValue.value,
effect,
ValueReason.Other,
functionEffects
);
const lvalue = instr.lvalue;
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
for (const place of eachPatternOperand(instrValue.lvalue.pattern)) {
state.alias(place, instrValue.value);
place.effect = Effect.Store;
}
continue;
}
case "GetIterator": {
const kind = state.kind(instrValue.collection).kind;
const isMutable =
kind === ValueKind.Mutable || kind === ValueKind.Context;
if (!isMutable || isArrayType(instrValue.collection.identifier)) {
effect = {
kind: Effect.Read,
reason: ValueReason.Other,
};
valueKind = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
} else {
effect = {
kind: Effect.Capture,
reason: ValueReason.Other,
};
valueKind = state.kind(instrValue.collection);
}
lvalueEffect = Effect.Store;
break;
}
case "IteratorNext": {
state.referenceAndRecordEffects(
instrValue.iterator,
Effect.ConditionallyMutate,
ValueReason.Other,
functionEffects
);
state.referenceAndRecordEffects(
instrValue.collection,
Effect.Capture,
ValueReason.Other,
functionEffects
);
state.initialize(instrValue, state.kind(instrValue.collection));
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.Store;
continue;
}
case "NextPropertyOf": {
effect = { kind: Effect.Read, reason: ValueReason.Other };
lvalueEffect = Effect.Store;
valueKind = {
kind: ValueKind.Primitive,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
break;
}
default: {
assertExhaustive(instrValue, "Unexpected instruction kind");
}
}
for (const operand of eachInstructionOperand(instr)) {
CompilerError.invariant(effect != null, {
reason: `effectKind must be set for instruction value \`${instrValue.kind}\``,
description: null,
loc: instrValue.loc,
suggestions: null,
});
state.referenceAndRecordEffects(
operand,
effect.kind,
effect.reason,
functionEffects
);
}
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = lvalueEffect;
}
for (const operand of eachTerminalOperand(block.terminal)) {
let effect;
if (block.terminal.kind === "return" || block.terminal.kind === "throw") {
if (
state.isDefined(operand) &&
state.kind(operand).kind === ValueKind.Context
) {
effect = Effect.ConditionallyMutate;
} else {
effect = Effect.Freeze;
}
} else {
effect = Effect.Read;
}
state.referenceAndRecordEffects(
operand,
effect,
ValueReason.Other,
functionEffects
);
}
}
function hasContextRefOperand(
state: InferenceState,
instrValue: InstructionValue
): boolean {
for (const place of eachInstructionValueOperand(instrValue)) {
if (
state.isDefined(place) &&
state.kind(place).kind === ValueKind.Context
) {
return true;
}
}
return false;
}
export function getFunctionCallSignature(
env: Environment,
type: Type
): FunctionSignature | null {
if (type.kind !== "Function") {
return null;
}
return env.getFunctionSignature(type);
}
function getFunctionEffects(
fn: MethodCall | CallExpression,
sig: FunctionSignature
): Array<Effect> | null {
const results = [];
for (let i = 0; i < fn.args.length; i++) {
const arg = fn.args[i];
if (i < sig.positionalParams.length) {
if (arg.kind === "Identifier") {
results.push(sig.positionalParams[i]);
} else {
return null;
}
} else if (sig.restParam !== null) {
results.push(sig.restParam);
} else {
return null;
}
}
return results;
}
function areArgumentsImmutableAndNonMutating(
state: InferenceState,
args: MethodCall["args"]
): boolean {
for (const arg of args) {
const place = arg.kind === "Identifier" ? arg : arg.place;
const kind = state.kind(place).kind;
switch (kind) {
case ValueKind.Global:
case ValueKind.Primitive:
case ValueKind.Frozen: {
break;
}
default: {
return false;
}
}
const values = state.values(place);
for (const value of values) {
if (
value.kind === "FunctionExpression" &&
value.loweredFunc.func.params.some((param) => {
const place = param.kind === "Identifier" ? param : param.place;
const range = place.identifier.mutableRange;
return range.end > range.start + 1;
})
) {
return false;
}
}
}
return true;
}
function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return "Writing to a variable defined outside a component or hook is not allowed. Consider using an effect";
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return "Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX";
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Mutating a value returned from 'useContext()', which should not be mutated`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return "Mutating a value returned from a function whose return value should not be mutated";
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return "Mutating component props or hook arguments is not allowed. Consider using a local variable instead";
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
} else {
return "This mutates a variable that React considers immutable";
}
}