mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Switch to new BuildReactiveFunction passes
This commit is contained in:
@@ -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 = {};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user