import { CompilerError } from "../CompilerError";
import {
BasicBlock,
BlockId,
HIRFunction,
Identifier,
InstructionKind,
LValue,
LValuePattern,
Phi,
Place,
} from "../HIR/HIR";
import { printIdentifier, printPlace } from "../HIR/PrintHIR";
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachPatternOperand,
eachTerminalOperand,
eachTerminalSuccessor,
terminalFallthrough,
} from "../HIR/visitors";
export function leaveSSA(fn: HIRFunction): void {
const declarations: Map<
string,
{ lvalue: LValue | LValuePattern; place: Place }
> = new Map();
for (const param of fn.params) {
let place: Place = param.kind === "Identifier" ? param : param.place;
if (place.identifier.name !== null) {
declarations.set(place.identifier.name.value, {
lvalue: {
kind: InstructionKind.Let,
place,
},
place,
});
}
}
const rewrites: Map<Identifier, Identifier> = new Map();
type PhiState = {
phi: Phi;
block: BasicBlock;
};
const seen = new Set<BlockId>();
const backEdgePhis = new Set<Phi>();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
for (const [pred] of phi.operands) {
if (!seen.has(pred)) {
backEdgePhis.add(phi);
break;
}
}
}
seen.add(block.id);
}
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const { lvalue, value } = instr;
if (value.kind === "DeclareLocal") {
const name = value.lvalue.place.identifier.name;
if (name !== null) {
CompilerError.invariant(!declarations.has(name.value), {
reason: `Unexpected duplicate declaration`,
description: `Found duplicate declaration for \`${name.value}\``,
loc: value.lvalue.place.loc,
suggestions: null,
});
declarations.set(name.value, {
lvalue: value.lvalue,
place: value.lvalue.place,
});
}
} else if (
value.kind === "PrefixUpdate" ||
value.kind === "PostfixUpdate"
) {
CompilerError.invariant(value.lvalue.identifier.name !== null, {
reason: `Expected update expression to be applied to a named variable`,
description: null,
loc: value.lvalue.loc,
suggestions: null,
});
const originalLVal = declarations.get(
value.lvalue.identifier.name.value
);
CompilerError.invariant(originalLVal !== undefined, {
reason: `Expected update expression to be applied to a previously defined variable`,
description: null,
loc: value.lvalue.loc,
suggestions: null,
});
originalLVal.lvalue.kind = InstructionKind.Let;
} else if (value.kind === "StoreLocal") {
if (value.lvalue.place.identifier.name != null) {
const originalLVal = declarations.get(
value.lvalue.place.identifier.name.value
);
if (
originalLVal === undefined ||
originalLVal.lvalue === value.lvalue
) {
CompilerError.invariant(
originalLVal !== undefined ||
block.kind === "block" ||
block.kind === "catch",
{
reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
description: null,
loc: value.lvalue.place.loc,
suggestions: null,
}
);
declarations.set(value.lvalue.place.identifier.name.value, {
lvalue: value.lvalue,
place: value.lvalue.place,
});
value.lvalue.kind = InstructionKind.Const;
} else {
originalLVal.lvalue.kind = InstructionKind.Let;
value.lvalue.kind = InstructionKind.Reassign;
}
} else if (rewrites.has(value.lvalue.place.identifier)) {
value.lvalue.kind = InstructionKind.Const;
}
} else if (value.kind === "Destructure") {
let kind: InstructionKind | null = null;
for (const place of eachPatternOperand(value.lvalue.pattern)) {
if (place.identifier.name == null) {
CompilerError.invariant(
kind === null || kind === InstructionKind.Const,
{
reason: `Expected consistent kind for destructuring`,
description: `other places were \`${kind}\` but '${printPlace(
place
)}' is const`,
loc: place.loc,
suggestions: null,
}
);
kind = InstructionKind.Const;
} else {
const originalLVal = declarations.get(place.identifier.name.value);
if (
originalLVal === undefined ||
originalLVal.lvalue === value.lvalue
) {
CompilerError.invariant(
originalLVal !== undefined || block.kind !== "value",
{
reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
description: null,
loc: place.loc,
suggestions: null,
}
);
declarations.set(place.identifier.name.value, {
lvalue: value.lvalue,
place,
});
CompilerError.invariant(
kind === null || kind === InstructionKind.Const,
{
reason: `Expected consistent kind for destructuring`,
description: `Other places were \`${kind}\` but '${printPlace(
place
)}' is const`,
loc: place.loc,
suggestions: null,
}
);
kind = InstructionKind.Const;
} else {
CompilerError.invariant(
kind === null || kind === InstructionKind.Reassign,
{
reason: `Expected consistent kind for destructuring`,
description: `Other places were \`${kind}\` but '${printPlace(
place
)}' is reassigned`,
loc: place.loc,
suggestions: null,
}
);
kind = InstructionKind.Reassign;
originalLVal.lvalue.kind = InstructionKind.Let;
}
}
}
CompilerError.invariant(kind !== null, {
reason: "Expected at least one operand",
description: null,
loc: null,
suggestions: null,
});
value.lvalue.kind = kind;
}
rewritePlace(lvalue, rewrites, declarations);
for (const operand of eachInstructionLValue(instr)) {
rewritePlace(operand, rewrites, declarations);
}
for (const operand of eachInstructionValueOperand(instr.value)) {
rewritePlace(operand, rewrites, declarations);
}
}
const terminal = block.terminal;
for (const operand of eachTerminalOperand(terminal)) {
rewritePlace(operand, rewrites, declarations);
}
const reassignmentPhis: Array<PhiState> = [];
const rewritePhis: Array<PhiState> = [];
function pushPhis(phiBlock: BasicBlock): void {
for (const phi of phiBlock.phis) {
if (phi.id.name === null) {
rewritePhis.push({ phi, block: phiBlock });
} else {
reassignmentPhis.push({ phi, block: phiBlock });
}
}
}
const fallthroughId = terminalFallthrough(terminal);
if (fallthroughId !== null) {
const fallthrough = fn.body.blocks.get(fallthroughId)!;
pushPhis(fallthrough);
}
if (terminal.kind === "while" || terminal.kind === "for") {
const test = fn.body.blocks.get(terminal.test)!;
pushPhis(test);
const loop = fn.body.blocks.get(terminal.loop)!;
pushPhis(loop);
}
if (
terminal.kind === "for" ||
terminal.kind === "for-of" ||
terminal.kind === "for-in"
) {
let init = fn.body.blocks.get(terminal.init)!;
pushPhis(init);
let initContinuation =
terminal.kind === "for" ? terminal.test : terminal.loop;
const queue: Array<BlockId> = [init.id];
while (queue.length !== 0) {
const blockId = queue.shift()!;
if (blockId === initContinuation) {
break;
}
const block = fn.body.blocks.get(blockId)!;
for (const instr of block.instructions) {
if (
instr.value.kind === "StoreLocal" &&
instr.value.lvalue.kind !== InstructionKind.Reassign
) {
const value = instr.value;
if (value.lvalue.place.identifier.name !== null) {
const originalLVal = declarations.get(
value.lvalue.place.identifier.name.value
);
if (originalLVal === undefined) {
declarations.set(value.lvalue.place.identifier.name.value, {
lvalue: value.lvalue,
place: value.lvalue.place,
});
value.lvalue.kind = InstructionKind.Const;
}
}
}
}
switch (block.terminal.kind) {
case "maybe-throw": {
queue.push(block.terminal.continuation);
break;
}
case "goto": {
queue.push(block.terminal.block);
break;
}
case "branch":
case "logical":
case "optional":
case "ternary":
case "label": {
for (const successor of eachTerminalSuccessor(block.terminal)) {
queue.push(successor);
}
break;
}
default: {
break;
}
}
}
if (terminal.kind === "for" && terminal.update !== null) {
const update = fn.body.blocks.get(terminal.update)!;
pushPhis(update);
}
}
for (const { phi, block: phiBlock } of reassignmentPhis) {
let initOperand: Identifier | null = null;
for (const [, operand] of phi.operands) {
if (operand.mutableRange.start < terminal.id) {
if (initOperand == null) {
initOperand = operand;
}
}
}
const isPhiMutatedAfterCreation: boolean =
phi.id.mutableRange.end >
(phiBlock.instructions.at(0)?.id ?? phiBlock.terminal.id);
CompilerError.invariant(phi.id.name != null, {
reason: "Expected reassignment phis to have a name",
description: null,
loc: null,
suggestions: null,
});
const declaration = declarations.get(phi.id.name.value);
CompilerError.invariant(declaration != null, {
loc: null,
reason: "Expected a declaration for all variables",
description: `${printIdentifier(phi.id)} in block bb${phiBlock.id}`,
suggestions: null,
});
if (isPhiMutatedAfterCreation) {
declaration.place.identifier.mutableRange.end = phi.id.mutableRange.end;
}
rewrites.set(phi.id, declaration.place.identifier);
}
for (const { phi } of rewritePhis) {
let canonicalId = rewrites.get(phi.id);
if (canonicalId === undefined) {
canonicalId = phi.id;
for (const [, operand] of phi.operands) {
let canonicalOperand = rewrites.get(operand) ?? operand;
if (canonicalOperand.id < canonicalId.id) {
canonicalId = canonicalOperand;
}
}
rewrites.set(phi.id, canonicalId);
if (canonicalId.name !== null) {
const declaration = declarations.get(canonicalId.name.value);
if (declaration !== undefined) {
declaration.lvalue.kind = InstructionKind.Let;
}
}
}
for (const [, operand] of phi.operands) {
rewrites.set(operand, canonicalId);
}
}
}
}
function rewritePlace(
place: Place,
rewrites: Map<Identifier, Identifier>,
declarations: Map<string, { lvalue: LValue | LValuePattern; place: Place }>
): void {
const prevIdentifier = place.identifier;
const nextIdentifier = rewrites.get(prevIdentifier);
if (nextIdentifier !== undefined) {
if (nextIdentifier === prevIdentifier) return;
place.identifier = nextIdentifier;
} else if (prevIdentifier.name != null) {
const declaration = declarations.get(prevIdentifier.name.value);
if (declaration === undefined) return;
const originalIdentifier = declaration.place.identifier;
prevIdentifier.id = originalIdentifier.id;
}
}