mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
faf092eb39
(needs cleanup) - Scopes no longer store a flat list of their dependencies. Instead: - Scope terminals are effectively a `goto` for scope dependency instructions (represented as value blocks that terminate with a `goto scopeBlock` for HIR and a series of ReactiveInstructions for ReactiveIR) - Scopes themselves store `dependencies: Array<Place>`, which refer to temporaries written to by scope dependency instructions Next steps: - new pass to dedupe scope dependency instructions after all dependency and scope pruning passes, effectively 'hoisting' dependencies out - more complex dependencies (unary ops like `Boolean` or `Not`, binary ops like `!==` or logical operators)
1553 lines
47 KiB
TypeScript
1553 lines
47 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 {CompilerError} from '../CompilerError';
|
|
import {
|
|
BasicBlock,
|
|
BlockId,
|
|
GotoVariant,
|
|
HIR,
|
|
InstructionId,
|
|
Place,
|
|
ReactiveBlock,
|
|
SourceLocation,
|
|
} from '../HIR';
|
|
import {
|
|
areEqualPaths,
|
|
GeneratedSource,
|
|
HIRFunction,
|
|
PrunedReactiveScopeBlock,
|
|
ReactiveBreakTerminal,
|
|
ReactiveContinueTerminal,
|
|
ReactiveFunction,
|
|
ReactiveInstructionStatement,
|
|
ReactiveLogicalValue,
|
|
ReactiveScopeBlock,
|
|
ReactiveSequenceValue,
|
|
ReactiveTerminalStatement,
|
|
ReactiveTerminalTargetKind,
|
|
ReactiveTernaryValue,
|
|
ReactiveValue,
|
|
Terminal,
|
|
} from '../HIR/HIR';
|
|
import {
|
|
readScopeDependencies,
|
|
readScopeDependenciesRHIR,
|
|
} from '../HIR/ScopeDependencyUtils';
|
|
import {assertExhaustive} from '../Utils/utils';
|
|
import {printDependency} from './PrintReactiveFunction';
|
|
|
|
/*
|
|
* Converts from HIR (lower-level CFG) to ReactiveFunction, a tree representation
|
|
* that is closer to an AST. This pass restores the original control flow constructs,
|
|
* including break/continue to labeled statements. Note that this pass naively emits
|
|
* labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels.
|
|
*/
|
|
export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
|
|
const cx = new Context(fn);
|
|
const driver = new Driver(cx);
|
|
const body = driver.traverseBlock(cx.block(fn.body.entry));
|
|
return {
|
|
loc: fn.loc,
|
|
id: fn.id,
|
|
params: fn.params,
|
|
generator: fn.generator,
|
|
async: fn.async,
|
|
body,
|
|
env: fn.env,
|
|
directives: fn.directives,
|
|
};
|
|
}
|
|
|
|
class Driver {
|
|
cx: Context;
|
|
|
|
constructor(cx: Context) {
|
|
this.cx = cx;
|
|
}
|
|
|
|
traverseBlock(block: BasicBlock): ReactiveBlock {
|
|
const blockValue: ReactiveBlock = [];
|
|
this.visitBlock(block, blockValue);
|
|
return blockValue;
|
|
}
|
|
|
|
visitBlock(block: BasicBlock, blockValue: ReactiveBlock): void {
|
|
CompilerError.invariant(!this.cx.emitted.has(block.id), {
|
|
reason: `Cannot emit the same block twice: bb${block.id}`,
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
this.cx.emitted.add(block.id);
|
|
for (const instruction of block.instructions) {
|
|
blockValue.push({
|
|
kind: 'instruction',
|
|
instruction,
|
|
});
|
|
}
|
|
|
|
const terminal = block.terminal;
|
|
const scheduleIds = [];
|
|
switch (terminal.kind) {
|
|
case 'return': {
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'return',
|
|
loc: terminal.loc,
|
|
value: terminal.value,
|
|
id: terminal.id,
|
|
},
|
|
label: null,
|
|
});
|
|
break;
|
|
}
|
|
case 'throw': {
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'throw',
|
|
loc: terminal.loc,
|
|
value: terminal.value,
|
|
id: terminal.id,
|
|
},
|
|
label: null,
|
|
});
|
|
break;
|
|
}
|
|
case 'if': {
|
|
const fallthroughId =
|
|
this.cx.reachable(terminal.fallthrough) &&
|
|
!this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
const alternateId =
|
|
terminal.alternate !== terminal.fallthrough
|
|
? terminal.alternate
|
|
: null;
|
|
|
|
if (fallthroughId !== null) {
|
|
const scheduleId = this.cx.schedule(fallthroughId, 'if');
|
|
scheduleIds.push(scheduleId);
|
|
}
|
|
|
|
let consequent: ReactiveBlock | null = null;
|
|
if (this.cx.isScheduled(terminal.consequent)) {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'if' where the consequent is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
} else {
|
|
consequent = this.traverseBlock(
|
|
this.cx.ir.blocks.get(terminal.consequent)!,
|
|
);
|
|
}
|
|
|
|
let alternate: ReactiveBlock | null = null;
|
|
if (alternateId !== null) {
|
|
if (this.cx.isScheduled(alternateId)) {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'if' where the alternate is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
} else {
|
|
alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
|
|
}
|
|
}
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'if',
|
|
loc: terminal.loc,
|
|
test: terminal.test,
|
|
consequent: consequent ?? this.emptyBlock(),
|
|
alternate: alternate,
|
|
id: terminal.id,
|
|
},
|
|
label:
|
|
fallthroughId == null
|
|
? null
|
|
: {
|
|
id: fallthroughId,
|
|
implicit: false,
|
|
},
|
|
});
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'switch': {
|
|
const fallthroughId =
|
|
this.cx.reachable(terminal.fallthrough) &&
|
|
!this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
if (fallthroughId !== null) {
|
|
const scheduleId = this.cx.schedule(fallthroughId, 'switch');
|
|
scheduleIds.push(scheduleId);
|
|
}
|
|
|
|
const cases: Array<{
|
|
test: Place | null;
|
|
block: ReactiveBlock;
|
|
}> = [];
|
|
[...terminal.cases].reverse().forEach((case_, _index) => {
|
|
const test = case_.test;
|
|
|
|
let consequent: ReactiveBlock;
|
|
if (this.cx.isScheduled(case_.block)) {
|
|
CompilerError.invariant(case_.block === terminal.fallthrough, {
|
|
reason: `Unexpected 'switch' where a case is already scheduled and block is not the fallthrough`,
|
|
loc: terminal.loc,
|
|
});
|
|
return;
|
|
} else {
|
|
consequent = this.traverseBlock(
|
|
this.cx.ir.blocks.get(case_.block)!,
|
|
);
|
|
const scheduleId = this.cx.schedule(case_.block, 'case');
|
|
scheduleIds.push(scheduleId);
|
|
}
|
|
cases.push({test, block: consequent});
|
|
});
|
|
cases.reverse();
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'switch',
|
|
loc: terminal.loc,
|
|
test: terminal.test,
|
|
cases,
|
|
id: terminal.id,
|
|
},
|
|
label:
|
|
fallthroughId == null
|
|
? null
|
|
: {
|
|
id: fallthroughId,
|
|
implicit: false,
|
|
},
|
|
});
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'do-while': {
|
|
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
const loopId =
|
|
!this.cx.isScheduled(terminal.loop) &&
|
|
terminal.loop !== terminal.fallthrough
|
|
? terminal.loop
|
|
: null;
|
|
const scheduleId = this.cx.scheduleLoop(
|
|
terminal.fallthrough,
|
|
terminal.test,
|
|
terminal.loop,
|
|
);
|
|
scheduleIds.push(scheduleId);
|
|
|
|
let loopBody: ReactiveBlock;
|
|
if (loopId) {
|
|
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
|
|
} else {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'do-while' where the loop is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
}
|
|
|
|
const testValue = this.visitValueBlock(
|
|
terminal.test,
|
|
terminal.loc,
|
|
).value;
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'do-while',
|
|
loc: terminal.loc,
|
|
test: testValue,
|
|
loop: loopBody,
|
|
id: terminal.id,
|
|
},
|
|
label:
|
|
fallthroughId == null
|
|
? null
|
|
: {
|
|
id: fallthroughId,
|
|
implicit: false,
|
|
},
|
|
});
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'while': {
|
|
const fallthroughId =
|
|
this.cx.reachable(terminal.fallthrough) &&
|
|
!this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
const loopId =
|
|
!this.cx.isScheduled(terminal.loop) &&
|
|
terminal.loop !== terminal.fallthrough
|
|
? terminal.loop
|
|
: null;
|
|
const scheduleId = this.cx.scheduleLoop(
|
|
terminal.fallthrough,
|
|
terminal.test,
|
|
terminal.loop,
|
|
);
|
|
scheduleIds.push(scheduleId);
|
|
|
|
const testValue = this.visitValueBlock(
|
|
terminal.test,
|
|
terminal.loc,
|
|
).value;
|
|
|
|
let loopBody: ReactiveBlock;
|
|
if (loopId) {
|
|
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
|
|
} else {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'while' where the loop is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
}
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'while',
|
|
loc: terminal.loc,
|
|
test: testValue,
|
|
loop: loopBody,
|
|
id: terminal.id,
|
|
},
|
|
label:
|
|
fallthroughId == null
|
|
? null
|
|
: {
|
|
id: fallthroughId,
|
|
implicit: false,
|
|
},
|
|
});
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'for': {
|
|
const loopId =
|
|
!this.cx.isScheduled(terminal.loop) &&
|
|
terminal.loop !== terminal.fallthrough
|
|
? terminal.loop
|
|
: null;
|
|
|
|
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
|
|
const scheduleId = this.cx.scheduleLoop(
|
|
terminal.fallthrough,
|
|
terminal.update ?? terminal.test,
|
|
terminal.loop,
|
|
);
|
|
scheduleIds.push(scheduleId);
|
|
|
|
const init = this.visitValueBlock(terminal.init, terminal.loc);
|
|
const initBlock = this.cx.ir.blocks.get(init.block)!;
|
|
let initValue = init.value;
|
|
if (initValue.kind === 'SequenceExpression') {
|
|
const last = initBlock.instructions.at(-1)!;
|
|
initValue.instructions.push(last);
|
|
initValue.value = {
|
|
kind: 'Primitive',
|
|
value: undefined,
|
|
loc: terminal.loc,
|
|
};
|
|
} else {
|
|
initValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: [initBlock.instructions.at(-1)!],
|
|
id: terminal.id,
|
|
loc: terminal.loc,
|
|
value: {
|
|
kind: 'Primitive',
|
|
value: undefined,
|
|
loc: terminal.loc,
|
|
},
|
|
};
|
|
}
|
|
|
|
const testValue = this.visitValueBlock(
|
|
terminal.test,
|
|
terminal.loc,
|
|
).value;
|
|
|
|
const updateValue =
|
|
terminal.update !== null
|
|
? this.visitValueBlock(terminal.update, terminal.loc).value
|
|
: null;
|
|
|
|
let loopBody: ReactiveBlock;
|
|
if (loopId) {
|
|
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
|
|
} else {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'for' where the loop is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
}
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'for',
|
|
loc: terminal.loc,
|
|
init: initValue,
|
|
test: testValue,
|
|
update: updateValue,
|
|
loop: loopBody,
|
|
id: terminal.id,
|
|
},
|
|
label:
|
|
fallthroughId == null ? null : {id: fallthroughId, implicit: false},
|
|
});
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'for-of': {
|
|
const loopId =
|
|
!this.cx.isScheduled(terminal.loop) &&
|
|
terminal.loop !== terminal.fallthrough
|
|
? terminal.loop
|
|
: null;
|
|
|
|
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
|
|
const scheduleId = this.cx.scheduleLoop(
|
|
terminal.fallthrough,
|
|
terminal.init,
|
|
terminal.loop,
|
|
);
|
|
scheduleIds.push(scheduleId);
|
|
|
|
const init = this.visitValueBlock(terminal.init, terminal.loc);
|
|
const initBlock = this.cx.ir.blocks.get(init.block)!;
|
|
let initValue = init.value;
|
|
if (initValue.kind === 'SequenceExpression') {
|
|
const last = initBlock.instructions.at(-1)!;
|
|
initValue.instructions.push(last);
|
|
initValue.value = {
|
|
kind: 'Primitive',
|
|
value: undefined,
|
|
loc: terminal.loc,
|
|
};
|
|
} else {
|
|
initValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: [initBlock.instructions.at(-1)!],
|
|
id: terminal.id,
|
|
loc: terminal.loc,
|
|
value: {
|
|
kind: 'Primitive',
|
|
value: undefined,
|
|
loc: terminal.loc,
|
|
},
|
|
};
|
|
}
|
|
|
|
const test = this.visitValueBlock(terminal.test, terminal.loc);
|
|
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
|
let testValue = test.value;
|
|
if (testValue.kind === 'SequenceExpression') {
|
|
const last = testBlock.instructions.at(-1)!;
|
|
testValue.instructions.push(last);
|
|
testValue.value = {
|
|
kind: 'Primitive',
|
|
value: undefined,
|
|
loc: terminal.loc,
|
|
};
|
|
} else {
|
|
testValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: [testBlock.instructions.at(-1)!],
|
|
id: terminal.id,
|
|
loc: terminal.loc,
|
|
value: {
|
|
kind: 'Primitive',
|
|
value: undefined,
|
|
loc: terminal.loc,
|
|
},
|
|
};
|
|
}
|
|
|
|
let loopBody: ReactiveBlock;
|
|
if (loopId) {
|
|
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
|
|
} else {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'for-of' where the loop is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
}
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'for-of',
|
|
loc: terminal.loc,
|
|
init: initValue,
|
|
test: testValue,
|
|
loop: loopBody,
|
|
id: terminal.id,
|
|
},
|
|
label:
|
|
fallthroughId == null ? null : {id: fallthroughId, implicit: false},
|
|
});
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'for-in': {
|
|
const loopId =
|
|
!this.cx.isScheduled(terminal.loop) &&
|
|
terminal.loop !== terminal.fallthrough
|
|
? terminal.loop
|
|
: null;
|
|
|
|
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
|
|
const scheduleId = this.cx.scheduleLoop(
|
|
terminal.fallthrough,
|
|
terminal.init,
|
|
terminal.loop,
|
|
);
|
|
scheduleIds.push(scheduleId);
|
|
|
|
const init = this.visitValueBlock(terminal.init, terminal.loc);
|
|
const initBlock = this.cx.ir.blocks.get(init.block)!;
|
|
let initValue = init.value;
|
|
if (initValue.kind === 'SequenceExpression') {
|
|
const last = initBlock.instructions.at(-1)!;
|
|
initValue.instructions.push(last);
|
|
initValue.value = {
|
|
kind: 'Primitive',
|
|
value: undefined,
|
|
loc: terminal.loc,
|
|
};
|
|
} else {
|
|
initValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: [initBlock.instructions.at(-1)!],
|
|
id: terminal.id,
|
|
loc: terminal.loc,
|
|
value: {
|
|
kind: 'Primitive',
|
|
value: undefined,
|
|
loc: terminal.loc,
|
|
},
|
|
};
|
|
}
|
|
|
|
let loopBody: ReactiveBlock;
|
|
if (loopId) {
|
|
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
|
|
} else {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'for-in' where the loop is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
}
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'for-in',
|
|
loc: terminal.loc,
|
|
init: initValue,
|
|
loop: loopBody,
|
|
id: terminal.id,
|
|
},
|
|
label:
|
|
fallthroughId == null ? null : {id: fallthroughId, implicit: false},
|
|
});
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'branch': {
|
|
let consequent: ReactiveBlock | null = null;
|
|
if (this.cx.isScheduled(terminal.consequent)) {
|
|
const break_ = this.visitBreak(
|
|
terminal.consequent,
|
|
terminal.id,
|
|
terminal.loc,
|
|
);
|
|
if (break_ !== null) {
|
|
consequent = [break_];
|
|
}
|
|
} else {
|
|
consequent = this.traverseBlock(
|
|
this.cx.ir.blocks.get(terminal.consequent)!,
|
|
);
|
|
}
|
|
|
|
let alternate: ReactiveBlock | null = null;
|
|
if (this.cx.isScheduled(terminal.alternate)) {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'branch' where the alternate is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
} else {
|
|
alternate = this.traverseBlock(
|
|
this.cx.ir.blocks.get(terminal.alternate)!,
|
|
);
|
|
}
|
|
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'if',
|
|
loc: terminal.loc,
|
|
test: terminal.test,
|
|
consequent: consequent ?? this.emptyBlock(),
|
|
alternate: alternate,
|
|
id: terminal.id,
|
|
},
|
|
label: null,
|
|
});
|
|
|
|
break;
|
|
}
|
|
case 'label': {
|
|
const fallthroughId =
|
|
this.cx.reachable(terminal.fallthrough) &&
|
|
!this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
if (fallthroughId !== null) {
|
|
const scheduleId = this.cx.schedule(fallthroughId, 'if');
|
|
scheduleIds.push(scheduleId);
|
|
}
|
|
|
|
let block: ReactiveBlock;
|
|
if (this.cx.isScheduled(terminal.block)) {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'label' where the block is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
} else {
|
|
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
|
|
}
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'label',
|
|
loc: terminal.loc,
|
|
block,
|
|
id: terminal.id,
|
|
},
|
|
label:
|
|
fallthroughId == null ? null : {id: fallthroughId, implicit: false},
|
|
});
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case 'sequence':
|
|
case 'optional':
|
|
case 'ternary':
|
|
case 'logical': {
|
|
const fallthroughId =
|
|
terminal.fallthrough !== null &&
|
|
!this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
if (fallthroughId !== null) {
|
|
const scheduleId = this.cx.schedule(fallthroughId, 'if');
|
|
scheduleIds.push(scheduleId);
|
|
}
|
|
|
|
const {place, value} = this.visitValueBlockTerminal(terminal);
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'instruction',
|
|
instruction: {
|
|
id: terminal.id,
|
|
lvalue: place,
|
|
value,
|
|
loc: terminal.loc,
|
|
},
|
|
});
|
|
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'goto': {
|
|
switch (terminal.variant) {
|
|
case GotoVariant.Break: {
|
|
const break_ = this.visitBreak(
|
|
terminal.block,
|
|
terminal.id,
|
|
terminal.loc,
|
|
);
|
|
if (break_ !== null) {
|
|
blockValue.push(break_);
|
|
}
|
|
break;
|
|
}
|
|
case GotoVariant.Continue: {
|
|
const continue_ = this.visitContinue(
|
|
terminal.block,
|
|
terminal.id,
|
|
terminal.loc,
|
|
);
|
|
if (continue_ !== null) {
|
|
blockValue.push(continue_);
|
|
}
|
|
break;
|
|
}
|
|
case GotoVariant.Try: {
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(
|
|
terminal.variant,
|
|
`Unexpected goto variant \`${terminal.variant}\``,
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'maybe-throw': {
|
|
/*
|
|
* ReactiveFunction does not explicit model maybe-throw semantics,
|
|
* so these terminals flatten away
|
|
*/
|
|
if (!this.cx.isScheduled(terminal.continuation)) {
|
|
this.visitBlock(
|
|
this.cx.ir.blocks.get(terminal.continuation)!,
|
|
blockValue,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case 'try': {
|
|
const fallthroughId =
|
|
this.cx.reachable(terminal.fallthrough) &&
|
|
!this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
if (fallthroughId !== null) {
|
|
const scheduleId = this.cx.schedule(fallthroughId, 'if');
|
|
scheduleIds.push(scheduleId);
|
|
}
|
|
this.cx.scheduleCatchHandler(terminal.handler);
|
|
|
|
const block = this.traverseBlock(
|
|
this.cx.ir.blocks.get(terminal.block)!,
|
|
);
|
|
const handler = this.traverseBlock(
|
|
this.cx.ir.blocks.get(terminal.handler)!,
|
|
);
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
blockValue.push({
|
|
kind: 'terminal',
|
|
label:
|
|
fallthroughId == null ? null : {id: fallthroughId, implicit: false},
|
|
terminal: {
|
|
kind: 'try',
|
|
loc: terminal.loc,
|
|
block,
|
|
handlerBinding: terminal.handlerBinding,
|
|
handler,
|
|
id: terminal.id,
|
|
},
|
|
});
|
|
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
break;
|
|
}
|
|
case 'pruned-scope':
|
|
case 'scope': {
|
|
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
|
|
? terminal.fallthrough
|
|
: null;
|
|
if (fallthroughId !== null) {
|
|
const scheduleId = this.cx.schedule(fallthroughId, 'if');
|
|
scheduleIds.push(scheduleId);
|
|
this.cx.scopeFallthroughs.add(fallthroughId);
|
|
}
|
|
|
|
let block: ReactiveBlock;
|
|
if (this.cx.isScheduled(terminal.block)) {
|
|
CompilerError.invariant(false, {
|
|
reason: `Unexpected 'scope' where the block is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
} else {
|
|
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
|
|
}
|
|
{
|
|
const scheduleId = this.cx.schedule(terminal.block, 'if');
|
|
scheduleIds.push(scheduleId);
|
|
this.cx.scopeFallthroughs.add(terminal.block);
|
|
}
|
|
CompilerError.invariant(!this.cx.isScheduled(terminal.dependencies), {
|
|
reason: `Unexpected 'scope' where the dependencies block is already scheduled`,
|
|
loc: terminal.loc,
|
|
});
|
|
const dependencies: Array<ReactiveInstructionStatement> =
|
|
this.traverseBlock(this.cx.ir.blocks.get(terminal.dependencies)!).map(
|
|
dep => {
|
|
CompilerError.invariant(dep.kind === 'instruction', {
|
|
reason: '[BuildReactiveFunction] Expected reactive instruction',
|
|
loc: GeneratedSource,
|
|
});
|
|
return dep;
|
|
},
|
|
);
|
|
|
|
this.cx.unscheduleAll(scheduleIds);
|
|
const scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock = {
|
|
kind: terminal.kind,
|
|
dependencyInstructions: dependencies,
|
|
instructions: block,
|
|
scope: terminal.scope,
|
|
};
|
|
blockValue.push(scopeBlock);
|
|
if (fallthroughId !== null) {
|
|
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
|
}
|
|
|
|
/**
|
|
* Sanity check: check that dependencies stay the same after converting
|
|
* to ReactiveFunction.
|
|
*/
|
|
const hirDeps = readScopeDependencies(this.cx.fn, terminal.scope.id);
|
|
const rhirDeps = [...readScopeDependenciesRHIR(scopeBlock)];
|
|
CompilerError.invariant(hirDeps.size === rhirDeps.length, {
|
|
reason: `[BuildReactiveFunction] Sanity check failed: mismatch in dependencies count`,
|
|
loc: terminal.loc,
|
|
});
|
|
for (const hirDep of hirDeps) {
|
|
CompilerError.invariant(
|
|
rhirDeps.some(
|
|
([, rhirDep]) =>
|
|
hirDep.identifier === rhirDep.identifier &&
|
|
areEqualPaths(hirDep.path, rhirDep.path),
|
|
),
|
|
{
|
|
reason:
|
|
'[BuildReactiveFunction] Sanity check failed: mismatching scope dependencies',
|
|
description: `No match found for ${printDependency(hirDep)}. Candidates are ${rhirDeps.map(dep => printDependency(dep[1]))}`,
|
|
loc: GeneratedSource,
|
|
},
|
|
);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case 'unreachable': {
|
|
// noop
|
|
break;
|
|
}
|
|
case 'unsupported': {
|
|
CompilerError.invariant(false, {
|
|
reason: 'Unexpected unsupported terminal',
|
|
description: null,
|
|
loc: terminal.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
default: {
|
|
assertExhaustive(terminal, 'Unexpected terminal');
|
|
}
|
|
}
|
|
}
|
|
|
|
visitValueBlock(
|
|
id: BlockId,
|
|
loc: SourceLocation,
|
|
): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
|
|
const defaultBlock = this.cx.ir.blocks.get(id)!;
|
|
if (defaultBlock.terminal.kind === 'branch') {
|
|
const instructions = defaultBlock.instructions;
|
|
if (instructions.length === 0) {
|
|
return {
|
|
block: defaultBlock.id,
|
|
place: defaultBlock.terminal.test,
|
|
value: {
|
|
kind: 'LoadLocal',
|
|
place: defaultBlock.terminal.test,
|
|
loc: defaultBlock.terminal.test.loc,
|
|
},
|
|
id: defaultBlock.terminal.id,
|
|
};
|
|
} else if (defaultBlock.instructions.length === 1) {
|
|
const instr = defaultBlock.instructions[0]!;
|
|
CompilerError.invariant(
|
|
instr.lvalue.identifier.id ===
|
|
defaultBlock.terminal.test.identifier.id,
|
|
{
|
|
reason:
|
|
'Expected branch block to end in an instruction that sets the test value',
|
|
description: null,
|
|
loc: instr.lvalue.loc,
|
|
suggestions: null,
|
|
},
|
|
);
|
|
return {
|
|
block: defaultBlock.id,
|
|
place: instr.lvalue!,
|
|
value: instr.value,
|
|
id: instr.id,
|
|
};
|
|
} else {
|
|
const instr = defaultBlock.instructions.at(-1)!;
|
|
const sequence: ReactiveSequenceValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: defaultBlock.instructions.slice(0, -1),
|
|
id: instr.id,
|
|
value: instr.value,
|
|
loc: loc,
|
|
};
|
|
return {
|
|
block: defaultBlock.id,
|
|
place: defaultBlock.terminal.test,
|
|
value: sequence,
|
|
id: defaultBlock.terminal.id,
|
|
};
|
|
}
|
|
} else if (defaultBlock.terminal.kind === 'goto') {
|
|
const instructions = defaultBlock.instructions;
|
|
if (instructions.length === 0) {
|
|
CompilerError.invariant(false, {
|
|
reason: 'Expected goto value block to have at least one instruction',
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
} else if (defaultBlock.instructions.length === 1) {
|
|
const instr = defaultBlock.instructions[0]!;
|
|
let place: Place = instr.lvalue;
|
|
let value: ReactiveValue = instr.value;
|
|
if (
|
|
/*
|
|
* Value blocks generally end in a StoreLocal to assign the value of the
|
|
* expression for this branch. These StoreLocal instructions can be pruned,
|
|
* since we represent the value blocks as a compund value in ReactiveFunction
|
|
* (no phis). However, it's also possible to have a value block that ends in
|
|
* an AssignmentExpression, which we need to keep. So we only prune
|
|
* StoreLocal for temporaries — any named/promoted values must be used
|
|
* elsewhere and aren't safe to prune.
|
|
*/
|
|
value.kind === 'StoreLocal' &&
|
|
value.lvalue.place.identifier.name === null
|
|
) {
|
|
place = value.lvalue.place;
|
|
value = {
|
|
kind: 'LoadLocal',
|
|
place: value.value,
|
|
loc: value.value.loc,
|
|
};
|
|
}
|
|
return {
|
|
block: defaultBlock.id,
|
|
place,
|
|
value,
|
|
id: instr.id,
|
|
};
|
|
} else {
|
|
const instr = defaultBlock.instructions.at(-1)!;
|
|
let place: Place = instr.lvalue;
|
|
let value: ReactiveValue = instr.value;
|
|
if (
|
|
/*
|
|
* Value blocks generally end in a StoreLocal to assign the value of the
|
|
* expression for this branch. These StoreLocal instructions can be pruned,
|
|
* since we represent the value blocks as a compund value in ReactiveFunction
|
|
* (no phis). However, it's also possible to have a value block that ends in
|
|
* an AssignmentExpression, which we need to keep. So we only prune
|
|
* StoreLocal for temporaries — any named/promoted values must be used
|
|
* elsewhere and aren't safe to prune.
|
|
*/
|
|
value.kind === 'StoreLocal' &&
|
|
value.lvalue.place.identifier.name === null
|
|
) {
|
|
place = value.lvalue.place;
|
|
value = {
|
|
kind: 'LoadLocal',
|
|
place: value.value,
|
|
loc: value.value.loc,
|
|
};
|
|
}
|
|
const sequence: ReactiveSequenceValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: defaultBlock.instructions.slice(0, -1),
|
|
id: instr.id,
|
|
value,
|
|
loc: loc,
|
|
};
|
|
return {
|
|
block: defaultBlock.id,
|
|
place,
|
|
value: sequence,
|
|
id: instr.id,
|
|
};
|
|
}
|
|
} else {
|
|
/*
|
|
* The value block ended in a value terminal, recurse to get the value
|
|
* of that terminal
|
|
*/
|
|
const init = this.visitValueBlockTerminal(defaultBlock.terminal);
|
|
// Code following the logical terminal
|
|
const final = this.visitValueBlock(init.fallthrough, loc);
|
|
// Stitch the two together...
|
|
const sequence: ReactiveSequenceValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: [
|
|
...defaultBlock.instructions,
|
|
{
|
|
id: init.id,
|
|
loc,
|
|
lvalue: init.place,
|
|
value: init.value,
|
|
},
|
|
],
|
|
id: final.id,
|
|
value: final.value,
|
|
loc,
|
|
};
|
|
return {
|
|
block: init.fallthrough,
|
|
value: sequence,
|
|
place: final.place,
|
|
id: final.id,
|
|
};
|
|
}
|
|
}
|
|
|
|
visitValueBlockTerminal(terminal: Terminal): {
|
|
value: ReactiveValue;
|
|
place: Place;
|
|
fallthrough: BlockId;
|
|
id: InstructionId;
|
|
} {
|
|
switch (terminal.kind) {
|
|
case 'sequence': {
|
|
const block = this.visitValueBlock(terminal.block, terminal.loc);
|
|
return {
|
|
value: block.value,
|
|
place: block.place,
|
|
fallthrough: terminal.fallthrough,
|
|
id: terminal.id,
|
|
};
|
|
}
|
|
case 'optional': {
|
|
const test = this.visitValueBlock(terminal.test, terminal.loc);
|
|
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
|
if (testBlock.terminal.kind !== 'branch') {
|
|
CompilerError.throwTodo({
|
|
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional test block`,
|
|
description: null,
|
|
loc: testBlock.terminal.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
const consequent = this.visitValueBlock(
|
|
testBlock.terminal.consequent,
|
|
terminal.loc,
|
|
);
|
|
const call: ReactiveSequenceValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: [
|
|
{
|
|
id: test.id,
|
|
loc: testBlock.terminal.loc,
|
|
lvalue: test.place,
|
|
value: test.value,
|
|
},
|
|
],
|
|
id: consequent.id,
|
|
value: consequent.value,
|
|
loc: terminal.loc,
|
|
};
|
|
return {
|
|
place: {...consequent.place},
|
|
value: {
|
|
kind: 'OptionalExpression',
|
|
optional: terminal.optional,
|
|
value: call,
|
|
id: terminal.id,
|
|
loc: terminal.loc,
|
|
},
|
|
fallthrough: terminal.fallthrough,
|
|
id: terminal.id,
|
|
};
|
|
}
|
|
case 'logical': {
|
|
const test = this.visitValueBlock(terminal.test, terminal.loc);
|
|
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
|
if (testBlock.terminal.kind !== 'branch') {
|
|
CompilerError.throwTodo({
|
|
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for logical test block`,
|
|
description: null,
|
|
loc: testBlock.terminal.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
|
|
const leftFinal = this.visitValueBlock(
|
|
testBlock.terminal.consequent,
|
|
terminal.loc,
|
|
);
|
|
const left: ReactiveSequenceValue = {
|
|
kind: 'SequenceExpression',
|
|
instructions: [
|
|
{
|
|
id: test.id,
|
|
loc: terminal.loc,
|
|
lvalue: test.place,
|
|
value: test.value,
|
|
},
|
|
],
|
|
id: leftFinal.id,
|
|
value: leftFinal.value,
|
|
loc: terminal.loc,
|
|
};
|
|
const right = this.visitValueBlock(
|
|
testBlock.terminal.alternate,
|
|
terminal.loc,
|
|
);
|
|
const value: ReactiveLogicalValue = {
|
|
kind: 'LogicalExpression',
|
|
operator: terminal.operator,
|
|
left: left,
|
|
right: right.value,
|
|
loc: terminal.loc,
|
|
};
|
|
return {
|
|
place: {...leftFinal.place},
|
|
value,
|
|
fallthrough: terminal.fallthrough,
|
|
id: terminal.id,
|
|
};
|
|
}
|
|
case 'ternary': {
|
|
const test = this.visitValueBlock(terminal.test, terminal.loc);
|
|
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
|
if (testBlock.terminal.kind !== 'branch') {
|
|
CompilerError.throwTodo({
|
|
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ternary test block`,
|
|
description: null,
|
|
loc: testBlock.terminal.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
const consequent = this.visitValueBlock(
|
|
testBlock.terminal.consequent,
|
|
terminal.loc,
|
|
);
|
|
const alternate = this.visitValueBlock(
|
|
testBlock.terminal.alternate,
|
|
terminal.loc,
|
|
);
|
|
const value: ReactiveTernaryValue = {
|
|
kind: 'ConditionalExpression',
|
|
test: test.value,
|
|
consequent: consequent.value,
|
|
alternate: alternate.value,
|
|
loc: terminal.loc,
|
|
};
|
|
|
|
return {
|
|
place: {...consequent.place},
|
|
value,
|
|
fallthrough: terminal.fallthrough,
|
|
id: terminal.id,
|
|
};
|
|
}
|
|
case 'maybe-throw': {
|
|
CompilerError.throwTodo({
|
|
reason: `Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`,
|
|
description: null,
|
|
loc: terminal.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
case 'label': {
|
|
CompilerError.throwTodo({
|
|
reason: `Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc)`,
|
|
description: null,
|
|
loc: terminal.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
default: {
|
|
CompilerError.throwTodo({
|
|
reason: `Support \`${terminal.kind}\` as a value block terminal (conditional, logical, optional chaining, etc)`,
|
|
description: null,
|
|
loc: terminal.loc,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
emptyBlock(): ReactiveBlock {
|
|
return [];
|
|
}
|
|
|
|
visitBreak(
|
|
block: BlockId,
|
|
id: InstructionId,
|
|
loc: SourceLocation,
|
|
): ReactiveTerminalStatement<ReactiveBreakTerminal> | null {
|
|
const target = this.cx.getBreakTarget(block);
|
|
if (target === null) {
|
|
CompilerError.invariant(false, {
|
|
reason: 'Expected a break target',
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
if (this.cx.scopeFallthroughs.has(target.block)) {
|
|
CompilerError.invariant(target.type === 'implicit', {
|
|
reason: 'Expected reactive scope to implicitly break to fallthrough',
|
|
loc,
|
|
});
|
|
return null;
|
|
}
|
|
return {
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'break',
|
|
loc,
|
|
target: target.block,
|
|
id,
|
|
targetKind: target.type,
|
|
},
|
|
label: null,
|
|
};
|
|
}
|
|
|
|
visitContinue(
|
|
block: BlockId,
|
|
id: InstructionId,
|
|
loc: SourceLocation,
|
|
): ReactiveTerminalStatement<ReactiveContinueTerminal> {
|
|
const target = this.cx.getContinueTarget(block);
|
|
CompilerError.invariant(target !== null, {
|
|
reason: `Expected continue target to be scheduled for bb${block}`,
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
|
|
return {
|
|
kind: 'terminal',
|
|
terminal: {
|
|
kind: 'continue',
|
|
loc,
|
|
target: target.block,
|
|
id,
|
|
targetKind: target.type,
|
|
},
|
|
label: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
class Context {
|
|
fn: HIRFunction;
|
|
ir: HIR;
|
|
#nextScheduleId: number = 0;
|
|
|
|
/*
|
|
* Used to track which blocks *have been* generated already in order to
|
|
* abort if a block is generated a second time. This is an error catching
|
|
* mechanism for debugging purposes, and is not used by the codegen algorithm
|
|
* to drive decisions about how to emit blocks.
|
|
*/
|
|
emitted: Set<BlockId> = new Set();
|
|
|
|
scopeFallthroughs: Set<BlockId> = new Set();
|
|
/*
|
|
* A set of blocks that are already scheduled to be emitted by eg a parent.
|
|
* This allows child nodes to avoid re-emitting the same block and emit eg
|
|
* a break instead.
|
|
*/
|
|
#scheduled: Set<BlockId> = new Set();
|
|
|
|
#catchHandlers: Set<BlockId> = new Set();
|
|
|
|
/*
|
|
* Represents which control flow operations are currently in scope, with the innermost
|
|
* scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
|
|
* control will implicitly transfer, such that gotos to that block can be elided. Gotos
|
|
* targeting items higher up the stack may need labeled break or continue; see
|
|
* getBreakTarget() and getContinueTarget() for more details.
|
|
*/
|
|
#controlFlowStack: Array<ControlFlowTarget> = [];
|
|
|
|
constructor(fn: HIRFunction) {
|
|
this.fn = fn;
|
|
this.ir = fn.body;
|
|
}
|
|
|
|
block(id: BlockId): BasicBlock {
|
|
return this.ir.blocks.get(id)!;
|
|
}
|
|
|
|
scheduleCatchHandler(block: BlockId): void {
|
|
this.#catchHandlers.add(block);
|
|
}
|
|
|
|
reachable(id: BlockId): boolean {
|
|
const block = this.ir.blocks.get(id)!;
|
|
return block.terminal.kind !== 'unreachable';
|
|
}
|
|
|
|
/*
|
|
* Record that the given block will be emitted (eg by the codegen of a parent node)
|
|
* so that child nodes can avoid re-emitting it.
|
|
*/
|
|
schedule(block: BlockId, type: 'if' | 'switch' | 'case'): number {
|
|
const id = this.#nextScheduleId++;
|
|
CompilerError.invariant(!this.#scheduled.has(block), {
|
|
reason: `Break block is already scheduled: bb${block}`,
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
this.#scheduled.add(block);
|
|
this.#controlFlowStack.push({block, id, type});
|
|
return id;
|
|
}
|
|
|
|
scheduleLoop(
|
|
fallthroughBlock: BlockId,
|
|
continueBlock: BlockId,
|
|
loopBlock: BlockId | null,
|
|
): number {
|
|
const id = this.#nextScheduleId++;
|
|
const ownsBlock = !this.#scheduled.has(fallthroughBlock);
|
|
this.#scheduled.add(fallthroughBlock);
|
|
CompilerError.invariant(!this.#scheduled.has(continueBlock), {
|
|
reason: `Continue block is already scheduled: bb${continueBlock}`,
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
this.#scheduled.add(continueBlock);
|
|
let ownsLoop = false;
|
|
if (loopBlock !== null) {
|
|
ownsLoop = !this.#scheduled.has(loopBlock);
|
|
this.#scheduled.add(loopBlock);
|
|
}
|
|
|
|
this.#controlFlowStack.push({
|
|
block: fallthroughBlock,
|
|
ownsBlock,
|
|
id,
|
|
type: 'loop',
|
|
continueBlock,
|
|
loopBlock,
|
|
ownsLoop,
|
|
});
|
|
return id;
|
|
}
|
|
|
|
// Removes a block that was scheduled; must be called after that block is emitted.
|
|
unschedule(scheduleId: number): void {
|
|
const last = this.#controlFlowStack.pop();
|
|
CompilerError.invariant(last !== undefined && last.id === scheduleId, {
|
|
reason: 'Can only unschedule the last target',
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
if (last.type !== 'loop' || last.ownsBlock !== null) {
|
|
this.#scheduled.delete(last.block);
|
|
}
|
|
if (last.type === 'loop') {
|
|
this.#scheduled.delete(last.continueBlock);
|
|
if (last.ownsLoop && last.loopBlock !== null) {
|
|
this.#scheduled.delete(last.loopBlock);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helper to unschedule multiple scheduled blocks. The ids should be in
|
|
* the order in which they were scheduled, ie most recently scheduled last.
|
|
*/
|
|
unscheduleAll(scheduleIds: Array<number>): void {
|
|
for (let i = scheduleIds.length - 1; i >= 0; i--) {
|
|
this.unschedule(scheduleIds[i]!);
|
|
}
|
|
}
|
|
|
|
// Check if the given @param block is scheduled or not.
|
|
isScheduled(block: BlockId): boolean {
|
|
return this.#scheduled.has(block) || this.#catchHandlers.has(block);
|
|
}
|
|
|
|
/*
|
|
* Given the current control flow stack, determines how a `break` to the given @param block
|
|
* must be emitted. Returns as follows:
|
|
* - 'implicit' if control would implicitly transfer to that block
|
|
* - 'labeled' if a labeled break is required to transfer control to that block
|
|
* - 'unlabeled' if an unlabeled break would transfer to that block
|
|
* - null if there is no information for this block
|
|
*
|
|
* The returned 'block' value should be used as the label if necessary.
|
|
*/
|
|
getBreakTarget(block: BlockId): {
|
|
block: BlockId;
|
|
type: ReactiveTerminalTargetKind;
|
|
} {
|
|
let hasPrecedingLoop = false;
|
|
for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
|
|
const target = this.#controlFlowStack[i]!;
|
|
if (target.block === block) {
|
|
let type: ReactiveTerminalTargetKind;
|
|
if (target.type === 'loop') {
|
|
/*
|
|
* breaking out of a loop requires an explicit break,
|
|
* but only requires a label if breaking past the innermost loop.
|
|
*/
|
|
type = hasPrecedingLoop ? 'labeled' : 'unlabeled';
|
|
} else if (i === this.#controlFlowStack.length - 1) {
|
|
/*
|
|
* breaking to the last break point, which is where control will transfer
|
|
* implicitly
|
|
*/
|
|
type = 'implicit';
|
|
} else {
|
|
// breaking somewhere else requires an explicit break
|
|
type = 'labeled';
|
|
}
|
|
return {
|
|
block: target.block,
|
|
type,
|
|
};
|
|
}
|
|
hasPrecedingLoop ||= target.type === 'loop';
|
|
}
|
|
|
|
CompilerError.invariant(false, {
|
|
reason: 'Expected a break target',
|
|
description: null,
|
|
loc: null,
|
|
suggestions: null,
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Given the current control flow stack, determines how a `continue` to the given @param block
|
|
* must be emitted. Returns as follows:
|
|
* - 'implicit' if control would implicitly continue to that block
|
|
* - 'labeled' if a labeled continue is required to continue to that block
|
|
* - 'unlabeled' if an unlabeled continue would transfer to that block
|
|
* - null if there is no information for this block
|
|
*
|
|
* The returned 'block' value should be used as the label if necessary.
|
|
*/
|
|
getContinueTarget(
|
|
block: BlockId,
|
|
): {block: BlockId; type: ReactiveTerminalTargetKind} | null {
|
|
let hasPrecedingLoop = false;
|
|
for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
|
|
const target = this.#controlFlowStack[i]!;
|
|
if (target.type == 'loop' && target.continueBlock === block) {
|
|
let type: ReactiveTerminalTargetKind;
|
|
if (hasPrecedingLoop) {
|
|
/*
|
|
* continuing to a loop that is not the innermost loop always requires
|
|
* a label
|
|
*/
|
|
type = 'labeled';
|
|
} else if (i === this.#controlFlowStack.length - 1) {
|
|
/*
|
|
* continuing to the last break point, which is where control will
|
|
* transfer to naturally
|
|
*/
|
|
type = 'implicit';
|
|
} else {
|
|
/*
|
|
* the continue is inside some conditional logic, requires an explicit
|
|
* continue
|
|
*/
|
|
type = 'unlabeled';
|
|
}
|
|
return {
|
|
block: target.block,
|
|
type,
|
|
};
|
|
}
|
|
hasPrecedingLoop ||= target.type === 'loop';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
debugBreakTargets(): Array<ControlFlowTarget> {
|
|
return this.#controlFlowStack.map(target => ({...target}));
|
|
}
|
|
}
|
|
|
|
type ControlFlowTarget =
|
|
| {type: 'if'; block: BlockId; id: number}
|
|
| {type: 'switch'; block: BlockId; id: number}
|
|
| {type: 'case'; block: BlockId; id: number}
|
|
| {
|
|
type: 'loop';
|
|
block: BlockId;
|
|
ownsBlock: boolean;
|
|
continueBlock: BlockId;
|
|
loopBlock: BlockId | null;
|
|
ownsLoop: boolean;
|
|
id: number;
|
|
};
|