import { NodePath, Scope } from "@babel/traverse";
import * as t from "@babel/types";
import { Expression } from "@babel/types";
import invariant from "invariant";
import {
CompilerError,
CompilerSuggestionOperation,
ErrorSeverity,
} from "../CompilerError";
import { Err, Ok, Result } from "../Utils/Result";
import { assertExhaustive, hasNode } from "../Utils/utils";
import { Environment } from "./Environment";
import {
ArrayExpression,
ArrayPattern,
BlockId,
BranchTerminal,
BuiltinTag,
Case,
Effect,
GeneratedSource,
GotoVariant,
HIRFunction,
IfTerminal,
InstructionKind,
InstructionValue,
JsxAttribute,
LoweredFunction,
ObjectPattern,
ObjectProperty,
ObjectPropertyKey,
Place,
ReturnTerminal,
SourceLocation,
SpreadPattern,
ThrowTerminal,
Type,
makeInstructionId,
makeType,
promoteTemporary,
} from "./HIR";
import HIRBuilder, { Bindings } from "./HIRBuilder";
import { BuiltInArrayId } from "./ObjectShape";
export function lower(
func: NodePath<t.Function>,
env: Environment,
bindings: Bindings | null = null,
capturedRefs: Array<t.Identifier> = [],
parent: NodePath<t.Function> | null = null
): Result<HIRFunction, CompilerError> {
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
const context: Array<Place> = [];
for (const ref of capturedRefs ?? []) {
context.push({
kind: "Identifier",
identifier: builder.resolveBinding(ref),
effect: Effect.Unknown,
reactive: false,
loc: ref.loc ?? GeneratedSource,
});
}
let id: string | null = null;
if (func.isFunctionDeclaration() || func.isFunctionExpression()) {
const idNode = (
func as NodePath<t.FunctionDeclaration | t.FunctionExpression>
).get("id");
if (hasNode(idNode)) {
id = idNode.node.name;
}
}
const params: Array<Place | SpreadPattern> = [];
func.get("params").forEach((param) => {
if (param.isIdentifier()) {
const binding = builder.resolveIdentifier(param);
if (binding.kind !== "Identifier") {
builder.errors.push({
reason: `(BuildHIR::lower) Could not find binding for param \`${param.node.name}\``,
severity: ErrorSeverity.Invariant,
loc: param.node.loc ?? null,
suggestions: null,
});
return;
}
const place: Place = {
kind: "Identifier",
identifier: binding.identifier,
effect: Effect.Unknown,
reactive: false,
loc: param.node.loc ?? GeneratedSource,
};
params.push(place);
} else if (
param.isObjectPattern() ||
param.isArrayPattern() ||
param.isAssignmentPattern()
) {
const place: Place = {
kind: "Identifier",
identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
effect: Effect.Unknown,
reactive: false,
loc: param.node.loc ?? GeneratedSource,
};
params.push(place);
lowerAssignment(
builder,
param.node.loc ?? GeneratedSource,
InstructionKind.Let,
param,
place,
"Assignment"
);
} else if (param.isRestElement()) {
const place: Place = {
kind: "Identifier",
identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
effect: Effect.Unknown,
reactive: false,
loc: param.node.loc ?? GeneratedSource,
};
params.push({
kind: "Spread",
place,
});
lowerAssignment(
builder,
param.node.loc ?? GeneratedSource,
InstructionKind.Let,
param.get("argument"),
place,
"Assignment"
);
} else {
builder.errors.push({
reason: `(BuildHIR::lower) Handle ${param.node.type} params`,
severity: ErrorSeverity.Todo,
loc: param.node.loc ?? null,
suggestions: null,
});
}
});
let directives: Array<string> = [];
const body = func.get("body");
if (body.isExpression()) {
const fallthrough = builder.reserve("block");
const terminal: ReturnTerminal = {
kind: "return",
loc: GeneratedSource,
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
};
builder.terminateWithContinuation(terminal, fallthrough);
} else if (body.isBlockStatement()) {
lowerStatement(builder, body);
directives = body.get("directives").map((d) => d.node.value.value);
} else {
builder.errors.push({
severity: ErrorSeverity.InvalidJS,
reason: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
loc: body.node.loc ?? null,
suggestions: null,
});
}
if (builder.errors.hasErrors()) {
return Err(builder.errors);
}
builder.terminate(
{
kind: "return",
loc: GeneratedSource,
value: lowerValueToTemporary(builder, {
kind: "Primitive",
value: undefined,
loc: GeneratedSource,
}),
id: makeInstructionId(0),
},
null
);
return Ok({
id,
params,
fnType: parent == null ? env.fnType : "Other",
returnType: null,
body: builder.build(),
context,
generator: func.node.generator === true,
async: func.node.async === true,
loc: func.node.loc ?? GeneratedSource,
env,
effects: null,
directives,
});
}
function lowerStatement(
builder: HIRBuilder,
stmtPath: NodePath<t.Statement>,
label: string | null = null
): void {
const stmtNode = stmtPath.node;
switch (stmtNode.type) {
case "ThrowStatement": {
const stmt = stmtPath as NodePath<t.ThrowStatement>;
const value = lowerExpressionToTemporary(builder, stmt.get("argument"));
const handler = builder.resolveThrowHandler();
if (handler != null) {
builder.errors.push({
reason:
"(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch",
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
}
const terminal: ThrowTerminal = {
kind: "throw",
value,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
};
builder.terminate(terminal, "block");
return;
}
case "ReturnStatement": {
const stmt = stmtPath as NodePath<t.ReturnStatement>;
const argument = stmt.get("argument");
let value;
if (argument.node === null) {
value = lowerValueToTemporary(builder, {
kind: "Primitive",
value: undefined,
loc: GeneratedSource,
});
} else {
value = lowerExpressionToTemporary(
builder,
argument as NodePath<t.Expression>
);
}
const terminal: ReturnTerminal = {
kind: "return",
loc: stmt.node.loc ?? GeneratedSource,
value,
id: makeInstructionId(0),
};
builder.terminate(terminal, "block");
return;
}
case "IfStatement": {
const stmt = stmtPath as NodePath<t.IfStatement>;
const continuationBlock = builder.reserve("block");
const consequentBlock = builder.enter("block", (_blockId) => {
const consequent = stmt.get("consequent");
lowerStatement(builder, consequent);
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: consequent.node.loc ?? GeneratedSource,
};
});
let alternateBlock: BlockId;
const alternate = stmt.get("alternate");
if (hasNode(alternate)) {
alternateBlock = builder.enter("block", (_blockId) => {
lowerStatement(builder, alternate);
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: alternate.node?.loc ?? GeneratedSource,
};
});
} else {
alternateBlock = continuationBlock.id;
}
const test = lowerExpressionToTemporary(builder, stmt.get("test"));
const terminal: IfTerminal = {
kind: "if",
test,
consequent: consequentBlock,
alternate: alternateBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
};
builder.terminateWithContinuation(terminal, continuationBlock);
return;
}
case "BlockStatement": {
const stmt = stmtPath as NodePath<t.BlockStatement>;
const statements = stmt.get("body");
const hoistableIdentifiers: Set<t.Identifier> = new Set();
for (const [, binding] of Object.entries(stmt.scope.bindings)) {
if (binding.kind !== "param") {
hoistableIdentifiers.add(binding.identifier);
}
}
for (const s of statements) {
const willHoist = new Set<NodePath<t.Identifier>>();
let fnDepth = s.isFunctionDeclaration() ? 1 : 0;
const withFunctionContext = {
enter: (): void => {
fnDepth++;
},
exit: (): void => {
fnDepth--;
},
};
s.traverse({
FunctionExpression: withFunctionContext,
FunctionDeclaration: withFunctionContext,
ArrowFunctionExpression: withFunctionContext,
ObjectMethod: withFunctionContext,
Identifier(id: NodePath<t.Identifier>) {
const id2 = id;
if (
!id2.isReferencedIdentifier() &&
id.parent.type !== "AssignmentExpression"
) {
return;
}
const binding = id.scope.getBinding(id.node.name);
if (
binding != null &&
hoistableIdentifiers.has(binding.identifier) &&
(fnDepth > 0 || binding.kind === "hoisted")
) {
willHoist.add(id);
}
},
});
s.traverse({
Identifier(path: NodePath<t.Identifier>) {
if (hoistableIdentifiers.has(path.node)) {
hoistableIdentifiers.delete(path.node);
}
},
});
for (const id of willHoist) {
const binding = stmt.scope.getBinding(id.node.name);
CompilerError.invariant(binding != null, {
reason: "Expected to find binding for hoisted identifier",
description: `Could not find a binding for ${id.node.name}`,
suggestions: null,
loc: id.node.loc ?? GeneratedSource,
});
if (builder.environment.isHoistedIdentifier(binding.identifier)) {
continue;
}
if (!binding.path.isVariableDeclarator()) {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: "Unsupported declaration type for hoisting",
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
suggestions: null,
loc: id.parentPath.node.loc ?? GeneratedSource,
});
continue;
} else if (!binding.path.get("id").isIdentifier()) {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: "Unsupported variable declaration type for hoisting",
description: `variable "${
binding.identifier.name
}" declared with ${binding.path.get("id").type}`,
suggestions: null,
loc: id.parentPath.node.loc ?? GeneratedSource,
});
continue;
} else if (binding.kind !== "const" && binding.kind !== "var") {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: "Handle non-const declarations for hoisting",
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
suggestions: null,
loc: id.parentPath.node.loc ?? GeneratedSource,
});
continue;
}
const identifier = builder.resolveIdentifier(id);
CompilerError.invariant(identifier.kind === "Identifier", {
reason:
"Expected hoisted binding to be a local identifier, not a global",
loc: id.node.loc ?? GeneratedSource,
});
const place: Place = {
effect: Effect.Unknown,
identifier: identifier.identifier,
kind: "Identifier",
reactive: false,
loc: id.node.loc ?? GeneratedSource,
};
lowerValueToTemporary(builder, {
kind: "DeclareContext",
lvalue: {
kind: InstructionKind.HoistedConst,
place,
},
loc: id.node.loc ?? GeneratedSource,
});
builder.environment.addHoistedIdentifier(binding.identifier);
}
lowerStatement(builder, s);
}
return;
}
case "BreakStatement": {
const stmt = stmtPath as NodePath<t.BreakStatement>;
const block = builder.lookupBreak(stmt.node.label?.name ?? null);
builder.terminate(
{
kind: "goto",
block,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
"block"
);
return;
}
case "ContinueStatement": {
const stmt = stmtPath as NodePath<t.ContinueStatement>;
const block = builder.lookupContinue(stmt.node.label?.name ?? null);
builder.terminate(
{
kind: "goto",
block,
variant: GotoVariant.Continue,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
"block"
);
return;
}
case "ForStatement": {
const stmt = stmtPath as NodePath<t.ForStatement>;
const testBlock = builder.reserve("loop");
const continuationBlock = builder.reserve("block");
const initBlock = builder.enter("loop", (_blockId) => {
const init = stmt.get("init");
if (!init.isVariableDeclaration()) {
builder.errors.push({
reason:
"(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement",
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
return {
kind: "unsupported",
id: makeInstructionId(0),
loc: init.node?.loc ?? GeneratedSource,
};
}
lowerStatement(builder, init);
return {
kind: "goto",
block: testBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: init.node.loc ?? GeneratedSource,
};
});
let updateBlock: BlockId | null = null;
const update = stmt.get("update");
if (hasNode(update)) {
updateBlock = builder.enter("loop", (_blockId) => {
lowerExpressionToTemporary(builder, update);
return {
kind: "goto",
block: testBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: update.node?.loc ?? GeneratedSource,
};
});
}
const bodyBlock = builder.enter("block", (_blockId) => {
return builder.loop(
label,
updateBlock ?? testBlock.id,
continuationBlock.id,
() => {
const body = stmt.get("body");
lowerStatement(builder, body);
return {
kind: "goto",
block: updateBlock ?? testBlock.id,
variant: GotoVariant.Continue,
id: makeInstructionId(0),
loc: body.node.loc ?? GeneratedSource,
};
}
);
});
builder.terminateWithContinuation(
{
kind: "for",
loc: stmtNode.loc ?? GeneratedSource,
init: initBlock,
test: testBlock.id,
update: updateBlock,
loop: bodyBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
},
testBlock
);
const test = stmt.get("test");
if (test.node == null) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
} else {
builder.terminateWithContinuation(
{
kind: "branch",
test: lowerExpressionToTemporary(
builder,
test as NodePath<t.Expression>
),
consequent: bodyBlock,
alternate: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock
);
}
return;
}
case "WhileStatement": {
const stmt = stmtPath as NodePath<t.WhileStatement>;
const conditionalBlock = builder.reserve("loop");
const continuationBlock = builder.reserve("block");
const loopBlock = builder.enter("block", (_blockId) => {
return builder.loop(
label,
conditionalBlock.id,
continuationBlock.id,
() => {
const body = stmt.get("body");
lowerStatement(builder, body);
return {
kind: "goto",
block: conditionalBlock.id,
variant: GotoVariant.Continue,
id: makeInstructionId(0),
loc: body.node.loc ?? GeneratedSource,
};
}
);
});
const loc = stmt.node.loc ?? GeneratedSource;
builder.terminateWithContinuation(
{
kind: "while",
loc,
test: conditionalBlock.id,
loop: loopBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
},
conditionalBlock
);
const test = lowerExpressionToTemporary(builder, stmt.get("test"));
const terminal: BranchTerminal = {
kind: "branch",
test,
consequent: loopBlock,
alternate: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
};
builder.terminateWithContinuation(terminal, continuationBlock);
return;
}
case "LabeledStatement": {
const stmt = stmtPath as NodePath<t.LabeledStatement>;
const label = stmt.node.label.name;
const body = stmt.get("body");
switch (body.node.type) {
case "ForInStatement":
case "ForOfStatement":
case "ForStatement":
case "WhileStatement":
case "DoWhileStatement": {
lowerStatement(builder, stmt.get("body"), label);
break;
}
default: {
const continuationBlock = builder.reserve("block");
const block = builder.enter("block", () => {
const body = stmt.get("body");
builder.label(label, continuationBlock.id, () => {
lowerStatement(builder, body);
});
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: body.node.loc ?? GeneratedSource,
};
});
builder.terminateWithContinuation(
{
kind: "label",
block,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock
);
}
}
return;
}
case "SwitchStatement": {
const stmt = stmtPath as NodePath<t.SwitchStatement>;
const continuationBlock = builder.reserve("block");
let fallthrough = continuationBlock.id;
const cases: Array<Case> = [];
let hasDefault = false;
for (let ii = stmt.get("cases").length - 1; ii >= 0; ii--) {
const case_: NodePath<t.SwitchCase> = stmt.get("cases")[ii];
const testExpr = case_.get("test");
if (testExpr.node == null) {
if (hasDefault) {
builder.errors.push({
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
severity: ErrorSeverity.InvalidJS,
loc: case_.node.loc ?? null,
suggestions: null,
});
break;
}
hasDefault = true;
}
const block = builder.enter("block", (_blockId) => {
return builder.switch(label, continuationBlock.id, () => {
case_
.get("consequent")
.forEach((consequent) => lowerStatement(builder, consequent));
return {
kind: "goto",
block: fallthrough,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: case_.node.loc ?? GeneratedSource,
};
});
});
let test: Place | null = null;
if (hasNode(testExpr)) {
test = lowerReorderableExpression(builder, testExpr);
}
cases.push({
test,
block,
});
fallthrough = block;
}
cases.reverse();
if (!hasDefault) {
cases.push({ test: null, block: continuationBlock.id });
}
const test = lowerExpressionToTemporary(
builder,
stmt.get("discriminant")
);
builder.terminateWithContinuation(
{
kind: "switch",
test,
cases,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock
);
return;
}
case "VariableDeclaration": {
const stmt = stmtPath as NodePath<t.VariableDeclaration>;
const nodeKind: t.VariableDeclaration["kind"] = stmt.node.kind;
if (nodeKind === "var") {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
return;
}
const kind =
nodeKind === "let" ? InstructionKind.Let : InstructionKind.Const;
for (const declaration of stmt.get("declarations")) {
const id = declaration.get("id");
const init = declaration.get("init");
if (hasNode(init)) {
const value = lowerExpressionToTemporary(builder, init);
lowerAssignment(
builder,
stmt.node.loc ?? GeneratedSource,
kind,
id,
value,
id.isObjectPattern() || id.isArrayPattern()
? "Destructure"
: "Assignment"
);
} else if (id.isIdentifier()) {
const binding = builder.resolveIdentifier(id);
if (binding.kind !== "Identifier") {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
severity: ErrorSeverity.Invariant,
loc: id.node.loc ?? null,
suggestions: null,
});
} else {
const place: Place = {
effect: Effect.Unknown,
identifier: binding.identifier,
kind: "Identifier",
reactive: false,
loc: id.node.loc ?? GeneratedSource,
};
if (builder.isContextIdentifier(id)) {
if (kind === InstructionKind.Const) {
const declRangeStart = declaration.parentPath.node.start!;
builder.errors.push({
reason: `Expect \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
loc: id.node.loc ?? null,
suggestions: [
{
description: "Change to a `let` declaration",
op: CompilerSuggestionOperation.Replace,
range: [declRangeStart, declRangeStart + 5],
text: "let",
},
],
});
}
lowerValueToTemporary(builder, {
kind: "DeclareContext",
lvalue: {
kind: InstructionKind.Let,
place,
},
loc: id.node.loc ?? GeneratedSource,
});
} else {
const typeAnnotation = id.get("typeAnnotation");
let type: t.FlowType | t.TSType | null;
if (typeAnnotation.isTSTypeAnnotation()) {
const typePath = typeAnnotation.get("typeAnnotation");
type = typePath.node;
} else if (typeAnnotation.isTypeAnnotation()) {
const typePath = typeAnnotation.get("typeAnnotation");
type = typePath.node;
} else {
type = null;
}
lowerValueToTemporary(builder, {
kind: "DeclareLocal",
lvalue: {
kind,
place,
},
type,
loc: id.node.loc ?? GeneratedSource,
});
}
}
} else {
builder.errors.push({
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
description: `Got a \`${id.type}\``,
severity: ErrorSeverity.InvalidJS,
loc: stmt.node.loc ?? null,
suggestions: null,
});
}
}
return;
}
case "ExpressionStatement": {
const stmt = stmtPath as NodePath<t.ExpressionStatement>;
const expression = stmt.get("expression");
lowerExpressionToTemporary(builder, expression);
return;
}
case "DoWhileStatement": {
const stmt = stmtPath as NodePath<t.DoWhileStatement>;
const conditionalBlock = builder.reserve("loop");
const continuationBlock = builder.reserve("block");
const loopBlock = builder.enter("block", (_loopBlockId) => {
return builder.loop(
label,
conditionalBlock.id,
continuationBlock.id,
() => {
const body = stmt.get("body");
lowerStatement(builder, body);
return {
kind: "goto",
block: conditionalBlock.id,
variant: GotoVariant.Continue,
id: makeInstructionId(0),
loc: body.node.loc ?? GeneratedSource,
};
}
);
});
const loc = stmt.node.loc ?? GeneratedSource;
builder.terminateWithContinuation(
{
kind: "do-while",
loc,
test: conditionalBlock.id,
loop: loopBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
},
conditionalBlock
);
const test = lowerExpressionToTemporary(builder, stmt.get("test"));
const terminal: BranchTerminal = {
kind: "branch",
test,
consequent: loopBlock,
alternate: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
builder.terminateWithContinuation(terminal, continuationBlock);
return;
}
case "FunctionDeclaration": {
const stmt = stmtPath as NodePath<t.FunctionDeclaration>;
stmt.skip();
CompilerError.invariant(stmt.get("id").type === "Identifier", {
reason: "function declarations must have a name",
description: null,
loc: stmt.node.loc ?? null,
suggestions: null,
});
const id = stmt.get("id") as NodePath<t.Identifier>;
const fn = lowerValueToTemporary(
builder,
lowerFunctionToValue(builder, stmt)
);
lowerAssignment(
builder,
stmt.node.loc ?? GeneratedSource,
InstructionKind.Let,
id,
fn,
"Assignment"
);
return;
}
case "ForOfStatement": {
const stmt = stmtPath as NodePath<t.ForOfStatement>;
const continuationBlock = builder.reserve("block");
const initBlock = builder.reserve("loop");
const testBlock = builder.reserve("loop");
if (stmt.node.await) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
return;
}
const loopBlock = builder.enter("block", (_blockId) => {
return builder.loop(label, initBlock.id, continuationBlock.id, () => {
const body = stmt.get("body");
lowerStatement(builder, body);
return {
kind: "goto",
block: initBlock.id,
variant: GotoVariant.Continue,
id: makeInstructionId(0),
loc: body.node.loc ?? GeneratedSource,
};
});
});
const loc = stmt.node.loc ?? GeneratedSource;
const value = lowerExpressionToTemporary(builder, stmt.get("right"));
builder.terminateWithContinuation(
{
kind: "for-of",
loc,
init: initBlock.id,
test: testBlock.id,
loop: loopBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
},
initBlock
);
const iterator = lowerValueToTemporary(builder, {
kind: "GetIterator",
loc: value.loc,
collection: { ...value },
});
builder.terminateWithContinuation(
{
id: makeInstructionId(0),
kind: "goto",
block: testBlock.id,
variant: GotoVariant.Break,
loc: stmt.node.loc ?? GeneratedSource,
},
testBlock
);
const left = stmt.get("left");
const leftLoc = left.node.loc ?? GeneratedSource;
let test: Place;
if (left.isVariableDeclaration()) {
const declarations = left.get("declarations");
CompilerError.invariant(declarations.length === 1, {
reason: `Expected only one declaration in the init of a ForOfStatement, got ${declarations.length}`,
description: null,
loc: left.node.loc ?? null,
suggestions: null,
});
const id = declarations[0].get("id");
const advanceIterator = lowerValueToTemporary(builder, {
kind: "IteratorNext",
loc: leftLoc,
iterator: { ...iterator },
collection: { ...value },
});
const assign = lowerAssignment(
builder,
leftLoc,
InstructionKind.Let,
id,
advanceIterator,
"Assignment"
);
test = lowerValueToTemporary(builder, assign);
} else {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForOfStatement`,
severity: ErrorSeverity.Todo,
loc: left.node.loc ?? null,
suggestions: null,
});
return;
}
builder.terminateWithContinuation(
{
id: makeInstructionId(0),
kind: "branch",
test,
consequent: loopBlock,
alternate: continuationBlock.id,
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock
);
return;
}
case "ForInStatement": {
const stmt = stmtPath as NodePath<t.ForInStatement>;
const continuationBlock = builder.reserve("block");
const initBlock = builder.reserve("loop");
const loopBlock = builder.enter("block", (_blockId) => {
return builder.loop(label, initBlock.id, continuationBlock.id, () => {
const body = stmt.get("body");
lowerStatement(builder, body);
return {
kind: "goto",
block: initBlock.id,
variant: GotoVariant.Continue,
id: makeInstructionId(0),
loc: body.node.loc ?? GeneratedSource,
};
});
});
const loc = stmt.node.loc ?? GeneratedSource;
const value = lowerExpressionToTemporary(builder, stmt.get("right"));
builder.terminateWithContinuation(
{
kind: "for-in",
loc,
init: initBlock.id,
loop: loopBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
},
initBlock
);
const left = stmt.get("left");
const leftLoc = left.node.loc ?? GeneratedSource;
let test: Place;
if (left.isVariableDeclaration()) {
const declarations = left.get("declarations");
CompilerError.invariant(declarations.length === 1, {
reason: `Expected only one declaration in the init of a ForInStatement, got ${declarations.length}`,
description: null,
loc: left.node.loc ?? null,
suggestions: null,
});
const id = declarations[0].get("id");
const nextPropertyTemp = lowerValueToTemporary(builder, {
kind: "NextPropertyOf",
loc: leftLoc,
value,
});
const assign = lowerAssignment(
builder,
leftLoc,
InstructionKind.Let,
id,
nextPropertyTemp,
"Assignment"
);
test = lowerValueToTemporary(builder, assign);
} else {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForInStatement`,
severity: ErrorSeverity.Todo,
loc: left.node.loc ?? null,
suggestions: null,
});
return;
}
builder.terminateWithContinuation(
{
id: makeInstructionId(0),
kind: "branch",
test,
consequent: loopBlock,
alternate: continuationBlock.id,
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock
);
return;
}
case "DebuggerStatement": {
const stmt = stmtPath as NodePath<t.DebuggerStatement>;
const loc = stmt.node.loc ?? GeneratedSource;
builder.push({
id: makeInstructionId(0),
lvalue: buildTemporaryPlace(builder, loc),
value: {
kind: "Debugger",
loc,
},
loc,
});
return;
}
case "EmptyStatement": {
return;
}
case "TryStatement": {
const stmt = stmtPath as NodePath<t.TryStatement>;
const continuationBlock = builder.reserve("block");
const handlerPath = stmt.get("handler");
if (!hasNode(handlerPath)) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
return;
}
if (hasNode(stmt.get("finalizer"))) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
}
const handlerBindingPath = handlerPath.get("param");
let handlerBinding: {
place: Place;
path: NodePath<t.Identifier | t.ArrayPattern | t.ObjectPattern>;
} | null = null;
if (hasNode(handlerBindingPath)) {
const place: Place = {
kind: "Identifier",
identifier: builder.makeTemporary(
handlerBindingPath.node.loc ?? GeneratedSource
),
effect: Effect.Unknown,
reactive: false,
loc: handlerBindingPath.node.loc ?? GeneratedSource,
};
promoteTemporary(place.identifier);
lowerValueToTemporary(builder, {
kind: "DeclareLocal",
lvalue: {
kind: InstructionKind.Catch,
place: { ...place },
},
type: null,
loc: handlerBindingPath.node.loc ?? GeneratedSource,
});
handlerBinding = {
path: handlerBindingPath,
place,
};
}
const handler = builder.enter("catch", (_blockId) => {
if (handlerBinding !== null) {
lowerAssignment(
builder,
handlerBinding.path.node.loc ?? GeneratedSource,
InstructionKind.Catch,
handlerBinding.path,
{ ...handlerBinding.place },
"Assignment"
);
}
lowerStatement(builder, handlerPath.get("body"));
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: handlerPath.node.loc ?? GeneratedSource,
};
});
const block = builder.enter("block", (_blockId) => {
const block = stmt.get("block");
builder.enterTryCatch(handler, () => {
lowerStatement(builder, block);
});
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Try,
id: makeInstructionId(0),
loc: block.node.loc ?? GeneratedSource,
};
});
builder.terminateWithContinuation(
{
kind: "try",
block,
handlerBinding:
handlerBinding !== null ? { ...handlerBinding.place } : null,
handler,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock
);
return;
}
case "TypeAlias":
case "TSTypeAliasDeclaration": {
return;
}
case "ClassDeclaration":
case "DeclareClass":
case "DeclareExportAllDeclaration":
case "DeclareExportDeclaration":
case "DeclareFunction":
case "DeclareInterface":
case "DeclareModule":
case "DeclareModuleExports":
case "DeclareOpaqueType":
case "DeclareTypeAlias":
case "DeclareVariable":
case "EnumDeclaration":
case "ExportAllDeclaration":
case "ExportDefaultDeclaration":
case "ExportNamedDeclaration":
case "ImportDeclaration":
case "InterfaceDeclaration":
case "OpaqueType":
case "TSDeclareFunction":
case "TSEnumDeclaration":
case "TSExportAssignment":
case "TSImportEqualsDeclaration":
case "TSInterfaceDeclaration":
case "TSModuleDeclaration":
case "TSNamespaceExportDeclaration":
case "WithStatement": {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
severity: ErrorSeverity.Todo,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
lowerValueToTemporary(builder, {
kind: "UnsupportedNode",
loc: stmtPath.node.loc ?? GeneratedSource,
node: stmtPath.node,
});
return;
}
default: {
return assertExhaustive(
stmtNode,
`Unsupported statement kind '${
(stmtNode as any as NodePath<t.Statement>).type
}'`
);
}
}
}
function lowerObjectMethod(
builder: HIRBuilder,
property: NodePath<t.ObjectMethod>
): InstructionValue {
const loc = property.node.loc ?? GeneratedSource;
const loweredFunc = lowerFunction(builder, property);
if (!loweredFunc) {
return { kind: "UnsupportedNode", node: property.node, loc: loc };
}
return {
kind: "ObjectMethod",
loc,
loweredFunc,
};
}
function lowerObjectPropertyKey(
builder: HIRBuilder,
property: NodePath<t.ObjectProperty | t.ObjectMethod>
): ObjectPropertyKey | null {
const key = property.get("key");
if (key.isStringLiteral()) {
return {
kind: "string",
name: key.node.value,
};
} else if (property.node.computed && key.isExpression()) {
if (!key.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
return null;
}
const place = lowerExpressionToTemporary(builder, key);
return {
kind: "computed",
name: place,
};
} else if (key.isIdentifier()) {
return {
kind: "identifier",
name: key.node.name,
};
}
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
return null;
}
function lowerExpression(
builder: HIRBuilder,
exprPath: NodePath<t.Expression>
): InstructionValue {
const exprNode = exprPath.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
switch (exprNode.type) {
case "Identifier": {
const expr = exprPath as NodePath<t.Identifier>;
const place = lowerIdentifier(builder, expr);
return {
kind: getLoadKind(builder, expr),
place,
loc: exprLoc,
};
}
case "NullLiteral": {
return {
kind: "Primitive",
value: null,
loc: exprLoc,
};
}
case "BooleanLiteral":
case "NumericLiteral":
case "StringLiteral": {
const expr = exprPath as NodePath<
t.StringLiteral | t.BooleanLiteral | t.NumericLiteral
>;
const value = expr.node.value;
return {
kind: "Primitive",
value,
loc: exprLoc,
};
}
case "ObjectExpression": {
const expr = exprPath as NodePath<t.ObjectExpression>;
const propertyPaths = expr.get("properties");
const properties: Array<ObjectProperty | SpreadPattern> = [];
for (const propertyPath of propertyPaths) {
if (propertyPath.isObjectProperty()) {
const loweredKey = lowerObjectPropertyKey(builder, propertyPath);
if (!loweredKey) {
continue;
}
const valuePath = propertyPath.get("value");
if (!valuePath.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
severity: ErrorSeverity.Todo,
loc: valuePath.node.loc ?? null,
suggestions: null,
});
continue;
}
const value = lowerExpressionToTemporary(builder, valuePath);
properties.push({
kind: "ObjectProperty",
type: "property",
place: value,
key: loweredKey,
});
} else if (propertyPath.isSpreadElement()) {
const place = lowerExpressionToTemporary(
builder,
propertyPath.get("argument")
);
properties.push({
kind: "Spread",
place,
});
} else if (propertyPath.isObjectMethod()) {
if (propertyPath.node.kind !== "method") {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
severity: ErrorSeverity.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
continue;
}
const method = lowerObjectMethod(builder, propertyPath);
const place = lowerValueToTemporary(builder, method);
const loweredKey = lowerObjectPropertyKey(builder, propertyPath);
if (!loweredKey) {
continue;
}
properties.push({
kind: "ObjectProperty",
type: "method",
place,
key: loweredKey,
});
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
severity: ErrorSeverity.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
continue;
}
}
return {
kind: "ObjectExpression",
properties,
loc: exprLoc,
};
}
case "ArrayExpression": {
const expr = exprPath as NodePath<t.ArrayExpression>;
let elements: ArrayExpression["elements"] = [];
for (const element of expr.get("elements")) {
if (element.node == null) {
elements.push({
kind: "Hole",
});
continue;
} else if (element.isExpression()) {
elements.push(lowerExpressionToTemporary(builder, element));
} else if (element.isSpreadElement()) {
const place = lowerExpressionToTemporary(
builder,
element.get("argument")
);
elements.push({ kind: "Spread", place });
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
severity: ErrorSeverity.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
continue;
}
}
return {
kind: "ArrayExpression",
elements,
loc: exprLoc,
};
}
case "NewExpression": {
const expr = exprPath as NodePath<t.NewExpression>;
const calleePath = expr.get("callee");
if (!calleePath.isExpression()) {
builder.errors.push({
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
description: `Got a \`${calleePath.node.type}\``,
severity: ErrorSeverity.InvalidJS,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
const callee = lowerExpressionToTemporary(builder, calleePath);
const args = lowerArguments(builder, expr.get("arguments"));
return {
kind: "NewExpression",
callee,
args,
loc: exprLoc,
};
}
case "OptionalCallExpression": {
const expr = exprPath as NodePath<t.OptionalCallExpression>;
return lowerOptionalCallExpression(builder, expr, null);
}
case "CallExpression": {
const expr = exprPath as NodePath<t.CallExpression>;
const calleePath = expr.get("callee");
if (!calleePath.isExpression()) {
builder.errors.push({
reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
severity: ErrorSeverity.Todo,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
if (calleePath.isMemberExpression()) {
const memberExpr = lowerMemberExpression(builder, calleePath);
const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
const args = lowerArguments(builder, expr.get("arguments"));
return {
kind: "MethodCall",
receiver: memberExpr.object,
property: { ...propertyPlace },
args,
loc: exprLoc,
};
} else {
const callee = lowerExpressionToTemporary(builder, calleePath);
const args = lowerArguments(builder, expr.get("arguments"));
return {
kind: "CallExpression",
callee,
args,
loc: exprLoc,
};
}
}
case "BinaryExpression": {
const expr = exprPath as NodePath<t.BinaryExpression>;
const leftPath = expr.get("left");
if (!leftPath.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
severity: ErrorSeverity.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
const left = lowerExpressionToTemporary(builder, leftPath);
const right = lowerExpressionToTemporary(builder, expr.get("right"));
const operator = expr.node.operator;
if (operator === "|>") {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
severity: ErrorSeverity.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
return {
kind: "BinaryExpression",
operator,
left,
right,
loc: exprLoc,
};
}
case "SequenceExpression": {
const expr = exprPath as NodePath<t.SequenceExpression>;
const exprLoc = expr.node.loc ?? GeneratedSource;
const continuationBlock = builder.reserve(builder.currentBlockKind());
const place = buildTemporaryPlace(builder, exprLoc);
const sequenceBlock = builder.enter("sequence", (_) => {
let last: Place | null = null;
for (const item of expr.get("expressions")) {
last = lowerExpressionToTemporary(builder, item);
}
if (last === null) {
builder.errors.push({
reason: `Expected sequence expression to have at least one expression`,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: null,
});
} else {
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: last,
type: null,
loc: exprLoc,
});
}
return {
kind: "goto",
id: makeInstructionId(0),
block: continuationBlock.id,
loc: exprLoc,
variant: GotoVariant.Break,
};
});
builder.terminateWithContinuation(
{
kind: "sequence",
block: sequenceBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: exprLoc,
},
continuationBlock
);
return { kind: "LoadLocal", place, loc: place.loc };
}
case "ConditionalExpression": {
const expr = exprPath as NodePath<t.ConditionalExpression>;
const exprLoc = expr.node.loc ?? GeneratedSource;
const continuationBlock = builder.reserve(builder.currentBlockKind());
const testBlock = builder.reserve("value");
const place = buildTemporaryPlace(builder, exprLoc);
const consequentBlock = builder.enter("value", (_blockId) => {
const consequentPath = expr.get("consequent");
const consequent = lowerExpressionToTemporary(builder, consequentPath);
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: consequent,
type: null,
loc: exprLoc,
});
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: consequentPath.node.loc ?? GeneratedSource,
};
});
const alternateBlock = builder.enter("value", (_blockId) => {
const alternatePath = expr.get("alternate");
const alternate = lowerExpressionToTemporary(builder, alternatePath);
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: alternate,
type: null,
loc: exprLoc,
});
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: alternatePath.node.loc ?? GeneratedSource,
};
});
builder.terminateWithContinuation(
{
kind: "ternary",
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
test: testBlock.id,
loc: exprLoc,
},
testBlock
);
const testPlace = lowerExpressionToTemporary(builder, expr.get("test"));
builder.terminateWithContinuation(
{
kind: "branch",
test: { ...testPlace },
consequent: consequentBlock,
alternate: alternateBlock,
id: makeInstructionId(0),
loc: exprLoc,
},
continuationBlock
);
return { kind: "LoadLocal", place, loc: place.loc };
}
case "LogicalExpression": {
const expr = exprPath as NodePath<t.LogicalExpression>;
const exprLoc = expr.node.loc ?? GeneratedSource;
const continuationBlock = builder.reserve(builder.currentBlockKind());
const testBlock = builder.reserve("value");
const place = buildTemporaryPlace(builder, exprLoc);
const leftPlace = buildTemporaryPlace(
builder,
expr.get("left").node.loc ?? GeneratedSource
);
const consequent = builder.enter("value", () => {
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: { ...leftPlace },
type: null,
loc: leftPlace.loc,
});
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: leftPlace.loc,
};
});
const alternate = builder.enter("value", () => {
const right = lowerExpressionToTemporary(builder, expr.get("right"));
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: { ...right },
type: null,
loc: right.loc,
});
return {
kind: "goto",
block: continuationBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: right.loc,
};
});
builder.terminateWithContinuation(
{
kind: "logical",
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
test: testBlock.id,
operator: expr.node.operator,
loc: exprLoc,
},
testBlock
);
const leftValue = lowerExpressionToTemporary(builder, expr.get("left"));
builder.push({
id: makeInstructionId(0),
lvalue: { ...leftPlace },
value: {
kind: "LoadLocal",
place: leftValue,
loc: exprLoc,
},
loc: exprLoc,
});
builder.terminateWithContinuation(
{
kind: "branch",
test: { ...leftPlace },
consequent,
alternate,
id: makeInstructionId(0),
loc: exprLoc,
},
continuationBlock
);
return { kind: "LoadLocal", place, loc: place.loc };
}
case "AssignmentExpression": {
const expr = exprPath as NodePath<t.AssignmentExpression>;
const operator = expr.node.operator;
if (operator === "=") {
const left = expr.get("left");
return lowerAssignment(
builder,
left.node.loc ?? GeneratedSource,
InstructionKind.Reassign,
left,
lowerExpressionToTemporary(builder, expr.get("right")),
left.isArrayPattern() || left.isObjectPattern()
? "Destructure"
: "Assignment"
);
}
const operators: {
[key: string]: Exclude<t.BinaryExpression["operator"], "|>">;
} = {
"+=": "+",
"-=": "-",
"/=": "/",
"%=": "%",
"*=": "*",
"**=": "**",
"&=": "&",
"|=": "|",
">>=": ">>",
">>>=": ">>>",
"<<=": "<<",
"^=": "^",
};
const binaryOperator = operators[operator];
if (binaryOperator == null) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
const left = expr.get("left");
const leftNode = left.node;
switch (leftNode.type) {
case "Identifier": {
const leftExpr = left as NodePath<t.Identifier>;
const identifier = lowerIdentifier(builder, leftExpr);
const leftPlace = lowerExpressionToTemporary(builder, leftExpr);
const right = lowerExpressionToTemporary(builder, expr.get("right"));
const binaryPlace = lowerValueToTemporary(builder, {
kind: "BinaryExpression",
operator: binaryOperator,
left: leftPlace,
right,
loc: exprLoc,
});
const kind = getStoreKind(builder, leftExpr);
if (kind === "StoreLocal") {
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: {
place: { ...identifier },
kind: InstructionKind.Reassign,
},
value: { ...binaryPlace },
type: null,
loc: exprLoc,
});
return { kind: "LoadLocal", place: identifier, loc: exprLoc };
} else {
lowerValueToTemporary(builder, {
kind: "StoreContext",
lvalue: {
place: { ...identifier },
kind: InstructionKind.Reassign,
},
value: { ...binaryPlace },
loc: exprLoc,
});
return { kind: "LoadContext", place: identifier, loc: exprLoc };
}
}
case "MemberExpression": {
const leftExpr = left as NodePath<t.MemberExpression>;
const { object, property, value } = lowerMemberExpression(
builder,
leftExpr
);
const previousValuePlace = lowerValueToTemporary(builder, value);
const newValuePlace = lowerValueToTemporary(builder, {
kind: "BinaryExpression",
operator: binaryOperator,
left: { ...previousValuePlace },
right: lowerExpressionToTemporary(builder, expr.get("right")),
loc: leftExpr.node.loc ?? GeneratedSource,
});
if (typeof property === "string") {
return {
kind: "PropertyStore",
object: { ...object },
property,
value: { ...newValuePlace },
loc: leftExpr.node.loc ?? GeneratedSource,
};
} else {
return {
kind: "ComputedStore",
object: { ...object },
property: { ...property },
value: { ...newValuePlace },
loc: leftExpr.node.loc ?? GeneratedSource,
};
}
}
default: {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
}
}
case "OptionalMemberExpression": {
const expr = exprPath as NodePath<t.OptionalMemberExpression>;
const { value } = lowerOptionalMemberExpression(builder, expr, null);
return { kind: "LoadLocal", place: value, loc: value.loc };
}
case "MemberExpression": {
const expr = exprPath as NodePath<
t.MemberExpression | t.OptionalMemberExpression
>;
const { value } = lowerMemberExpression(builder, expr);
const place = lowerValueToTemporary(builder, value);
return { kind: "LoadLocal", place, loc: place.loc };
}
case "JSXElement": {
const expr = exprPath as NodePath<t.JSXElement>;
const opening = expr.get("openingElement");
const openingLoc = opening.node.loc ?? GeneratedSource;
const tag = lowerJsxElementName(builder, opening.get("name"));
const props: Array<JsxAttribute> = [];
for (const attribute of opening.get("attributes")) {
if (attribute.isJSXSpreadAttribute()) {
const argument = lowerExpressionToTemporary(
builder,
attribute.get("argument")
);
props.push({ kind: "JsxSpreadAttribute", argument });
continue;
}
if (!attribute.isJSXAttribute()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
severity: ErrorSeverity.Todo,
loc: attribute.node.loc ?? null,
suggestions: null,
});
continue;
}
const namePath = attribute.get("name");
let propName;
if (namePath.isJSXIdentifier()) {
propName = namePath.node.name;
if (propName.indexOf(":") !== -1) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${name}\``,
severity: ErrorSeverity.Todo,
loc: namePath.node.loc ?? null,
suggestions: null,
});
}
} else {
CompilerError.invariant(namePath.isJSXNamespacedName(), {
reason: "Refinement",
description: null,
loc: namePath.node.loc ?? null,
suggestions: null,
});
const namespace = namePath.node.namespace.name;
const name = namePath.node.name.name;
propName = `${namespace}:${name}`;
}
const valueExpr = attribute.get("value");
let value;
if (valueExpr.isJSXElement() || valueExpr.isStringLiteral()) {
value = lowerExpressionToTemporary(builder, valueExpr);
} else if (valueExpr.type == null) {
value = lowerValueToTemporary(builder, {
kind: "Primitive",
value: true,
loc: attribute.node.loc ?? GeneratedSource,
});
} else {
if (!valueExpr.isJSXExpressionContainer()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
severity: ErrorSeverity.Todo,
loc: valueExpr.node?.loc ?? null,
suggestions: null,
});
continue;
}
const expression = valueExpr.get("expression");
if (!expression.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
severity: ErrorSeverity.Todo,
loc: valueExpr.node.loc ?? null,
suggestions: null,
});
continue;
}
value = lowerExpressionToTemporary(builder, expression);
}
props.push({ kind: "JsxAttribute", name: propName, place: value });
}
if (
tag.kind === "BuiltinTag" &&
(tag.name === "fbt" || tag.name === "fbs")
) {
const tagName = tag.name;
const openingIdentifier = opening.get("name");
const tagIdentifier = openingIdentifier.isJSXIdentifier()
? builder.resolveIdentifier(openingIdentifier)
: null;
if (tagIdentifier != null && tagIdentifier.kind === "Identifier") {
CompilerError.throwTodo({
reason: `Support <${tagName}> tags where '${tagName}' is a local variable instead of a global`,
loc: openingIdentifier.node.loc ?? GeneratedSource,
description: null,
suggestions: null,
});
}
const fbtEnumLocations: Array<SourceLocation> = [];
expr.traverse({
JSXNamespacedName(path) {
if (
path.node.namespace.name === tagName &&
path.node.name.name === "enum"
) {
fbtEnumLocations.push(path.node.loc ?? GeneratedSource);
}
},
});
if (fbtEnumLocations.length > 1) {
CompilerError.throwTodo({
reason: `Support <${tagName}> tags with multiple <${tagName}:enum> values`,
loc: fbtEnumLocations.at(-1) ?? GeneratedSource,
description: null,
suggestions: null,
});
}
}
let children: Array<Place>;
if (
tag.kind === "BuiltinTag" &&
(tag.name === "fbt" || tag.name === "fbs")
) {
children = expr
.get("children")
.map((child) => {
if (child.isJSXText()) {
/*
* FBT whitespace normalization differs from standard JSX:
* https://github.com/facebook/fbt/blob/0b4e0d13c30bffd0daa2a75715d606e3587b4e40/packages/babel-plugin-fbt/src/FbtUtil.js#L76-L87
*/
const text = child.node.value.replace(/[^\S\u00A0]+/g, " ");
return lowerValueToTemporary(builder, {
kind: "JSXText",
value: text,
loc: child.node.loc ?? GeneratedSource,
});
}
return lowerJsxElement(builder, child);
})
.filter(notNull);
} else {
children = expr
.get("children")
.map((child) => lowerJsxElement(builder, child))
.filter(notNull);
}
return {
kind: "JsxExpression",
tag,
props,
children: children.length === 0 ? null : children,
loc: exprLoc,
openingLoc: openingLoc,
closingLoc: expr.get("closingElement").node?.loc ?? GeneratedSource,
};
}
case "JSXFragment": {
const expr = exprPath as NodePath<t.JSXFragment>;
const children: Array<Place> = expr
.get("children")
.map((child) => lowerJsxElement(builder, child))
.filter(notNull);
return {
kind: "JsxFragment",
children,
loc: exprLoc,
};
}
case "ArrowFunctionExpression":
case "FunctionExpression": {
const expr = exprPath as NodePath<
t.FunctionExpression | t.ArrowFunctionExpression
>;
return lowerFunctionToValue(builder, expr);
}
case "TaggedTemplateExpression": {
const expr = exprPath as NodePath<t.TaggedTemplateExpression>;
if (expr.get("quasi").get("expressions").length !== 0) {
builder.errors.push({
reason:
"(BuildHIR::lowerExpression) Handle tagged template with interpolations",
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
CompilerError.invariant(expr.get("quasi").get("quasis").length == 1, {
reason:
"there should be only one quasi as we don't support interpolations yet",
description: null,
loc: expr.node.loc ?? null,
suggestions: null,
});
const value = expr.get("quasi").get("quasis").at(0)!.node.value;
if (value.raw !== value.cooked) {
builder.errors.push({
reason:
"(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value",
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
return {
kind: "TaggedTemplateExpression",
tag: lowerExpressionToTemporary(builder, expr.get("tag")),
value,
loc: exprLoc,
};
}
case "TemplateLiteral": {
const expr = exprPath as NodePath<t.TemplateLiteral>;
const subexprs = expr.get("expressions");
const quasis = expr.get("quasis");
if (subexprs.length !== quasis.length - 1) {
builder.errors.push({
reason: `Unexpected quasi and subexpression lengths in template literal`,
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
if (subexprs.some((e) => !e.isExpression())) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
const subexprPlaces = subexprs.map((e) =>
lowerExpressionToTemporary(builder, e as NodePath<t.Expression>)
);
return {
kind: "TemplateLiteral",
subexprs: subexprPlaces,
quasis: expr.get("quasis").map((q) => q.node.value),
loc: exprLoc,
};
}
case "UnaryExpression": {
let expr = exprPath as NodePath<t.UnaryExpression>;
if (expr.node.operator === "delete") {
const argument = expr.get("argument");
if (argument.isMemberExpression()) {
const { object, property } = lowerMemberExpression(builder, argument);
if (typeof property === "string") {
return {
kind: "PropertyDelete",
object,
property,
loc: exprLoc,
};
} else {
return {
kind: "ComputedDelete",
object,
property,
loc: exprLoc,
};
}
} else {
builder.errors.push({
reason: `Only object properties can be deleted`,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
description: "Remove this line",
range: [expr.node.start!, expr.node.end!],
op: CompilerSuggestionOperation.Remove,
},
],
});
return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
}
} else if (expr.node.operator === "throw") {
builder.errors.push({
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
description: "Remove this line",
range: [expr.node.start!, expr.node.end!],
op: CompilerSuggestionOperation.Remove,
},
],
});
return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
} else {
return {
kind: "UnaryExpression",
operator: expr.node.operator,
value: lowerExpressionToTemporary(builder, expr.get("argument")),
loc: exprLoc,
};
}
}
case "AwaitExpression": {
let expr = exprPath as NodePath<t.AwaitExpression>;
return {
kind: "Await",
value: lowerExpressionToTemporary(builder, expr.get("argument")),
loc: exprLoc,
};
}
case "TypeCastExpression": {
let expr = exprPath as NodePath<t.TypeCastExpression>;
const typeAnnotation = expr.get("typeAnnotation").get("typeAnnotation");
return {
kind: "TypeCastExpression",
value: lowerExpressionToTemporary(builder, expr.get("expression")),
typeAnnotation: typeAnnotation.node,
type: lowerType(typeAnnotation.node),
loc: exprLoc,
};
}
case "TSAsExpression": {
let expr = exprPath as NodePath<t.TSAsExpression>;
const typeAnnotation = expr.get("typeAnnotation");
return {
kind: "TypeCastExpression",
value: lowerExpressionToTemporary(builder, expr.get("expression")),
typeAnnotation: typeAnnotation.node,
type: lowerType(typeAnnotation.node),
loc: exprLoc,
};
}
case "UpdateExpression": {
let expr = exprPath as NodePath<t.UpdateExpression>;
const argument = expr.get("argument");
if (!argument.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
} else if (builder.isContextIdentifier(argument)) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
const lvalue = lowerIdentifierForAssignment(
builder,
argument.node.loc ?? GeneratedSource,
InstructionKind.Reassign,
argument
);
if (lvalue === null) {
/*
* lowerIdentifierForAssignment should have already reported an error if it returned null,
* we check here just in case
*/
if (!builder.errors.hasErrors()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
severity: ErrorSeverity.Invariant,
loc: exprLoc,
suggestions: null,
});
}
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
} else if (lvalue.kind === "Global") {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
severity: ErrorSeverity.Todo,
loc: exprLoc,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
const value = lowerIdentifier(builder, argument);
if (expr.node.prefix) {
return {
kind: "PrefixUpdate",
lvalue,
operation: expr.node.operator,
value,
loc: exprLoc,
};
} else {
return {
kind: "PostfixUpdate",
lvalue,
operation: expr.node.operator,
value,
loc: exprLoc,
};
}
}
case "RegExpLiteral": {
let expr = exprPath as NodePath<t.RegExpLiteral>;
return {
kind: "RegExpLiteral",
pattern: expr.node.pattern,
flags: expr.node.flags,
loc: expr.node.loc ?? GeneratedSource,
};
}
case "TSNonNullExpression": {
let expr = exprPath as NodePath<t.TSNonNullExpression>;
return lowerExpression(builder, expr.get("expression"));
}
case "MetaProperty": {
let expr = exprPath as NodePath<t.MetaProperty>;
if (
expr.node.meta.name === "import" &&
expr.node.property.name === "meta"
) {
return {
kind: "MetaProperty",
meta: expr.node.meta.name,
property: expr.node.property.name,
loc: expr.node.loc ?? GeneratedSource,
};
}
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
default: {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
}
}
function lowerOptionalMemberExpression(
builder: HIRBuilder,
expr: NodePath<t.OptionalMemberExpression>,
parentAlternate: BlockId | null
): { object: Place; value: Place } {
const optional = expr.node.optional;
const loc = expr.node.loc ?? GeneratedSource;
const place = buildTemporaryPlace(builder, loc);
const continuationBlock = builder.reserve(builder.currentBlockKind());
const consequent = builder.reserve("value");
/*
* block to evaluate if the callee is null/undefined, this sets the result of the call to undefined.
* note that we only create an alternate when first entering an optional subtree of the ast: if this
* is a child of an optional node, we use the alterate created by the parent.
*/
const alternate =
parentAlternate !== null
? parentAlternate
: builder.enter("value", () => {
const temp = lowerValueToTemporary(builder, {
kind: "Primitive",
value: undefined,
loc,
});
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: { ...temp },
type: null,
loc,
});
return {
kind: "goto",
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
});
let object: Place | null = null;
const testBlock = builder.enter("value", () => {
const objectPath = expr.get("object");
if (objectPath.isOptionalMemberExpression()) {
const { value } = lowerOptionalMemberExpression(
builder,
objectPath,
alternate
);
object = value;
} else if (objectPath.isOptionalCallExpression()) {
const value = lowerOptionalCallExpression(builder, objectPath, alternate);
object = lowerValueToTemporary(builder, value);
} else {
object = lowerExpressionToTemporary(builder, objectPath);
}
return {
kind: "branch",
test: { ...object },
consequent: consequent.id,
alternate,
id: makeInstructionId(0),
loc,
};
});
CompilerError.invariant(object !== null, {
reason: "Satisfy type checker",
description: null,
loc: null,
suggestions: null,
});
/*
* block to evaluate if the callee is non-null/undefined. arguments are lowered in this block to preserve
* the semantic of conditional evaluation depending on the callee
*/
builder.enterReserved(consequent, () => {
const { value } = lowerMemberExpression(builder, expr, object);
const temp = lowerValueToTemporary(builder, value);
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: { ...temp },
type: null,
loc,
});
return {
kind: "goto",
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
});
builder.terminateWithContinuation(
{
kind: "optional",
optional,
test: testBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc,
},
continuationBlock
);
return { object, value: place };
}
function lowerOptionalCallExpression(
builder: HIRBuilder,
expr: NodePath<t.OptionalCallExpression>,
parentAlternate: BlockId | null
): InstructionValue {
const optional = expr.node.optional;
const calleePath = expr.get("callee");
const loc = expr.node.loc ?? GeneratedSource;
const place = buildTemporaryPlace(builder, loc);
const continuationBlock = builder.reserve(builder.currentBlockKind());
const consequent = builder.reserve("value");
/*
* block to evaluate if the callee is null/undefined, this sets the result of the call to undefined.
* note that we only create an alternate when first entering an optional subtree of the ast: if this
* is a child of an optional node, we use the alterate created by the parent.
*/
const alternate =
parentAlternate !== null
? parentAlternate
: builder.enter("value", () => {
const temp = lowerValueToTemporary(builder, {
kind: "Primitive",
value: undefined,
loc,
});
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: { ...temp },
type: null,
loc,
});
return {
kind: "goto",
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
});
/*
* Lower the callee within the test block to represent the fact that the code for the callee is
* scoped within the optional
*/
let callee:
| { kind: "CallExpression"; callee: Place }
| { kind: "MethodCall"; receiver: Place; property: Place };
const testBlock = builder.enter("value", () => {
if (calleePath.isOptionalCallExpression()) {
// Recursively call lowerOptionalCallExpression to thread down the alternate block
const value = lowerOptionalCallExpression(builder, calleePath, alternate);
const valuePlace = lowerValueToTemporary(builder, value);
callee = {
kind: "CallExpression",
callee: valuePlace,
};
} else if (calleePath.isOptionalMemberExpression()) {
const { object, value } = lowerOptionalMemberExpression(
builder,
calleePath,
alternate
);
callee = {
kind: "MethodCall",
receiver: object,
property: value,
};
} else if (calleePath.isMemberExpression()) {
const memberExpr = lowerMemberExpression(builder, calleePath);
const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
callee = {
kind: "MethodCall",
receiver: memberExpr.object,
property: propertyPlace,
};
} else {
callee = {
kind: "CallExpression",
callee: lowerExpressionToTemporary(builder, calleePath),
};
}
const testPlace =
callee.kind === "CallExpression" ? callee.callee : callee.property;
return {
kind: "branch",
test: { ...testPlace },
consequent: consequent.id,
alternate,
id: makeInstructionId(0),
loc,
};
});
/*
* block to evaluate if the callee is non-null/undefined. arguments are lowered in this block to preserve
* the semantic of conditional evaluation depending on the callee
*/
builder.enterReserved(consequent, () => {
const args = lowerArguments(builder, expr.get("arguments"));
const temp = buildTemporaryPlace(builder, loc);
if (callee.kind === "CallExpression") {
builder.push({
id: makeInstructionId(0),
lvalue: { ...temp },
value: {
kind: "CallExpression",
callee: { ...callee.callee },
args,
loc,
},
loc,
});
} else {
builder.push({
id: makeInstructionId(0),
lvalue: { ...temp },
value: {
kind: "MethodCall",
receiver: { ...callee.receiver },
property: { ...callee.property },
args,
loc,
},
loc,
});
}
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: { ...temp },
type: null,
loc,
});
return {
kind: "goto",
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
});
builder.terminateWithContinuation(
{
kind: "optional",
optional,
test: testBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc,
},
continuationBlock
);
return { kind: "LoadLocal", place, loc: place.loc };
}
/*
* There are a few places where we do not preserve original evaluation ordering and/or control flow, such as
* switch case test values and default values in destructuring (assignment patterns). In these cases we allow
* simple expressions whose evaluation cannot be observed:
* - primitives
* - arrays/objects whose values are also safely reorderable.
*/
function lowerReorderableExpression(
builder: HIRBuilder,
expr: NodePath<t.Expression>
): Place {
if (!isReorderableExpression(builder, expr, true)) {
builder.errors.push({
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
}
return lowerExpressionToTemporary(builder, expr);
}
function isReorderableExpression(
builder: HIRBuilder,
expr: NodePath<t.Expression>,
allowLocalIdentifiers: boolean
): boolean {
switch (expr.node.type) {
case "Identifier": {
const binding = builder.resolveIdentifier(expr as NodePath<t.Identifier>);
if (binding.kind === "Identifier") {
return allowLocalIdentifiers;
} else {
return true;
}
}
case "RegExpLiteral":
case "StringLiteral":
case "NumericLiteral":
case "NullLiteral":
case "BooleanLiteral":
case "BigIntLiteral": {
return true;
}
case "UnaryExpression": {
const unary = expr as NodePath<t.UnaryExpression>;
switch (expr.node.operator) {
case "!":
case "+":
case "-": {
return isReorderableExpression(
builder,
unary.get("argument"),
allowLocalIdentifiers
);
}
default: {
return false;
}
}
}
case "TypeCastExpression": {
return isReorderableExpression(
builder,
(expr as NodePath<t.TypeCastExpression>).get("expression"),
allowLocalIdentifiers
);
}
case "ConditionalExpression": {
const conditional = expr as NodePath<t.ConditionalExpression>;
return (
isReorderableExpression(
builder,
conditional.get("test"),
allowLocalIdentifiers
) &&
isReorderableExpression(
builder,
conditional.get("consequent"),
allowLocalIdentifiers
) &&
isReorderableExpression(
builder,
conditional.get("alternate"),
allowLocalIdentifiers
)
);
}
case "ArrayExpression": {
return (expr as NodePath<t.ArrayExpression>)
.get("elements")
.every(
(element) =>
element.isExpression() &&
isReorderableExpression(builder, element, allowLocalIdentifiers)
);
}
case "ObjectExpression": {
return (expr as NodePath<t.ObjectExpression>)
.get("properties")
.every((property) => {
if (!property.isObjectProperty() || property.node.computed) {
return false;
}
const value = property.get("value");
return (
value.isExpression() &&
isReorderableExpression(builder, value, allowLocalIdentifiers)
);
});
}
case "MemberExpression": {
const test = expr as NodePath<t.MemberExpression>;
let innerObject: NodePath<t.Expression> = test;
while (innerObject.isMemberExpression()) {
innerObject = innerObject.get("object");
}
if (
innerObject.isIdentifier() &&
builder.resolveIdentifier(innerObject).kind !== "Identifier"
) {
return true;
} else {
return false;
}
}
case "ArrowFunctionExpression": {
const fn = expr as NodePath<t.ArrowFunctionExpression>;
const body = fn.get("body");
if (body.node.type === "BlockStatement") {
return body.node.body.length === 0;
} else {
invariant(body.isExpression(), "Expected an expression");
return isReorderableExpression(
builder,
body,
false
);
}
}
case "CallExpression": {
const call = expr as NodePath<t.CallExpression>;
const callee = call.get("callee");
return (
callee.isExpression() &&
isReorderableExpression(builder, callee, allowLocalIdentifiers) &&
call
.get("arguments")
.every(
(arg) =>
arg.isExpression() &&
isReorderableExpression(builder, arg, allowLocalIdentifiers)
)
);
}
default: {
return false;
}
}
}
function lowerArguments(
builder: HIRBuilder,
expr: Array<
NodePath<
| t.Expression
| t.SpreadElement
| t.JSXNamespacedName
| t.ArgumentPlaceholder
>
>
): Array<Place | SpreadPattern> {
let args: Array<Place | SpreadPattern> = [];
for (const argPath of expr) {
if (argPath.isSpreadElement()) {
args.push({
kind: "Spread",
place: lowerExpressionToTemporary(builder, argPath.get("argument")),
});
} else if (argPath.isExpression()) {
args.push(lowerExpressionToTemporary(builder, argPath));
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
severity: ErrorSeverity.Todo,
loc: argPath.node.loc ?? null,
suggestions: null,
});
}
}
return args;
}
type LoweredMemberExpression = {
object: Place;
property: Place | string;
value: InstructionValue;
};
function lowerMemberExpression(
builder: HIRBuilder,
expr: NodePath<t.MemberExpression | t.OptionalMemberExpression>,
loweredObject: Place | null = null
): LoweredMemberExpression {
const exprNode = expr.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
const objectNode = expr.get("object");
const propertyNode = expr.get("property");
const object =
loweredObject ?? lowerExpressionToTemporary(builder, objectNode);
if (!expr.node.computed) {
if (!propertyNode.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
severity: ErrorSeverity.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
return {
object,
property: propertyNode.toString(),
value: { kind: "UnsupportedNode", node: exprNode, loc: exprLoc },
};
}
const value: InstructionValue = {
kind: "PropertyLoad",
object: { ...object },
property: propertyNode.node.name,
loc: exprLoc,
};
return { object, property: propertyNode.node.name, value };
} else {
if (!propertyNode.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
severity: ErrorSeverity.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
return {
object,
property: propertyNode.toString(),
value: {
kind: "UnsupportedNode",
node: exprNode,
loc: exprLoc,
},
};
}
const property = lowerExpressionToTemporary(builder, propertyNode);
const value: InstructionValue = {
kind: "ComputedLoad",
object: { ...object },
property: { ...property },
loc: exprLoc,
};
return { object, property, value };
}
}
function lowerJsxElementName(
builder: HIRBuilder,
exprPath: NodePath<
t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName
>
): Place | BuiltinTag {
const exprNode = exprPath.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
if (exprPath.isJSXIdentifier()) {
const tag: string = exprPath.node.name;
if (tag.match(/^[A-Z]/)) {
const kind = getLoadKind(builder, exprPath);
return lowerValueToTemporary(builder, {
kind: kind,
place: lowerIdentifier(builder, exprPath),
loc: exprLoc,
});
} else {
return {
kind: "BuiltinTag",
name: tag,
loc: exprLoc,
};
}
} else if (exprPath.isJSXMemberExpression()) {
return lowerJsxMemberExpression(builder, exprPath);
} else if (exprPath.isJSXNamespacedName()) {
const namespace = exprPath.node.namespace.name;
const name = exprPath.node.name.name;
const tag = `${namespace}:${name}`;
if (namespace.indexOf(":") !== -1 || name.indexOf(":") !== -1) {
builder.errors.push({
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
description: `Got \`${namespace}\` : \`${name}\``,
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
}
const place = lowerValueToTemporary(builder, {
kind: "Primitive",
value: tag,
loc: exprLoc,
});
return place;
} else {
builder.errors.push({
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
return lowerValueToTemporary(builder, {
kind: "UnsupportedNode",
node: exprNode,
loc: exprLoc,
});
}
}
function lowerJsxMemberExpression(
builder: HIRBuilder,
exprPath: NodePath<t.JSXMemberExpression>
): Place {
const loc = exprPath.node.loc ?? GeneratedSource;
const object = exprPath.get("object");
let objectPlace: Place;
if (object.isJSXMemberExpression()) {
objectPlace = lowerJsxMemberExpression(builder, object);
} else {
CompilerError.invariant(object.isJSXIdentifier(), {
reason: `TypeScript refinement fail: expected 'JsxIdentifier', got \`${object.node.type}\``,
description: null,
loc: object.node.loc ?? null,
suggestions: null,
});
objectPlace = lowerIdentifier(builder, object);
}
const property = exprPath.get("property").node.name;
return lowerValueToTemporary(builder, {
kind: "PropertyLoad",
object: objectPlace,
property,
loc,
});
}
function lowerJsxElement(
builder: HIRBuilder,
exprPath: NodePath<
| t.JSXText
| t.JSXExpressionContainer
| t.JSXSpreadChild
| t.JSXElement
| t.JSXFragment
>
): Place | null {
const exprNode = exprPath.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
if (exprPath.isJSXElement() || exprPath.isJSXFragment()) {
return lowerExpressionToTemporary(builder, exprPath);
} else if (exprPath.isJSXExpressionContainer()) {
const expression = exprPath.get("expression");
if (expression.isJSXEmptyExpression()) {
return null;
} else {
CompilerError.invariant(expression.isExpression(), {
reason: `(BuildHIR::lowerJsxElement) Expected Expression but found ${expression.type}!`,
description: null,
loc: expression.node.loc ?? null,
suggestions: null,
});
return lowerExpressionToTemporary(builder, expression);
}
} else if (exprPath.isJSXText()) {
const text = trimJsxText(exprPath.node.value);
if (text === null) {
return null;
}
const place = lowerValueToTemporary(builder, {
kind: "JSXText",
value: text,
loc: exprLoc,
});
return place;
} else {
builder.errors.push({
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
const place = lowerValueToTemporary(builder, {
kind: "UnsupportedNode",
node: exprNode,
loc: exprLoc,
});
return place;
}
}
function trimJsxText(original: string): string | null {
const lines = original.split(/\r\n|\n|\r/);
let lastNonEmptyLine = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i].match(/[^ \t]/)) {
lastNonEmptyLine = i;
}
}
let str = "";
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isFirstLine = i === 0;
const isLastLine = i === lines.length - 1;
const isLastNonEmptyLine = i === lastNonEmptyLine;
let trimmedLine = line.replace(/\t/g, " ");
if (!isFirstLine) {
trimmedLine = trimmedLine.replace(/^[ ]+/, "");
}
if (!isLastLine) {
trimmedLine = trimmedLine.replace(/[ ]+$/, "");
}
if (trimmedLine) {
if (!isLastNonEmptyLine) {
trimmedLine += " ";
}
str += trimmedLine;
}
}
if (str.length !== 0) {
return str;
} else {
return null;
}
}
function lowerFunctionToValue(
builder: HIRBuilder,
expr: NodePath<
t.FunctionExpression | t.ArrowFunctionExpression | t.FunctionDeclaration
>
): InstructionValue {
const exprNode = expr.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
let name: string | null = null;
if (expr.isFunctionExpression()) {
name = expr.get("id")?.node?.name ?? null;
}
const loweredFunc = lowerFunction(builder, expr);
if (!loweredFunc) {
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
return {
kind: "FunctionExpression",
name,
expr: expr.node,
loc: exprLoc,
loweredFunc,
};
}
function lowerFunction(
builder: HIRBuilder,
expr: NodePath<
| t.FunctionExpression
| t.ArrowFunctionExpression
| t.FunctionDeclaration
| t.ObjectMethod
>
): LoweredFunction | null {
const componentScope: Scope = builder.parentFunction.scope;
const captured = gatherCapturedDeps(builder, expr, componentScope);
const lowering = lower(
expr,
builder.environment,
builder.bindings,
[...builder.context, ...captured.identifiers],
builder.parentFunction
);
let loweredFunc: HIRFunction;
if (lowering.isErr()) {
lowering
.unwrapErr()
.details.forEach((detail) => builder.errors.pushErrorDetail(detail));
return null;
}
loweredFunc = lowering.unwrap();
return {
func: loweredFunc,
dependencies: captured.refs,
};
}
function lowerExpressionToTemporary(
builder: HIRBuilder,
exprPath: NodePath<t.Expression>
): Place {
const value = lowerExpression(builder, exprPath);
return lowerValueToTemporary(builder, value);
}
function lowerValueToTemporary(
builder: HIRBuilder,
value: InstructionValue
): Place {
if (value.kind === "LoadLocal" && value.place.identifier.name === null) {
return value.place;
}
const place: Place = buildTemporaryPlace(builder, value.loc);
builder.push({
id: makeInstructionId(0),
value: value,
loc: value.loc,
lvalue: { ...place },
});
return place;
}
function lowerIdentifier(
builder: HIRBuilder,
exprPath: NodePath<t.Identifier | t.JSXIdentifier>
): Place {
const exprNode = exprPath.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
const binding = builder.resolveIdentifier(exprPath);
switch (binding.kind) {
case "Identifier": {
const place: Place = {
kind: "Identifier",
identifier: binding.identifier,
effect: Effect.Unknown,
reactive: false,
loc: exprLoc,
};
return place;
}
default: {
return lowerValueToTemporary(builder, {
kind: "LoadGlobal",
binding,
loc: exprLoc,
});
}
}
}
function buildTemporaryPlace(builder: HIRBuilder, loc: SourceLocation): Place {
const place: Place = {
kind: "Identifier",
identifier: builder.makeTemporary(loc),
effect: Effect.Unknown,
reactive: false,
loc,
};
return place;
}
function getStoreKind(
builder: HIRBuilder,
identifier: NodePath<t.Identifier>
): "StoreLocal" | "StoreContext" {
const isContext = builder.isContextIdentifier(identifier);
return isContext ? "StoreContext" : "StoreLocal";
}
function getLoadKind(
builder: HIRBuilder,
identifier: NodePath<t.Identifier | t.JSXIdentifier>
): "LoadLocal" | "LoadContext" {
const isContext = builder.isContextIdentifier(identifier);
return isContext ? "LoadContext" : "LoadLocal";
}
function lowerIdentifierForAssignment(
builder: HIRBuilder,
loc: SourceLocation,
kind: InstructionKind,
path: NodePath<t.Identifier>
): Place | { kind: "Global"; name: string } | null {
const binding = builder.resolveIdentifier(path);
if (binding.kind !== "Identifier") {
if (kind === InstructionKind.Reassign) {
return { kind: "Global", name: path.node.name };
} else {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
severity: ErrorSeverity.Invariant,
loc: path.node.loc ?? null,
suggestions: null,
});
return null;
}
} else if (
binding.bindingKind === "const" &&
kind === InstructionKind.Reassign
) {
builder.errors.push({
reason: `Cannot reassign a \`const\` variable`,
severity: ErrorSeverity.InvalidJS,
loc: path.node.loc ?? null,
description:
binding.identifier.name != null
? `\`${binding.identifier.name.value}\` is declared as const`
: null,
});
return null;
}
const place: Place = {
kind: "Identifier",
identifier: binding.identifier,
effect: Effect.Unknown,
reactive: false,
loc,
};
return place;
}
function lowerAssignment(
builder: HIRBuilder,
loc: SourceLocation,
kind: InstructionKind,
lvaluePath: NodePath<t.LVal>,
value: Place,
assignmentKind: "Destructure" | "Assignment"
): InstructionValue {
const lvalueNode = lvaluePath.node;
switch (lvalueNode.type) {
case "Identifier": {
const lvalue = lvaluePath as NodePath<t.Identifier>;
const place = lowerIdentifierForAssignment(builder, loc, kind, lvalue);
if (place === null) {
return {
kind: "UnsupportedNode",
loc: lvalue.node.loc ?? GeneratedSource,
node: lvalue.node,
};
} else if (place.kind === "Global") {
const temporary = lowerValueToTemporary(builder, {
kind: "StoreGlobal",
name: place.name,
value,
loc,
});
return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
}
const isHoistedIdentifier = builder.environment.isHoistedIdentifier(
lvalue.node
);
let temporary;
if (builder.isContextIdentifier(lvalue)) {
if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
if (kind === InstructionKind.Const) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
}
lowerValueToTemporary(builder, {
kind: "DeclareContext",
lvalue: {
kind: InstructionKind.Let,
place: { ...place },
},
loc: place.loc,
});
}
temporary = lowerValueToTemporary(builder, {
kind: "StoreContext",
lvalue: { place: { ...place }, kind: InstructionKind.Reassign },
value,
loc,
});
} else {
const typeAnnotation = lvalue.get("typeAnnotation");
let type: t.FlowType | t.TSType | null;
if (typeAnnotation.isTSTypeAnnotation()) {
const typePath = typeAnnotation.get("typeAnnotation");
type = typePath.node;
} else if (typeAnnotation.isTypeAnnotation()) {
const typePath = typeAnnotation.get("typeAnnotation");
type = typePath.node;
} else {
type = null;
}
temporary = lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { place: { ...place }, kind },
value,
type,
loc,
});
}
return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
}
case "MemberExpression": {
CompilerError.invariant(kind === InstructionKind.Reassign, {
reason: "MemberExpression may only appear in an assignment expression",
description: null,
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});
const lvalue = lvaluePath as NodePath<t.MemberExpression>;
const property = lvalue.get("property");
const object = lowerExpressionToTemporary(builder, lvalue.get("object"));
if (!lvalue.node.computed) {
if (!property.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: lvalueNode, loc };
}
const temporary = lowerValueToTemporary(builder, {
kind: "PropertyStore",
object,
property: property.node.name,
value,
loc,
});
return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
} else {
if (!property.isExpression()) {
builder.errors.push({
reason:
"(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property",
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: lvalueNode, loc };
}
const propertyPlace = lowerExpressionToTemporary(builder, property);
const temporary = lowerValueToTemporary(builder, {
kind: "ComputedStore",
object,
property: propertyPlace,
value,
loc,
});
return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
}
}
case "ArrayPattern": {
const lvalue = lvaluePath as NodePath<t.ArrayPattern>;
const elements = lvalue.get("elements");
const items: ArrayPattern["items"] = [];
const followups: Array<{ place: Place; path: NodePath<t.LVal> }> = [];
const forceTemporaries =
kind === InstructionKind.Reassign &&
(elements.some((element) => !element.isIdentifier()) ||
elements.some(
(element) =>
element.isIdentifier() &&
(getStoreKind(builder, element) !== "StoreLocal" ||
builder.resolveIdentifier(element).kind !== "Identifier")
));
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.node == null) {
items.push({
kind: "Hole",
});
continue;
}
if (element.isRestElement()) {
const argument = element.get("argument");
if (
argument.isIdentifier() &&
!forceTemporaries &&
(assignmentKind === "Assignment" ||
getStoreKind(builder, argument) === "StoreLocal")
) {
const identifier = lowerIdentifierForAssignment(
builder,
element.node.loc ?? GeneratedSource,
kind,
argument
);
if (identifier === null) {
continue;
} else if (identifier.kind === "Global") {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason:
"Expected reassignment of globals to enable forceTemporaries",
loc: element.node.loc ?? GeneratedSource,
});
continue;
}
items.push({
kind: "Spread",
place: identifier,
});
} else {
const temp = buildTemporaryPlace(
builder,
element.node.loc ?? GeneratedSource
);
promoteTemporary(temp.identifier);
items.push({
kind: "Spread",
place: { ...temp },
});
followups.push({ place: temp, path: argument as NodePath<t.LVal> });
}
} else if (
element.isIdentifier() &&
!forceTemporaries &&
(assignmentKind === "Assignment" ||
getStoreKind(builder, element) === "StoreLocal")
) {
const identifier = lowerIdentifierForAssignment(
builder,
element.node.loc ?? GeneratedSource,
kind,
element
);
if (identifier === null) {
continue;
} else if (identifier.kind === "Global") {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason:
"Expected reassignment of globals to enable forceTemporaries",
loc: element.node.loc ?? GeneratedSource,
});
continue;
}
items.push(identifier);
} else {
const temp = buildTemporaryPlace(
builder,
element.node.loc ?? GeneratedSource
);
promoteTemporary(temp.identifier);
items.push({ ...temp });
followups.push({ place: temp, path: element as NodePath<t.LVal> });
}
}
const temporary = lowerValueToTemporary(builder, {
kind: "Destructure",
lvalue: {
kind,
pattern: {
kind: "ArrayPattern",
items,
},
},
value,
loc,
});
for (const { place, path } of followups) {
lowerAssignment(
builder,
path.node.loc ?? loc,
kind,
path,
place,
assignmentKind
);
}
return { kind: "LoadLocal", place: temporary, loc: value.loc };
}
case "ObjectPattern": {
const lvalue = lvaluePath as NodePath<t.ObjectPattern>;
const propertiesPaths = lvalue.get("properties");
const properties: ObjectPattern["properties"] = [];
const followups: Array<{ place: Place; path: NodePath<t.LVal> }> = [];
const forceTemporaries =
kind === InstructionKind.Reassign &&
propertiesPaths.some(
(property) =>
property.isRestElement() ||
(property.isObjectProperty() &&
(!property.get("value").isIdentifier() ||
builder.resolveIdentifier(
property.get("value") as NodePath<t.Identifier>
).kind !== "Identifier"))
);
for (let i = 0; i < propertiesPaths.length; i++) {
const property = propertiesPaths[i];
if (property.isRestElement()) {
const argument = property.get("argument");
if (!argument.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
severity: ErrorSeverity.Todo,
loc: argument.node.loc ?? null,
suggestions: null,
});
continue;
}
if (
forceTemporaries ||
getStoreKind(builder, argument) === "StoreContext"
) {
const temp = buildTemporaryPlace(
builder,
property.node.loc ?? GeneratedSource
);
promoteTemporary(temp.identifier);
properties.push({
kind: "Spread",
place: { ...temp },
});
followups.push({ place: temp, path: argument as NodePath<t.LVal> });
} else {
const identifier = lowerIdentifierForAssignment(
builder,
property.node.loc ?? GeneratedSource,
kind,
argument
);
if (identifier === null) {
continue;
} else if (identifier.kind === "Global") {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason:
"Expected reassignment of globals to enable forceTemporaries",
loc: property.node.loc ?? GeneratedSource,
});
continue;
}
properties.push({
kind: "Spread",
place: identifier,
});
}
} else {
if (!property.isObjectProperty()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
continue;
}
if (property.node.computed) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
continue;
}
const loweredKey = lowerObjectPropertyKey(builder, property);
if (!loweredKey) {
continue;
}
const element = property.get("value");
if (!element.isLVal()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
severity: ErrorSeverity.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
continue;
}
if (
element.isIdentifier() &&
!forceTemporaries &&
(assignmentKind === "Assignment" ||
getStoreKind(builder, element) === "StoreLocal")
) {
const identifier = lowerIdentifierForAssignment(
builder,
element.node.loc ?? GeneratedSource,
kind,
element
);
if (identifier === null) {
continue;
} else if (identifier.kind === "Global") {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason:
"Expected reassignment of globals to enable forceTemporaries",
loc: element.node.loc ?? GeneratedSource,
});
continue;
}
properties.push({
kind: "ObjectProperty",
type: "property",
place: identifier,
key: loweredKey,
});
} else {
const temp = buildTemporaryPlace(
builder,
element.node.loc ?? GeneratedSource
);
promoteTemporary(temp.identifier);
properties.push({
kind: "ObjectProperty",
type: "property",
place: { ...temp },
key: loweredKey,
});
followups.push({ place: temp, path: element as NodePath<t.LVal> });
}
}
}
const temporary = lowerValueToTemporary(builder, {
kind: "Destructure",
lvalue: {
kind,
pattern: {
kind: "ObjectPattern",
properties,
},
},
value,
loc,
});
for (const { place, path } of followups) {
lowerAssignment(
builder,
path.node.loc ?? loc,
kind,
path,
place,
assignmentKind
);
}
return { kind: "LoadLocal", place: temporary, loc: value.loc };
}
case "AssignmentPattern": {
const lvalue = lvaluePath as NodePath<t.AssignmentPattern>;
const loc = lvalue.node.loc ?? GeneratedSource;
const temp = buildTemporaryPlace(builder, loc);
const testBlock = builder.reserve("value");
const continuationBlock = builder.reserve(builder.currentBlockKind());
const consequent = builder.enter("value", () => {
const defaultValue = lowerReorderableExpression(
builder,
lvalue.get("right")
);
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...temp } },
value: { ...defaultValue },
type: null,
loc,
});
return {
kind: "goto",
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
});
const alternate = builder.enter("value", () => {
lowerValueToTemporary(builder, {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...temp } },
value: { ...value },
type: null,
loc,
});
return {
kind: "goto",
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
});
builder.terminateWithContinuation(
{
kind: "ternary",
test: testBlock.id,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc,
},
testBlock
);
const undef = lowerValueToTemporary(builder, {
kind: "Primitive",
value: undefined,
loc,
});
const test = lowerValueToTemporary(builder, {
kind: "BinaryExpression",
left: { ...value },
operator: "===",
right: { ...undef },
loc,
});
builder.terminateWithContinuation(
{
kind: "branch",
test: { ...test },
consequent,
alternate,
id: makeInstructionId(0),
loc,
},
continuationBlock
);
return lowerAssignment(
builder,
loc,
kind,
lvalue.get("left"),
temp,
assignmentKind
);
}
default: {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
severity: ErrorSeverity.Todo,
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: lvalueNode, loc };
}
}
}
function isValidDependency(path: NodePath<t.MemberExpression>): boolean {
const parent: NodePath<t.Node> = path.parentPath;
return (
!path.node.computed &&
!(parent.isCallExpression() && parent.get("callee") === path)
);
}
function captureScopes({ from, to }: { from: Scope; to: Scope }): Set<Scope> {
let scopes: Set<Scope> = new Set();
while (from) {
scopes.add(from);
if (from === to) {
break;
}
from = from.parent;
}
return scopes;
}
function gatherCapturedDeps(
builder: HIRBuilder,
fn: NodePath<
| t.FunctionExpression
| t.ArrowFunctionExpression
| t.FunctionDeclaration
| t.ObjectMethod
>,
componentScope: Scope
): { identifiers: Array<t.Identifier>; refs: Array<Place> } {
const capturedIds: Map<t.Identifier, number> = new Map();
const capturedRefs: Set<Place> = new Set();
const seenPaths: Set<string> = new Set();
const pureScopes: Set<Scope> = captureScopes({
from: fn.scope.parent,
to: componentScope,
});
function addCapturedId(bindingIdentifier: t.Identifier): number {
if (!capturedIds.has(bindingIdentifier)) {
const index = capturedIds.size;
capturedIds.set(bindingIdentifier, index);
return index;
} else {
return capturedIds.get(bindingIdentifier)!;
}
}
function handleMaybeDependency(
path:
| NodePath<t.MemberExpression>
| NodePath<t.Identifier>
| NodePath<t.JSXOpeningElement>
): void {
let baseIdentifier: NodePath<t.Identifier> | NodePath<t.JSXIdentifier>;
let dependency:
| NodePath<t.MemberExpression>
| NodePath<t.JSXMemberExpression>
| NodePath<t.Identifier>
| NodePath<t.JSXIdentifier>;
if (path.isJSXOpeningElement()) {
const name = path.get("name");
if (!(name.isJSXMemberExpression() || name.isJSXIdentifier())) {
return;
}
let current: NodePath<t.JSXMemberExpression | t.JSXIdentifier> = name;
while (current.isJSXMemberExpression()) {
current = current.get("object");
}
invariant(
current.isJSXIdentifier(),
"Invalid logic in gatherCapturedDeps"
);
baseIdentifier = current;
let currentDep:
| NodePath<t.JSXMemberExpression>
| NodePath<t.Identifier>
| NodePath<t.JSXIdentifier> = baseIdentifier;
while (true) {
const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
if (nextDep && nextDep.isJSXMemberExpression()) {
currentDep = nextDep;
} else {
break;
}
}
dependency = currentDep;
} else if (path.isMemberExpression()) {
let currentId: NodePath<Expression> = path;
while (currentId.isMemberExpression()) {
currentId = currentId.get("object");
}
if (!currentId.isIdentifier()) {
return;
}
baseIdentifier = currentId;
let currentDep:
| NodePath<t.MemberExpression>
| NodePath<t.Identifier>
| NodePath<t.JSXIdentifier> = baseIdentifier;
while (true) {
const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
if (
nextDep &&
nextDep.isMemberExpression() &&
isValidDependency(nextDep)
) {
currentDep = nextDep;
} else {
break;
}
}
dependency = currentDep;
} else {
baseIdentifier = path;
dependency = path;
}
dependency.skip();
const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
if (binding === undefined || !pureScopes.has(binding.scope)) {
return;
}
const idKey = String(addCapturedId(binding.identifier));
let exprKey = idKey;
if (dependency.isMemberExpression()) {
let pathTokens = [];
let current: NodePath<Expression> = dependency;
while (current.isMemberExpression()) {
const property = current.get("property") as NodePath<t.Identifier>;
pathTokens.push(property.node.name);
current = current.get("object");
}
exprKey += "." + pathTokens.reverse().join(".");
} else if (dependency.isJSXMemberExpression()) {
let pathTokens = [];
let current: NodePath<t.JSXMemberExpression | t.JSXIdentifier> =
dependency;
while (current.isJSXMemberExpression()) {
const property = current.get("property");
pathTokens.push(property.node.name);
current = current.get("object");
}
}
if (!seenPaths.has(exprKey)) {
let loweredDep: Place;
if (dependency.isJSXIdentifier()) {
loweredDep = lowerValueToTemporary(builder, {
kind: "LoadLocal",
place: lowerIdentifier(builder, dependency),
loc: path.node.loc ?? GeneratedSource,
});
} else if (dependency.isJSXMemberExpression()) {
loweredDep = lowerJsxMemberExpression(builder, dependency);
} else {
loweredDep = lowerExpressionToTemporary(builder, dependency);
}
capturedRefs.add(loweredDep);
seenPaths.add(exprKey);
}
}
fn.traverse({
TypeAnnotation(path) {
path.skip();
},
TSTypeAnnotation(path) {
path.skip();
},
TypeAlias(path) {
path.skip();
},
TSTypeAliasDeclaration(path) {
path.skip();
},
Expression(path) {
if (path.isAssignmentExpression()) {
const left = path.get("left");
if (left.isIdentifier()) {
handleMaybeDependency(left);
}
return;
} else if (path.isJSXElement()) {
handleMaybeDependency(path.get("openingElement"));
} else if (path.isMemberExpression() || path.isIdentifier()) {
handleMaybeDependency(path);
}
},
});
return { identifiers: [...capturedIds.keys()], refs: [...capturedRefs] };
}
function notNull<T>(value: T | null): value is T {
return value !== null;
}
export function lowerType(node: t.FlowType | t.TSType): Type {
switch (node.type) {
case "GenericTypeAnnotation": {
const id = node.id;
if (id.type === "Identifier" && id.name === "Array") {
return { kind: "Object", shapeId: BuiltInArrayId };
}
return makeType();
}
case "TSTypeReference": {
const typeName = node.typeName;
if (typeName.type === "Identifier" && typeName.name === "Array") {
return { kind: "Object", shapeId: BuiltInArrayId };
}
return makeType();
}
case "ArrayTypeAnnotation":
case "TSArrayType": {
return { kind: "Object", shapeId: BuiltInArrayId };
}
case "BooleanLiteralTypeAnnotation":
case "BooleanTypeAnnotation":
case "NullLiteralTypeAnnotation":
case "NumberLiteralTypeAnnotation":
case "NumberTypeAnnotation":
case "StringLiteralTypeAnnotation":
case "StringTypeAnnotation":
case "TSBooleanKeyword":
case "TSNullKeyword":
case "TSNumberKeyword":
case "TSStringKeyword":
case "TSSymbolKeyword":
case "TSUndefinedKeyword":
case "TSVoidKeyword":
case "VoidTypeAnnotation": {
return { kind: "Primitive" };
}
default: {
return makeType();
}
}
}