[HIR] Nonnullable block fallthroughs

ghstack-source-id: 6f24b60056c741e5d4e9836f91bc4fce0f9e8fdd
Pull Request resolved: https://github.com/facebook/react-forget/pull/2865
This commit is contained in:
Mofei Zhang
2024-04-23 10:18:49 +01:00
parent 1efb3cae0a
commit 555100ca83
11 changed files with 172 additions and 295 deletions
@@ -362,6 +362,8 @@ export type Terminal =
| TryTerminal
| ReactiveScopeTerminal;
export type TerminalWithFallthrough = Terminal & { fallthrough: BlockId };
function _staticInvariantTerminalHasLocation(
terminal: Terminal
): SourceLocation {
@@ -376,6 +378,13 @@ function _staticInvariantTerminalHasInstructionId(
return terminal.id;
}
function _staticInvariantTerminalHasFallthrough(
terminal: Terminal
): BlockId | never | undefined {
// If this fails, it is because a variant of Terminal is missing a fallthrough annotation
return terminal.fallthrough;
}
/*
* Terminal nodes allowed for a value block
* A terminal that couldn't be lowered correctly.
@@ -384,6 +393,7 @@ export type UnsupportedTerminal = {
kind: "unsupported";
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
};
/**
@@ -395,6 +405,7 @@ export type UnreachableTerminal = {
kind: "unreachable";
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
};
export type ThrowTerminal = {
@@ -402,6 +413,7 @@ export type ThrowTerminal = {
value: Place;
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
};
export type Case = { test: Place | null; block: BlockId };
@@ -410,6 +422,7 @@ export type ReturnTerminal = {
loc: SourceLocation;
value: Place;
id: InstructionId;
fallthrough?: never;
};
export type GotoTerminal = {
@@ -418,6 +431,7 @@ export type GotoTerminal = {
variant: GotoVariant;
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
};
export enum GotoVariant {
@@ -431,7 +445,7 @@ export type IfTerminal = {
test: Place;
consequent: BlockId;
alternate: BlockId;
fallthrough: BlockId | null;
fallthrough: BlockId;
id: InstructionId;
loc: SourceLocation;
};
@@ -443,13 +457,14 @@ export type BranchTerminal = {
alternate: BlockId;
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
};
export type SwitchTerminal = {
kind: "switch";
test: Place;
cases: Array<Case>;
fallthrough: BlockId | null;
fallthrough: BlockId;
id: InstructionId;
loc: SourceLocation;
};
@@ -521,7 +536,7 @@ export type TernaryTerminal = {
export type LabelTerminal = {
kind: "label";
block: BlockId;
fallthrough: BlockId | null;
fallthrough: BlockId;
id: InstructionId;
loc: SourceLocation;
};
@@ -555,7 +570,7 @@ export type TryTerminal = {
handlerBinding: Place | null;
handler: BlockId;
// TODO: support `finally`
fallthrough: BlockId | null;
fallthrough: BlockId;
id: InstructionId;
loc: SourceLocation;
};
@@ -566,6 +581,7 @@ export type MaybeThrowTerminal = {
handler: BlockId;
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
};
export type ReactiveScopeTerminal = {
@@ -8,7 +8,6 @@
import { Binding, NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import { CompilerError } from "../CompilerError";
import { assertExhaustive } from "../Utils/utils";
import { Environment } from "./Environment";
import { Global } from "./Globals";
import {
@@ -32,8 +31,8 @@ import {
import { printInstruction } from "./PrintHIR";
import {
eachTerminalSuccessor,
mapOptionalFallthroughs,
mapTerminalSuccessors,
terminalFallthrough,
} from "./visitors";
/*
@@ -329,7 +328,6 @@ export default class HIRBuilder {
ir.blocks = rpoBlocks;
removeUnreachableForUpdates(ir);
removeUnreachableFallthroughs(ir);
removeDeadDoWhileStatements(ir);
removeUnnecessaryTryCatch(ir);
markInstructionIds(ir);
@@ -629,24 +627,6 @@ export function removeUnreachableForUpdates(fn: HIR): void {
}
}
export function removeUnreachableFallthroughs(func: HIR): void {
const visited: Set<BlockId> = new Set();
for (const [_, block] of func.blocks) {
visited.add(block.id);
}
// Cleanup any fallthrough blocks that weren't visited
for (const [_, block] of func.blocks) {
mapOptionalFallthroughs(block.terminal, (fallthrough) => {
if (visited.has(fallthrough)) {
return fallthrough;
} else {
return null;
}
});
}
}
export function removeDeadDoWhileStatements(func: HIR): void {
const visited: Set<BlockId> = new Set();
for (const [_, block] of func.blocks) {
@@ -691,14 +671,19 @@ export function reversePostorderBlocks(func: HIR): void {
*/
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): void {
if (visited.has(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;
}
visited.add(blockId);
const block = func.blocks.get(blockId)!;
const { terminal } = block;
/*
* Note that we visit successors in reverse order. This ensures that when we
@@ -710,7 +695,7 @@ function getReversePostorderedBlocks(func: HIR): HIR["blocks"] {
* // bb1
* x = 1;
* } else {
* // b2
* // bb2
* x = 2;
* }
* // bb3
@@ -721,104 +706,50 @@ function getReversePostorderedBlocks(func: HIR): HIR["blocks"] {
* program order for visual debugging. By visiting the successors in reverse order
* (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
*/
switch (terminal.kind) {
case "return":
case "throw": {
// no-op, no successors
break;
}
case "goto": {
visit(terminal.block);
break;
}
case "if": {
/*
* can ignore fallthrough, if its reachable it will be reached through
* consequent/alternate
*/
const { consequent, alternate } = terminal;
visit(alternate);
visit(consequent);
break;
}
case "branch": {
const { consequent, alternate } = terminal;
visit(alternate);
visit(consequent);
break;
}
case "switch": {
/*
* can ignore fallthrough, if its reachable it will be reached through
* a case
*/
const { cases } = terminal;
for (const case_ of [...cases].reverse()) {
visit(case_.block);
}
break;
}
case "optional":
case "ternary":
case "logical": {
visit(terminal.test);
break;
}
case "do-while": {
visit(terminal.loop);
break;
}
case "while": {
visit(terminal.test);
break;
}
case "for":
case "for-in":
case "for-of": {
visit(terminal.init);
break;
}
case "label": {
visit(terminal.block);
break;
}
case "sequence": {
visit(terminal.block);
break;
}
case "maybe-throw": {
visit(terminal.handler);
visit(terminal.continuation);
break;
}
case "try": {
visit(terminal.block);
break;
}
case "scope": {
visit(terminal.block);
break;
}
case "unreachable":
case "unsupported": {
break;
}
default: {
assertExhaustive(
terminal,
`Unexpected terminal kind \`${(terminal as any).kind}\``
);
const block = func.blocks.get(blockId)!;
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
const fallthrough = terminalFallthrough(block.terminal);
/**
* Fallthrough blocks are only used to record original program block structure. If the
* fallthrough is actually reachable, it will be reached through terminal successors.
* To retain program structure, we visit fallthrough blocks first (marking them as not
* actually used yet) to ensure their block IDs emitted in the correct order.
*/
if (fallthrough != null) {
if (isUsed) {
usedFallthroughs.add(fallthrough);
}
visit(fallthrough, false);
}
for (const successor of successors) {
visit(successor, isUsed);
}
postorder.push(blockId);
if (!wasVisited) {
postorder.push(blockId);
}
}
visit(func.entry);
const blocks = new Map();
visit(func.entry, true);
const blocks = new Map<BlockId, BasicBlock>();
for (const blockId of postorder.reverse()) {
blocks.set(blockId, func.blocks.get(blockId)!);
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,
},
});
}
// otherwise this block is unreachable
}
return blocks;
}
@@ -847,6 +778,14 @@ export function markPredecessors(func: HIR): void {
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);
}
@@ -879,7 +818,7 @@ function getTargetIfIndirection(block: BasicBlock): number | null {
/*
* Finds try terminals where the handler is unreachable, and converts the try
* to a goto(terminal.fallthrough)
* to a goto(terminal.block)
*/
export function removeUnnecessaryTryCatch(fn: HIR): void {
for (const [, block] of fn.blocks) {
@@ -887,6 +826,9 @@ export function removeUnnecessaryTryCatch(fn: HIR): void {
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,
@@ -894,6 +836,15 @@ export function removeUnnecessaryTryCatch(fn: HIR): void {
loc: block.terminal.loc,
variant: GotoVariant.Break,
};
if (fallthrough != null) {
if (fallthrough.preds.size === 1 && fallthrough.preds.has(handlerId)) {
// delete fallthrough
fn.blocks.delete(fallthroughId);
} else {
fallthrough.preds.delete(handlerId);
}
}
}
}
}
@@ -13,8 +13,8 @@ import {
HIRFunction,
Instruction,
} from "./HIR";
import { markPredecessors, removeUnreachableFallthroughs } from "./HIRBuilder";
import { mapOptionalFallthroughs, terminalFallthrough } from "./visitors";
import { markPredecessors } from "./HIRBuilder";
import { terminalFallthrough, terminalHasFallthrough } from "./visitors";
/*
* Merges sequences of blocks that will always execute consecutively —
@@ -113,10 +113,11 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
fn.body.blocks.delete(block.id);
}
markPredecessors(fn.body);
for (const [, block] of fn.body.blocks) {
mapOptionalFallthroughs(block.terminal, (blockId) => merged.get(blockId));
for (const [, { terminal }] of fn.body.blocks) {
if (terminalHasFallthrough(terminal)) {
terminal.fallthrough = merged.get(terminal.fallthrough);
}
}
removeUnreachableFallthroughs(fn.body);
}
class MergedBlocks {
@@ -23,7 +23,6 @@ export {
markInstructionIds,
markPredecessors,
removeUnnecessaryTryCatch,
removeUnreachableFallthroughs,
reversePostorderBlocks,
} from "./HIRBuilder";
export { mergeConsecutiveBlocks } from "./MergeConsecutiveBlocks";
@@ -634,8 +634,7 @@ export function mapTerminalSuccessors(
case "if": {
const consequent = fn(terminal.consequent);
const alternate = fn(terminal.alternate);
const fallthrough =
terminal.fallthrough !== null ? fn(terminal.fallthrough) : null;
const fallthrough = fn(terminal.fallthrough);
return {
kind: "if",
test: terminal.test,
@@ -666,8 +665,7 @@ export function mapTerminalSuccessors(
block: target,
};
});
const fallthrough =
terminal.fallthrough !== null ? fn(terminal.fallthrough) : null;
const fallthrough = fn(terminal.fallthrough);
return {
kind: "switch",
test: terminal.test,
@@ -794,8 +792,7 @@ export function mapTerminalSuccessors(
}
case "label": {
const block = fn(terminal.block);
const fallthrough =
terminal.fallthrough !== null ? fn(terminal.fallthrough) : null;
const fallthrough = fn(terminal.fallthrough);
return {
kind: "label",
block,
@@ -829,8 +826,7 @@ export function mapTerminalSuccessors(
case "try": {
const block = fn(terminal.block);
const handler = fn(terminal.handler);
const fallthrough =
terminal.fallthrough !== null ? fn(terminal.fallthrough) : null;
const fallthrough = fn(terminal.fallthrough);
return {
kind: "try",
block,
@@ -866,12 +862,10 @@ export function mapTerminalSuccessors(
}
}
/*
* Helper to get a terminal's fallthrough. The main reason to extract this as a helper
* function is to ensure that we use an exhaustive switch to ensure that we add new terminal
* variants as appropriate.
*/
export function terminalFallthrough(terminal: Terminal): BlockId | null {
export function terminalHasFallthrough<
T extends Terminal,
U extends T & { fallthrough: BlockId }
>(terminal: T): terminal is U {
switch (terminal.kind) {
case "maybe-throw":
case "branch":
@@ -880,7 +874,8 @@ export function terminalFallthrough(terminal: Terminal): BlockId | null {
case "throw":
case "unreachable":
case "unsupported": {
return null;
const _: undefined = terminal.fallthrough;
return false;
}
case "try":
case "do-while":
@@ -896,7 +891,8 @@ export function terminalFallthrough(terminal: Terminal): BlockId | null {
case "ternary":
case "while":
case "scope": {
return terminal.fallthrough;
const _: BlockId = terminal.fallthrough;
return true;
}
default: {
assertExhaustive(
@@ -907,104 +903,16 @@ export function terminalFallthrough(terminal: Terminal): BlockId | null {
}
}
export function mapOptionalFallthroughs(
terminal: Terminal,
fn: (block: BlockId) => BlockId | null
): void {
switch (terminal.kind) {
case "maybe-throw":
case "branch":
case "goto":
case "return":
case "throw":
case "unreachable":
case "unsupported": {
return;
}
/*
* NOTE: TypeScript has a bug where it does not correctly model properties whose values are
* non-null in some cases and nullable in other cases, if those cases are joined together.
* Thus we use one block per case here to ensure that any changes to the types will cause
* a compiler error.
*/
case "do-while": {
const _: BlockId = terminal.fallthrough;
break;
}
case "for-of": {
const _: BlockId = terminal.fallthrough;
break;
}
case "for-in": {
const _: BlockId = terminal.fallthrough;
break;
}
case "for": {
const _: BlockId = terminal.fallthrough;
break;
}
case "logical": {
const _: BlockId = terminal.fallthrough;
break;
}
case "optional": {
const _: BlockId = terminal.fallthrough;
break;
}
case "ternary": {
const _: BlockId = terminal.fallthrough;
break;
}
case "while": {
const _: BlockId = terminal.fallthrough;
break;
}
case "scope": {
const _: BlockId = terminal.fallthrough;
break;
}
case "switch": {
if (terminal.fallthrough !== null) {
terminal.fallthrough = fn(terminal.fallthrough);
} else {
terminal.fallthrough = null;
}
break;
}
case "if": {
if (terminal.fallthrough !== null) {
terminal.fallthrough = fn(terminal.fallthrough);
} else {
terminal.fallthrough = null;
}
break;
}
case "label": {
if (terminal.fallthrough !== null) {
terminal.fallthrough = fn(terminal.fallthrough);
} else {
terminal.fallthrough = null;
}
break;
}
case "sequence": {
const _: BlockId = terminal.fallthrough;
break;
}
case "try": {
if (terminal.fallthrough !== null) {
terminal.fallthrough = fn(terminal.fallthrough);
} else {
terminal.fallthrough = null;
}
break;
}
default: {
assertExhaustive(
terminal,
`Unexpected terminal kind \`${(terminal as any).kind}\``
);
}
/*
* Helper to get a terminal's fallthrough. The main reason to extract this as a helper
* function is to ensure that we use an exhaustive switch to ensure that we add new terminal
* variants as appropriate.
*/
export function terminalFallthrough(terminal: Terminal): BlockId | null {
if (terminalHasFallthrough(terminal)) {
return terminal.fallthrough;
} else {
return null;
}
}
@@ -23,7 +23,6 @@ import {
markInstructionIds,
markPredecessors,
mergeConsecutiveBlocks,
removeUnreachableFallthroughs,
reversePostorderBlocks,
} from "../HIR";
import {
@@ -72,7 +71,6 @@ function constantPropagationImpl(fn: HIRFunction, constants: Constants): void {
* Re-run minification of the graph (incl reordering instruction ids)
*/
reversePostorderBlocks(fn.body);
removeUnreachableFallthroughs(fn.body);
removeUnreachableForUpdates(fn.body);
removeDeadDoWhileStatements(fn.body);
removeUnnecessaryTryCatch(fn.body);
@@ -15,7 +15,6 @@ import {
assertConsistentIdentifiers,
assertTerminalSuccessorsExist,
mergeConsecutiveBlocks,
removeUnreachableFallthroughs,
reversePostorderBlocks,
} from "../HIR";
import {
@@ -39,7 +38,6 @@ export function pruneMaybeThrows(fn: HIRFunction): void {
* Re-run minification of the graph (incl reordering instruction ids)
*/
reversePostorderBlocks(fn.body);
removeUnreachableFallthroughs(fn.body);
removeUnreachableForUpdates(fn.body);
removeDeadDoWhileStatements(fn.body);
removeUnnecessaryTryCatch(fn.body);
@@ -112,7 +112,7 @@ class Driver {
}
case "if": {
const fallthroughId =
terminal.fallthrough !== null &&
this.cx.reachable(terminal.fallthrough) &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
@@ -176,7 +176,7 @@ class Driver {
}
case "switch": {
const fallthroughId =
terminal.fallthrough !== null &&
this.cx.reachable(terminal.fallthrough) &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
@@ -289,7 +289,7 @@ class Driver {
}
case "while": {
const fallthroughId =
terminal.fallthrough !== null &&
this.cx.reachable(terminal.fallthrough) &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
@@ -350,11 +350,9 @@ class Driver {
? terminal.loop
: null;
const fallthroughId =
terminal.fallthrough !== null &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
const scheduleId = this.cx.scheduleLoop(
terminal.fallthrough,
@@ -437,11 +435,9 @@ class Driver {
? terminal.loop
: null;
const fallthroughId =
terminal.fallthrough !== null &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
const scheduleId = this.cx.scheduleLoop(
terminal.fallthrough,
@@ -512,11 +508,9 @@ class Driver {
? terminal.loop
: null;
const fallthroughId =
terminal.fallthrough !== null &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
const scheduleId = this.cx.scheduleLoop(
terminal.fallthrough,
@@ -626,7 +620,7 @@ class Driver {
}
case "label": {
const fallthroughId =
terminal.fallthrough !== null &&
this.cx.reachable(terminal.fallthrough) &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
@@ -747,7 +741,7 @@ class Driver {
}
case "try": {
const fallthroughId =
terminal.fallthrough !== null &&
this.cx.reachable(terminal.fallthrough) &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
@@ -1241,6 +1235,11 @@ class Context {
this.#catchHandlers.add(block);
}
reachable(id: BlockId): boolean {
const block = this.ir.blocks.get(id)!;
return block.terminal.kind !== "unreachable";
}
/*
* Record that the given block will be emitted (eg by the codegen of a parent node)
* so that child nodes can avoid re-emitting it.
@@ -40,38 +40,46 @@ import { unstable_useMemoCache as useMemoCache } from "react";
import { mutate, setProperty, throwErrorWithMessageIf } from "shared-runtime";
function useFoo(t0) {
const $ = useMemoCache(3);
const $ = useMemoCache(6);
const { value, cond } = t0;
let y;
let t1;
if ($[0] !== value || $[1] !== cond) {
t1 = Symbol.for("react.early_return_sentinel");
bb0: {
const y = [value];
const x = { cond };
try {
mutate(x);
throwErrorWithMessageIf(x.cond, "error");
} catch {
setProperty(x, "henderson");
t1 = x;
break bb0;
y = [value];
let x;
if ($[4] !== cond) {
x = { cond };
try {
mutate(x);
throwErrorWithMessageIf(x.cond, "error");
} catch {
setProperty(x, "henderson");
t1 = x;
break bb0;
}
setProperty(x, "nevada");
$[4] = cond;
$[5] = x;
} else {
x = $[5];
}
setProperty(x, "nevada");
y.push(x);
t1 = y;
break bb0;
}
$[0] = value;
$[1] = cond;
$[2] = t1;
$[2] = y;
$[3] = t1;
} else {
t1 = $[2];
y = $[2];
t1 = $[3];
}
if (t1 !== Symbol.for("react.early_return_sentinel")) {
return t1;
}
return y;
}
export const FIXTURE_ENTRYPOINT = {
@@ -45,9 +45,6 @@ function Component(props) {
t0 = e;
break bb0;
}
t0 = null;
break bb0;
}
$[0] = props.y;
$[1] = props.e;
@@ -58,6 +55,7 @@ function Component(props) {
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;
}
return null;
}
export const FIXTURE_ENTRYPOINT = {
@@ -31,12 +31,13 @@ import { unstable_useMemoCache as useMemoCache } from "react";
const { throwInput } = require("shared-runtime");
function Component(props) {
const $ = useMemoCache(1);
const $ = useMemoCache(2);
let x;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
const x = [];
x = [];
try {
throwInput(x);
} catch (t1) {
@@ -45,17 +46,17 @@ function Component(props) {
t0 = e;
break bb0;
}
t0 = x;
break bb0;
}
$[0] = t0;
$[0] = x;
$[1] = t0;
} else {
t0 = $[0];
x = $[0];
t0 = $[1];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;
}
return x;
}
export const FIXTURE_ENTRYPOINT = {