import { CompilerError, Effect } from "..";
import { HIRFunction, IdentifierId, Place } from "../HIR";
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from "../HIR/visitors";
export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
const contextVariables = new Set<IdentifierId>();
const reassignment = getContextReassignment(
fn,
contextVariables,
false,
false
);
if (reassignment !== null) {
CompilerError.throwInvalidReact({
reason:
"Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead",
description:
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === "named"
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
: "",
loc: reassignment.loc,
});
}
}
function getContextReassignment(
fn: HIRFunction,
contextVariables: Set<IdentifierId>,
isFunctionExpression: boolean,
isAsync: boolean
): Place | null {
const reassigningFunctions = new Map<IdentifierId, Place>();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const { lvalue, value } = instr;
switch (value.kind) {
case "FunctionExpression":
case "ObjectMethod": {
let reassignment = getContextReassignment(
value.loweredFunc.func,
contextVariables,
true,
isAsync || value.loweredFunc.func.async
);
if (reassignment === null) {
for (const operand of eachInstructionValueOperand(value)) {
const reassignmentFromOperand = reassigningFunctions.get(
operand.identifier.id
);
if (reassignmentFromOperand !== undefined) {
reassignment = reassignmentFromOperand;
break;
}
}
}
if (reassignment !== null) {
if (isAsync || value.loweredFunc.func.async) {
CompilerError.throwInvalidReact({
reason:
"Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead",
description:
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === "named"
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
: "",
loc: reassignment.loc,
});
}
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}
break;
}
case "StoreLocal": {
const reassignment = reassigningFunctions.get(
value.value.identifier.id
);
if (reassignment !== undefined) {
reassigningFunctions.set(
value.lvalue.place.identifier.id,
reassignment
);
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}
break;
}
case "LoadLocal": {
const reassignment = reassigningFunctions.get(
value.place.identifier.id
);
if (reassignment !== undefined) {
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}
break;
}
case "DeclareContext": {
if (!isFunctionExpression) {
contextVariables.add(value.lvalue.place.identifier.id);
}
break;
}
case "StoreContext": {
if (isFunctionExpression) {
if (contextVariables.has(value.lvalue.place.identifier.id)) {
return value.lvalue.place;
}
} else {
contextVariables.add(value.lvalue.place.identifier.id);
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(value)) {
CompilerError.invariant(operand.effect !== Effect.Unknown, {
reason: `Expected effects to be inferred prior to ValidateLocalsNotReassignedAfterRender`,
loc: operand.loc,
});
const reassignment = reassigningFunctions.get(
operand.identifier.id
);
if (
reassignment !== undefined &&
operand.effect === Effect.Freeze
) {
return reassignment;
}
}
break;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
const reassignment = reassigningFunctions.get(operand.identifier.id);
if (reassignment !== undefined) {
return reassignment;
}
}
}
return null;
}