import {
CompilerError,
Effect,
ErrorSeverity,
SourceLocation,
ValueKind,
} from '..';
import {
BasicBlock,
BlockId,
DeclarationId,
Environment,
FunctionExpression,
HIRFunction,
Hole,
IdentifierId,
Instruction,
InstructionKind,
InstructionValue,
isArrayType,
isMapType,
isPrimitiveType,
isRefOrRefValue,
isSetType,
makeIdentifierId,
Phi,
Place,
SpreadPattern,
ValueReason,
} from '../HIR';
import {
eachInstructionValueLValue,
eachInstructionValueOperand,
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
import {
getArgumentEffect,
getFunctionCallSignature,
isKnownMutableEffect,
mergeValueKinds,
} from './InferReferenceEffects';
import {
assertExhaustive,
getOrInsertDefault,
getOrInsertWith,
Set_isSuperset,
} from '../Utils/utils';
import {
printAliasingEffect,
printAliasingSignature,
printIdentifier,
printInstruction,
printInstructionValue,
printPlace,
printSourceLocation,
} from '../HIR/PrintHIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {getWriteErrorReason} from './InferFunctionEffects';
import prettyFormat from 'pretty-format';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
const DEBUG = false;
export function inferMutationAliasingEffects(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean} = {
isFunctionExpression: false,
},
): Result<void, CompilerError> {
const initialState = InferenceState.empty(fn.env, isFunctionExpression);
const statesByBlock: Map<BlockId, InferenceState> = new Map();
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]),
});
initialState.define(ref, value);
}
const paramKind: AbstractValue = isFunctionExpression
? {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
}
: {
kind: ValueKind.Frozen,
reason: new Set([ValueReason.ReactiveFunctionArgument]),
};
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;
if (props != null) {
inferParam(props, initialState, paramKind);
}
if (ref != null) {
const place = ref.kind === 'Identifier' ? ref : ref.place;
const value: InstructionValue = {
kind: 'ObjectExpression',
properties: [],
loc: place.loc,
};
initialState.initialize(value, {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
});
initialState.define(place, value);
}
} else {
for (const param of fn.params) {
inferParam(param, initialState, paramKind);
}
}
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 hoistedContextDeclarations = findHoistedContextDeclarations(fn);
const context = new Context(
isFunctionExpression,
fn,
hoistedContextDeclarations,
);
let iterationCount = 0;
while (queuedStates.size !== 0) {
iterationCount++;
if (iterationCount > 100) {
CompilerError.invariant(false, {
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
description: `A value, temporary place, or effect was not cached properly`,
loc: fn.loc,
});
}
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(context, state, block);
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
queue(nextBlockId, state);
}
}
}
return Ok(undefined);
}
function findHoistedContextDeclarations(
fn: HIRFunction,
): Map<DeclarationId, Place | null> {
const hoisted = new Map<DeclarationId, Place | null>();
function visit(place: Place): void {
if (
hoisted.has(place.identifier.declarationId) &&
hoisted.get(place.identifier.declarationId) == null
) {
hoisted.set(place.identifier.declarationId, place);
}
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
if (instr.value.kind === 'DeclareContext') {
const kind = instr.value.lvalue.kind;
if (
kind == InstructionKind.HoistedConst ||
kind == InstructionKind.HoistedFunction ||
kind == InstructionKind.HoistedLet
) {
hoisted.set(instr.value.lvalue.place.identifier.declarationId, null);
}
} else {
for (const operand of eachInstructionValueOperand(instr.value)) {
visit(operand);
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visit(operand);
}
}
return hoisted;
}
class Context {
internedEffects: Map<string, AliasingEffect> = new Map();
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
new Map();
applySignatureCache: Map<
AliasingSignature,
Map<AliasingEffect, Array<AliasingEffect> | null>
> = new Map();
catchHandlers: Map<BlockId, Place> = new Map();
functionSignatureCache: Map<FunctionExpression, AliasingSignature> =
new Map();
isFuctionExpression: boolean;
fn: HIRFunction;
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
constructor(
isFunctionExpression: boolean,
fn: HIRFunction,
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
) {
this.isFuctionExpression = isFunctionExpression;
this.fn = fn;
this.hoistedContextDeclarations = hoistedContextDeclarations;
}
cacheApplySignature(
signature: AliasingSignature,
effect: Extract<AliasingEffect, {kind: 'Apply'}>,
f: () => Array<AliasingEffect> | null,
): Array<AliasingEffect> | null {
const inner = getOrInsertDefault(
this.applySignatureCache,
signature,
new Map(),
);
return getOrInsertWith(inner, effect, f);
}
internEffect(effect: AliasingEffect): AliasingEffect {
const hash = hashEffect(effect);
let interned = this.internedEffects.get(hash);
if (interned == null) {
this.internedEffects.set(hash, effect);
interned = effect;
}
return interned;
}
}
function inferParam(
param: Place | SpreadPattern,
initialState: InferenceState,
paramKind: AbstractValue,
): void {
const place = param.kind === 'Identifier' ? param : param.place;
const value: InstructionValue = {
kind: 'Primitive',
loc: place.loc,
value: undefined,
};
initialState.initialize(value, paramKind);
initialState.define(place, value);
}
function inferBlock(
context: Context,
state: InferenceState,
block: BasicBlock,
): void {
for (const phi of block.phis) {
state.inferPhi(phi);
}
for (const instr of block.instructions) {
let instructionSignature = context.instructionSignatureCache.get(instr);
if (instructionSignature == null) {
instructionSignature = computeSignatureForInstruction(
context,
state.env,
instr,
);
context.instructionSignatureCache.set(instr, instructionSignature);
}
const effects = applySignature(context, state, instructionSignature, instr);
instr.effects = effects;
}
const terminal = block.terminal;
if (terminal.kind === 'try' && terminal.handlerBinding != null) {
context.catchHandlers.set(terminal.handler, terminal.handlerBinding);
} else if (terminal.kind === 'maybe-throw') {
const handlerParam = context.catchHandlers.get(terminal.handler);
if (handlerParam != null) {
CompilerError.invariant(state.kind(handlerParam) != null, {
reason:
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
loc: terminal.loc,
});
const effects: Array<AliasingEffect> = [];
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' ||
instr.value.kind === 'MethodCall'
) {
state.appendAlias(handlerParam, instr.lvalue);
const kind = state.kind(instr.lvalue).kind;
if (kind === ValueKind.Mutable || kind == ValueKind.Context) {
effects.push(
context.internEffect({
kind: 'Alias',
from: instr.lvalue,
into: handlerParam,
}),
);
}
}
}
terminal.effects = effects.length !== 0 ? effects : null;
}
} else if (terminal.kind === 'return') {
if (!context.isFuctionExpression) {
terminal.effects = [
context.internEffect({
kind: 'Freeze',
value: terminal.value,
reason: ValueReason.JsxCaptured,
}),
];
}
}
}
function applySignature(
context: Context,
state: InferenceState,
signature: InstructionSignature,
instruction: Instruction,
): Array<AliasingEffect> | null {
const effects: Array<AliasingEffect> = [];
if (
instruction.value.kind === 'FunctionExpression' ||
instruction.value.kind === 'ObjectMethod'
) {
const aliasingEffects =
instruction.value.loweredFunc.func.aliasingEffects ?? [];
const context = new Set(
instruction.value.loweredFunc.func.context.map(p => p.identifier.id),
);
for (const effect of aliasingEffects) {
if (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') {
if (!context.has(effect.value.identifier.id)) {
continue;
}
const value = state.kind(effect.value);
switch (value.kind) {
case ValueKind.Frozen: {
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
effects.push({
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason,
description:
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Found mutation of \`${effect.value.identifier.name.value}\``
: null,
loc: effect.value.loc,
suggestions: null,
},
});
}
}
}
}
}
const initialized = new Set<IdentifierId>();
if (DEBUG) {
console.log(printInstruction(instruction));
}
for (const effect of signature.effects) {
applyEffect(context, state, effect, initialized, effects);
}
if (DEBUG) {
console.log(
prettyFormat(state.debugAbstractValue(state.kind(instruction.lvalue))),
);
console.log(
effects.map(effect => ` ${printAliasingEffect(effect)}`).join('\n'),
);
}
if (
!(state.isDefined(instruction.lvalue) && state.kind(instruction.lvalue))
) {
CompilerError.invariant(false, {
reason: `Expected instruction lvalue to be initialized`,
loc: instruction.loc,
});
}
return effects.length !== 0 ? effects : null;
}
function applyEffect(
context: Context,
state: InferenceState,
_effect: AliasingEffect,
initialized: Set<IdentifierId>,
effects: Array<AliasingEffect>,
): void {
const effect = context.internEffect(_effect);
if (DEBUG) {
console.log(printAliasingEffect(effect));
}
switch (effect.kind) {
case 'Freeze': {
const didFreeze = state.freeze(effect.value, effect.reason);
if (didFreeze) {
effects.push(effect);
}
break;
}
case 'Create': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
kind: 'ObjectExpression',
properties: [],
loc: effect.into.loc,
};
context.effectInstructionValueCache.set(effect, value);
}
state.initialize(value, {
kind: effect.value,
reason: new Set([effect.reason]),
});
state.define(effect.into, value);
effects.push(effect);
break;
}
case 'ImmutableCapture': {
const kind = state.kind(effect.from).kind;
switch (kind) {
case ValueKind.Global:
case ValueKind.Primitive: {
break;
}
default: {
effects.push(effect);
}
}
break;
}
case 'CreateFrom': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
const fromValue = state.kind(effect.from);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
kind: 'ObjectExpression',
properties: [],
loc: effect.into.loc,
};
context.effectInstructionValueCache.set(effect, value);
}
state.initialize(value, {
kind: fromValue.kind,
reason: new Set(fromValue.reason),
});
state.define(effect.into, value);
switch (fromValue.kind) {
case ValueKind.Primitive:
case ValueKind.Global: {
effects.push({
kind: 'Create',
value: fromValue.kind,
into: effect.into,
reason: [...fromValue.reason][0] ?? ValueReason.Other,
});
break;
}
case ValueKind.Frozen: {
effects.push({
kind: 'Create',
value: fromValue.kind,
into: effect.into,
reason: [...fromValue.reason][0] ?? ValueReason.Other,
});
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
break;
}
default: {
effects.push(effect);
}
}
break;
}
case 'CreateFunction': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
effects.push(effect);
const hasCaptures = effect.captures.some(capture => {
switch (state.kind(capture).kind) {
case ValueKind.Context:
case ValueKind.Mutable: {
return true;
}
default: {
return false;
}
}
});
const hasTrackedSideEffects =
effect.function.loweredFunc.func.aliasingEffects?.some(
effect =>
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure',
);
const capturesRef = effect.function.loweredFunc.func.context.some(
operand => isRefOrRefValue(operand.identifier),
);
const isMutable = hasCaptures || hasTrackedSideEffects || capturesRef;
for (const operand of effect.function.loweredFunc.func.context) {
if (operand.effect !== Effect.Capture) {
continue;
}
const kind = state.kind(operand).kind;
if (
kind === ValueKind.Primitive ||
kind == ValueKind.Frozen ||
kind == ValueKind.Global
) {
operand.effect = Effect.Read;
}
}
state.initialize(effect.function, {
kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen,
reason: new Set([]),
});
state.define(effect.into, effect.function);
for (const capture of effect.captures) {
applyEffect(
context,
state,
{
kind: 'Capture',
from: capture,
into: effect.into,
},
initialized,
effects,
);
}
break;
}
case 'Alias':
case 'Capture': {
CompilerError.invariant(
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
{
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
loc: effect.into.loc,
},
);
const intoKind = state.kind(effect.into).kind;
let isMutableDesination: boolean;
switch (intoKind) {
case ValueKind.Context:
case ValueKind.Mutable:
case ValueKind.MaybeFrozen: {
isMutableDesination = true;
break;
}
default: {
isMutableDesination = false;
break;
}
}
const fromKind = state.kind(effect.from).kind;
let isMutableReferenceType: boolean;
switch (fromKind) {
case ValueKind.Global:
case ValueKind.Primitive: {
isMutableReferenceType = false;
break;
}
case ValueKind.Frozen: {
isMutableReferenceType = false;
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
break;
}
default: {
isMutableReferenceType = true;
break;
}
}
if (isMutableDesination && isMutableReferenceType) {
effects.push(effect);
}
break;
}
case 'Assign': {
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
const fromValue = state.kind(effect.from);
const fromKind = fromValue.kind;
switch (fromKind) {
case ValueKind.Frozen: {
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
kind: 'Primitive',
value: undefined,
loc: effect.from.loc,
};
context.effectInstructionValueCache.set(effect, value);
}
state.initialize(value, {
kind: fromKind,
reason: new Set(fromValue.reason),
});
state.define(effect.into, value);
break;
}
case ValueKind.Global:
case ValueKind.Primitive: {
let value = context.effectInstructionValueCache.get(effect);
if (value == null) {
value = {
kind: 'Primitive',
value: undefined,
loc: effect.from.loc,
};
context.effectInstructionValueCache.set(effect, value);
}
state.initialize(value, {
kind: fromKind,
reason: new Set(fromValue.reason),
});
state.define(effect.into, value);
break;
}
default: {
state.assign(effect.into, effect.from);
effects.push(effect);
break;
}
}
break;
}
case 'Apply': {
const functionValues = state.values(effect.function);
if (
functionValues.length === 1 &&
functionValues[0].kind === 'FunctionExpression' &&
functionValues[0].loweredFunc.func.aliasingEffects != null
) {
const functionExpr = functionValues[0];
let signature = context.functionSignatureCache.get(functionExpr);
if (signature == null) {
signature = buildSignatureFromFunctionExpression(
state.env,
functionExpr,
);
context.functionSignatureCache.set(functionExpr, signature);
}
if (DEBUG) {
console.log(
`constructed alias signature:\n${printAliasingSignature(signature)}`,
);
}
const signatureEffects = context.cacheApplySignature(
signature,
effect,
() =>
computeEffectsForSignature(
state.env,
signature,
effect.into,
effect.receiver,
effect.args,
functionExpr.loweredFunc.func.context,
effect.loc,
),
);
if (signatureEffects != null) {
applyEffect(
context,
state,
{kind: 'MutateTransitiveConditionally', value: effect.function},
initialized,
effects,
);
for (const signatureEffect of signatureEffects) {
applyEffect(context, state, signatureEffect, initialized, effects);
}
break;
}
}
let signatureEffects = null;
if (effect.signature?.aliasing != null) {
const signature = effect.signature.aliasing;
signatureEffects = context.cacheApplySignature(
effect.signature.aliasing,
effect,
() =>
computeEffectsForSignature(
state.env,
signature,
effect.into,
effect.receiver,
effect.args,
[],
effect.loc,
),
);
}
if (signatureEffects != null) {
for (const signatureEffect of signatureEffects) {
applyEffect(context, state, signatureEffect, initialized, effects);
}
} else if (effect.signature != null) {
const legacyEffects = computeEffectsForLegacySignature(
state,
effect.signature,
effect.into,
effect.receiver,
effect.args,
effect.loc,
);
for (const legacyEffect of legacyEffects) {
applyEffect(context, state, legacyEffect, initialized, effects);
}
} else {
applyEffect(
context,
state,
{
kind: 'Create',
into: effect.into,
value: ValueKind.Mutable,
reason: ValueReason.Other,
},
initialized,
effects,
);
for (const arg of [effect.receiver, effect.function, ...effect.args]) {
if (arg.kind === 'Hole') {
continue;
}
const operand = arg.kind === 'Identifier' ? arg : arg.place;
if (operand !== effect.function || effect.mutatesFunction) {
applyEffect(
context,
state,
{
kind: 'MutateTransitiveConditionally',
value: operand,
},
initialized,
effects,
);
}
const mutateIterator =
arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null;
if (mutateIterator) {
applyEffect(context, state, mutateIterator, initialized, effects);
}
applyEffect(
context,
state,
{kind: 'Alias', from: operand, into: effect.into},
initialized,
effects,
);
for (const otherArg of [
effect.receiver,
effect.function,
...effect.args,
]) {
if (otherArg.kind === 'Hole') {
continue;
}
const other =
otherArg.kind === 'Identifier' ? otherArg : otherArg.place;
if (other === arg) {
continue;
}
applyEffect(
context,
state,
{
kind: 'Capture',
from: operand,
into: other,
},
initialized,
effects,
);
}
}
}
break;
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
const mutationKind = state.mutate(effect.kind, effect.value);
if (mutationKind === 'mutate') {
effects.push(effect);
} else if (mutationKind === 'mutate-ref') {
} else if (
mutationKind !== 'none' &&
(effect.kind === 'Mutate' || effect.kind === 'MutateTransitive')
) {
const value = state.kind(effect.value);
if (DEBUG) {
console.log(`invalid mutation: ${printAliasingEffect(effect)}`);
console.log(prettyFormat(state.debugAbstractValue(value)));
}
if (
mutationKind === 'mutate-frozen' &&
context.hoistedContextDeclarations.has(
effect.value.identifier.declarationId,
)
) {
const description =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared`
: null;
const hoistedAccess = context.hoistedContextDeclarations.get(
effect.value.identifier.declarationId,
);
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
applyEffect(
context,
state,
{
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`,
description,
loc: hoistedAccess.loc,
suggestions: null,
},
},
initialized,
effects,
);
}
applyEffect(
context,
state,
{
kind: 'MutateFrozen',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
description,
loc: effect.value.loc,
suggestions: null,
},
},
initialized,
effects,
);
} else {
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
const description =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `Found mutation of \`${effect.value.identifier.name.value}\``
: null;
applyEffect(
context,
state,
{
kind:
value.kind === ValueKind.Frozen
? 'MutateFrozen'
: 'MutateGlobal',
place: effect.value,
error: {
severity: ErrorSeverity.InvalidReact,
reason,
description,
loc: effect.value.loc,
suggestions: null,
},
},
initialized,
effects,
);
}
}
break;
}
case 'Impure':
case 'Render':
case 'MutateFrozen':
case 'MutateGlobal': {
effects.push(effect);
break;
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind '${(effect as any).kind as any}'`,
);
}
}
}
class InferenceState {
env: Environment;
#isFunctionExpression: boolean;
#values: Map<InstructionValue, AbstractValue>;
#variables: Map<IdentifierId, Set<InstructionValue>>;
constructor(
env: Environment,
isFunctionExpression: boolean,
values: Map<InstructionValue, AbstractValue>,
variables: Map<IdentifierId, Set<InstructionValue>>,
) {
this.env = env;
this.#isFunctionExpression = isFunctionExpression;
this.#values = values;
this.#variables = variables;
}
static empty(
env: Environment,
isFunctionExpression: boolean,
): InferenceState {
return new InferenceState(env, isFunctionExpression, new Map(), new Map());
}
get isFunctionExpression(): boolean {
return this.#isFunctionExpression;
}
initialize(value: InstructionValue, kind: AbstractValue): void {
CompilerError.invariant(value.kind !== 'LoadLocal', {
reason:
'[InferMutationAliasingEffects] 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: `[InferMutationAliasingEffects] 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: `[InferMutationAliasingEffects] 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: `[InferMutationAliasingEffects] Expected at least one value`,
description: `No value found at \`${printPlace(place)}\``,
loc: place.loc,
suggestions: null,
});
return mergedKind;
}
assign(place: Place, value: Place): void {
const values = this.#variables.get(value.identifier.id);
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] 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));
}
appendAlias(place: Place, value: Place): void {
const values = this.#variables.get(value.identifier.id);
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
description: `${printIdentifier(value.identifier)}`,
loc: value.loc,
suggestions: null,
});
const prevValues = this.values(place);
this.#variables.set(
place.identifier.id,
new Set([...prevValues, ...values]),
);
}
define(place: Place, value: InstructionValue): void {
CompilerError.invariant(this.#values.has(value), {
reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation(
value.loc,
)}'`,
description: printInstructionValue(value),
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);
}
freeze(place: Place, reason: ValueReason): boolean {
const value = this.kind(place);
switch (value.kind) {
case ValueKind.Context:
case ValueKind.Mutable:
case ValueKind.MaybeFrozen: {
const values = this.values(place);
for (const instrValue of values) {
this.freezeValue(instrValue, reason);
}
return true;
}
case ValueKind.Frozen:
case ValueKind.Global:
case ValueKind.Primitive: {
return false;
}
default: {
assertExhaustive(
value.kind,
`Unexpected value kind '${(value as any).kind}'`,
);
}
}
}
freezeValue(value: InstructionValue, reason: ValueReason): void {
this.#values.set(value, {
kind: ValueKind.Frozen,
reason: new Set([reason]),
});
if (
value.kind === 'FunctionExpression' &&
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
this.env.config.enableTransitivelyFreezeFunctionExpressions)
) {
for (const place of value.loweredFunc.func.context) {
this.freeze(place, reason);
}
}
}
mutate(
variant:
| 'Mutate'
| 'MutateConditionally'
| 'MutateTransitive'
| 'MutateTransitiveConditionally',
place: Place,
): 'none' | 'mutate' | 'mutate-frozen' | 'mutate-global' | 'mutate-ref' {
if (isRefOrRefValue(place.identifier)) {
return 'mutate-ref';
}
const kind = this.kind(place).kind;
switch (variant) {
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
switch (kind) {
case ValueKind.Mutable:
case ValueKind.Context: {
return 'mutate';
}
default: {
return 'none';
}
}
}
case 'Mutate':
case 'MutateTransitive': {
switch (kind) {
case ValueKind.Mutable:
case ValueKind.Context: {
return 'mutate';
}
case ValueKind.Primitive: {
return 'none';
}
case ValueKind.Frozen: {
return 'mutate-frozen';
}
case ValueKind.Global: {
return 'mutate-global';
}
case ValueKind.MaybeFrozen: {
return 'none';
}
default: {
assertExhaustive(kind, `Unexpected kind ${kind}`);
}
}
}
default: {
assertExhaustive(variant, `Unexpected mutation variant ${variant}`);
}
}
}
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,
this.#isFunctionExpression,
nextValues ?? new Map(this.#values),
nextVariables ?? new Map(this.#variables),
);
}
}
clone(): InferenceState {
return new InferenceState(
this.env,
this.#isFunctionExpression,
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] = {
abstract: this.debugAbstractValue(kind),
value: printInstructionValue(value),
};
}
for (const [variable, values] of this.#variables) {
result.variables[`$${variable}`] = [...values].map(identify);
}
return result;
}
debugAbstractValue(value: AbstractValue): any {
return {
kind: value.kind,
reason: [...value.reason],
};
}
inferPhi(phi: Phi): void {
const values: Set<InstructionValue> = new Set();
for (const [_, operand] of phi.operands) {
const operandValues = this.#variables.get(operand.identifier.id);
if (operandValues === undefined) continue;
for (const v of operandValues) {
values.add(v);
}
}
if (values.size > 0) {
this.#variables.set(phi.place.identifier.id, values);
}
}
}
function mergeAbstractValues(
a: AbstractValue,
b: AbstractValue,
): AbstractValue {
const kind = mergeValueKinds(a.kind, b.kind);
if (
kind === a.kind &&
kind === b.kind &&
Set_isSuperset(a.reason, b.reason)
) {
return a;
}
const reason = new Set(a.reason);
for (const r of b.reason) {
reason.add(r);
}
return {kind, reason};
}
type InstructionSignature = {
effects: ReadonlyArray<AliasingEffect>;
};
function conditionallyMutateIterator(place: Place): AliasingEffect | null {
if (
!(
isArrayType(place.identifier) ||
isSetType(place.identifier) ||
isMapType(place.identifier)
)
) {
return {
kind: 'MutateTransitiveConditionally',
value: place,
};
}
return null;
}
function computeSignatureForInstruction(
context: Context,
env: Environment,
instr: Instruction,
): InstructionSignature {
const {lvalue, value} = instr;
const effects: Array<AliasingEffect> = [];
switch (value.kind) {
case 'ArrayExpression': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Mutable,
reason: ValueReason.Other,
});
for (const element of value.elements) {
if (element.kind === 'Identifier') {
effects.push({
kind: 'Capture',
from: element,
into: lvalue,
});
} else if (element.kind === 'Spread') {
const mutateIterator = conditionallyMutateIterator(element.place);
if (mutateIterator != null) {
effects.push(mutateIterator);
}
effects.push({
kind: 'Capture',
from: element.place,
into: lvalue,
});
} else {
continue;
}
}
break;
}
case 'ObjectExpression': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Mutable,
reason: ValueReason.Other,
});
for (const property of value.properties) {
if (property.kind === 'ObjectProperty') {
effects.push({
kind: 'Capture',
from: property.place,
into: lvalue,
});
} else {
effects.push({
kind: 'Capture',
from: property.place,
into: lvalue,
});
}
}
break;
}
case 'Await': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Mutable,
reason: ValueReason.Other,
});
effects.push({kind: 'MutateTransitiveConditionally', value: value.value});
effects.push({
kind: 'Capture',
from: value.value,
into: lvalue,
});
break;
}
case 'NewExpression':
case 'CallExpression':
case 'MethodCall': {
let callee;
let receiver;
let mutatesCallee;
if (value.kind === 'NewExpression') {
callee = value.callee;
receiver = value.callee;
mutatesCallee = false;
} else if (value.kind === 'CallExpression') {
callee = value.callee;
receiver = value.callee;
mutatesCallee = true;
} else if (value.kind === 'MethodCall') {
callee = value.property;
receiver = value.receiver;
mutatesCallee = false;
} else {
assertExhaustive(
value,
`Unexpected value kind '${(value as any).kind}'`,
);
}
const signature = getFunctionCallSignature(env, callee.identifier.type);
effects.push({
kind: 'Apply',
receiver,
function: callee,
mutatesFunction: mutatesCallee,
args: value.args,
into: lvalue,
signature,
loc: value.loc,
});
break;
}
case 'PropertyDelete':
case 'ComputedDelete': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({kind: 'Mutate', value: value.object});
break;
}
case 'PropertyLoad':
case 'ComputedLoad': {
if (isPrimitiveType(lvalue.identifier)) {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
} else {
effects.push({
kind: 'CreateFrom',
from: value.object,
into: lvalue,
});
}
break;
}
case 'PropertyStore':
case 'ComputedStore': {
effects.push({kind: 'Mutate', value: value.object});
effects.push({
kind: 'Capture',
from: value.value,
into: value.object,
});
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
effects.push({
kind: 'CreateFunction',
into: lvalue,
function: value,
captures: value.loweredFunc.func.context.filter(
operand => operand.effect === Effect.Capture,
),
});
break;
}
case 'GetIterator': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Mutable,
reason: ValueReason.Other,
});
if (
isArrayType(value.collection.identifier) ||
isMapType(value.collection.identifier) ||
isSetType(value.collection.identifier)
) {
effects.push({
kind: 'Capture',
from: value.collection,
into: lvalue,
});
} else {
effects.push({kind: 'Alias', from: value.collection, into: lvalue});
effects.push({
kind: 'MutateTransitiveConditionally',
value: value.collection,
});
}
break;
}
case 'IteratorNext': {
effects.push({kind: 'MutateConditionally', value: value.iterator});
effects.push({
kind: 'CreateFrom',
from: value.collection,
into: lvalue,
});
break;
}
case 'NextPropertyOf': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
break;
}
case 'JsxExpression':
case 'JsxFragment': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Frozen,
reason: ValueReason.JsxCaptured,
});
for (const operand of eachInstructionValueOperand(value)) {
effects.push({
kind: 'Freeze',
value: operand,
reason: ValueReason.JsxCaptured,
});
effects.push({
kind: 'Capture',
from: operand,
into: lvalue,
});
}
if (value.kind === 'JsxExpression') {
if (value.tag.kind === 'Identifier') {
effects.push({
kind: 'Render',
place: value.tag,
});
}
if (value.children != null) {
for (const child of value.children) {
effects.push({
kind: 'Render',
place: child,
});
}
}
}
break;
}
case 'DeclareLocal': {
effects.push({
kind: 'Create',
into: value.lvalue.place,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
break;
}
case 'Destructure': {
for (const patternLValue of eachInstructionValueLValue(value)) {
if (isPrimitiveType(patternLValue.identifier)) {
effects.push({
kind: 'Create',
into: patternLValue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
} else {
effects.push({
kind: 'CreateFrom',
from: value.value,
into: patternLValue,
});
}
}
effects.push({kind: 'Assign', from: value.value, into: lvalue});
break;
}
case 'LoadContext': {
effects.push({kind: 'CreateFrom', from: value.place, into: lvalue});
break;
}
case 'DeclareContext': {
const kind = value.lvalue.kind;
if (
!context.hoistedContextDeclarations.has(
value.lvalue.place.identifier.declarationId,
) ||
kind === InstructionKind.HoistedConst ||
kind === InstructionKind.HoistedFunction ||
kind === InstructionKind.HoistedLet
) {
effects.push({
kind: 'Create',
into: value.lvalue.place,
value: ValueKind.Mutable,
reason: ValueReason.Other,
});
} else {
effects.push({kind: 'Mutate', value: value.lvalue.place});
}
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
break;
}
case 'StoreContext': {
if (
value.lvalue.kind === InstructionKind.Reassign ||
context.hoistedContextDeclarations.has(
value.lvalue.place.identifier.declarationId,
)
) {
effects.push({kind: 'Mutate', value: value.lvalue.place});
} else {
effects.push({
kind: 'Create',
into: value.lvalue.place,
value: ValueKind.Mutable,
reason: ValueReason.Other,
});
}
effects.push({
kind: 'Capture',
from: value.value,
into: value.lvalue.place,
});
effects.push({kind: 'Assign', from: value.value, into: lvalue});
break;
}
case 'LoadLocal': {
effects.push({kind: 'Assign', from: value.place, into: lvalue});
break;
}
case 'StoreLocal': {
effects.push({
kind: 'Assign',
from: value.value,
into: value.lvalue.place,
});
effects.push({kind: 'Assign', from: value.value, into: lvalue});
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
effects.push({
kind: 'Create',
into: value.lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
break;
}
case 'StoreGlobal': {
effects.push({
kind: 'MutateGlobal',
place: value.value,
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,
},
});
effects.push({kind: 'Assign', from: value.value, into: lvalue});
break;
}
case 'TypeCastExpression': {
effects.push({kind: 'Assign', from: value.value, into: lvalue});
break;
}
case 'LoadGlobal': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Global,
reason: ValueReason.Global,
});
break;
}
case 'StartMemoize':
case 'FinishMemoize': {
if (env.config.enablePreserveExistingMemoizationGuarantees) {
for (const operand of eachInstructionValueOperand(value)) {
effects.push({
kind: 'Freeze',
value: operand,
reason: ValueReason.Other,
});
}
}
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
break;
}
case 'TaggedTemplateExpression':
case 'BinaryExpression':
case 'Debugger':
case 'JSXText':
case 'MetaProperty':
case 'Primitive':
case 'RegExpLiteral':
case 'TemplateLiteral':
case 'UnaryExpression':
case 'UnsupportedNode': {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
reason: ValueReason.Other,
});
break;
}
}
return {
effects,
};
}
function computeEffectsForLegacySignature(
state: InferenceState,
signature: FunctionSignature,
lvalue: Place,
receiver: Place,
args: Array<Place | SpreadPattern | Hole>,
loc: SourceLocation,
): Array<AliasingEffect> {
const returnValueReason = signature.returnValueReason ?? ValueReason.Other;
const effects: Array<AliasingEffect> = [];
effects.push({
kind: 'Create',
into: lvalue,
value: signature.returnValueKind,
reason: returnValueReason,
});
if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) {
effects.push({
kind: 'Impure',
place: receiver,
error: {
reason:
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
description:
signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
: null,
severity: ErrorSeverity.InvalidReact,
loc,
suggestions: null,
},
});
}
const stores: Array<Place> = [];
const captures: Array<Place> = [];
function visit(place: Place, effect: Effect): void {
switch (effect) {
case Effect.Store: {
effects.push({
kind: 'Mutate',
value: place,
});
stores.push(place);
break;
}
case Effect.Capture: {
captures.push(place);
break;
}
case Effect.ConditionallyMutate: {
effects.push({
kind: 'MutateTransitiveConditionally',
value: place,
});
break;
}
case Effect.ConditionallyMutateIterator: {
const mutateIterator = conditionallyMutateIterator(place);
if (mutateIterator != null) {
effects.push(mutateIterator);
}
effects.push({
kind: 'Capture',
from: place,
into: lvalue,
});
break;
}
case Effect.Freeze: {
effects.push({
kind: 'Freeze',
value: place,
reason: returnValueReason,
});
break;
}
case Effect.Mutate: {
effects.push({kind: 'MutateTransitive', value: place});
break;
}
case Effect.Read: {
effects.push({
kind: 'ImmutableCapture',
from: place,
into: lvalue,
});
break;
}
}
}
if (
signature.mutableOnlyIfOperandsAreMutable &&
areArgumentsImmutableAndNonMutating(state, args)
) {
effects.push({
kind: 'Alias',
from: receiver,
into: lvalue,
});
for (const arg of args) {
if (arg.kind === 'Hole') {
continue;
}
const place = arg.kind === 'Identifier' ? arg : arg.place;
effects.push({
kind: 'ImmutableCapture',
from: place,
into: lvalue,
});
}
return effects;
}
if (signature.calleeEffect !== Effect.Capture) {
effects.push({
kind: 'Alias',
from: receiver,
into: lvalue,
});
}
visit(receiver, signature.calleeEffect);
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.kind === 'Hole') {
continue;
}
const place = arg.kind === 'Identifier' ? arg : arg.place;
const signatureEffect =
arg.kind === 'Identifier' && i < signature.positionalParams.length
? signature.positionalParams[i]!
: (signature.restParam ?? Effect.ConditionallyMutate);
const effect = getArgumentEffect(signatureEffect, arg);
visit(place, effect);
}
if (captures.length !== 0) {
if (stores.length === 0) {
for (const capture of captures) {
effects.push({kind: 'Alias', from: capture, into: lvalue});
}
} else {
for (const capture of captures) {
for (const store of stores) {
effects.push({kind: 'Capture', from: capture, into: store});
}
}
}
}
return effects;
}
function areArgumentsImmutableAndNonMutating(
state: InferenceState,
args: Array<Place | SpreadPattern | Hole>,
): boolean {
for (const arg of args) {
if (arg.kind === 'Hole') {
continue;
}
if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') {
const fnShape = state.env.getFunctionSignature(arg.identifier.type);
if (fnShape != null) {
return (
!fnShape.positionalParams.some(isKnownMutableEffect) &&
(fnShape.restParam == null ||
!isKnownMutableEffect(fnShape.restParam))
);
}
}
const place = arg.kind === 'Identifier' ? arg : arg.place;
const kind = state.kind(place).kind;
switch (kind) {
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 computeEffectsForSignature(
env: Environment,
signature: AliasingSignature,
lvalue: Place,
receiver: Place,
args: Array<Place | SpreadPattern | Hole>,
context: Array<Place> = [],
loc: SourceLocation,
): Array<AliasingEffect> | null {
if (
signature.params.length > args.length ||
(args.length > signature.params.length && signature.rest == null)
) {
return null;
}
const mutableSpreads = new Set<IdentifierId>();
const substitutions: Map<IdentifierId, Array<Place>> = new Map();
substitutions.set(signature.receiver, [receiver]);
substitutions.set(signature.returns, [lvalue]);
const params = signature.params;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.kind === 'Hole') {
continue;
} else if (params == null || i >= params.length || arg.kind === 'Spread') {
if (signature.rest == null) {
return null;
}
const place = arg.kind === 'Identifier' ? arg : arg.place;
getOrInsertWith(substitutions, signature.rest, () => []).push(place);
if (arg.kind === 'Spread') {
const mutateIterator = conditionallyMutateIterator(arg.place);
if (mutateIterator != null) {
mutableSpreads.add(arg.place.identifier.id);
}
}
} else {
const param = params[i];
substitutions.set(param, [arg]);
}
}
for (const operand of context) {
substitutions.set(operand.identifier.id, [operand]);
}
const effects: Array<AliasingEffect> = [];
for (const signatureTemporary of signature.temporaries) {
const temp = createTemporaryPlace(env, receiver.loc);
substitutions.set(signatureTemporary.identifier.id, [temp]);
}
for (const effect of signature.effects) {
switch (effect.kind) {
case 'Assign':
case 'ImmutableCapture':
case 'Alias':
case 'CreateFrom':
case 'Capture': {
const from = substitutions.get(effect.from.identifier.id) ?? [];
const to = substitutions.get(effect.into.identifier.id) ?? [];
for (const fromId of from) {
for (const toId of to) {
effects.push({
kind: effect.kind,
from: fromId,
into: toId,
});
}
}
break;
}
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
const values = substitutions.get(effect.place.identifier.id) ?? [];
for (const value of values) {
effects.push({kind: effect.kind, place: value, error: effect.error});
}
break;
}
case 'Render': {
const values = substitutions.get(effect.place.identifier.id) ?? [];
for (const value of values) {
effects.push({kind: effect.kind, place: value});
}
break;
}
case 'Mutate':
case 'MutateTransitive':
case 'MutateTransitiveConditionally':
case 'MutateConditionally': {
const values = substitutions.get(effect.value.identifier.id) ?? [];
for (const id of values) {
effects.push({kind: effect.kind, value: id});
}
break;
}
case 'Freeze': {
const values = substitutions.get(effect.value.identifier.id) ?? [];
for (const value of values) {
if (mutableSpreads.has(value.identifier.id)) {
CompilerError.throwTodo({
reason: 'Support spread syntax for hook arguments',
loc: value.loc,
});
}
effects.push({kind: 'Freeze', value, reason: effect.reason});
}
break;
}
case 'Create': {
const into = substitutions.get(effect.into.identifier.id) ?? [];
for (const value of into) {
effects.push({
kind: 'Create',
into: value,
value: effect.value,
reason: effect.reason,
});
}
break;
}
case 'Apply': {
const applyReceiver = substitutions.get(effect.receiver.identifier.id);
if (applyReceiver == null || applyReceiver.length !== 1) {
return null;
}
const applyFunction = substitutions.get(effect.function.identifier.id);
if (applyFunction == null || applyFunction.length !== 1) {
return null;
}
const applyInto = substitutions.get(effect.into.identifier.id);
if (applyInto == null || applyInto.length !== 1) {
return null;
}
const applyArgs: Array<Place | SpreadPattern | Hole> = [];
for (const arg of effect.args) {
if (arg.kind === 'Hole') {
applyArgs.push(arg);
} else if (arg.kind === 'Identifier') {
const applyArg = substitutions.get(arg.identifier.id);
if (applyArg == null || applyArg.length !== 1) {
return null;
}
applyArgs.push(applyArg[0]);
} else {
const applyArg = substitutions.get(arg.place.identifier.id);
if (applyArg == null || applyArg.length !== 1) {
return null;
}
applyArgs.push({kind: 'Spread', place: applyArg[0]});
}
}
effects.push({
kind: 'Apply',
mutatesFunction: effect.mutatesFunction,
receiver: applyReceiver[0],
args: applyArgs,
function: applyFunction[0],
into: applyInto[0],
signature: effect.signature,
loc,
});
break;
}
case 'CreateFunction': {
CompilerError.throwTodo({
reason: `Support CreateFrom effects in signatures`,
loc: receiver.loc,
});
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind '${(effect as any).kind}'`,
);
}
}
}
return effects;
}
function buildSignatureFromFunctionExpression(
env: Environment,
fn: FunctionExpression,
): AliasingSignature {
let rest: IdentifierId | null = null;
const params: Array<IdentifierId> = [];
for (const param of fn.loweredFunc.func.params) {
if (param.kind === 'Identifier') {
params.push(param.identifier.id);
} else {
rest = param.place.identifier.id;
}
}
return {
receiver: makeIdentifierId(0),
params,
rest: rest ?? createTemporaryPlace(env, fn.loc).identifier.id,
returns: fn.loweredFunc.func.returns.identifier.id,
effects: fn.loweredFunc.func.aliasingEffects ?? [],
temporaries: [],
};
}
export type AbstractValue = {
kind: ValueKind;
reason: ReadonlySet<ValueReason>;
};