mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Mechanical PR to migrate existing invariants to use the new CompilerDiagnostic infra @josephsavona added. Will tackle the others at a later time. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34403). * #34409 * #34404 * __->__ #34403
1296 lines
30 KiB
TypeScript
1296 lines
30 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import {assertExhaustive} from '../Utils/utils';
|
|
import {CompilerError} from '..';
|
|
import {
|
|
BasicBlock,
|
|
BlockId,
|
|
Instruction,
|
|
InstructionValue,
|
|
makeInstructionId,
|
|
Pattern,
|
|
Place,
|
|
ReactiveInstruction,
|
|
ReactiveScope,
|
|
ReactiveValue,
|
|
ScopeId,
|
|
SpreadPattern,
|
|
Terminal,
|
|
} from './HIR';
|
|
|
|
export function* eachInstructionLValue(
|
|
instr: ReactiveInstruction,
|
|
): Iterable<Place> {
|
|
if (instr.lvalue !== null) {
|
|
yield instr.lvalue;
|
|
}
|
|
yield* eachInstructionValueLValue(instr.value);
|
|
}
|
|
|
|
export function* eachInstructionValueLValue(
|
|
value: ReactiveValue,
|
|
): Iterable<Place> {
|
|
switch (value.kind) {
|
|
case 'DeclareContext':
|
|
case 'StoreContext':
|
|
case 'DeclareLocal':
|
|
case 'StoreLocal': {
|
|
yield value.lvalue.place;
|
|
break;
|
|
}
|
|
case 'Destructure': {
|
|
yield* eachPatternOperand(value.lvalue.pattern);
|
|
break;
|
|
}
|
|
case 'PostfixUpdate':
|
|
case 'PrefixUpdate': {
|
|
yield value.lvalue;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function* eachInstructionOperand(instr: Instruction): Iterable<Place> {
|
|
yield* eachInstructionValueOperand(instr.value);
|
|
}
|
|
export function* eachInstructionValueOperand(
|
|
instrValue: InstructionValue,
|
|
): Iterable<Place> {
|
|
switch (instrValue.kind) {
|
|
case 'NewExpression':
|
|
case 'CallExpression': {
|
|
yield instrValue.callee;
|
|
yield* eachCallArgument(instrValue.args);
|
|
break;
|
|
}
|
|
case 'BinaryExpression': {
|
|
yield instrValue.left;
|
|
yield instrValue.right;
|
|
break;
|
|
}
|
|
case 'MethodCall': {
|
|
yield instrValue.receiver;
|
|
yield instrValue.property;
|
|
yield* eachCallArgument(instrValue.args);
|
|
break;
|
|
}
|
|
case 'DeclareContext':
|
|
case 'DeclareLocal': {
|
|
break;
|
|
}
|
|
case 'LoadLocal':
|
|
case 'LoadContext': {
|
|
yield instrValue.place;
|
|
break;
|
|
}
|
|
case 'StoreLocal': {
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'StoreContext': {
|
|
yield instrValue.lvalue.place;
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'StoreGlobal': {
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'Destructure': {
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'PropertyLoad': {
|
|
yield instrValue.object;
|
|
break;
|
|
}
|
|
case 'PropertyDelete': {
|
|
yield instrValue.object;
|
|
break;
|
|
}
|
|
case 'PropertyStore': {
|
|
yield instrValue.object;
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'ComputedLoad': {
|
|
yield instrValue.object;
|
|
yield instrValue.property;
|
|
break;
|
|
}
|
|
case 'ComputedDelete': {
|
|
yield instrValue.object;
|
|
yield instrValue.property;
|
|
break;
|
|
}
|
|
case 'ComputedStore': {
|
|
yield instrValue.object;
|
|
yield instrValue.property;
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'UnaryExpression': {
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'JsxExpression': {
|
|
if (instrValue.tag.kind === 'Identifier') {
|
|
yield instrValue.tag;
|
|
}
|
|
for (const attribute of instrValue.props) {
|
|
switch (attribute.kind) {
|
|
case 'JsxAttribute': {
|
|
yield attribute.place;
|
|
break;
|
|
}
|
|
case 'JsxSpreadAttribute': {
|
|
yield attribute.argument;
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
attribute,
|
|
`Unexpected attribute kind \`${(attribute as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (instrValue.children) {
|
|
yield* instrValue.children;
|
|
}
|
|
break;
|
|
}
|
|
case 'JsxFragment': {
|
|
yield* instrValue.children;
|
|
break;
|
|
}
|
|
case 'ObjectExpression': {
|
|
for (const property of instrValue.properties) {
|
|
if (
|
|
property.kind === 'ObjectProperty' &&
|
|
property.key.kind === 'computed'
|
|
) {
|
|
yield property.key.name;
|
|
}
|
|
yield property.place;
|
|
}
|
|
break;
|
|
}
|
|
case 'ArrayExpression': {
|
|
for (const element of instrValue.elements) {
|
|
if (element.kind === 'Identifier') {
|
|
yield element;
|
|
} else if (element.kind === 'Spread') {
|
|
yield element.place;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'ObjectMethod':
|
|
case 'FunctionExpression': {
|
|
yield* instrValue.loweredFunc.func.context;
|
|
break;
|
|
}
|
|
case 'TaggedTemplateExpression': {
|
|
yield instrValue.tag;
|
|
break;
|
|
}
|
|
case 'TypeCastExpression': {
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'TemplateLiteral': {
|
|
yield* instrValue.subexprs;
|
|
break;
|
|
}
|
|
case 'Await': {
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'GetIterator': {
|
|
yield instrValue.collection;
|
|
break;
|
|
}
|
|
case 'IteratorNext': {
|
|
yield instrValue.iterator;
|
|
yield instrValue.collection;
|
|
break;
|
|
}
|
|
case 'NextPropertyOf': {
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'PostfixUpdate':
|
|
case 'PrefixUpdate': {
|
|
yield instrValue.value;
|
|
break;
|
|
}
|
|
case 'StartMemoize': {
|
|
if (instrValue.deps != null) {
|
|
for (const dep of instrValue.deps) {
|
|
if (dep.root.kind === 'NamedLocal') {
|
|
yield dep.root.value;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'FinishMemoize': {
|
|
yield instrValue.decl;
|
|
break;
|
|
}
|
|
case 'Debugger':
|
|
case 'RegExpLiteral':
|
|
case 'MetaProperty':
|
|
case 'LoadGlobal':
|
|
case 'UnsupportedNode':
|
|
case 'Primitive':
|
|
case 'JSXText': {
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
instrValue,
|
|
`Unexpected instruction kind \`${(instrValue as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function* eachCallArgument(
|
|
args: Array<Place | SpreadPattern>,
|
|
): Iterable<Place> {
|
|
for (const arg of args) {
|
|
if (arg.kind === 'Identifier') {
|
|
yield arg;
|
|
} else {
|
|
yield arg.place;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function doesPatternContainSpreadElement(pattern: Pattern): boolean {
|
|
switch (pattern.kind) {
|
|
case 'ArrayPattern': {
|
|
for (const item of pattern.items) {
|
|
if (item.kind === 'Spread') {
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'ObjectPattern': {
|
|
for (const property of pattern.properties) {
|
|
if (property.kind === 'Spread') {
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
pattern,
|
|
`Unexpected pattern kind \`${(pattern as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function* eachPatternOperand(pattern: Pattern): Iterable<Place> {
|
|
switch (pattern.kind) {
|
|
case 'ArrayPattern': {
|
|
for (const item of pattern.items) {
|
|
if (item.kind === 'Identifier') {
|
|
yield item;
|
|
} else if (item.kind === 'Spread') {
|
|
yield item.place;
|
|
} else if (item.kind === 'Hole') {
|
|
continue;
|
|
} else {
|
|
assertExhaustive(
|
|
item,
|
|
`Unexpected item kind \`${(item as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'ObjectPattern': {
|
|
for (const property of pattern.properties) {
|
|
if (property.kind === 'ObjectProperty') {
|
|
yield property.place;
|
|
} else if (property.kind === 'Spread') {
|
|
yield property.place;
|
|
} else {
|
|
assertExhaustive(
|
|
property,
|
|
`Unexpected item kind \`${(property as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
pattern,
|
|
`Unexpected pattern kind \`${(pattern as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function* eachPatternItem(
|
|
pattern: Pattern,
|
|
): Iterable<Place | SpreadPattern> {
|
|
switch (pattern.kind) {
|
|
case 'ArrayPattern': {
|
|
for (const item of pattern.items) {
|
|
if (item.kind === 'Identifier') {
|
|
yield item;
|
|
} else if (item.kind === 'Spread') {
|
|
yield item;
|
|
} else if (item.kind === 'Hole') {
|
|
continue;
|
|
} else {
|
|
assertExhaustive(
|
|
item,
|
|
`Unexpected item kind \`${(item as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'ObjectPattern': {
|
|
for (const property of pattern.properties) {
|
|
if (property.kind === 'ObjectProperty') {
|
|
yield property.place;
|
|
} else if (property.kind === 'Spread') {
|
|
yield property;
|
|
} else {
|
|
assertExhaustive(
|
|
property,
|
|
`Unexpected item kind \`${(property as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
pattern,
|
|
`Unexpected pattern kind \`${(pattern as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function mapInstructionLValues(
|
|
instr: Instruction,
|
|
fn: (place: Place) => Place,
|
|
): void {
|
|
switch (instr.value.kind) {
|
|
case 'DeclareLocal':
|
|
case 'StoreLocal': {
|
|
const lvalue = instr.value.lvalue;
|
|
lvalue.place = fn(lvalue.place);
|
|
break;
|
|
}
|
|
case 'Destructure': {
|
|
mapPatternOperands(instr.value.lvalue.pattern, fn);
|
|
break;
|
|
}
|
|
case 'PostfixUpdate':
|
|
case 'PrefixUpdate': {
|
|
instr.value.lvalue = fn(instr.value.lvalue);
|
|
break;
|
|
}
|
|
}
|
|
if (instr.lvalue !== null) {
|
|
instr.lvalue = fn(instr.lvalue);
|
|
}
|
|
}
|
|
|
|
export function mapInstructionOperands(
|
|
instr: Instruction,
|
|
fn: (place: Place) => Place,
|
|
): void {
|
|
mapInstructionValueOperands(instr.value, fn);
|
|
}
|
|
|
|
export function mapInstructionValueOperands(
|
|
instrValue: InstructionValue,
|
|
fn: (place: Place) => Place,
|
|
): void {
|
|
switch (instrValue.kind) {
|
|
case 'BinaryExpression': {
|
|
instrValue.left = fn(instrValue.left);
|
|
instrValue.right = fn(instrValue.right);
|
|
break;
|
|
}
|
|
case 'PropertyLoad': {
|
|
instrValue.object = fn(instrValue.object);
|
|
break;
|
|
}
|
|
case 'PropertyDelete': {
|
|
instrValue.object = fn(instrValue.object);
|
|
break;
|
|
}
|
|
case 'PropertyStore': {
|
|
instrValue.object = fn(instrValue.object);
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'ComputedLoad': {
|
|
instrValue.object = fn(instrValue.object);
|
|
instrValue.property = fn(instrValue.property);
|
|
break;
|
|
}
|
|
case 'ComputedDelete': {
|
|
instrValue.object = fn(instrValue.object);
|
|
instrValue.property = fn(instrValue.property);
|
|
break;
|
|
}
|
|
case 'ComputedStore': {
|
|
instrValue.object = fn(instrValue.object);
|
|
instrValue.property = fn(instrValue.property);
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'DeclareContext':
|
|
case 'DeclareLocal': {
|
|
break;
|
|
}
|
|
case 'LoadLocal':
|
|
case 'LoadContext': {
|
|
instrValue.place = fn(instrValue.place);
|
|
break;
|
|
}
|
|
case 'StoreLocal': {
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'StoreContext': {
|
|
instrValue.lvalue.place = fn(instrValue.lvalue.place);
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'StoreGlobal': {
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'Destructure': {
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'NewExpression':
|
|
case 'CallExpression': {
|
|
instrValue.callee = fn(instrValue.callee);
|
|
instrValue.args = mapCallArguments(instrValue.args, fn);
|
|
break;
|
|
}
|
|
case 'MethodCall': {
|
|
instrValue.receiver = fn(instrValue.receiver);
|
|
instrValue.property = fn(instrValue.property);
|
|
instrValue.args = mapCallArguments(instrValue.args, fn);
|
|
break;
|
|
}
|
|
case 'UnaryExpression': {
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'JsxExpression': {
|
|
if (instrValue.tag.kind === 'Identifier') {
|
|
instrValue.tag = fn(instrValue.tag);
|
|
}
|
|
for (const attribute of instrValue.props) {
|
|
switch (attribute.kind) {
|
|
case 'JsxAttribute': {
|
|
attribute.place = fn(attribute.place);
|
|
break;
|
|
}
|
|
case 'JsxSpreadAttribute': {
|
|
attribute.argument = fn(attribute.argument);
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
attribute,
|
|
`Unexpected attribute kind \`${(attribute as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (instrValue.children) {
|
|
instrValue.children = instrValue.children.map(p => fn(p));
|
|
}
|
|
break;
|
|
}
|
|
case 'ObjectExpression': {
|
|
for (const property of instrValue.properties) {
|
|
if (
|
|
property.kind === 'ObjectProperty' &&
|
|
property.key.kind === 'computed'
|
|
) {
|
|
property.key.name = fn(property.key.name);
|
|
}
|
|
property.place = fn(property.place);
|
|
}
|
|
break;
|
|
}
|
|
case 'ArrayExpression': {
|
|
instrValue.elements = instrValue.elements.map(element => {
|
|
if (element.kind === 'Identifier') {
|
|
return fn(element);
|
|
} else if (element.kind === 'Spread') {
|
|
element.place = fn(element.place);
|
|
return element;
|
|
} else {
|
|
return element;
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case 'JsxFragment': {
|
|
instrValue.children = instrValue.children.map(e => fn(e));
|
|
break;
|
|
}
|
|
case 'ObjectMethod':
|
|
case 'FunctionExpression': {
|
|
instrValue.loweredFunc.func.context =
|
|
instrValue.loweredFunc.func.context.map(d => fn(d));
|
|
|
|
break;
|
|
}
|
|
case 'TaggedTemplateExpression': {
|
|
instrValue.tag = fn(instrValue.tag);
|
|
break;
|
|
}
|
|
case 'TypeCastExpression': {
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'TemplateLiteral': {
|
|
instrValue.subexprs = instrValue.subexprs.map(fn);
|
|
break;
|
|
}
|
|
case 'Await': {
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'GetIterator': {
|
|
instrValue.collection = fn(instrValue.collection);
|
|
break;
|
|
}
|
|
case 'IteratorNext': {
|
|
instrValue.iterator = fn(instrValue.iterator);
|
|
instrValue.collection = fn(instrValue.collection);
|
|
break;
|
|
}
|
|
case 'NextPropertyOf': {
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'PostfixUpdate':
|
|
case 'PrefixUpdate': {
|
|
instrValue.value = fn(instrValue.value);
|
|
break;
|
|
}
|
|
case 'StartMemoize': {
|
|
if (instrValue.deps != null) {
|
|
for (const dep of instrValue.deps) {
|
|
if (dep.root.kind === 'NamedLocal') {
|
|
dep.root.value = fn(dep.root.value);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'FinishMemoize': {
|
|
instrValue.decl = fn(instrValue.decl);
|
|
break;
|
|
}
|
|
case 'Debugger':
|
|
case 'RegExpLiteral':
|
|
case 'MetaProperty':
|
|
case 'LoadGlobal':
|
|
case 'UnsupportedNode':
|
|
case 'Primitive':
|
|
case 'JSXText': {
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(instrValue, 'Unexpected instruction kind');
|
|
}
|
|
}
|
|
}
|
|
|
|
export function mapCallArguments(
|
|
args: Array<Place | SpreadPattern>,
|
|
fn: (place: Place) => Place,
|
|
): Array<Place | SpreadPattern> {
|
|
return args.map(arg => {
|
|
if (arg.kind === 'Identifier') {
|
|
return fn(arg);
|
|
} else {
|
|
arg.place = fn(arg.place);
|
|
return arg;
|
|
}
|
|
});
|
|
}
|
|
|
|
export function mapPatternOperands(
|
|
pattern: Pattern,
|
|
fn: (place: Place) => Place,
|
|
): void {
|
|
switch (pattern.kind) {
|
|
case 'ArrayPattern': {
|
|
pattern.items = pattern.items.map(item => {
|
|
if (item.kind === 'Identifier') {
|
|
return fn(item);
|
|
} else if (item.kind === 'Spread') {
|
|
item.place = fn(item.place);
|
|
return item;
|
|
} else {
|
|
return item;
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case 'ObjectPattern': {
|
|
for (const property of pattern.properties) {
|
|
property.place = fn(property.place);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
pattern,
|
|
`Unexpected pattern kind \`${(pattern as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Maps a terminal node's block assignments using the provided function.
|
|
export function mapTerminalSuccessors(
|
|
terminal: Terminal,
|
|
fn: (block: BlockId) => BlockId,
|
|
): Terminal {
|
|
switch (terminal.kind) {
|
|
case 'goto': {
|
|
const target = fn(terminal.block);
|
|
return {
|
|
kind: 'goto',
|
|
block: target,
|
|
variant: terminal.variant,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'if': {
|
|
const consequent = fn(terminal.consequent);
|
|
const alternate = fn(terminal.alternate);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'if',
|
|
test: terminal.test,
|
|
consequent,
|
|
alternate,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'branch': {
|
|
const consequent = fn(terminal.consequent);
|
|
const alternate = fn(terminal.alternate);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'branch',
|
|
test: terminal.test,
|
|
consequent,
|
|
alternate,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'switch': {
|
|
const cases = terminal.cases.map(case_ => {
|
|
const target = fn(case_.block);
|
|
return {
|
|
test: case_.test,
|
|
block: target,
|
|
};
|
|
});
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'switch',
|
|
test: terminal.test,
|
|
cases,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'logical': {
|
|
const test = fn(terminal.test);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'logical',
|
|
test,
|
|
fallthrough,
|
|
operator: terminal.operator,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'ternary': {
|
|
const test = fn(terminal.test);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'ternary',
|
|
test,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'optional': {
|
|
const test = fn(terminal.test);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'optional',
|
|
optional: terminal.optional,
|
|
test,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'return': {
|
|
return {
|
|
kind: 'return',
|
|
returnVariant: terminal.returnVariant,
|
|
loc: terminal.loc,
|
|
value: terminal.value,
|
|
id: makeInstructionId(0),
|
|
effects: terminal.effects,
|
|
};
|
|
}
|
|
case 'throw': {
|
|
return terminal;
|
|
}
|
|
case 'do-while': {
|
|
const loop = fn(terminal.loop);
|
|
const test = fn(terminal.test);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'do-while',
|
|
loc: terminal.loc,
|
|
test,
|
|
loop,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
};
|
|
}
|
|
case 'while': {
|
|
const test = fn(terminal.test);
|
|
const loop = fn(terminal.loop);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'while',
|
|
loc: terminal.loc,
|
|
test,
|
|
loop,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
};
|
|
}
|
|
case 'for': {
|
|
const init = fn(terminal.init);
|
|
const test = fn(terminal.test);
|
|
const update = terminal.update !== null ? fn(terminal.update) : null;
|
|
const loop = fn(terminal.loop);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'for',
|
|
loc: terminal.loc,
|
|
init,
|
|
test,
|
|
update,
|
|
loop,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
};
|
|
}
|
|
case 'for-of': {
|
|
const init = fn(terminal.init);
|
|
const loop = fn(terminal.loop);
|
|
const test = fn(terminal.test);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'for-of',
|
|
loc: terminal.loc,
|
|
init,
|
|
test,
|
|
loop,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
};
|
|
}
|
|
case 'for-in': {
|
|
const init = fn(terminal.init);
|
|
const loop = fn(terminal.loop);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'for-in',
|
|
loc: terminal.loc,
|
|
init,
|
|
loop,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
};
|
|
}
|
|
case 'label': {
|
|
const block = fn(terminal.block);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'label',
|
|
block,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'sequence': {
|
|
const block = fn(terminal.block);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'sequence',
|
|
block,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'maybe-throw': {
|
|
const continuation = fn(terminal.continuation);
|
|
const handler = fn(terminal.handler);
|
|
return {
|
|
kind: 'maybe-throw',
|
|
continuation,
|
|
handler,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
effects: terminal.effects,
|
|
};
|
|
}
|
|
case 'try': {
|
|
const block = fn(terminal.block);
|
|
const handler = fn(terminal.handler);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: 'try',
|
|
block,
|
|
handlerBinding: terminal.handlerBinding,
|
|
handler,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'scope':
|
|
case 'pruned-scope': {
|
|
const block = fn(terminal.block);
|
|
const fallthrough = fn(terminal.fallthrough);
|
|
return {
|
|
kind: terminal.kind,
|
|
scope: terminal.scope,
|
|
block,
|
|
fallthrough,
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
};
|
|
}
|
|
case 'unreachable':
|
|
case 'unsupported': {
|
|
return terminal;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
terminal,
|
|
`Unexpected terminal kind \`${(terminal as any as Terminal).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function terminalHasFallthrough<
|
|
T extends Terminal,
|
|
U extends T & {fallthrough: BlockId},
|
|
>(terminal: T): terminal is U {
|
|
switch (terminal.kind) {
|
|
case 'maybe-throw':
|
|
case 'goto':
|
|
case 'return':
|
|
case 'throw':
|
|
case 'unreachable':
|
|
case 'unsupported': {
|
|
const _: undefined = terminal.fallthrough;
|
|
return false;
|
|
}
|
|
case 'branch':
|
|
case 'try':
|
|
case 'do-while':
|
|
case 'for-of':
|
|
case 'for-in':
|
|
case 'for':
|
|
case 'if':
|
|
case 'label':
|
|
case 'logical':
|
|
case 'optional':
|
|
case 'sequence':
|
|
case 'switch':
|
|
case 'ternary':
|
|
case 'while':
|
|
case 'scope':
|
|
case 'pruned-scope': {
|
|
const _: BlockId = terminal.fallthrough;
|
|
return true;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
terminal,
|
|
`Unexpected terminal kind \`${(terminal as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helper to get a terminal's fallthrough. The main reason to extract this as a helper
|
|
* function is to ensure that we use an exhaustive switch to ensure that we add new terminal
|
|
* variants as appropriate.
|
|
*/
|
|
export function terminalFallthrough(terminal: Terminal): BlockId | null {
|
|
if (terminalHasFallthrough(terminal)) {
|
|
return terminal.fallthrough;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Iterates over the successor block ids of the provided terminal. The function is called
|
|
* specifically for the successors that define the standard control flow, and not
|
|
* pseduo-successors such as fallthroughs.
|
|
*/
|
|
export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
|
|
switch (terminal.kind) {
|
|
case 'goto': {
|
|
yield terminal.block;
|
|
break;
|
|
}
|
|
case 'if': {
|
|
yield terminal.consequent;
|
|
yield terminal.alternate;
|
|
break;
|
|
}
|
|
case 'branch': {
|
|
yield terminal.consequent;
|
|
yield terminal.alternate;
|
|
break;
|
|
}
|
|
case 'switch': {
|
|
for (const case_ of terminal.cases) {
|
|
yield case_.block;
|
|
}
|
|
break;
|
|
}
|
|
case 'optional':
|
|
case 'ternary':
|
|
case 'logical': {
|
|
yield terminal.test;
|
|
break;
|
|
}
|
|
case 'return': {
|
|
break;
|
|
}
|
|
case 'throw': {
|
|
break;
|
|
}
|
|
case 'do-while': {
|
|
yield terminal.loop;
|
|
break;
|
|
}
|
|
case 'while': {
|
|
yield terminal.test;
|
|
break;
|
|
}
|
|
case 'for': {
|
|
yield terminal.init;
|
|
break;
|
|
}
|
|
case 'for-of': {
|
|
yield terminal.init;
|
|
break;
|
|
}
|
|
case 'for-in': {
|
|
yield terminal.init;
|
|
break;
|
|
}
|
|
case 'label': {
|
|
yield terminal.block;
|
|
break;
|
|
}
|
|
case 'sequence': {
|
|
yield terminal.block;
|
|
break;
|
|
}
|
|
case 'maybe-throw': {
|
|
yield terminal.continuation;
|
|
yield terminal.handler;
|
|
break;
|
|
}
|
|
case 'try': {
|
|
yield terminal.block;
|
|
break;
|
|
}
|
|
case 'scope':
|
|
case 'pruned-scope': {
|
|
yield terminal.block;
|
|
break;
|
|
}
|
|
case 'unreachable':
|
|
case 'unsupported':
|
|
break;
|
|
default: {
|
|
assertExhaustive(
|
|
terminal,
|
|
`Unexpected terminal kind \`${(terminal as any as Terminal).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function mapTerminalOperands(
|
|
terminal: Terminal,
|
|
fn: (place: Place) => Place,
|
|
): void {
|
|
switch (terminal.kind) {
|
|
case 'if': {
|
|
terminal.test = fn(terminal.test);
|
|
break;
|
|
}
|
|
case 'branch': {
|
|
terminal.test = fn(terminal.test);
|
|
break;
|
|
}
|
|
case 'switch': {
|
|
terminal.test = fn(terminal.test);
|
|
for (const case_ of terminal.cases) {
|
|
if (case_.test === null) {
|
|
continue;
|
|
}
|
|
case_.test = fn(case_.test);
|
|
}
|
|
break;
|
|
}
|
|
case 'return':
|
|
case 'throw': {
|
|
terminal.value = fn(terminal.value);
|
|
break;
|
|
}
|
|
case 'try': {
|
|
if (terminal.handlerBinding !== null) {
|
|
terminal.handlerBinding = fn(terminal.handlerBinding);
|
|
} else {
|
|
terminal.handlerBinding = null;
|
|
}
|
|
break;
|
|
}
|
|
case 'maybe-throw':
|
|
case 'sequence':
|
|
case 'label':
|
|
case 'optional':
|
|
case 'ternary':
|
|
case 'logical':
|
|
case 'do-while':
|
|
case 'while':
|
|
case 'for':
|
|
case 'for-of':
|
|
case 'for-in':
|
|
case 'goto':
|
|
case 'unreachable':
|
|
case 'unsupported':
|
|
case 'scope':
|
|
case 'pruned-scope': {
|
|
// no-op
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
terminal,
|
|
`Unexpected terminal kind \`${(terminal as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
|
|
switch (terminal.kind) {
|
|
case 'if': {
|
|
yield terminal.test;
|
|
break;
|
|
}
|
|
case 'branch': {
|
|
yield terminal.test;
|
|
break;
|
|
}
|
|
case 'switch': {
|
|
yield terminal.test;
|
|
for (const case_ of terminal.cases) {
|
|
if (case_.test === null) {
|
|
continue;
|
|
}
|
|
yield case_.test;
|
|
}
|
|
break;
|
|
}
|
|
case 'return':
|
|
case 'throw': {
|
|
yield terminal.value;
|
|
break;
|
|
}
|
|
case 'try': {
|
|
if (terminal.handlerBinding !== null) {
|
|
yield terminal.handlerBinding;
|
|
}
|
|
break;
|
|
}
|
|
case 'maybe-throw':
|
|
case 'sequence':
|
|
case 'label':
|
|
case 'optional':
|
|
case 'ternary':
|
|
case 'logical':
|
|
case 'do-while':
|
|
case 'while':
|
|
case 'for':
|
|
case 'for-of':
|
|
case 'for-in':
|
|
case 'goto':
|
|
case 'unreachable':
|
|
case 'unsupported':
|
|
case 'scope':
|
|
case 'pruned-scope': {
|
|
// no-op
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
terminal,
|
|
`Unexpected terminal kind \`${(terminal as any).kind}\``,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper class for traversing scope blocks in HIR-form.
|
|
*/
|
|
export class ScopeBlockTraversal {
|
|
// Live stack of active scopes
|
|
#activeScopes: Array<ScopeId> = [];
|
|
blockInfos: Map<
|
|
BlockId,
|
|
| {
|
|
kind: 'end';
|
|
scope: ReactiveScope;
|
|
pruned: boolean;
|
|
}
|
|
| {
|
|
kind: 'begin';
|
|
scope: ReactiveScope;
|
|
pruned: boolean;
|
|
fallthrough: BlockId;
|
|
}
|
|
> = new Map();
|
|
|
|
recordScopes(block: BasicBlock): void {
|
|
const blockInfo = this.blockInfos.get(block.id);
|
|
if (blockInfo?.kind === 'begin') {
|
|
this.#activeScopes.push(blockInfo.scope.id);
|
|
} else if (blockInfo?.kind === 'end') {
|
|
const top = this.#activeScopes.at(-1);
|
|
CompilerError.invariant(blockInfo.scope.id === top, {
|
|
reason:
|
|
'Expected traversed block fallthrough to match top-most active scope',
|
|
description: null,
|
|
details: [
|
|
{
|
|
kind: 'error',
|
|
loc: block.instructions[0]?.loc ?? block.terminal.id,
|
|
message: null,
|
|
},
|
|
],
|
|
});
|
|
this.#activeScopes.pop();
|
|
}
|
|
|
|
if (
|
|
block.terminal.kind === 'scope' ||
|
|
block.terminal.kind === 'pruned-scope'
|
|
) {
|
|
CompilerError.invariant(
|
|
!this.blockInfos.has(block.terminal.block) &&
|
|
!this.blockInfos.has(block.terminal.fallthrough),
|
|
{
|
|
reason: 'Expected unique scope blocks and fallthroughs',
|
|
description: null,
|
|
details: [
|
|
{
|
|
kind: 'error',
|
|
loc: block.terminal.loc,
|
|
message: null,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
this.blockInfos.set(block.terminal.block, {
|
|
kind: 'begin',
|
|
scope: block.terminal.scope,
|
|
pruned: block.terminal.kind === 'pruned-scope',
|
|
fallthrough: block.terminal.fallthrough,
|
|
});
|
|
this.blockInfos.set(block.terminal.fallthrough, {
|
|
kind: 'end',
|
|
scope: block.terminal.scope,
|
|
pruned: block.terminal.kind === 'pruned-scope',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns if the given scope is currently 'active', i.e. if the scope start
|
|
* block but not the scope fallthrough has been recorded.
|
|
*/
|
|
isScopeActive(scopeId: ScopeId): boolean {
|
|
return this.#activeScopes.indexOf(scopeId) !== -1;
|
|
}
|
|
|
|
/**
|
|
* The current, innermost active scope.
|
|
*/
|
|
get currentScope(): ScopeId | null {
|
|
return this.#activeScopes.at(-1) ?? null;
|
|
}
|
|
}
|