import generate from "@babel/generator";
import { printReactiveFunction } from "..";
import { CompilerError } from "../CompilerError";
import { printReactiveScopeSummary } from "../ReactiveScopes/PrintReactiveFunction";
import DisjointSet from "../Utils/DisjointSet";
import { assertExhaustive } from "../Utils/utils";
import type {
FunctionExpression,
HIR,
HIRFunction,
Identifier,
IdentifierName,
Instruction,
InstructionValue,
LValue,
ManualMemoDependency,
MutableRange,
ObjectMethod,
ObjectPropertyKey,
Pattern,
Phi,
Place,
ReactiveInstruction,
ReactiveScope,
ReactiveValue,
SourceLocation,
SpreadPattern,
Terminal,
Type,
} from "./HIR";
import { GotoVariant, InstructionKind } from "./HIR";
export type Options = {
indent: number;
};
export function printFunction(fn: HIRFunction): string {
const output = [];
let definition = "";
if (fn.id !== null) {
definition += fn.id;
}
if (fn.params.length !== 0) {
definition +=
"(" +
fn.params
.map((param) => {
if (param.kind === "Identifier") {
return printPlace(param);
} else {
return `...${printPlace(param.place)}`;
}
})
.join(", ") +
")";
}
if (definition.length !== 0) {
output.push(definition);
}
output.push(printHIR(fn.body));
output.push(...fn.directives);
return output.join("\n");
}
export function printHIR(ir: HIR, options: Options | null = null): string {
let output = [];
let indent = " ".repeat(options?.indent ?? 0);
const push = (text: string, indent: string = " "): void => {
output.push(`${indent}${text}`);
};
for (const [blockId, block] of ir.blocks) {
output.push(`bb${blockId} (${block.kind}):`);
if (block.preds.size > 0) {
const preds = ["predecessor blocks:"];
for (const pred of block.preds) {
preds.push(`bb${pred}`);
}
push(preds.join(" "));
}
for (const phi of block.phis) {
push(printPhi(phi));
}
for (const instr of block.instructions) {
push(printInstruction(instr));
}
const terminal = printTerminal(block.terminal);
if (Array.isArray(terminal)) {
terminal.forEach((line) => push(line));
} else {
push(terminal);
}
}
return output.map((line) => indent + line).join("\n");
}
export function printMixedHIR(
value: Instruction | InstructionValue | Terminal
): string {
if (!("kind" in value)) {
return printInstruction(value);
}
switch (value.kind) {
case "try":
case "maybe-throw":
case "sequence":
case "label":
case "optional":
case "branch":
case "if":
case "logical":
case "ternary":
case "return":
case "switch":
case "throw":
case "while":
case "for":
case "unreachable":
case "unsupported":
case "goto":
case "do-while":
case "for-in":
case "for-of":
case "scope":
case "pruned-scope": {
const terminal = printTerminal(value);
if (Array.isArray(terminal)) {
return terminal.join("; ");
}
return terminal;
}
default: {
return printInstructionValue(value);
}
}
}
export function printInstruction(instr: ReactiveInstruction): string {
const id = `[${instr.id}]`;
const value = printInstructionValue(instr.value);
if (instr.lvalue !== null) {
return `${id} ${printPlace(instr.lvalue)} = ${value}`;
} else {
return `${id} ${value}`;
}
}
export function printPhi(phi: Phi): string {
const items = [];
items.push(printIdentifier(phi.id));
items.push(printMutableRange(phi.id));
items.push(printType(phi.type));
items.push(": phi(");
const phis = [];
for (const [blockId, id] of phi.operands) {
phis.push(`bb${blockId}: ${printIdentifier(id)}`);
}
items.push(phis.join(", "));
items.push(")");
return items.join("");
}
export function printTerminal(terminal: Terminal): Array<string> | string {
let value;
switch (terminal.kind) {
case "if": {
value = `[${terminal.id}] If (${printPlace(terminal.test)}) then:bb${
terminal.consequent
} else:bb${terminal.alternate}${
terminal.fallthrough ? ` fallthrough=bb${terminal.fallthrough}` : ""
}`;
break;
}
case "branch": {
value = `[${terminal.id}] Branch (${printPlace(terminal.test)}) then:bb${
terminal.consequent
} else:bb${terminal.alternate}`;
break;
}
case "logical": {
value = `[${terminal.id}] Logical ${terminal.operator} test:bb${terminal.test} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "ternary": {
value = `[${terminal.id}] Ternary test:bb${terminal.test} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "optional": {
value = `[${terminal.id}] Optional test:bb${terminal.test} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "throw": {
value = `[${terminal.id}] Throw ${printPlace(terminal.value)}`;
break;
}
case "return": {
value = `[${terminal.id}] Return${
terminal.value != null ? " " + printPlace(terminal.value) : ""
}`;
break;
}
case "goto": {
value = `[${terminal.id}] Goto${
terminal.variant === GotoVariant.Continue ? "(Continue)" : ""
} bb${terminal.block}`;
break;
}
case "switch": {
const output = [];
output.push(`[${terminal.id}] Switch (${printPlace(terminal.test)})`);
terminal.cases.forEach((case_) => {
if (case_.test !== null) {
output.push(` Case ${printPlace(case_.test)}: bb${case_.block}`);
} else {
output.push(` Default: bb${case_.block}`);
}
});
if (terminal.fallthrough) {
output.push(` Fallthrough: bb${terminal.fallthrough}`);
}
value = output;
break;
}
case "do-while": {
value = `[${terminal.id}] DoWhile loop=${`bb${terminal.loop}`} test=bb${
terminal.test
} fallthrough=${`bb${terminal.fallthrough}`}`;
break;
}
case "while": {
value = `[${terminal.id}] While test=bb${terminal.test} loop=${
terminal.loop !== null ? `bb${terminal.loop}` : ""
} fallthrough=${terminal.fallthrough ? `bb${terminal.fallthrough}` : ""}`;
break;
}
case "for": {
value = `[${terminal.id}] For init=bb${terminal.init} test=bb${terminal.test} loop=bb${terminal.loop} update=bb${terminal.update} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "for-of": {
value = `[${terminal.id}] ForOf init=bb${terminal.init} test=bb${terminal.test} loop=bb${terminal.loop} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "for-in": {
value = `[${terminal.id}] ForIn init=bb${terminal.init} loop=bb${terminal.loop} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "label": {
value = `[${terminal.id}] Label block=bb${terminal.block} fallthrough=${
terminal.fallthrough ? `bb${terminal.fallthrough}` : ""
}`;
break;
}
case "sequence": {
value = `[${terminal.id}] Sequence block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "unreachable": {
value = `[${terminal.id}] Unreachable`;
break;
}
case "unsupported": {
value = `Unsupported`;
break;
}
case "maybe-throw": {
value = `MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
break;
}
case "scope": {
value = `Scope ${printReactiveScopeSummary(terminal.scope)} block=bb${
terminal.block
} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "pruned-scope": {
value = `<pruned> Scope ${printReactiveScopeSummary(terminal.scope)} block=bb${
terminal.block
} fallthrough=bb${terminal.fallthrough}`;
break;
}
case "try": {
value = `Try block=bb${terminal.block} handler=bb${terminal.handler}${
terminal.handlerBinding !== null
? ` handlerBinding=(${printPlace(terminal.handlerBinding)})`
: ""
} fallthrough=${
terminal.fallthrough != null ? `bb${terminal.fallthrough}` : ""
}`;
break;
}
default: {
assertExhaustive(
terminal,
`Unexpected terminal kind \`${terminal as any as Terminal}\``
);
}
}
return value;
}
function printHole(): string {
return "<hole>";
}
function printObjectPropertyKey(key: ObjectPropertyKey): string {
switch (key.kind) {
case "identifier":
return key.name;
case "string":
return `"${key.name}"`;
case "computed": {
return `[${printPlace(key.name)}]`;
}
}
}
export function printInstructionValue(instrValue: ReactiveValue): string {
let value = "";
switch (instrValue.kind) {
case "ArrayExpression": {
value = `Array [${instrValue.elements
.map((element) => {
if (element.kind === "Identifier") {
return printPlace(element);
} else if (element.kind === "Hole") {
return printHole();
} else {
return `...${printPlace(element.place)}`;
}
})
.join(", ")}]`;
break;
}
case "ObjectExpression": {
const properties = [];
if (instrValue.properties !== null) {
for (const property of instrValue.properties) {
if (property.kind === "ObjectProperty") {
properties.push(
`${printObjectPropertyKey(property.key)}: ${printPlace(
property.place
)}`
);
} else {
properties.push(`...${printPlace(property.place)}`);
}
}
}
value = `Object { ${properties.join(", ")} }`;
break;
}
case "UnaryExpression": {
value = `Unary ${printPlace(instrValue.value)}`;
break;
}
case "BinaryExpression": {
value = `Binary ${printPlace(instrValue.left)} ${
instrValue.operator
} ${printPlace(instrValue.right)}`;
break;
}
case "NewExpression": {
value = `New ${printPlace(instrValue.callee)}(${instrValue.args
.map((arg) => printPattern(arg))
.join(", ")})`;
break;
}
case "CallExpression": {
value = `Call ${printPlace(instrValue.callee)}(${instrValue.args
.map((arg) => printPattern(arg))
.join(", ")})`;
break;
}
case "MethodCall": {
value = `MethodCall ${printPlace(instrValue.receiver)}.${printPlace(
instrValue.property
)}(${instrValue.args.map((arg) => printPattern(arg)).join(", ")})`;
break;
}
case "JSXText": {
value = `JSXText ${JSON.stringify(instrValue.value)}`;
break;
}
case "Primitive": {
if (instrValue.value === undefined) {
value = "<undefined>";
} else {
value = JSON.stringify(instrValue.value);
}
break;
}
case "TypeCastExpression": {
value = `TypeCast ${printPlace(instrValue.value)}: ${printType(
instrValue.type
)}`;
break;
}
case "JsxExpression": {
const propItems = [];
for (const attribute of instrValue.props) {
if (attribute.kind === "JsxAttribute") {
propItems.push(
`${attribute.name}={${
attribute.place !== null ? printPlace(attribute.place) : "<empty>"
}}`
);
} else {
propItems.push(`...${printPlace(attribute.argument)}`);
}
}
const tag =
instrValue.tag.kind === "Identifier"
? printPlace(instrValue.tag)
: instrValue.tag.name;
const props = propItems.length !== 0 ? " " + propItems.join(" ") : "";
if (instrValue.children !== null) {
const children = instrValue.children.map((child) => {
return `{${printPlace(child)}}`;
});
value = `JSX <${tag}${props}${
props.length > 0 ? " " : ""
}>${children.join("")}</${tag}>`;
} else {
value = `JSX <${tag}${props}${props.length > 0 ? " " : ""}/>`;
}
break;
}
case "JsxFragment": {
value = `JsxFragment [${instrValue.children
.map((child) => printPlace(child))
.join(", ")}]`;
break;
}
case "UnsupportedNode": {
value = `UnsupportedNode(${generate(instrValue.node).code})`;
break;
}
case "LoadLocal": {
value = `LoadLocal ${printPlace(instrValue.place)}`;
break;
}
case "DeclareLocal": {
value = `DeclareLocal ${instrValue.lvalue.kind} ${printPlace(
instrValue.lvalue.place
)}`;
break;
}
case "DeclareContext": {
value = `DeclareContext ${instrValue.lvalue.kind} ${printPlace(
instrValue.lvalue.place
)}`;
break;
}
case "StoreLocal": {
value = `StoreLocal ${instrValue.lvalue.kind} ${printPlace(
instrValue.lvalue.place
)} = ${printPlace(instrValue.value)}`;
break;
}
case "LoadContext": {
value = `LoadContext ${printPlace(instrValue.place)}`;
break;
}
case "StoreContext": {
value = `StoreContext ${instrValue.lvalue.kind} ${printPlace(
instrValue.lvalue.place
)} = ${printPlace(instrValue.value)}`;
break;
}
case "Destructure": {
value = `Destructure ${instrValue.lvalue.kind} ${printPattern(
instrValue.lvalue.pattern
)} = ${printPlace(instrValue.value)}`;
break;
}
case "PropertyLoad": {
value = `PropertyLoad ${printPlace(instrValue.object)}.${
instrValue.property
}`;
break;
}
case "PropertyStore": {
value = `PropertyStore ${printPlace(instrValue.object)}.${
instrValue.property
} = ${printPlace(instrValue.value)}`;
break;
}
case "PropertyDelete": {
value = `PropertyDelete ${printPlace(instrValue.object)}.${
instrValue.property
}`;
break;
}
case "ComputedLoad": {
value = `ComputedLoad ${printPlace(instrValue.object)}[${printPlace(
instrValue.property
)}]`;
break;
}
case "ComputedStore": {
value = `ComputedStore ${printPlace(instrValue.object)}[${printPlace(
instrValue.property
)}] = ${printPlace(instrValue.value)}`;
break;
}
case "ComputedDelete": {
value = `ComputedDelete ${printPlace(instrValue.object)}[${printPlace(
instrValue.property
)}]`;
break;
}
case "ObjectMethod":
case "FunctionExpression": {
const kind =
instrValue.kind === "FunctionExpression" ? "Function" : "ObjectMethod";
const name = getFunctionName(instrValue, "");
const fn = printFunction(instrValue.loweredFunc.func)
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
const deps = instrValue.loweredFunc.dependencies
.map((dep) => printPlace(dep))
.join(",");
const context = instrValue.loweredFunc.func.context
.map((dep) => printPlace(dep))
.join(",");
const effects =
instrValue.loweredFunc.func.effects
?.map((effect) => {
if (effect.kind === "ContextMutation") {
return `ContextMutation places=[${[...effect.places]
.map((place) => printPlace(place))
.join(", ")}] effect=${effect.effect}`;
} else {
return `GlobalMutation`;
}
})
.join(", ") ?? "";
value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]:\n${fn}`;
break;
}
case "TaggedTemplateExpression": {
value = `${printPlace(instrValue.tag)}\`${instrValue.value.raw}\``;
break;
}
case "LogicalExpression": {
value = `Logical ${printInstructionValue(instrValue.left)} ${
instrValue.operator
} ${printInstructionValue(instrValue.right)}`;
break;
}
case "SequenceExpression": {
value = [
`Sequence`,
...instrValue.instructions.map(
(instr) => ` ${printInstruction(instr)}`
),
` ${printInstructionValue(instrValue.value)}`,
].join("\n");
break;
}
case "ConditionalExpression": {
value = `Ternary ${printInstructionValue(
instrValue.test
)} ? ${printInstructionValue(
instrValue.consequent
)} : ${printInstructionValue(instrValue.alternate)}`;
break;
}
case "TemplateLiteral": {
value = "`";
CompilerError.invariant(
instrValue.subexprs.length === instrValue.quasis.length - 1,
{
reason: "Bad assumption about quasi length.",
description: null,
loc: instrValue.loc,
suggestions: null,
}
);
for (let i = 0; i < instrValue.subexprs.length; i++) {
value += instrValue.quasis[i].raw;
value += `\${${printPlace(instrValue.subexprs[i])}}`;
}
value += instrValue.quasis.at(-1)!.raw + "`";
break;
}
case "LoadGlobal": {
switch (instrValue.binding.kind) {
case "Global": {
value = `LoadGlobal(global) ${instrValue.binding.name}`;
break;
}
case "ModuleLocal": {
value = `LoadGlobal(module) ${instrValue.binding.name}`;
break;
}
case "ImportDefault": {
value = `LoadGlobal import ${instrValue.binding.name} from '${instrValue.binding.module}'`;
break;
}
case "ImportNamespace": {
value = `LoadGlobal import * as ${instrValue.binding.name} from '${instrValue.binding.module}'`;
break;
}
case "ImportSpecifier": {
if (instrValue.binding.imported !== instrValue.binding.name) {
value = `LoadGlobal import { ${instrValue.binding.imported} as ${instrValue.binding.name} } from '${instrValue.binding.module}'`;
} else {
value = `LoadGlobal import { ${instrValue.binding.name} } from '${instrValue.binding.module}'`;
}
break;
}
default: {
assertExhaustive(
instrValue.binding,
`Unexpected binding kind \`${(instrValue.binding as any).kind}\``
);
}
}
break;
}
case "StoreGlobal": {
value = `StoreGlobal ${instrValue.name} = ${printPlace(
instrValue.value
)}`;
break;
}
case "OptionalExpression": {
value = `OptionalExpression ${printInstructionValue(instrValue.value)}`;
break;
}
case "RegExpLiteral": {
value = `RegExp /${instrValue.pattern}/${instrValue.flags}`;
break;
}
case "MetaProperty": {
value = `MetaProperty ${instrValue.meta}.${instrValue.property}`;
break;
}
case "Await": {
value = `Await ${printPlace(instrValue.value)}`;
break;
}
case "GetIterator": {
value = `GetIterator collection=${printPlace(instrValue.collection)}`;
break;
}
case "IteratorNext": {
value = `IteratorNext iterator=${printPlace(
instrValue.iterator
)} collection=${printPlace(instrValue.collection)}`;
break;
}
case "NextPropertyOf": {
value = `NextPropertyOf ${printPlace(instrValue.value)}`;
break;
}
case "Debugger": {
value = `Debugger`;
break;
}
case "PostfixUpdate": {
value = `PostfixUpdate ${printPlace(instrValue.lvalue)} = ${printPlace(
instrValue.value
)} ${instrValue.operation}`;
break;
}
case "PrefixUpdate": {
value = `PrefixUpdate ${printPlace(instrValue.lvalue)} = ${
instrValue.operation
} ${printPlace(instrValue.value)}`;
break;
}
case "StartMemoize": {
value = `StartMemoize deps=${
instrValue.deps?.map((dep) => printManualMemoDependency(dep, false)) ??
"(none)"
}`;
break;
}
case "FinishMemoize": {
value = `FinishMemoize decl=${printPlace(instrValue.decl)}`;
break;
}
case "ReactiveFunctionValue": {
value = `FunctionValue ${printReactiveFunction(instrValue.fn)}`;
break;
}
default: {
assertExhaustive(
instrValue,
`Unexpected instruction kind '${
(instrValue as any as InstructionValue).kind
}'`
);
}
}
return value;
}
function isMutable(range: MutableRange): boolean {
return range.end > range.start + 1;
}
const DEBUG_MUTABLE_RANGES = false;
function printMutableRange(identifier: Identifier): string {
if (DEBUG_MUTABLE_RANGES) {
const range = identifier.mutableRange;
const scopeRange = identifier.scope?.range;
if (
scopeRange != null &&
(scopeRange.start !== range.start || scopeRange.end !== range.end)
) {
return `[${range.start}:${range.end}] scope=[${scopeRange.start}:${scopeRange.end}]`;
}
return isMutable(range) ? `[${range.start}:${range.end}]` : "";
}
const range = identifier.scope?.range ?? identifier.mutableRange;
return isMutable(range) ? `[${range.start}:${range.end}]` : "";
}
export function printLValue(lval: LValue): string {
let lvalue = `${printPlace(lval.place)}`;
switch (lval.kind) {
case InstructionKind.Let: {
return `Let ${lvalue}`;
}
case InstructionKind.Const: {
return `Const ${lvalue}$`;
}
case InstructionKind.Reassign: {
return `Reassign ${lvalue}`;
}
case InstructionKind.Catch: {
return `Catch ${lvalue}`;
}
case InstructionKind.HoistedConst: {
return `HoistedConst ${lvalue}$`;
}
default: {
assertExhaustive(lval.kind, `Unexpected lvalue kind \`${lval.kind}\``);
}
}
}
export function printPattern(pattern: Pattern | Place | SpreadPattern): string {
switch (pattern.kind) {
case "ArrayPattern": {
return (
"[ " +
pattern.items
.map((item) => {
if (item.kind === "Hole") {
return "<hole>";
}
return printPattern(item);
})
.join(", ") +
" ]"
);
}
case "ObjectPattern": {
return (
"{ " +
pattern.properties
.map((item) => {
switch (item.kind) {
case "ObjectProperty": {
return `${printObjectPropertyKey(item.key)}: ${printPattern(
item.place
)}`;
}
case "Spread": {
return printPattern(item);
}
default: {
assertExhaustive(item, "Unexpected object property kind");
}
}
})
.join(", ") +
" }"
);
}
case "Spread": {
return `...${printPlace(pattern.place)}`;
}
case "Identifier": {
return printPlace(pattern);
}
default: {
assertExhaustive(
pattern,
`Unexpected pattern kind \`${(pattern as any).kind}\``
);
}
}
}
export function printPlace(place: Place): string {
const items = [
place.effect,
" ",
printIdentifier(place.identifier),
printMutableRange(place.identifier),
printType(place.identifier.type),
place.reactive ? "{reactive}" : null,
];
return items.filter((x) => x != null).join("");
}
export function printIdentifier(id: Identifier): string {
return `${printName(id.name)}\$${id.id}${printScope(id.scope)}`;
}
function printName(name: IdentifierName | null): string {
if (name === null) {
return "";
}
return name.value;
}
function printScope(scope: ReactiveScope | null): string {
return `${scope !== null ? `_@${scope.id}` : ""}`;
}
export function printManualMemoDependency(
val: ManualMemoDependency,
nameOnly: boolean
): string {
let rootStr;
if (val.root.kind === "Global") {
rootStr = val.root.identifierName;
} else {
CompilerError.invariant(val.root.value.identifier.name?.kind === "named", {
reason: "DepsValidation: expected named local variable in depslist",
suggestions: null,
loc: val.root.value.loc,
});
rootStr = nameOnly
? val.root.value.identifier.name.value
: printIdentifier(val.root.value.identifier);
}
return `${rootStr}${val.path.length > 0 ? "." : ""}${val.path.join(".")}`;
}
export function printType(type: Type): string {
if (type.kind === "Type") return "";
if (type.kind === "Object" && type.shapeId != null) {
return `:T${type.kind}<${type.shapeId}>`;
} else if (type.kind === "Function" && type.shapeId != null) {
return `:T${type.kind}<${type.shapeId}>`;
} else {
return `:T${type.kind}`;
}
}
export function printSourceLocation(loc: SourceLocation): string {
if (typeof loc === "symbol") {
return "generated";
} else {
return `${loc.start.line}:${loc.start.column}:${loc.end.line}:${loc.end.column}`;
}
}
export function printAliases(aliases: DisjointSet<Identifier>): string {
const aliasSets = aliases.buildSets();
const items = [];
for (const aliasSet of aliasSets) {
items.push([...aliasSet].map((id) => printIdentifier(id)).join(","));
}
return items.join("\n");
}
function getFunctionName(
instrValue: ObjectMethod | FunctionExpression,
defaultValue: string
): string {
switch (instrValue.kind) {
case "FunctionExpression":
return instrValue.name ?? defaultValue;
case "ObjectMethod":
return defaultValue;
}
}