mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
There are a few places where we want to check whether a value actually got memoized, and we currently have to infer this based on values that "should" have a scope and whether a corresponding scope actually exists. This PR adds a new ReactiveStatement variant to model a reactive scope block that was pruned for some reason, and updates all the passes that prune scopes to instead produce this new variant.
ghstack-source-id: aea6dab469
Pull Request resolved: https://github.com/facebook/react/pull/29781
434 lines
12 KiB
TypeScript
434 lines
12 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import { CompilerError } from "../CompilerError";
|
|
import {
|
|
PrunedReactiveScopeBlock,
|
|
ReactiveFunction,
|
|
ReactiveScope,
|
|
ReactiveScopeBlock,
|
|
ReactiveScopeDependency,
|
|
ReactiveStatement,
|
|
ReactiveTerminal,
|
|
ReactiveValue,
|
|
} from "../HIR/HIR";
|
|
import {
|
|
printIdentifier,
|
|
printInstructionValue,
|
|
printPlace,
|
|
printType,
|
|
} from "../HIR/PrintHIR";
|
|
import { assertExhaustive } from "../Utils/utils";
|
|
|
|
export function printReactiveFunction(fn: ReactiveFunction): string {
|
|
const writer = new Writer();
|
|
writer.writeLine(`function ${fn.id !== null ? fn.id : "<unknown>"}(`);
|
|
writer.indented(() => {
|
|
for (const param of fn.params) {
|
|
if (param.kind === "Identifier") {
|
|
writer.writeLine(`${printPlace(param)},`);
|
|
} else {
|
|
writer.writeLine(`...${printPlace(param.place)},`);
|
|
}
|
|
}
|
|
});
|
|
writer.writeLine(") {");
|
|
writeReactiveInstructions(writer, fn.body);
|
|
writer.writeLine("}");
|
|
return writer.complete();
|
|
}
|
|
|
|
export function printReactiveScopeSummary(scope: ReactiveScope): string {
|
|
const items = [];
|
|
// If the scope has a return value it needs a label
|
|
items.push("scope");
|
|
items.push(`@${scope.id}`);
|
|
items.push(`[${scope.range.start}:${scope.range.end}]`);
|
|
items.push(
|
|
`dependencies=[${Array.from(scope.dependencies)
|
|
.map((dep) => printDependency(dep))
|
|
.join(", ")}]`
|
|
);
|
|
items.push(
|
|
`declarations=[${Array.from(scope.declarations)
|
|
.map(([, decl]) =>
|
|
printIdentifier({ ...decl.identifier, scope: decl.scope })
|
|
)
|
|
.join(", ")}]`
|
|
);
|
|
items.push(
|
|
`reassignments=[${Array.from(scope.reassignments).map((reassign) =>
|
|
printIdentifier(reassign)
|
|
)}]`
|
|
);
|
|
if (scope.earlyReturnValue !== null) {
|
|
items.push(
|
|
`earlyReturn={id: ${printIdentifier(
|
|
scope.earlyReturnValue.value
|
|
)}, label: ${scope.earlyReturnValue.label}}}`
|
|
);
|
|
}
|
|
return items.join(" ");
|
|
}
|
|
|
|
export function writeReactiveBlock(
|
|
writer: Writer,
|
|
block: ReactiveScopeBlock
|
|
): void {
|
|
writer.writeLine(`${printReactiveScopeSummary(block.scope)} {`);
|
|
writeReactiveInstructions(writer, block.instructions);
|
|
writer.writeLine("}");
|
|
}
|
|
|
|
export function writePrunedScope(
|
|
writer: Writer,
|
|
block: PrunedReactiveScopeBlock
|
|
): void {
|
|
writer.writeLine(`<pruned> ${printReactiveScopeSummary(block.scope)} {`);
|
|
writeReactiveInstructions(writer, block.instructions);
|
|
writer.writeLine("}");
|
|
}
|
|
|
|
export function printDependency(dependency: ReactiveScopeDependency): string {
|
|
const identifier =
|
|
printIdentifier(dependency.identifier) +
|
|
printType(dependency.identifier.type);
|
|
return `${identifier}${dependency.path.map((prop) => `.${prop}`).join("")}`;
|
|
}
|
|
|
|
export function printReactiveInstructions(
|
|
instructions: Array<ReactiveStatement>
|
|
): string {
|
|
const writer = new Writer();
|
|
writeReactiveInstructions(writer, instructions);
|
|
return writer.complete();
|
|
}
|
|
|
|
export function writeReactiveInstructions(
|
|
writer: Writer,
|
|
instructions: Array<ReactiveStatement>
|
|
): void {
|
|
writer.indented(() => {
|
|
for (const instr of instructions) {
|
|
writeReactiveInstruction(writer, instr);
|
|
}
|
|
});
|
|
}
|
|
|
|
function writeReactiveInstruction(
|
|
writer: Writer,
|
|
instr: ReactiveStatement
|
|
): void {
|
|
switch (instr.kind) {
|
|
case "instruction": {
|
|
const { instruction } = instr;
|
|
const id = `[${instruction.id}]`;
|
|
|
|
if (instruction.lvalue !== null) {
|
|
writer.write(`${id} ${printPlace(instruction.lvalue)} = `);
|
|
writeReactiveValue(writer, instruction.value);
|
|
writer.newline();
|
|
} else {
|
|
writer.write(`${id} `);
|
|
writeReactiveValue(writer, instruction.value);
|
|
writer.newline();
|
|
}
|
|
break;
|
|
}
|
|
case "scope": {
|
|
writeReactiveBlock(writer, instr);
|
|
break;
|
|
}
|
|
case "pruned-scope": {
|
|
writePrunedScope(writer, instr);
|
|
break;
|
|
}
|
|
case "terminal": {
|
|
if (instr.label !== null) {
|
|
writer.write(`bb${instr.label.id}: `);
|
|
}
|
|
writeTerminal(writer, instr.terminal);
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
instr,
|
|
`Unexpected terminal kind \`${(instr as any).kind}\``
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function printReactiveValue(value: ReactiveValue): string {
|
|
const writer = new Writer();
|
|
writeReactiveValue(writer, value);
|
|
return writer.complete();
|
|
}
|
|
|
|
function writeReactiveValue(writer: Writer, value: ReactiveValue): void {
|
|
switch (value.kind) {
|
|
case "ConditionalExpression": {
|
|
writer.writeLine(`Ternary `);
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, value.test);
|
|
writer.writeLine(`? `);
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, value.consequent);
|
|
});
|
|
writer.writeLine(`: `);
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, value.alternate);
|
|
});
|
|
});
|
|
writer.newline();
|
|
break;
|
|
}
|
|
case "LogicalExpression": {
|
|
writer.writeLine(`Logical`);
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, value.left);
|
|
writer.write(`${value.operator} `);
|
|
writeReactiveValue(writer, value.right);
|
|
});
|
|
writer.newline();
|
|
break;
|
|
}
|
|
case "SequenceExpression": {
|
|
writer.writeLine(`Sequence`);
|
|
writer.indented(() => {
|
|
writer.indented(() => {
|
|
value.instructions.forEach((instr) =>
|
|
writeReactiveInstruction(writer, {
|
|
kind: "instruction",
|
|
instruction: instr,
|
|
})
|
|
);
|
|
writer.write(`[${value.id}] `);
|
|
writeReactiveValue(writer, value.value);
|
|
});
|
|
});
|
|
writer.newline();
|
|
break;
|
|
}
|
|
case "OptionalExpression": {
|
|
writer.append(`OptionalExpression optional=${value.optional}`);
|
|
writer.newline();
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, value.value);
|
|
});
|
|
writer.newline();
|
|
break;
|
|
}
|
|
default: {
|
|
const printed = printInstructionValue(value);
|
|
const lines = printed.split("\n");
|
|
if (lines.length === 1) {
|
|
writer.writeLine(printed);
|
|
} else {
|
|
writer.indented(() => {
|
|
for (const line of lines) {
|
|
writer.writeLine(line);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
|
|
switch (terminal.kind) {
|
|
case "break": {
|
|
const id = terminal.id !== null ? `[${terminal.id}]` : [];
|
|
writer.writeLine(
|
|
`${id} break bb${terminal.target} (${terminal.targetKind})`
|
|
);
|
|
|
|
break;
|
|
}
|
|
case "continue": {
|
|
const id = `[${terminal.id}]`;
|
|
writer.writeLine(
|
|
`${id} continue bb${terminal.target} (${terminal.targetKind})`
|
|
);
|
|
break;
|
|
}
|
|
case "do-while": {
|
|
writer.writeLine(`[${terminal.id}] do-while {`);
|
|
writeReactiveInstructions(writer, terminal.loop);
|
|
writer.writeLine("} (");
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, terminal.test);
|
|
});
|
|
writer.writeLine(")");
|
|
break;
|
|
}
|
|
case "while": {
|
|
writer.writeLine(`[${terminal.id}] while (`);
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, terminal.test);
|
|
});
|
|
writer.writeLine(") {");
|
|
writeReactiveInstructions(writer, terminal.loop);
|
|
writer.writeLine("}");
|
|
break;
|
|
}
|
|
case "if": {
|
|
const { test, consequent, alternate } = terminal;
|
|
writer.writeLine(`[${terminal.id}] if (${printPlace(test)}) {`);
|
|
writeReactiveInstructions(writer, consequent);
|
|
if (alternate !== null) {
|
|
writer.writeLine("} else {");
|
|
writeReactiveInstructions(writer, alternate);
|
|
}
|
|
writer.writeLine("}");
|
|
break;
|
|
}
|
|
case "switch": {
|
|
writer.writeLine(
|
|
`[${terminal.id}] switch (${printPlace(terminal.test)}) {`
|
|
);
|
|
writer.indented(() => {
|
|
for (const case_ of terminal.cases) {
|
|
let prefix =
|
|
case_.test !== null ? `case ${printPlace(case_.test)}` : "default";
|
|
writer.writeLine(`${prefix}: {`);
|
|
writer.indented(() => {
|
|
const block = case_.block;
|
|
CompilerError.invariant(block != null, {
|
|
reason: "Expected case to have a block",
|
|
description: null,
|
|
loc: case_.test?.loc ?? null,
|
|
suggestions: null,
|
|
});
|
|
writeReactiveInstructions(writer, block);
|
|
});
|
|
writer.writeLine("}");
|
|
}
|
|
});
|
|
writer.writeLine("}");
|
|
break;
|
|
}
|
|
case "for": {
|
|
writer.writeLine(`[${terminal.id}] for (`);
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, terminal.init);
|
|
writer.writeLine(";");
|
|
writeReactiveValue(writer, terminal.test);
|
|
writer.writeLine(";");
|
|
if (terminal.update !== null) {
|
|
writeReactiveValue(writer, terminal.update);
|
|
}
|
|
});
|
|
writer.writeLine(") {");
|
|
writeReactiveInstructions(writer, terminal.loop);
|
|
writer.writeLine("}");
|
|
break;
|
|
}
|
|
case "for-of": {
|
|
writer.writeLine(`[${terminal.id}] for-of (`);
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, terminal.init);
|
|
writer.writeLine(";");
|
|
writeReactiveValue(writer, terminal.test);
|
|
});
|
|
writer.writeLine(") {");
|
|
writeReactiveInstructions(writer, terminal.loop);
|
|
writer.writeLine("}");
|
|
break;
|
|
}
|
|
case "for-in": {
|
|
writer.writeLine(`[${terminal.id}] for-in (`);
|
|
writer.indented(() => {
|
|
writeReactiveValue(writer, terminal.init);
|
|
});
|
|
writer.writeLine(") {");
|
|
writeReactiveInstructions(writer, terminal.loop);
|
|
writer.writeLine("}");
|
|
break;
|
|
}
|
|
case "throw": {
|
|
writer.writeLine(`[${terminal.id}] throw ${printPlace(terminal.value)}`);
|
|
break;
|
|
}
|
|
case "return": {
|
|
writer.writeLine(`[${terminal.id}] return ${printPlace(terminal.value)}`);
|
|
break;
|
|
}
|
|
case "label": {
|
|
writer.writeLine("{");
|
|
writeReactiveInstructions(writer, terminal.block);
|
|
writer.writeLine("}");
|
|
break;
|
|
}
|
|
case "try": {
|
|
writer.writeLine(`[${terminal.id}] try {`);
|
|
writeReactiveInstructions(writer, terminal.block);
|
|
writer.write(`} catch `);
|
|
if (terminal.handlerBinding !== null) {
|
|
writer.writeLine(`(${printPlace(terminal.handlerBinding)}) {`);
|
|
} else {
|
|
writer.writeLine(`{`);
|
|
}
|
|
writeReactiveInstructions(writer, terminal.handler);
|
|
writer.writeLine("}");
|
|
break;
|
|
}
|
|
default:
|
|
assertExhaustive(terminal, `Unhandled terminal ${terminal}`);
|
|
}
|
|
}
|
|
|
|
export class Writer {
|
|
#out: Array<string> = [];
|
|
#line: string;
|
|
#depth: number;
|
|
|
|
constructor({ depth }: { depth: number } = { depth: 0 }) {
|
|
this.#depth = Math.max(depth, 0);
|
|
this.#line = "";
|
|
}
|
|
|
|
complete(): string {
|
|
const line = this.#line.trimEnd();
|
|
if (line.length > 0) {
|
|
this.#out.push(line);
|
|
}
|
|
return this.#out.join("\n");
|
|
}
|
|
|
|
append(s: string): void {
|
|
this.write(s);
|
|
}
|
|
|
|
newline(): void {
|
|
const line = this.#line.trimEnd();
|
|
if (line.length > 0) {
|
|
this.#out.push(line);
|
|
}
|
|
this.#line = "";
|
|
}
|
|
|
|
write(s: string): void {
|
|
if (this.#line.length === 0 && this.#depth > 0) {
|
|
// indent before writing
|
|
this.#line = " ".repeat(this.#depth);
|
|
}
|
|
this.#line += s;
|
|
}
|
|
|
|
writeLine(s: string): void {
|
|
this.write(s);
|
|
this.newline();
|
|
}
|
|
|
|
indented(f: () => void): void {
|
|
this.#depth++;
|
|
f();
|
|
this.#depth--;
|
|
}
|
|
}
|