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,
};
promoteTemporary(place.identifier);
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',
returnTypeAnnotation: null,
returnType: makeType(),
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;
}
let kind:
| InstructionKind.Let
| InstructionKind.HoistedConst
| InstructionKind.HoistedLet
| InstructionKind.HoistedFunction;
if (binding.kind === 'const' || binding.kind === 'var') {
kind = InstructionKind.HoistedConst;
} else if (binding.kind === 'let') {
kind = InstructionKind.HoistedLet;
} else if (binding.path.isFunctionDeclaration()) {
kind = InstructionKind.HoistedFunction;
} else 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 {
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,
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,
fallthrough: 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,
fallthrough: conditionalBlock.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,
fallthrough: conditionalBlock.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.Function,
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,
fallthrough: continuationBlock.id,
},
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,
fallthrough: 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 'TSInterfaceDeclaration':
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 '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,
fallthrough: continuationBlock.id,
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,
fallthrough: continuationBlock.id,
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 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 binding = builder.resolveIdentifier(leftExpr);
if (binding.kind === 'Identifier') {
const identifier = lowerIdentifier(builder, leftExpr);
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};
}
} else {
const temporary = lowerValueToTemporary(builder, {
kind: 'StoreGlobal',
name: leftExpr.node.name,
value: {...binaryPlace},
loc: exprLoc,
});
return {kind: 'LoadLocal', place: temporary, loc: temporary.loc};
}
}
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});
}
const isFbt =
tag.kind === 'BuiltinTag' && (tag.name === 'fbt' || tag.name === 'fbs');
if (isFbt) {
const tagName = tag.name;
const openingIdentifier = opening.get('name');
const tagIdentifier = openingIdentifier.isJSXIdentifier()
? builder.resolveIdentifier(openingIdentifier)
: null;
if (tagIdentifier != null) {
// This is already checked in builder.resolveIdentifier
CompilerError.invariant(tagIdentifier.kind !== 'Identifier', {
reason: `<${tagName}> tags should be module-level imports`,
loc: openingIdentifier.node.loc ?? GeneratedSource,
description: null,
suggestions: null,
});
}
// see `error.todo-multiple-fbt-plural` fixture for explanation
const fbtLocations = {
enum: new Array<SourceLocation>(),
plural: new Array<SourceLocation>(),
pronoun: new Array<SourceLocation>(),
};
expr.traverse({
JSXClosingElement(path) {
path.skip();
},
JSXNamespacedName(path) {
if (path.node.namespace.name === tagName) {
switch (path.node.name.name) {
case 'enum':
fbtLocations.enum.push(path.node.loc ?? GeneratedSource);
break;
case 'plural':
fbtLocations.plural.push(path.node.loc ?? GeneratedSource);
break;
case 'pronoun':
fbtLocations.pronoun.push(path.node.loc ?? GeneratedSource);
break;
}
}
},
});
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
CompilerError.throwTodo({
reason: `Support <${tagName}> tags with multiple <${tagName}:${name}> values`,
loc: locations.at(-1) ?? GeneratedSource,
description: null,
suggestions: null,
});
}
}
}
/**
* Increment fbt counter before traversing into children, as whitespace
* in jsx text is handled differently for fbt subtrees.
*/
isFbt && builder.fbtDepth++;
const children: Array<Place> = expr
.get('children')
.map(child => lowerJsxElement(builder, child))
.filter(notNull);
isFbt && builder.fbtDepth--;
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.isMemberExpression()) {
const binaryOperator = expr.node.operator === '++' ? '+' : '-';
const leftExpr = argument as NodePath<t.MemberExpression>;
const {object, property, value} = lowerMemberExpression(
builder,
leftExpr,
);
// Store the previous value to a temporary
const previousValuePlace = lowerValueToTemporary(builder, value);
// Store the new value to a temporary
const updatedValue = lowerValueToTemporary(builder, {
kind: 'BinaryExpression',
operator: binaryOperator,
left: {...previousValuePlace},
right: lowerValueToTemporary(builder, {
kind: 'Primitive',
value: 1,
loc: GeneratedSource,
}),
loc: leftExpr.node.loc ?? GeneratedSource,
});
// Save the result back to the property
let newValuePlace;
if (typeof property === 'string') {
newValuePlace = lowerValueToTemporary(builder, {
kind: 'PropertyStore',
object: {...object},
property,
value: {...updatedValue},
loc: leftExpr.node.loc ?? GeneratedSource,
});
} else {
newValuePlace = lowerValueToTemporary(builder, {
kind: 'ComputedStore',
object: {...object},
property: {...property},
value: {...updatedValue},
loc: leftExpr.node.loc ?? GeneratedSource,
});
}
return {
kind: 'LoadLocal',
place: expr.node.prefix
? {...newValuePlace}
: {...previousValuePlace},
loc: exprLoc,
};
}
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,
fallthrough: continuationBlock.id,
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,
fallthrough: continuationBlock.id,
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 'LogicalExpression': {
const logical = expr as NodePath<t.LogicalExpression>;
return (
isReorderableExpression(
builder,
logical.get('left'),
allowLocalIdentifiers,
) &&
isReorderableExpression(
builder,
logical.get('right'),
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()) {
let text: string | null;
if (builder.fbtDepth > 0) {
text = exprPath.node.value;
} else {
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,
type: expr.node.type,
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,
fallthrough: continuationBlock.id,
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();
}
}
}