/** * 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 : ""}(`); 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(` ${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 ): string { const writer = new Writer(); writeReactiveInstructions(writer, instructions); return writer.complete(); } export function writeReactiveInstructions( writer: Writer, instructions: Array ): 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 = []; #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--; } }