Switch to new BuildReactiveFunction passes

This commit is contained in:
Joe Savona
2023-01-12 11:06:40 -08:00
parent a40ade1e61
commit da21e9c525
9 changed files with 31 additions and 1823 deletions
+2 -6
View File
@@ -5,10 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
type Flags = {
enableNewReactiveFunctionBuilder: boolean;
};
type Flags = {};
export const flags: Flags = {
enableNewReactiveFunctionBuilder: false,
};
export const flags: Flags = {};
+24 -40
View File
@@ -6,7 +6,6 @@
*/
import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import { flags } from "./CompilerFlags";
import {
Environment,
HIRFunction,
@@ -20,10 +19,8 @@ import {
alignReactiveScopesToBlockScopes,
buildReactiveBlocks,
buildReactiveFunction,
buildReactiveFunctionWithoutScopes,
codegenReactiveFunction,
flattenReactiveLoops,
inferReactiveScopes,
inferReactiveScopeVariables,
mergeOverlappingReactiveScopes,
propagateScopeDependencies,
@@ -77,46 +74,33 @@ export function* run(
inferReactiveScopeVariables(hir);
yield log({ kind: "hir", name: "InferReactiveScopeVariables", value: hir });
let reactiveFunction: ReactiveFunction;
if (!flags.enableNewReactiveFunctionBuilder) {
inferReactiveScopes(hir);
yield log({ kind: "hir", name: "InferReactiveScopes", value: hir });
const reactiveFunction = buildReactiveFunction(hir);
yield log({
kind: "reactive",
name: "BuildReactiveFunction",
value: reactiveFunction,
});
reactiveFunction = buildReactiveFunction(hir);
yield log({
kind: "reactive",
name: "BuildReactiveFunction",
value: reactiveFunction,
});
} else {
reactiveFunction = buildReactiveFunctionWithoutScopes(hir);
yield log({
kind: "reactive",
name: "BuildReactiveFunction",
value: reactiveFunction,
});
alignReactiveScopesToBlockScopes(reactiveFunction);
yield log({
kind: "reactive",
name: "AlignReactiveScopesToBlockScopes",
value: reactiveFunction,
});
alignReactiveScopesToBlockScopes(reactiveFunction);
yield log({
kind: "reactive",
name: "AlignReactiveScopesToBlockScopes",
value: reactiveFunction,
});
mergeOverlappingReactiveScopes(reactiveFunction);
yield log({
kind: "reactive",
name: "MergeOverlappingReactiveScopes",
value: reactiveFunction,
});
mergeOverlappingReactiveScopes(reactiveFunction);
yield log({
kind: "reactive",
name: "MergeOverlappingReactiveScopes",
value: reactiveFunction,
});
buildReactiveBlocks(reactiveFunction);
yield log({
kind: "reactive",
name: "BuildReactiveBlocks",
value: reactiveFunction,
});
}
buildReactiveBlocks(reactiveFunction);
yield log({
kind: "reactive",
name: "BuildReactiveBlocks",
value: reactiveFunction,
});
pruneUnusedLabels(reactiveFunction);
yield log({
@@ -1,958 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import invariant from "invariant";
import todo from "../Utils/todo";
import { assertExhaustive } from "../Utils/utils";
import {
BasicBlock,
BlockId,
GotoVariant,
HIR,
HIRFunction,
Instruction,
InstructionId,
InstructionValue,
Place,
SourceLocation,
} from "./HIR";
/**
* Function to visit HIR as a tree of high-level constructs rather than as a sequence
* of lower-level basic blocks. Intended for use in codegen and reactive scope
* construction which need to see the original "shape" of the code.
*
* See the {@link Visitor} interface for more about implementing a visitor.
*/
export function visitTreeForReactiveFunction<
TBlockBuilder,
TBlock,
TInit,
TValueBlock,
TValue,
TStatement,
TCase
>(
fn: HIRFunction,
visitor: Visitor<
TBlockBuilder,
TBlock,
TInit,
TValueBlock,
TValue,
TStatement,
TCase
>
): TBlock {
const cx = new Context(fn.body);
const driver = new Driver(cx, visitor);
return driver.traverseBlock(cx.block(fn.body.entry));
}
class Driver<
TBlockBuilder,
TBlock,
TInit,
TValueBlock,
TValue,
TStatement,
TCase
> {
cx: Context;
visitor: Visitor<
TBlockBuilder,
TBlock,
TInit,
TValueBlock,
TValue,
TStatement,
TCase
>;
constructor(
cx: Context,
visitor: Visitor<
TBlockBuilder,
TBlock,
TInit,
TValueBlock,
TValue,
TStatement,
TCase
>
) {
this.cx = cx;
this.visitor = visitor;
}
traverseBlock(block: BasicBlock): TBlock {
const blockValue = this.visitor.enterBlock();
this.visitBlock(block, blockValue);
return this.visitor.leaveBlock(blockValue);
}
visitBlock(block: BasicBlock, blockValue: TBlockBuilder): void {
invariant(
!this.cx.emitted.has(block.id),
`Cannot emit the same block twice: bb${block.id}`
);
this.cx.emitted.add(block.id);
for (const instr of block.instructions) {
this.visitInstr(instr, blockValue);
}
const terminal = block.terminal;
const scheduleIds = [];
switch (terminal.kind) {
case "return": {
const value =
terminal.value != null
? this.visitPlace(terminal.value, terminal.id)
: null;
this.visitor.visitTerminalId(terminal.id);
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "return",
loc: terminal.loc,
value,
id: terminal.id,
})
);
break;
}
case "throw": {
const value = this.visitPlace(terminal.value, terminal.id);
this.visitor.visitTerminalId(terminal.id);
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "throw",
value,
id: terminal.id,
})
);
break;
}
case "if": {
const test = this.visitPlace(terminal.test, terminal.id);
const fallthroughId =
terminal.fallthrough !== null &&
!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);
}
this.visitor.visitTerminalId(terminal.id);
let consequent: TBlock | null = null;
if (this.cx.isScheduled(terminal.consequent)) {
const break_ = this.visitBreak(terminal.consequent, null);
if (break_ !== null) {
const builder = this.visitor.enterBlock();
this.visitor.appendBlock(builder, break_);
consequent = this.visitor.leaveBlock(builder);
}
} else {
consequent = this.traverseBlock(
this.cx.ir.blocks.get(terminal.consequent)!
);
}
let alternate: TBlock | null = null;
if (alternateId !== null) {
if (this.cx.isScheduled(alternateId)) {
const break_ = this.visitBreak(alternateId, null);
if (break_ !== null) {
const builder = this.visitor.enterBlock();
this.visitor.appendBlock(builder, break_);
alternate = this.visitor.leaveBlock(builder);
}
} else {
alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
}
}
this.cx.unscheduleAll(scheduleIds);
if (fallthroughId !== null) {
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "if",
test,
consequent: consequent ?? this.emptyBlock(),
alternate: alternate,
id: terminal.id,
}),
fallthroughId
);
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
} else {
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "if",
test,
consequent: consequent ?? this.emptyBlock(),
alternate: alternate,
id: terminal.id,
})
);
}
break;
}
case "switch": {
const test = this.visitPlace(terminal.test, terminal.id);
const fallthroughId =
terminal.fallthrough !== null &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
if (fallthroughId !== null) {
const scheduleId = this.cx.schedule(fallthroughId, "switch");
scheduleIds.push(scheduleId);
}
this.visitor.visitTerminalId(terminal.id);
const cases: Array<TCase> = [];
[...terminal.cases].reverse().forEach((case_, index) => {
const test =
case_.test !== null
? this.visitPlace(case_.test, terminal.id)
: null;
let consequent;
if (this.cx.isScheduled(case_.block)) {
// cases which are empty or contain only a `break` may point to blocks
// that are already scheduled. emit as follows:
// - if the block is for another case branch, don't emit a break and fall-through
// - else, emit an explicit break.
const break_ = this.visitBreak(case_.block, null);
if (
index === 0 &&
break_ === null &&
case_.block === terminal.fallthrough &&
case_.test === null
) {
// If the last case statement (first in reverse order) is a default that
// jumps to the fallthrough, then we would emit a useless `default: {}`,
// so instead skip this case.
return;
}
const block = this.visitor.enterBlock();
if (break_ !== null) {
this.visitor.appendBlock(block, break_);
}
consequent = this.visitor.leaveBlock(block);
} else {
consequent = this.traverseBlock(
this.cx.ir.blocks.get(case_.block)!
);
const scheduleId = this.cx.schedule(case_.block, "case");
scheduleIds.push(scheduleId);
}
cases.push(this.visitor.visitCase(test, consequent));
});
cases.reverse();
this.cx.unscheduleAll(scheduleIds);
if (fallthroughId !== null) {
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "switch",
test,
cases,
id: terminal.id,
}),
fallthroughId
);
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
} else {
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "switch",
test,
cases,
id: terminal.id,
})
);
}
break;
}
case "while": {
const fallthroughId =
terminal.fallthrough !== null &&
!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);
this.visitor.visitTerminalId(terminal.id);
const testBlock = this.cx.ir.blocks.get(terminal.test)!;
const testTerminal = testBlock.terminal;
invariant(
testTerminal.kind === "if",
"Expected while loop test block to end in an if"
);
const testValue = this.visitValueBlock(blockValue, testBlock, {
value: testTerminal.test,
id: testTerminal.id,
});
let loopBody: TBlock;
if (loopId) {
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
} else {
const break_ = this.visitBreak(terminal.loop, null);
invariant(
break_ !== null,
"If loop body is already scheduled it must be a break"
);
const body = this.visitor.enterBlock();
this.visitor.appendBlock(body, break_);
loopBody = this.visitor.leaveBlock(body);
}
this.cx.unscheduleAll(scheduleIds);
if (fallthroughId !== null) {
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "while",
loc: terminal.loc,
test: testValue,
loop: loopBody,
id: terminal.id,
}),
fallthroughId
);
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
} else {
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "while",
loc: terminal.loc,
test: testValue,
loop: loopBody,
id: terminal.id,
})
);
}
break;
}
case "for": {
const loopId =
!this.cx.isScheduled(terminal.loop) &&
terminal.loop !== terminal.fallthrough
? terminal.loop
: null;
const fallthroughId =
terminal.fallthrough !== null &&
!this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
const scheduleId = this.cx.scheduleLoop(
terminal.fallthrough,
terminal.update,
terminal.loop
);
scheduleIds.push(scheduleId);
this.visitor.visitTerminalId(terminal.id);
const initBlock = this.cx.ir.blocks.get(terminal.init)!;
const initTerminal = initBlock.terminal;
invariant(
initTerminal.kind === "goto",
"Expected for loop init block to end in a goto"
);
const initValue = this.visitInitBlock(blockValue, initBlock);
const testBlock = this.cx.ir.blocks.get(terminal.test)!;
const testTerminal = testBlock.terminal;
invariant(
testTerminal.kind === "if",
"Expected for loop test block to end in an if"
);
const testValue = this.visitValueBlock(blockValue, testBlock, {
value: testTerminal.test,
id: testTerminal.id,
});
const updateBlock = this.cx.ir.blocks.get(terminal.update)!;
const updateTerminal = updateBlock.terminal;
invariant(
updateTerminal.kind === "goto",
"Expected for loop update block to end in a goto"
);
const updateValue = this.visitValueBlock(blockValue, updateBlock);
let loopBody: TBlock;
if (loopId) {
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
} else {
const break_ = this.visitBreak(terminal.loop, null);
invariant(
break_ !== null,
"If loop body is already scheduled it must be a break"
);
const body = this.visitor.enterBlock();
this.visitor.appendBlock(body, break_);
loopBody = this.visitor.leaveBlock(body);
}
this.cx.unscheduleAll(scheduleIds);
if (fallthroughId !== null) {
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "for",
init: initValue,
test: testValue,
update: updateValue,
loop: loopBody,
id: terminal.id,
}),
fallthroughId
);
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
} else {
this.visitor.appendBlock(
blockValue,
this.visitor.visitTerminal({
kind: "for",
init: initValue,
test: testValue,
update: updateValue,
loop: loopBody,
id: terminal.id,
})
);
}
break;
}
case "logical": {
todo("Implement tree visitor for logical terminal");
}
case "goto": {
this.visitor.visitTerminalId(terminal.id);
switch (terminal.variant) {
case GotoVariant.Break: {
const break_ = this.visitBreak(terminal.block, terminal.id);
if (break_ !== null) {
this.visitor.appendBlock(blockValue, break_);
}
break;
}
case GotoVariant.Continue: {
const continue_ = this.visitContinue(terminal.block, terminal.id);
if (continue_ !== null) {
this.visitor.appendBlock(blockValue, continue_);
}
break;
}
default: {
assertExhaustive(
terminal.variant,
`Unexpected goto variant '${terminal.variant}'`
);
}
}
break;
}
default: {
assertExhaustive(terminal, "Unexpected terminal");
}
}
}
visitInitBlock(parent: TBlockBuilder, block: BasicBlock): TInit {
const initBlock = this.visitor.enterInitBlock(parent);
for (const instr of block.instructions) {
const value = this.visitor.visitValue(instr.value, instr.id);
const item = this.visitor.visitInstruction(instr, value);
this.visitor.appendInitBlock(initBlock, item);
}
return this.visitor.leaveInitBlock(initBlock);
}
visitValueBlock(
parent: TBlockBuilder,
block: BasicBlock,
terminalValue?: { value: InstructionValue; id: InstructionId }
): TValue {
const valueBlock = this.visitor.enterValueBlock(parent);
const instructions = [...block.instructions];
let lastValue: { value: InstructionValue; id: InstructionId } | null = null;
if (terminalValue != null) {
lastValue = terminalValue;
}
for (const instr of instructions) {
const value = this.visitor.visitValue(instr.value, instr.id);
const item = this.visitor.visitInstruction(instr, value);
this.visitor.appendValueBlock(valueBlock, item);
}
const value =
lastValue !== null
? this.visitor.visitValue(lastValue.value, lastValue.id)
: null;
return this.visitor.leaveValueBlock(valueBlock, value);
}
emptyBlock(): TBlock {
const block = this.visitor.enterBlock();
return this.visitor.leaveBlock(block);
}
visitBreak(block: BlockId, id: InstructionId | null): TStatement | null {
const target = this.cx.getBreakTarget(block);
if (target === null) {
// TODO: we should always have a target
return null;
}
switch (target.type) {
case "implicit": {
return this.visitor.visitImplicitTerminal();
}
case "unlabeled": {
return this.visitor.visitTerminal({ kind: "break", label: null, id });
}
case "labeled": {
return this.visitor.visitTerminal({
kind: "break",
label: target.block,
id,
});
}
}
}
visitContinue(block: BlockId, id: InstructionId): TStatement | null {
const target = this.cx.getContinueTarget(block);
invariant(
target !== null,
`Expected continue target to be scheduled for bb${block}`
);
switch (target.type) {
case "labeled": {
return this.visitor.visitTerminal({
kind: "continue",
label: target.block,
id,
});
}
case "unlabeled": {
return this.visitor.visitTerminal({
kind: "continue",
label: null,
id,
});
}
case "implicit": {
return this.visitor.visitImplicitTerminal();
}
default: {
assertExhaustive(
target.type,
`Unexpected continue target kind '${(target as any).type}'`
);
}
}
}
visitInstr(instr: Instruction, blockValue: TBlockBuilder): void {
const value = this.visitor.visitValue(instr.value, instr.id);
const item = this.visitor.visitInstruction(instr, value);
this.visitor.appendBlock(blockValue, item);
}
visitPlace(place: Place, id: InstructionId): TValue {
return this.visitor.visitValue(place, id);
}
}
class Context {
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();
/**
* 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();
/**
* 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(ir: HIR) {
this.ir = ir;
}
block(id: BlockId): BasicBlock {
return this.ir.blocks.get(id)!;
}
/**
* 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++;
invariant(
!this.#scheduled.has(block),
`Break block is already scheduled: bb${block}`
);
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);
invariant(
!this.#scheduled.has(continueBlock),
`Continue block is already scheduled: bb${continueBlock}`
);
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();
invariant(
last !== undefined && last.id === scheduleId,
"Can only unschedule the last target"
);
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);
}
/**
* 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: ControlFlowKind } | null {
let hasPrecedingLoop = false;
for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
const target = this.#controlFlowStack[i]!;
if (target.block === block) {
let type: ControlFlowKind;
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";
}
return 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: ControlFlowKind } | 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: ControlFlowKind;
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 ControlFlowKind = "implicit" | "labeled" | "unlabeled";
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;
};
/**
* An object that can receive structured callbacks to visit HIR as a tree,
* and convert it to an alternate format.
*
* TBlock = representation of a list of statements
* TValue = represenation of an InstructionValue
* TStatement = representation of an Instruction
* TCase = representation of a switch case
*/
export interface Visitor<
TBlockBuilder,
TBlock,
TInit,
TValueBlock,
TValue,
TStatement,
TCase
> {
/**
* Must create an "empty" instance of the visitor's represenation for
* the contents of a block.
*/
enterBlock(): TBlockBuilder;
/**
* Appends an item onto the given block, with an optional label. The label
* indicates that a break/continue will proceed to code *after* the given item.
*/
appendBlock(block: TBlockBuilder, item: TStatement, label?: BlockId): void;
/**
* Converts the visitor's block representation into the representation of a
* block item, simultaneously "closing" the given block. The block will no
* longer be modified by the visitor driver.
*/
leaveBlock(block: TBlockBuilder): TBlock;
/**
* Must create an "empty" instance of the visitor's representation for a value
* block, which can generally contain only expressions but not statements. The
* currently active parent block is passed as an argument.
*
* The exception is the for initializer, which may contain declarations but not
* other statement types.
*/
enterValueBlock(block: TBlockBuilder): TValueBlock;
/**
* Appends an item onto the given value block.
*/
appendValueBlock(block: TValueBlock, item: TStatement): void;
/**
* Converts the visitor's value block (and final value) to the visitor's
* value representation.
*/
leaveValueBlock(block: TValueBlock, value: TValue | null): TValue;
enterInitBlock(block: TBlockBuilder): TValueBlock;
appendInitBlock(block: TValueBlock, item: TStatement): void;
leaveInitBlock(block: TValueBlock): TInit;
/**
* Convert an InstructionValue into the visitor's own representation
* of a value.
*/
visitValue(value: InstructionValue, id: InstructionId): TValue;
/**
* Convert an Instruction into the visitor's own representation of
* a block item.
*/
visitInstruction(instruction: Instruction, value: TValue): TStatement;
/**
* Called when a terminal is reached, before processing any of its
* possible branches.
*/
visitTerminalId(id: InstructionId): void;
/**
* Converts a break/continue that is implicit — that does not strictly
* have to be emitted — to the visitor's representation. The visitor
* can choose to return null if this does not need to be represented.
*/
visitImplicitTerminal(): TStatement | null;
/**
* Converts a terminal into the visitor's own representation of a block
* item. Note that the terminal differs from HIR Terminals, because
* values and block ids will have already been converted into the visitor's
* own representations.
*/
visitTerminal(
terminal: BlockTerminal<TInit, TValue, TBlock, TCase>
): TStatement;
/**
* Visits a switch case statement, which is collected into a switch terminal
* variant.
*/
visitCase(test: TValue | null, block: TBlock): TCase;
}
export type BlockTerminal<TInit, TValue, TBlock, TCase> =
| {
kind: "return";
loc: SourceLocation;
value: TValue | null;
id: InstructionId;
}
| { kind: "throw"; value: TValue; id: InstructionId }
| {
kind: "if";
test: TValue;
consequent: TBlock;
alternate: TBlock | null;
id: InstructionId;
}
| { kind: "switch"; test: TValue; cases: Array<TCase>; id: InstructionId }
| {
kind: "while";
loc: SourceLocation;
test: TValue;
loop: TBlock;
id: InstructionId;
}
| {
kind: "for";
init: TInit;
test: TValue;
update: TValue;
loop: TBlock;
id: InstructionId;
}
| { kind: "break"; label: BlockId | null; id: InstructionId | null }
| { kind: "continue"; label: BlockId | null; id: InstructionId };
@@ -15,15 +15,11 @@ import {
Place,
ReactiveBlock,
ReactiveFunction,
ReactiveScope,
ReactiveScopeBlock,
ReactiveStatement,
ReactiveTerminal,
ReactiveValueBlock,
ScopeId,
} from "../HIR/HIR";
import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
import { eachInstructionOperand } from "../HIR/visitors";
import { assertExhaustive } from "../Utils/utils";
export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
@@ -40,22 +36,8 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
};
}
type BlockKind =
| { kind: "block"; block: ReactiveBlock }
| { kind: "scope"; block: ReactiveBlock; scope: ReactiveScope };
class Builder {
#instructions: ReactiveBlock;
#stack: Array<
| { kind: "scope"; block: ReactiveScopeBlock }
| { kind: "block"; block: ReactiveBlock }
>;
constructor() {
const block: ReactiveBlock = [];
this.#instructions = block;
this.#stack = [{ kind: "block", block }];
}
#instructions: ReactiveBlock = [];
append(item: ReactiveStatement, label: BlockId | undefined): void {
if (label !== undefined) {
@@ -65,45 +47,8 @@ class Builder {
this.#instructions.push(item);
}
startScope(scope: ReactiveScope): void {
const block: ReactiveScopeBlock = {
kind: "scope",
scope,
instructions: [],
};
this.append(block, undefined);
this.#instructions = block.instructions;
this.#stack.push({ kind: "scope", block });
}
visitId(id: InstructionId): void {
for (let i = 0; i < this.#stack.length; i++) {
const entry = this.#stack[i]!;
if (entry.kind === "scope" && id >= entry.block.scope.range.end) {
this.#stack.length = i;
break;
}
}
const last = this.#stack[this.#stack.length - 1]!;
if (last.kind === "block") {
this.#instructions = last.block;
} else {
this.#instructions = last.block.instructions;
}
}
complete(): ReactiveBlock {
// TODO @josephsavona: debug two failures of this
// invariant(
// this.#stack.length === 1,
// "Expected all scopes to be closed when exiting a block"
// );
const first = this.#stack[0]!;
invariant(
first.kind === "block",
"Expected first stack item to be a basic block"
);
return first.block;
return this.#instructions;
}
}
@@ -119,18 +64,8 @@ class ReactiveFunctionBuilder
{ test: InstructionValue | null; block: ReactiveBlock }
>
{
#builders: Array<Builder> = [];
#scopes: Set<ScopeId> = new Set();
visitId(id: InstructionId): void {
const builder = this.#builders[this.#builders.length - 1]!;
builder.visitId(id);
}
enterBlock(): Builder {
const builder = new Builder();
this.#builders.push(builder);
return builder;
return new Builder();
}
appendBlock(
block: Builder,
@@ -140,11 +75,6 @@ class ReactiveFunctionBuilder
block.append(item, label);
}
leaveBlock(block: Builder): ReactiveBlock {
const builder = this.#builders.pop();
invariant(
builder === block,
"Expected enterBlock/leaveBlock to be called 1:1"
);
return block.complete();
}
@@ -196,18 +126,9 @@ class ReactiveFunctionBuilder
instruction: Instruction,
value: InstructionValue | ReactiveValueBlock
): ReactiveStatement {
this.visitId(instruction.id);
const scope = getInstructionScope(instruction);
if (scope !== null && !this.#scopes.has(scope.id)) {
this.#scopes.add(scope.id);
const builder = this.#builders[this.#builders.length - 1]!;
builder.startScope(scope);
}
return { kind: "instruction", instruction };
}
visitTerminalId(id: InstructionId): void {
this.visitId(id);
}
visitTerminalId(id: InstructionId): void {}
visitImplicitTerminal(): ReactiveStatement | null {
return null;
}
@@ -311,26 +232,3 @@ class ReactiveFunctionBuilder
return { test, block };
}
}
function getInstructionScope(instr: Instruction): ReactiveScope | null {
if (
instr.lvalue.place.identifier.scope !== null &&
isScopeActive(instr.lvalue.place.identifier.scope, instr.id)
) {
return instr.lvalue.place.identifier.scope;
} else {
for (const operand of eachInstructionOperand(instr)) {
if (
operand.identifier.scope !== null &&
isScopeActive(operand.identifier.scope, instr.id)
) {
return operand.identifier.scope;
}
}
}
return null;
}
function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
return id >= scope.range.start && id < scope.range.end;
}
@@ -1,234 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import invariant from "invariant";
import {
BlockId,
HIRFunction,
Instruction,
InstructionId,
InstructionValue,
Place,
ReactiveBlock,
ReactiveFunction,
ReactiveStatement,
ReactiveTerminal,
ReactiveValueBlock,
} from "../HIR/HIR";
import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
import { assertExhaustive } from "../Utils/utils";
export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
const builder = new ReactiveFunctionBuilder();
const body = visitTree(fn, builder);
invariant(body != null, "Expected a root block");
return {
loc: fn.loc,
id: fn.id,
params: fn.params,
generator: fn.generator,
async: fn.async,
body,
};
}
class Builder {
#instructions: ReactiveBlock = [];
append(item: ReactiveStatement, label: BlockId | undefined): void {
if (label !== undefined) {
invariant(item.kind === "terminal", "Only terminals may have a label");
item.label = label;
}
this.#instructions.push(item);
}
complete(): ReactiveBlock {
return this.#instructions;
}
}
class ReactiveFunctionBuilder
implements
Visitor<
Builder,
ReactiveBlock,
ReactiveValueBlock,
ReactiveValueBlock,
InstructionValue | ReactiveValueBlock,
ReactiveStatement,
{ test: InstructionValue | null; block: ReactiveBlock }
>
{
enterBlock(): Builder {
return new Builder();
}
appendBlock(
block: Builder,
item: ReactiveStatement,
label?: BlockId | undefined
): void {
block.append(item, label);
}
leaveBlock(block: Builder): ReactiveBlock {
return block.complete();
}
enterValueBlock(block: Builder): ReactiveValueBlock {
return {
kind: "value-block",
instructions: [],
last: null,
};
}
appendValueBlock(block: ReactiveValueBlock, item: ReactiveStatement): void {
block.instructions.push(item);
}
leaveValueBlock(
block: ReactiveValueBlock,
last: {
value: InstructionValue | ReactiveValueBlock;
id: InstructionId;
} | null
): InstructionValue | ReactiveValueBlock {
if (last !== null) {
const { id, value } = last;
invariant(
value.kind !== "value-block",
"Expected value block to end in a value"
);
block.last = { id, value };
}
return block;
}
enterInitBlock(block: Builder): ReactiveValueBlock {
return this.enterValueBlock(block);
}
appendInitBlock(block: ReactiveValueBlock, item: ReactiveStatement): void {
this.appendValueBlock(block, item);
}
leaveInitBlock(block: ReactiveValueBlock): ReactiveValueBlock {
return block;
}
visitValue(
value: InstructionValue,
id: InstructionId
): InstructionValue | ReactiveValueBlock {
return value;
}
visitInstruction(
instruction: Instruction,
value: InstructionValue | ReactiveValueBlock
): ReactiveStatement {
return { kind: "instruction", instruction };
}
visitTerminalId(id: InstructionId): void {}
visitImplicitTerminal(): ReactiveStatement | null {
return null;
}
visitTerminal(
terminal: BlockTerminal<
ReactiveValueBlock,
InstructionValue | ReactiveValueBlock,
ReactiveBlock,
{ test: InstructionValue | null; block: ReactiveBlock }
>
): ReactiveStatement {
let result: ReactiveTerminal;
switch (terminal.kind) {
case "break": {
result = { kind: "break", label: terminal.label, id: terminal.id };
break;
}
case "continue": {
result = { kind: "continue", label: terminal.label, id: terminal.id };
break;
}
case "for": {
const { test, update } = terminal;
result = {
kind: "for",
init: terminal.init,
test: terminal.test as ReactiveValueBlock,
update: terminal.update as ReactiveValueBlock,
loop: terminal.loop,
id: terminal.id,
};
break;
}
case "if": {
result = {
kind: "if",
test: terminal.test as Place,
consequent: terminal.consequent,
alternate: terminal.alternate,
id: terminal.id,
};
break;
}
case "return": {
const value = terminal.value;
if (value !== null && value.kind !== "Identifier") {
invariant(false, "Expected return to be a Place");
}
result = { kind: "return", value, id: terminal.id };
break;
}
case "switch": {
result = {
kind: "switch",
test: terminal.test as Place,
cases: terminal.cases as Array<{
test: Place | null;
block: ReactiveBlock | void;
}>,
id: terminal.id,
};
break;
}
case "throw": {
result = {
kind: "throw",
value: terminal.value as Place,
id: terminal.id,
};
break;
}
case "while": {
result = {
kind: "while",
test: terminal.test as ReactiveValueBlock,
loop: terminal.loop,
id: terminal.id,
};
break;
}
default: {
assertExhaustive(
terminal,
`Unexpected terminal kind '${(terminal as any).kind}'`
);
}
}
return {
kind: "terminal",
terminal: result,
label: null,
};
}
visitCase(
test: InstructionValue | ReactiveValueBlock | null,
block: ReactiveBlock
): { test: InstructionValue | null; block: ReactiveBlock } {
if (test !== null && test.kind !== "Identifier") {
invariant(false, "Expected a Place");
}
return { test, block };
}
}
@@ -1,470 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import invariant from "invariant";
import {
BlockId,
HIRFunction,
Instruction,
InstructionId,
InstructionValue,
makeInstructionId,
MutableRange,
ReactiveScope,
ScopeId,
} from "../HIR/HIR";
import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
import { printFunction } from "../HIR/PrintHIR";
import {
eachInstructionOperand,
eachInstructionValueOperand,
} from "../HIR/visitors";
import DisjointSet from "../Utils/DisjointSet";
import { log } from "../Utils/logger";
import { retainWhere } from "../Utils/utils";
/**
* This is a second (final) stage of constructing reactive scopes. Prior to this pass,
* InferReactiveScopeVariables infers the sets of identifiers that "construct together",
* assigning each identifier in each scope the same ScopeId and same MutableRange which
* describes that span.
*
* Note that at this point reactive scopes describe ranges based on specific instructions
* at arbitrary points in the control flow graph. However, reactive scopes must align
* with control-flow boundaries — we can't memoize half of a loop!
*
* This pass refines the reactive scopes as follows:
*
* ## Expanding each reactive scope to align with control-flow boundaries (DONE)
*
* This corresponds with the shape of the AST: a scope that extends into an if consequent
* would expand across the alternate branch, A scope that extends partway into an if
* would expand to cover the full loop body, etc.
*
* ```javascript
* function foo(cond, a) {
* ⌵ original scope
* ⌵ expanded scope
* const x = []; ⌝ ⌝
* if (cond) { ⎮ ⎮
* ... ⎮ ⎮
* x.push(a); ⌟ ⎮
* ... ⎮
* } ⌟
* }
* ```
*
* ## Merging (some) overlapping reactive scopes (TODO)
*
* Two scopes overlap if there is one or more instruction that is inside the range
* of both scopes. In general, overlapping scopes are merged togther. The only
* exception to this is when one scope *shadows* another scope. For example:
*
* ```javascript
* function foo(cond, a) {
* ⌵ scope for x
* let x = []; ⌝
* if (cond) { ⎮
* ⌵ scope for y ⎮
* let y = []; ⌝ ⎮
* if (b) { ⎮ ⎮
* y.push(b); ⌟ ⎮
* } ⎮
* x.push(<div>{y}</div>); ⎮
* } ⌟
* }
* ```
*
* In this example the two scopes overlap, but mutation of the two scopes is not
* interleaved. Specifically within the y scope there are no instructions that
* modify any other scope: the inner scope "shadows" the outer one. This category
* of overlap does *NOT* merge the scopes together.
*
* The implementation is inspired by the Rust notion of "stacked borrows". We traverse
* the control-flow graph in tree form, at each point keeping track of which scopes are
* active. So initially we see
*
* `let x = []`
* active scopes: [x]
*
* and mark the x scope as active.
*
* Then we later encounter
*
* `let y = [];`
* active scopes: [x, y]
*
* Here we first check to see if 'y' is already in the list of active scopes. It isn't,
* so we push it to the stop of the stack.
*
* Then
*
* `y.push(b)`
* active scopes: [x, y]
*
* Mutates y, so we check if y is the top of the stack. It is, so no merging must occur.
*
* If instead we saw eg
*
* `x.push(b)`
* active scopes: [x, y]
*
* Then we would see that 'x' is active, but that it is shadowed. The two scopes would have
* to be merged.
*/
export function inferReactiveScopes(fn: HIRFunction) {
// Note sure if this is strictly required: in general the only way for two scopes to have
// the same range is if they were already inferred as aliasing together, and therefore
// they should end up with the same range and scope anyway.
mergeScopesWithIdenticalRanges(fn);
visitTree(fn, new AlignReactiveScopesToBlockScopeRangeVisitor());
log(
() =>
`AlignReactiveScopesToBlockScopeRangeVisitor:\n${printFunction(fn)}\n\n`
);
visitTree(fn, new MergeOverlappingReactiveScopesVisitor());
}
/**
* Finds scopes with identical ranges and merges them
*/
function mergeScopesWithIdenticalRanges(fn: HIRFunction) {
const scopesByRange: Map<string, ReactiveScope> = new Map();
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const instrScope = getInstructionScope(instr);
if (instrScope === null) {
continue;
}
const rangeKey = `${instrScope.range.start}:${instrScope.range.end}`;
let scope = scopesByRange.get(rangeKey);
if (scope === undefined) {
scope = instrScope;
scopesByRange.set(rangeKey, scope);
}
if (scope.id !== instrScope.id) {
instrScope.id = scope.id;
instrScope.range = scope.range;
}
}
}
}
class BlockScope {
seen: Set<ScopeId> = new Set();
scopes: Array<ShadowableReactiveScope> = [];
}
type ShadowableReactiveScope = {
scope: ReactiveScope;
shadowedBy: ReactiveScope | null;
};
class MergeOverlappingReactiveScopesVisitor
implements Visitor<void, void, void, void, void, void, void>
{
scopes: Array<BlockScope> = [];
seenScopes: Set<ScopeId> = new Set();
joinedScopes: DisjointSet<ReactiveScope> = new DisjointSet();
/**
* Determine if this scope is interleaved with any other scopes,
* and if so merge them.
*/
visitScope(scope: ReactiveScope) {
const currentBlock = this.scopes[this.scopes.length - 1]!;
// Fast-path for the first time we see a new scope
if (!this.seenScopes.has(scope.id)) {
this.seenScopes.add(scope.id);
currentBlock.seen.add(scope.id);
currentBlock.scopes.push({ shadowedBy: null, scope });
return;
}
// Scope has already been seen, find it in the current block or a parent
let index = this.scopes.length - 1;
let nextBlock = currentBlock;
while (!nextBlock.seen.has(scope.id)) {
// scopes that cross control-flow boundaries are merged with overlapping
// scopes
this.joinedScopes.union([scope, ...nextBlock.scopes.map((s) => s.scope)]);
index--;
if (index < 0) {
// TODO: handle reassignments in multiple branches. these create new identifiers that
// add an entry to this.seenScopes but which are then removed when their blocks exit.
// this is also wrong for codegen, different versions of an identifier could be cached
// differently and so a reassigned version of a variable needs a separate declaration.
// console.log(`scope ${scope.id} not found`);
// for (let i = this.scopes.length - 1; i > index; i--) {
// const s = this.scopes[i];
// console.log(
// JSON.stringify(
// {
// seen: Array.from(s.seen),
// scopes: s.scopes,
// },
// null,
// 2
// )
// );
// }
currentBlock.seen.add(scope.id);
currentBlock.scopes.push({ shadowedBy: null, scope });
return;
}
nextBlock = this.scopes[index]!;
}
// Handle interleaving within a given block scope
let found = false;
for (let i = 0; i < nextBlock.scopes.length; i++) {
const current = nextBlock.scopes[i]!;
if (current.scope.id === scope.id) {
found = true;
if (current.shadowedBy !== null) {
this.joinedScopes.union([current.shadowedBy, current.scope]);
}
} else if (found && current.shadowedBy === null) {
// `scope` is shadowing `current`, but we don't know they are interleaved yet
current.shadowedBy = scope;
}
}
if (!currentBlock.seen.has(scope.id)) {
currentBlock.seen.add(scope.id);
currentBlock.scopes.push({ shadowedBy: null, scope });
}
}
/**
* Prune any scopes that are out of range
*/
visitId(id: InstructionId) {
// console.log(`visitId: ${id}`);
const currentBlock = this.scopes[this.scopes.length - 1]!;
retainWhere(currentBlock.scopes, (pending) => {
if (pending.scope.range.end > id) {
return true;
} else {
currentBlock.seen.delete(pending.scope.id);
return false;
}
});
}
enterBlock(): void {
this.scopes.push(new BlockScope());
}
enterValueBlock(): void {
this.enterBlock();
}
enterInitBlock(block: void): void {
this.enterBlock();
}
leaveInitBlock(block: void): void {
this.leaveBlock();
}
leaveValueBlock(
block: void,
value: { value: void; id: InstructionId } | null
): void {
this.leaveBlock();
}
visitValue(value: InstructionValue, id: InstructionId): void {
this.visitId(id);
for (const operand of eachInstructionValueOperand(value)) {
if (
operand.identifier.scope !== null &&
id >= operand.identifier.scope.range.start &&
id < operand.identifier.scope.range.end
) {
this.visitScope(operand.identifier.scope);
}
}
}
visitInstruction(instruction: Instruction, value: void): void {
this.visitId(instruction.id);
if (
instruction.lvalue.place.identifier.scope !== null &&
instruction.id >= instruction.lvalue.place.identifier.scope.range.start &&
instruction.id < instruction.lvalue.place.identifier.scope.range.end
) {
this.visitScope(instruction.lvalue.place.identifier.scope);
}
}
visitTerminalId(id: InstructionId): void {
this.visitId(id);
}
visitImplicitTerminal(): void | null {}
visitTerminal(terminal: BlockTerminal<void, void, void, void>): void {}
visitCase(test: void | null, block: void): void {}
appendBlock(block: void, item: void, label?: BlockId | undefined): void {}
appendValueBlock(block: void, item: void): void {}
appendInitBlock(block: void, item: void): void {}
leaveBlock(block: void): void {
this.scopes.pop();
if (this.scopes.length === 0) {
this.joinedScopes.forEach((scope, groupScope) => {
if (scope !== groupScope) {
groupScope.range.start = makeInstructionId(
Math.min(groupScope.range.start, scope.range.start)
);
groupScope.range.end = makeInstructionId(
Math.max(groupScope.range.end, scope.range.end)
);
scope.range = groupScope.range;
scope.id = groupScope.id;
}
});
}
}
}
type PendingReactiveScope = { active: boolean; scope: ReactiveScope };
/**
* Aligns scopes to block scope boundaries.
*
* TODO @josephsavona this algorithm isn't quite right. we need to ensure that
* reactive scopes can only be closed (end updated) at the same block scope as they
* were opened (start encountered).
*/
class AlignReactiveScopesToBlockScopeRangeVisitor
implements Visitor<void, void, void, void, void, void, void>
{
// For each block scope (outer array) stores a list of ReactiveScopes that start
// in that block scope.
blockScopes: Array<{
kind: "block" | "value";
scopes: Array<PendingReactiveScope>;
}> = [];
// ReactiveScopes whose declaring block scope has ended but may still need to
// be "closed" (ie have their range.end be updated). A given scope can be in
// blockScopes OR this array but not both.
unclosedScopes: Array<PendingReactiveScope> = [];
// Set of all scope ids that have been seen so far, regardless of which of
// the above data structures they're in, to avoid tracking the same scope twice.
seenScopes: Set<ScopeId> = new Set();
visitId(id: InstructionId) {
const currentScopes = this.blockScopes[this.blockScopes.length - 1]!;
if (currentScopes.kind === "value") {
return;
}
const scopes = [...currentScopes.scopes, ...this.unclosedScopes];
for (const pending of scopes) {
if (!pending.active) {
continue;
}
if (id >= pending.scope.range.end) {
pending.active = false;
pending.scope.range.end = id;
}
}
}
enterBlock(): void {
this.blockScopes.push({ kind: "block", scopes: [] });
}
appendBlock(block: void, item: void, label?: BlockId | undefined): void {}
leaveBlock(block: void): void {
const lastScope = this.blockScopes.pop();
invariant(
lastScope !== undefined && lastScope.kind === "block",
"Expected enterBlock/leaveBlock to be called 1:1"
);
for (const scope of lastScope.scopes) {
if (scope.active) {
this.unclosedScopes.push(scope);
}
}
}
enterValueBlock(): void {
this.blockScopes.push({ kind: "value", scopes: [] });
}
appendValueBlock(block: void, item: void): void {}
leaveValueBlock(
block: void,
value: { value: void; id: InstructionId } | null
): void {
const lastScope = this.blockScopes.pop();
invariant(
lastScope !== undefined && lastScope.kind === "value",
"Expected enterValueBlock/leaveValueBlock to be called 1:1"
);
for (const scope of lastScope.scopes) {
invariant(
scope.active,
"Value scopes cannot be closed separately from the parent block"
);
this.unclosedScopes.push(scope);
}
}
enterInitBlock(block: void): void {
this.enterValueBlock();
}
appendInitBlock(block: void, item: void): void {}
leaveInitBlock(block: void): void {
this.leaveValueBlock(block, null);
}
visitInstruction(instruction: Instruction, value: void): void {
this.visitId(instruction.id);
const scope = getInstructionScope(instruction);
if (scope !== null) {
if (!this.seenScopes.has(scope.id)) {
const currentScopes = this.blockScopes[this.blockScopes.length - 1]!;
this.seenScopes.add(scope.id);
currentScopes.scopes.push({
active: true,
scope,
});
}
}
}
visitTerminalId(id: InstructionId): void {
this.visitId(id);
}
visitTerminal(terminal: BlockTerminal<void, void, void, void>): void {}
visitImplicitTerminal(): void | null {}
// no-ops
visitValue(value: InstructionValue): void {}
visitCase(test: void | null, block: void): void {}
}
function getInstructionScope(instr: Instruction): ReactiveScope | null {
if (
instr.lvalue.place.identifier.scope !== null &&
isActive(instr, instr.lvalue.place.identifier.scope.range)
) {
return instr.lvalue.place.identifier.scope;
} else {
for (const operand of eachInstructionOperand(instr)) {
if (
operand.identifier.scope !== null &&
isActive(instr, operand.identifier.scope.range)
) {
return operand.identifier.scope;
}
}
}
return null;
}
function isActive(instr: Instruction, range: MutableRange): boolean {
return instr.id >= range.start && instr.id < range.end;
}
@@ -8,10 +8,8 @@
export { alignReactiveScopesToBlockScopes } from "./AlignReactiveScopesToBlockScopes";
export { buildReactiveBlocks } from "./BuildReactiveBlocks";
export { buildReactiveFunction } from "./BuildReactiveFunction";
export { buildReactiveFunction as buildReactiveFunctionWithoutScopes } from "./BuildReactiveFunctionWithoutScopes";
export { codegenReactiveFunction } from "./CodegenReactiveFunction";
export { flattenReactiveLoops } from "./FlattenReactiveLoops";
export { inferReactiveScopes } from "./InferReactiveScopes";
export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
export { printReactiveFunction } from "./PrintReactiveFunction";
@@ -13,7 +13,6 @@ import traverse from "@babel/traverse";
import { wasmFolder } from "@hpcc-js/wasm";
import path from "path";
import prettier from "prettier";
import { flags } from "../CompilerFlags";
import { compile } from "../CompilerPipeline";
import { toggleLogging } from "../Utils/logger";
import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
@@ -31,9 +30,6 @@ wasmFolder(
const Pragma_RE = /\/\/\s*@enable\((\w+)\)$/gm;
describe("React Forget (HIR version)", () => {
flags.enableNewReactiveFunctionBuilder =
String(process.env["ENABLE_NEW_BUILDER"]) === "1";
generateTestsFromFixtures(
path.join(__dirname, "fixtures", "hir"),
(input, file, options) => {
+1 -3
View File
@@ -20,10 +20,9 @@ import codegen from "./HIR/Codegen";
import { Environment } from "./HIR/HIRBuilder";
import printHIR, { printFunction } from "./HIR/PrintHIR";
import { inferMutableRanges, inferReferenceEffects } from "./Inference";
import { buildReactiveFunction } from "./ReactiveScopes/BuildReactiveFunctionWithoutScopes";
import { buildReactiveFunction } from "./ReactiveScopes/BuildReactiveFunction";
import { codegenReactiveFunction } from "./ReactiveScopes/CodegenReactiveFunction";
import { flattenReactiveLoops } from "./ReactiveScopes/FlattenReactiveLoops";
import { inferReactiveScopes } from "./ReactiveScopes/InferReactiveScopes";
import { inferReactiveScopeVariables } from "./ReactiveScopes/InferReactiveScopeVariables";
import { printReactiveFunction } from "./ReactiveScopes/PrintReactiveFunction";
import { propagateScopeDependencies } from "./ReactiveScopes/PropagateScopeDependencies";
@@ -65,7 +64,6 @@ export const HIR = {
Environment,
flattenReactiveLoops,
inferMutableRanges,
inferReactiveScopes,
inferReactiveScopeVariables,
inferReferenceEffects,
inferTypes,