import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
BlockId,
BlockKind,
Effect,
GeneratedSource,
GotoVariant,
HIR,
Identifier,
IdentifierId,
Instruction,
Place,
SourceLocation,
Terminal,
VariableBinding,
makeBlockId,
makeDeclarationId,
makeIdentifierName,
makeInstructionId,
makeTemporaryIdentifier,
makeType,
} from './HIR';
import {printInstruction} from './PrintHIR';
import {
eachTerminalSuccessor,
mapTerminalSuccessors,
terminalFallthrough,
} from './visitors';
export type WipBlock = {
id: BlockId;
instructions: Array<Instruction>;
kind: BlockKind;
};
type Scope = LoopScope | LabelScope | SwitchScope;
type LoopScope = {
kind: 'loop';
label: string | null;
continueBlock: BlockId;
breakBlock: BlockId;
};
type SwitchScope = {
kind: 'switch';
breakBlock: BlockId;
label: string | null;
};
type LabelScope = {
kind: 'label';
label: string;
breakBlock: BlockId;
};
function newBlock(id: BlockId, kind: BlockKind): WipBlock {
return {id, kind, instructions: []};
}
export type Bindings = Map<
string,
{node: t.Identifier; identifier: Identifier}
>;
export type ExceptionsMode =
| {kind: 'ThrowExceptions'}
| {kind: 'CatchExceptions'; handler: BlockId};
export default class HIRBuilder {
#completed: Map<BlockId, BasicBlock> = new Map();
#current: WipBlock;
#entry: BlockId;
#scopes: Array<Scope> = [];
#context: Array<t.Identifier>;
#bindings: Bindings;
#env: Environment;
#exceptionHandlerStack: Array<BlockId> = [];
parentFunction: NodePath<t.Function>;
errors: CompilerError = new CompilerError();
fbtDepth: number = 0;
get nextIdentifierId(): IdentifierId {
return this.#env.nextIdentifierId;
}
get context(): Array<t.Identifier> {
return this.#context;
}
get bindings(): Bindings {
return this.#bindings;
}
get environment(): Environment {
return this.#env;
}
constructor(
env: Environment,
parentFunction: NodePath<t.Function>,
bindings: Bindings | null = null,
context: Array<t.Identifier> | null = null,
) {
this.#env = env;
this.#bindings = bindings ?? new Map();
this.parentFunction = parentFunction;
this.#context = context ?? [];
this.#entry = makeBlockId(env.nextBlockId);
this.#current = newBlock(this.#entry, 'block');
}
currentBlockKind(): BlockKind {
return this.#current.kind;
}
push(instruction: Instruction): void {
this.#current.instructions.push(instruction);
const exceptionHandler = this.#exceptionHandlerStack.at(-1);
if (exceptionHandler !== undefined) {
const continuationBlock = this.reserve(this.currentBlockKind());
this.terminateWithContinuation(
{
kind: 'maybe-throw',
continuation: continuationBlock.id,
handler: exceptionHandler,
id: makeInstructionId(0),
loc: instruction.loc,
},
continuationBlock,
);
}
}
enterTryCatch(handler: BlockId, fn: () => void): void {
this.#exceptionHandlerStack.push(handler);
fn();
this.#exceptionHandlerStack.pop();
}
resolveThrowHandler(): BlockId | null {
const handler = this.#exceptionHandlerStack.at(-1);
return handler ?? null;
}
makeTemporary(loc: SourceLocation): Identifier {
const id = this.nextIdentifierId;
return makeTemporaryIdentifier(id, loc);
}
#resolveBabelBinding(
path: NodePath<t.Identifier | t.JSXIdentifier>,
): Binding | null {
const originalName = path.node.name;
const binding = path.scope.getBinding(originalName);
if (binding == null) {
return null;
}
return binding;
}
resolveIdentifier(
path: NodePath<t.Identifier | t.JSXIdentifier>,
): VariableBinding {
const originalName = path.node.name;
const babelBinding = this.#resolveBabelBinding(path);
if (babelBinding == null) {
return {kind: 'Global', name: originalName};
}
const outerBinding =
this.parentFunction.scope.parent.getBinding(originalName);
if (babelBinding === outerBinding) {
const path = babelBinding.path;
if (path.isImportDefaultSpecifier()) {
const importDeclaration =
path.parentPath as NodePath<t.ImportDeclaration>;
return {
kind: 'ImportDefault',
name: originalName,
module: importDeclaration.node.source.value,
};
} else if (path.isImportSpecifier()) {
const importDeclaration =
path.parentPath as NodePath<t.ImportDeclaration>;
return {
kind: 'ImportSpecifier',
name: originalName,
module: importDeclaration.node.source.value,
imported:
path.node.imported.type === 'Identifier'
? path.node.imported.name
: path.node.imported.value,
};
} else if (path.isImportNamespaceSpecifier()) {
const importDeclaration =
path.parentPath as NodePath<t.ImportDeclaration>;
return {
kind: 'ImportNamespace',
name: originalName,
module: importDeclaration.node.source.value,
};
} else {
return {
kind: 'ModuleLocal',
name: originalName,
};
}
}
const resolvedBinding = this.resolveBinding(babelBinding.identifier);
if (resolvedBinding.name && resolvedBinding.name.value !== originalName) {
babelBinding.scope.rename(originalName, resolvedBinding.name.value);
}
return {
kind: 'Identifier',
identifier: resolvedBinding,
bindingKind: babelBinding.kind,
};
}
isContextIdentifier(path: NodePath<t.Identifier | t.JSXIdentifier>): boolean {
const binding = this.#resolveBabelBinding(path);
if (binding) {
const outerBinding = this.parentFunction.scope.parent.getBinding(
path.node.name,
);
if (binding === outerBinding) {
return false;
}
return this.#env.isContextIdentifier(binding.identifier);
} else {
return false;
}
}
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwTodo({
reason: 'Support local variables named "fbt"',
loc: node.loc ?? null,
});
}
const originalName = node.name;
let name = originalName;
let index = 0;
while (true) {
const mapping = this.#bindings.get(name);
if (mapping === undefined) {
const id = this.nextIdentifierId;
const identifier: Identifier = {
id,
declarationId: makeDeclarationId(id),
name: makeIdentifierName(name),
mutableRange: {
start: makeInstructionId(0),
end: makeInstructionId(0),
},
scope: null,
type: makeType(),
loc: node.loc ?? GeneratedSource,
};
this.#bindings.set(name, {node, identifier});
return identifier;
} else if (mapping.node === node) {
return mapping.identifier;
} else {
name = `${originalName}_${index++}`;
}
}
}
build(): HIR {
let ir: HIR = {
blocks: this.#completed,
entry: this.#entry,
};
const rpoBlocks = getReversePostorderedBlocks(ir);
for (const [id, block] of ir.blocks) {
if (
!rpoBlocks.has(id) &&
block.instructions.some(
instr => instr.value.kind === 'FunctionExpression',
)
) {
CompilerError.throwTodo({
reason: `Support functions with unreachable code that may contain hoisted declarations`,
loc: block.instructions[0]?.loc ?? block.terminal.loc,
description: null,
suggestions: null,
});
}
}
ir.blocks = rpoBlocks;
removeUnreachableForUpdates(ir);
removeDeadDoWhileStatements(ir);
removeUnnecessaryTryCatch(ir);
markInstructionIds(ir);
markPredecessors(ir);
return ir;
}
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
const {id: blockId, kind, instructions} = this.#current;
this.#completed.set(blockId, {
kind,
id: blockId,
instructions,
terminal,
preds: new Set(),
phis: new Set(),
});
if (nextBlockKind) {
const nextId = this.#env.nextBlockId;
this.#current = newBlock(nextId, nextBlockKind);
}
}
terminateWithContinuation(terminal: Terminal, continuation: WipBlock): void {
const {id: blockId, kind, instructions} = this.#current;
this.#completed.set(blockId, {
kind: kind,
id: blockId,
instructions,
terminal: terminal,
preds: new Set(),
phis: new Set(),
});
this.#current = continuation;
}
reserve(kind: BlockKind): WipBlock {
return newBlock(makeBlockId(this.#env.nextBlockId), kind);
}
complete(block: WipBlock, terminal: Terminal): void {
const {id: blockId, kind, instructions} = block;
this.#completed.set(blockId, {
kind,
id: blockId,
instructions,
terminal,
preds: new Set(),
phis: new Set(),
});
}
enterReserved(wip: WipBlock, fn: () => Terminal): void {
const current = this.#current;
this.#current = wip;
const terminal = fn();
const {id: blockId, kind, instructions} = this.#current;
this.#completed.set(blockId, {
kind,
id: blockId,
instructions,
terminal,
preds: new Set(),
phis: new Set(),
});
this.#current = current;
}
enter(nextBlockKind: BlockKind, fn: (blockId: BlockId) => Terminal): BlockId {
const wip = this.reserve(nextBlockKind);
this.enterReserved(wip, () => {
return fn(wip.id);
});
return wip.id;
}
label<T>(label: string, breakBlock: BlockId, fn: () => T): T {
this.#scopes.push({
kind: 'label',
breakBlock,
label,
});
const value = fn();
const last = this.#scopes.pop();
CompilerError.invariant(
last != null &&
last.kind === 'label' &&
last.label === label &&
last.breakBlock === breakBlock,
{
reason: 'Mismatched label',
description: null,
loc: null,
suggestions: null,
},
);
return value;
}
switch<T>(label: string | null, breakBlock: BlockId, fn: () => T): T {
this.#scopes.push({
kind: 'switch',
breakBlock,
label,
});
const value = fn();
const last = this.#scopes.pop();
CompilerError.invariant(
last != null &&
last.kind === 'switch' &&
last.label === label &&
last.breakBlock === breakBlock,
{
reason: 'Mismatched label',
description: null,
loc: null,
suggestions: null,
},
);
return value;
}
loop<T>(
label: string | null,
continueBlock: BlockId,
breakBlock: BlockId,
fn: () => T,
): T {
this.#scopes.push({
kind: 'loop',
label,
continueBlock,
breakBlock,
});
const value = fn();
const last = this.#scopes.pop();
CompilerError.invariant(
last != null &&
last.kind === 'loop' &&
last.label === label &&
last.continueBlock === continueBlock &&
last.breakBlock === breakBlock,
{
reason: 'Mismatched loops',
description: null,
loc: null,
suggestions: null,
},
);
return value;
}
lookupBreak(label: string | null): BlockId {
for (let ii = this.#scopes.length - 1; ii >= 0; ii--) {
const scope = this.#scopes[ii];
if (
(label === null &&
(scope.kind === 'loop' || scope.kind === 'switch')) ||
label === scope.label
) {
return scope.breakBlock;
}
}
CompilerError.invariant(false, {
reason: 'Expected a loop or switch to be in scope',
description: null,
loc: null,
suggestions: null,
});
}
lookupContinue(label: string | null): BlockId {
for (let ii = this.#scopes.length - 1; ii >= 0; ii--) {
const scope = this.#scopes[ii];
if (scope.kind === 'loop') {
if (label === null || label === scope.label) {
return scope.continueBlock;
}
} else if (label !== null && scope.label === label) {
CompilerError.invariant(false, {
reason: 'Continue may only refer to a labeled loop',
description: null,
loc: null,
suggestions: null,
});
}
}
CompilerError.invariant(false, {
reason: 'Expected a loop to be in scope',
description: null,
loc: null,
suggestions: null,
});
}
}
function _shrink(func: HIR): void {
const gotos = new Map();
function resolveBlockTarget(blockId: BlockId): BlockId {
let target = gotos.get(blockId) ?? null;
if (target !== null) {
return target;
}
const block = func.blocks.get(blockId);
CompilerError.invariant(block != null, {
reason: `expected block ${blockId} to exist`,
description: null,
loc: null,
suggestions: null,
});
target = getTargetIfIndirection(block);
if (target !== null) {
target = resolveBlockTarget(target) ?? target;
gotos.set(blockId, target);
return target;
} else {
return blockId;
}
}
const queue = [func.entry];
const reachable = new Set<BlockId>();
while (queue.length !== 0) {
const blockId = queue.shift()!;
if (reachable.has(blockId)) {
continue;
}
reachable.add(blockId);
const block = func.blocks.get(blockId)!;
block.terminal = mapTerminalSuccessors(block.terminal, prevTarget => {
const target = resolveBlockTarget(prevTarget);
queue.push(target);
return target;
});
}
for (const [blockId] of func.blocks) {
if (!reachable.has(blockId)) {
func.blocks.delete(blockId);
}
}
}
export function removeUnreachableForUpdates(fn: HIR): void {
for (const [, block] of fn.blocks) {
if (
block.terminal.kind === 'for' &&
block.terminal.update !== null &&
!fn.blocks.has(block.terminal.update)
) {
block.terminal.update = null;
}
}
}
export function removeDeadDoWhileStatements(func: HIR): void {
const visited: Set<BlockId> = new Set();
for (const [_, block] of func.blocks) {
visited.add(block.id);
}
for (const [_, block] of func.blocks) {
if (block.terminal.kind === 'do-while') {
if (!visited.has(block.terminal.test)) {
block.terminal = {
kind: 'goto',
block: block.terminal.loop,
variant: GotoVariant.Break,
id: block.terminal.id,
loc: block.terminal.loc,
};
}
}
}
}
export function reversePostorderBlocks(func: HIR): void {
const rpoBlocks = getReversePostorderedBlocks(func);
func.blocks = rpoBlocks;
}
function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
const visited: Set<BlockId> = new Set();
const used: Set<BlockId> = new Set();
const usedFallthroughs: Set<BlockId> = new Set();
const postorder: Array<BlockId> = [];
function visit(blockId: BlockId, isUsed: boolean): void {
const wasUsed = used.has(blockId);
const wasVisited = visited.has(blockId);
visited.add(blockId);
if (isUsed) {
used.add(blockId);
}
if (wasVisited && (wasUsed || !isUsed)) {
return;
}
const block = func.blocks.get(blockId)!;
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
const fallthrough = terminalFallthrough(block.terminal);
if (fallthrough != null) {
if (isUsed) {
usedFallthroughs.add(fallthrough);
}
visit(fallthrough, false);
}
for (const successor of successors) {
visit(successor, isUsed);
}
if (!wasVisited) {
postorder.push(blockId);
}
}
visit(func.entry, true);
const blocks = new Map<BlockId, BasicBlock>();
for (const blockId of postorder.reverse()) {
const block = func.blocks.get(blockId)!;
if (used.has(blockId)) {
blocks.set(blockId, func.blocks.get(blockId)!);
} else if (usedFallthroughs.has(blockId)) {
blocks.set(blockId, {
...block,
instructions: [],
terminal: {
kind: 'unreachable',
id: block.terminal.id,
loc: block.terminal.loc,
},
});
}
}
return blocks;
}
export function markInstructionIds(func: HIR): void {
let id = 0;
const visited = new Set<Instruction>();
for (const [_, block] of func.blocks) {
for (const instr of block.instructions) {
CompilerError.invariant(!visited.has(instr), {
reason: `${printInstruction(instr)} already visited!`,
description: null,
loc: instr.loc,
suggestions: null,
});
visited.add(instr);
instr.id = makeInstructionId(++id);
}
block.terminal.id = makeInstructionId(++id);
}
}
export function markPredecessors(func: HIR): void {
for (const [, block] of func.blocks) {
block.preds.clear();
}
const visited: Set<BlockId> = new Set();
function visit(blockId: BlockId, prevBlock: BasicBlock | null): void {
const block = func.blocks.get(blockId)!;
if (block == null) {
return;
}
CompilerError.invariant(block != null, {
reason: 'unexpected missing block',
description: `block ${blockId}`,
loc: GeneratedSource,
});
if (prevBlock) {
block.preds.add(prevBlock.id);
}
if (visited.has(blockId)) {
return;
}
visited.add(blockId);
const {terminal} = block;
for (const successor of eachTerminalSuccessor(terminal)) {
visit(successor, block);
}
}
visit(func.entry, null);
}
function getTargetIfIndirection(block: BasicBlock): number | null {
return block.instructions.length === 0 &&
block.terminal.kind === 'goto' &&
block.terminal.variant === GotoVariant.Break
? block.terminal.block
: null;
}
export function removeUnnecessaryTryCatch(fn: HIR): void {
for (const [, block] of fn.blocks) {
if (
block.terminal.kind === 'try' &&
!fn.blocks.has(block.terminal.handler)
) {
const handlerId = block.terminal.handler;
const fallthroughId = block.terminal.fallthrough;
const fallthrough = fn.blocks.get(fallthroughId);
block.terminal = {
kind: 'goto',
block: block.terminal.block,
id: makeInstructionId(0),
loc: block.terminal.loc,
variant: GotoVariant.Break,
};
if (fallthrough != null) {
if (fallthrough.preds.size === 1 && fallthrough.preds.has(handlerId)) {
fn.blocks.delete(fallthroughId);
} else {
fallthrough.preds.delete(handlerId);
}
}
}
}
}
export function createTemporaryPlace(
env: Environment,
loc: SourceLocation,
): Place {
return {
kind: 'Identifier',
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
reactive: false,
effect: Effect.Unknown,
loc: GeneratedSource,
};
}
export function clonePlaceToTemporary(env: Environment, place: Place): Place {
const temp = createTemporaryPlace(env, place.loc);
temp.effect = place.effect;
temp.identifier.type = place.identifier.type;
temp.reactive = place.reactive;
return temp;
}
export function fixScopeAndIdentifierRanges(func: HIR): void {
for (const [, block] of func.blocks) {
const terminal = block.terminal;
if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {
const fallthroughBlock = func.blocks.get(terminal.fallthrough)!;
const firstId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
terminal.scope.range.start = terminal.id;
terminal.scope.range.end = firstId;
}
}
}