import { CompilerError, SourceLocation } from "..";
import { Environment } from "../HIR";
import {
GeneratedSource,
HIRFunction,
Identifier,
Instruction,
Place,
ReactiveScope,
makeInstructionId,
} from "../HIR/HIR";
import {
doesPatternContainSpreadElement,
eachInstructionOperand,
eachPatternOperand,
} from "../HIR/visitors";
import DisjointSet from "../Utils/DisjointSet";
import { logHIRFunction } from "../Utils/logger";
import { assertExhaustive } from "../Utils/utils";
export function inferReactiveScopeVariables(fn: HIRFunction): void {
const scopeIdentifiers = findDisjointMutableValues(fn);
const scopes: Map<Identifier, ReactiveScope> = new Map();
scopeIdentifiers.forEach((identifier, groupIdentifier) => {
let scope = scopes.get(groupIdentifier);
if (scope === undefined) {
scope = {
id: fn.env.nextScopeId,
range: identifier.mutableRange,
dependencies: new Set(),
declarations: new Map(),
reassignments: new Set(),
earlyReturnValue: null,
merged: new Set(),
loc: identifier.loc,
};
scopes.set(groupIdentifier, scope);
} else {
scope.range.start = makeInstructionId(
Math.min(scope.range.start, identifier.mutableRange.start)
);
scope.range.end = makeInstructionId(
Math.max(scope.range.end, identifier.mutableRange.end)
);
scope.loc = mergeLocation(scope.loc, identifier.loc);
}
identifier.scope = scope;
identifier.mutableRange = scope.range;
});
let maxInstruction = 0;
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
maxInstruction = makeInstructionId(Math.max(maxInstruction, instr.id));
}
maxInstruction = makeInstructionId(
Math.max(maxInstruction, block.terminal.id)
);
}
for (const [, scope] of scopes) {
if (
scope.range.start === 0 ||
scope.range.end === 0 ||
maxInstruction === 0 ||
scope.range.end > maxInstruction + 1
) {
logHIRFunction("InferReactiveScopeVariables (invalid scope)", fn);
CompilerError.invariant(false, {
reason: `Invalid mutable range for scope`,
loc: GeneratedSource,
description: `Scope @${scope.id} has range [${scope.range.start}:${
scope.range.end
}] but the valid range is [1:${maxInstruction + 1}]`,
});
}
}
}
function mergeLocation(l: SourceLocation, r: SourceLocation): SourceLocation {
if (l === GeneratedSource) {
return r;
} else if (r === GeneratedSource) {
return l;
} else {
return {
start: {
line: Math.min(l.start.line, r.start.line),
column: Math.min(l.start.column, r.start.column),
},
end: {
line: Math.max(l.end.line, r.end.line),
column: Math.max(l.end.column, r.end.column),
},
};
}
}
export function isMutable({ id }: Instruction, place: Place): boolean {
const range = place.identifier.mutableRange;
return id >= range.start && id < range.end;
}
export function mayAllocate(
env: Environment,
instruction: Instruction
): boolean {
const { value } = instruction;
switch (value.kind) {
case "Destructure": {
return doesPatternContainSpreadElement(value.lvalue.pattern);
}
case "PostfixUpdate":
case "PrefixUpdate":
case "Await":
case "DeclareLocal":
case "DeclareContext":
case "StoreLocal":
case "LoadGlobal":
case "MetaProperty":
case "TypeCastExpression":
case "LoadLocal":
case "LoadContext":
case "StoreContext":
case "PropertyDelete":
case "ComputedLoad":
case "ComputedDelete":
case "JSXText":
case "TemplateLiteral":
case "Primitive":
case "GetIterator":
case "IteratorNext":
case "NextPropertyOf":
case "Debugger":
case "StartMemoize":
case "FinishMemoize":
case "UnaryExpression":
case "BinaryExpression":
case "PropertyLoad":
case "StoreGlobal": {
return false;
}
case "CallExpression":
case "MethodCall": {
return instruction.lvalue.identifier.type.kind !== "Primitive";
}
case "RegExpLiteral":
case "PropertyStore":
case "ComputedStore":
case "ArrayExpression":
case "JsxExpression":
case "JsxFragment":
case "NewExpression":
case "ObjectExpression":
case "UnsupportedNode":
case "ObjectMethod":
case "FunctionExpression":
case "TaggedTemplateExpression": {
return true;
}
default: {
assertExhaustive(
value,
`Unexpected value kind \`${(value as any).kind}\``
);
}
}
}
export function findDisjointMutableValues(
fn: HIRFunction
): DisjointSet<Identifier> {
const scopeIdentifiers = new DisjointSet<Identifier>();
for (const [_, block] of fn.body.blocks) {
for (const phi of block.phis) {
if (
phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end &&
phi.id.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id)
) {
for (const [, phiId] of phi.operands) {
scopeIdentifiers.union([phi.id, phiId]);
}
} else if (fn.env.config.enableForest) {
for (const [, phiId] of phi.operands) {
scopeIdentifiers.union([phi.id, phiId]);
}
}
}
for (const instr of block.instructions) {
const operands: Array<Identifier> = [];
const range = instr.lvalue.identifier.mutableRange;
if (range.end > range.start + 1 || mayAllocate(fn.env, instr)) {
operands.push(instr.lvalue!.identifier);
}
if (
instr.value.kind === "StoreLocal" ||
instr.value.kind === "StoreContext"
) {
if (
instr.value.lvalue.place.identifier.mutableRange.end >
instr.value.lvalue.place.identifier.mutableRange.start + 1
) {
operands.push(instr.value.lvalue.place.identifier);
}
if (
isMutable(instr, instr.value.value) &&
instr.value.value.identifier.mutableRange.start > 0
) {
operands.push(instr.value.value.identifier);
}
} else if (instr.value.kind === "Destructure") {
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
if (
place.identifier.mutableRange.end >
place.identifier.mutableRange.start + 1
) {
operands.push(place.identifier);
}
}
if (
isMutable(instr, instr.value.value) &&
instr.value.value.identifier.mutableRange.start > 0
) {
operands.push(instr.value.value.identifier);
}
} else if (instr.value.kind === "MethodCall") {
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
operand.identifier.mutableRange.start > 0
) {
operands.push(operand.identifier);
}
}
operands.push(instr.value.property.identifier);
} else {
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
operand.identifier.mutableRange.start > 0
) {
operands.push(operand.identifier);
}
}
}
if (operands.length !== 0) {
scopeIdentifiers.union(operands);
}
}
}
return scopeIdentifiers;
}