diff --git a/compiler/forget/src/CompilerFlags.ts b/compiler/forget/src/CompilerFlags.ts index 30d6be8756..abd58ef284 100644 --- a/compiler/forget/src/CompilerFlags.ts +++ b/compiler/forget/src/CompilerFlags.ts @@ -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 = {}; diff --git a/compiler/forget/src/CompilerPipeline.ts b/compiler/forget/src/CompilerPipeline.ts index a77c71e9ce..44cfc54bd5 100644 --- a/compiler/forget/src/CompilerPipeline.ts +++ b/compiler/forget/src/CompilerPipeline.ts @@ -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({ diff --git a/compiler/forget/src/HIR/ReactiveFunctionVisitor.ts b/compiler/forget/src/HIR/ReactiveFunctionVisitor.ts deleted file mode 100644 index 579c462a03..0000000000 --- a/compiler/forget/src/HIR/ReactiveFunctionVisitor.ts +++ /dev/null @@ -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 = []; - [...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 = 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 = 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 = []; - - 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): 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 { - 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 - ): TStatement; - - /** - * Visits a switch case statement, which is collected into a switch terminal - * variant. - */ - visitCase(test: TValue | null, block: TBlock): TCase; -} - -export type BlockTerminal = - | { - 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; 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 }; diff --git a/compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts b/compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts index 5b39e05dd1..de081db311 100644 --- a/compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts +++ b/compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts @@ -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 = []; - #scopes: Set = 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; -} diff --git a/compiler/forget/src/ReactiveScopes/BuildReactiveFunctionWithoutScopes.ts b/compiler/forget/src/ReactiveScopes/BuildReactiveFunctionWithoutScopes.ts deleted file mode 100644 index de081db311..0000000000 --- a/compiler/forget/src/ReactiveScopes/BuildReactiveFunctionWithoutScopes.ts +++ /dev/null @@ -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 }; - } -} diff --git a/compiler/forget/src/ReactiveScopes/InferReactiveScopes.ts b/compiler/forget/src/ReactiveScopes/InferReactiveScopes.ts deleted file mode 100644 index f2c47bd8a4..0000000000 --- a/compiler/forget/src/ReactiveScopes/InferReactiveScopes.ts +++ /dev/null @@ -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(
{y}
); ⎮ - * } ⌟ - * } - * ``` - * - * 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 = 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 = new Set(); - scopes: Array = []; -} - -type ShadowableReactiveScope = { - scope: ReactiveScope; - shadowedBy: ReactiveScope | null; -}; - -class MergeOverlappingReactiveScopesVisitor - implements Visitor -{ - scopes: Array = []; - seenScopes: Set = new Set(); - joinedScopes: DisjointSet = 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 {} - 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 -{ - // For each block scope (outer array) stores a list of ReactiveScopes that start - // in that block scope. - blockScopes: Array<{ - kind: "block" | "value"; - scopes: Array; - }> = []; - - // 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 = []; - - // 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 = 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 {} - - 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; -} diff --git a/compiler/forget/src/ReactiveScopes/index.ts b/compiler/forget/src/ReactiveScopes/index.ts index 5c032071e8..b848df584e 100644 --- a/compiler/forget/src/ReactiveScopes/index.ts +++ b/compiler/forget/src/ReactiveScopes/index.ts @@ -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"; diff --git a/compiler/forget/src/__tests__/hir-test.ts b/compiler/forget/src/__tests__/hir-test.ts index c0d6c13f0e..13e4c364c0 100644 --- a/compiler/forget/src/__tests__/hir-test.ts +++ b/compiler/forget/src/__tests__/hir-test.ts @@ -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) => { diff --git a/compiler/forget/src/index.ts b/compiler/forget/src/index.ts index fe64c5b104..ebff2493c9 100644 --- a/compiler/forget/src/index.ts +++ b/compiler/forget/src/index.ts @@ -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,