/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import {CompilerError} from '../CompilerError'; import { BasicBlock, BlockId, GotoVariant, HIR, InstructionId, Place, ReactiveBlock, SourceLocation, } from '../HIR'; import { HIRFunction, ReactiveBreakTerminal, ReactiveContinueTerminal, ReactiveFunction, ReactiveLogicalValue, ReactiveSequenceValue, ReactiveTerminalStatement, ReactiveTerminalTargetKind, ReactiveTernaryValue, ReactiveValue, Terminal, } from '../HIR/HIR'; import {assertExhaustive} from '../Utils/utils'; /* * Converts from HIR (lower-level CFG) to ReactiveFunction, a tree representation * that is closer to an AST. This pass restores the original control flow constructs, * including break/continue to labeled statements. Note that this pass naively emits * labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels. */ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction { const cx = new Context(fn.body); const driver = new Driver(cx); const body = driver.traverseBlock(cx.block(fn.body.entry)); return { loc: fn.loc, id: fn.id, nameHint: fn.nameHint, params: fn.params, generator: fn.generator, async: fn.async, body, env: fn.env, directives: fn.directives, }; } class Driver { cx: Context; constructor(cx: Context) { this.cx = cx; } traverseBlock(block: BasicBlock): ReactiveBlock { const blockValue: ReactiveBlock = []; this.visitBlock(block, blockValue); return blockValue; } visitBlock(block: BasicBlock, blockValue: ReactiveBlock): void { CompilerError.invariant(!this.cx.emitted.has(block.id), { reason: `Cannot emit the same block twice: bb${block.id}`, description: null, details: [ { kind: 'error', loc: null, message: null, }, ], suggestions: null, }); this.cx.emitted.add(block.id); for (const instruction of block.instructions) { blockValue.push({ kind: 'instruction', instruction, }); } const terminal = block.terminal; const scheduleIds = []; switch (terminal.kind) { case 'return': { blockValue.push({ kind: 'terminal', terminal: { kind: 'return', loc: terminal.loc, value: terminal.value, id: terminal.id, }, label: null, }); break; } case 'throw': { blockValue.push({ kind: 'terminal', terminal: { kind: 'throw', loc: terminal.loc, value: terminal.value, id: terminal.id, }, label: null, }); break; } case 'if': { const fallthroughId = this.cx.reachable(terminal.fallthrough) && !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; const alternateId = terminal.alternate !== terminal.fallthrough ? terminal.alternate : null; if (fallthroughId !== null) { const scheduleId = this.cx.schedule(fallthroughId, 'if'); scheduleIds.push(scheduleId); } let consequent: ReactiveBlock | null = null; if (this.cx.isScheduled(terminal.consequent)) { CompilerError.invariant(false, { reason: `Unexpected 'if' where the consequent is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } else { consequent = this.traverseBlock( this.cx.ir.blocks.get(terminal.consequent)!, ); } let alternate: ReactiveBlock | null = null; if (alternateId !== null) { if (this.cx.isScheduled(alternateId)) { CompilerError.invariant(false, { reason: `Unexpected 'if' where the alternate is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } else { alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!); } } this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', terminal: { kind: 'if', loc: terminal.loc, test: terminal.test, consequent: consequent ?? this.emptyBlock(), alternate: alternate, id: terminal.id, }, label: fallthroughId == null ? null : { id: fallthroughId, implicit: false, }, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'switch': { const fallthroughId = this.cx.reachable(terminal.fallthrough) && !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; if (fallthroughId !== null) { const scheduleId = this.cx.schedule(fallthroughId, 'switch'); scheduleIds.push(scheduleId); } const cases: Array<{ test: Place | null; block: ReactiveBlock; }> = []; [...terminal.cases].reverse().forEach((case_, _index) => { const test = case_.test; let consequent: ReactiveBlock; if (this.cx.isScheduled(case_.block)) { CompilerError.invariant(case_.block === terminal.fallthrough, { reason: `Unexpected 'switch' where a case is already scheduled and block is not the fallthrough`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); return; } else { consequent = this.traverseBlock( this.cx.ir.blocks.get(case_.block)!, ); const scheduleId = this.cx.schedule(case_.block, 'case'); scheduleIds.push(scheduleId); } cases.push({test, block: consequent}); }); cases.reverse(); this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', terminal: { kind: 'switch', loc: terminal.loc, test: terminal.test, cases, id: terminal.id, }, label: fallthroughId == null ? null : { id: fallthroughId, implicit: false, }, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'do-while': { const fallthroughId = !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; const loopId = !this.cx.isScheduled(terminal.loop) && terminal.loop !== terminal.fallthrough ? terminal.loop : null; const scheduleId = this.cx.scheduleLoop( terminal.fallthrough, terminal.test, terminal.loop, ); scheduleIds.push(scheduleId); let loopBody: ReactiveBlock; if (loopId) { loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!); } else { CompilerError.invariant(false, { reason: `Unexpected 'do-while' where the loop is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } const testValue = this.visitValueBlock( terminal.test, terminal.loc, ).value; this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', terminal: { kind: 'do-while', loc: terminal.loc, test: testValue, loop: loopBody, id: terminal.id, }, label: fallthroughId == null ? null : { id: fallthroughId, implicit: false, }, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'while': { const fallthroughId = this.cx.reachable(terminal.fallthrough) && !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; const loopId = !this.cx.isScheduled(terminal.loop) && terminal.loop !== terminal.fallthrough ? terminal.loop : null; const scheduleId = this.cx.scheduleLoop( terminal.fallthrough, terminal.test, terminal.loop, ); scheduleIds.push(scheduleId); const testValue = this.visitValueBlock( terminal.test, terminal.loc, ).value; let loopBody: ReactiveBlock; if (loopId) { loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!); } else { CompilerError.invariant(false, { reason: `Unexpected 'while' where the loop is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', terminal: { kind: 'while', loc: terminal.loc, test: testValue, loop: loopBody, id: terminal.id, }, label: fallthroughId == null ? null : { id: fallthroughId, implicit: false, }, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'for': { const loopId = !this.cx.isScheduled(terminal.loop) && terminal.loop !== terminal.fallthrough ? terminal.loop : null; const fallthroughId = !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; const scheduleId = this.cx.scheduleLoop( terminal.fallthrough, terminal.update ?? terminal.test, terminal.loop, ); scheduleIds.push(scheduleId); const init = this.visitValueBlock(terminal.init, terminal.loc); const initBlock = this.cx.ir.blocks.get(init.block)!; let initValue = init.value; if (initValue.kind === 'SequenceExpression') { const last = initBlock.instructions.at(-1)!; initValue.instructions.push(last); initValue.value = { kind: 'Primitive', value: undefined, loc: terminal.loc, }; } else { initValue = { kind: 'SequenceExpression', instructions: [initBlock.instructions.at(-1)!], id: terminal.id, loc: terminal.loc, value: { kind: 'Primitive', value: undefined, loc: terminal.loc, }, }; } const testValue = this.visitValueBlock( terminal.test, terminal.loc, ).value; const updateValue = terminal.update !== null ? this.visitValueBlock(terminal.update, terminal.loc).value : null; let loopBody: ReactiveBlock; if (loopId) { loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!); } else { CompilerError.invariant(false, { reason: `Unexpected 'for' where the loop is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', terminal: { kind: 'for', loc: terminal.loc, init: initValue, test: testValue, update: updateValue, loop: loopBody, id: terminal.id, }, label: fallthroughId == null ? null : {id: fallthroughId, implicit: false}, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'for-of': { const loopId = !this.cx.isScheduled(terminal.loop) && terminal.loop !== terminal.fallthrough ? terminal.loop : null; const fallthroughId = !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; const scheduleId = this.cx.scheduleLoop( terminal.fallthrough, terminal.init, terminal.loop, ); scheduleIds.push(scheduleId); const init = this.visitValueBlock(terminal.init, terminal.loc); const initBlock = this.cx.ir.blocks.get(init.block)!; let initValue = init.value; if (initValue.kind === 'SequenceExpression') { const last = initBlock.instructions.at(-1)!; initValue.instructions.push(last); initValue.value = { kind: 'Primitive', value: undefined, loc: terminal.loc, }; } else { initValue = { kind: 'SequenceExpression', instructions: [initBlock.instructions.at(-1)!], id: terminal.id, loc: terminal.loc, value: { kind: 'Primitive', value: undefined, loc: terminal.loc, }, }; } const test = this.visitValueBlock(terminal.test, terminal.loc); const testBlock = this.cx.ir.blocks.get(test.block)!; let testValue = test.value; if (testValue.kind === 'SequenceExpression') { const last = testBlock.instructions.at(-1)!; testValue.instructions.push(last); testValue.value = { kind: 'Primitive', value: undefined, loc: terminal.loc, }; } else { testValue = { kind: 'SequenceExpression', instructions: [testBlock.instructions.at(-1)!], id: terminal.id, loc: terminal.loc, value: { kind: 'Primitive', value: undefined, loc: terminal.loc, }, }; } let loopBody: ReactiveBlock; if (loopId) { loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!); } else { CompilerError.invariant(false, { reason: `Unexpected 'for-of' where the loop is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', terminal: { kind: 'for-of', loc: terminal.loc, init: initValue, test: testValue, loop: loopBody, id: terminal.id, }, label: fallthroughId == null ? null : {id: fallthroughId, implicit: false}, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'for-in': { const loopId = !this.cx.isScheduled(terminal.loop) && terminal.loop !== terminal.fallthrough ? terminal.loop : null; const fallthroughId = !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; const scheduleId = this.cx.scheduleLoop( terminal.fallthrough, terminal.init, terminal.loop, ); scheduleIds.push(scheduleId); const init = this.visitValueBlock(terminal.init, terminal.loc); const initBlock = this.cx.ir.blocks.get(init.block)!; let initValue = init.value; if (initValue.kind === 'SequenceExpression') { const last = initBlock.instructions.at(-1)!; initValue.instructions.push(last); initValue.value = { kind: 'Primitive', value: undefined, loc: terminal.loc, }; } else { initValue = { kind: 'SequenceExpression', instructions: [initBlock.instructions.at(-1)!], id: terminal.id, loc: terminal.loc, value: { kind: 'Primitive', value: undefined, loc: terminal.loc, }, }; } let loopBody: ReactiveBlock; if (loopId) { loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!); } else { CompilerError.invariant(false, { reason: `Unexpected 'for-in' where the loop is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', terminal: { kind: 'for-in', loc: terminal.loc, init: initValue, loop: loopBody, id: terminal.id, }, label: fallthroughId == null ? null : {id: fallthroughId, implicit: false}, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'branch': { let consequent: ReactiveBlock | null = null; if (this.cx.isScheduled(terminal.consequent)) { const break_ = this.visitBreak( terminal.consequent, terminal.id, terminal.loc, ); if (break_ !== null) { consequent = [break_]; } } else { consequent = this.traverseBlock( this.cx.ir.blocks.get(terminal.consequent)!, ); } let alternate: ReactiveBlock | null = null; if (this.cx.isScheduled(terminal.alternate)) { CompilerError.invariant(false, { reason: `Unexpected 'branch' where the alternate is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } else { alternate = this.traverseBlock( this.cx.ir.blocks.get(terminal.alternate)!, ); } blockValue.push({ kind: 'terminal', terminal: { kind: 'if', loc: terminal.loc, test: terminal.test, consequent: consequent ?? this.emptyBlock(), alternate: alternate, id: terminal.id, }, label: null, }); break; } case 'label': { const fallthroughId = this.cx.reachable(terminal.fallthrough) && !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; if (fallthroughId !== null) { const scheduleId = this.cx.schedule(fallthroughId, 'if'); scheduleIds.push(scheduleId); } let block: ReactiveBlock; if (this.cx.isScheduled(terminal.block)) { CompilerError.invariant(false, { reason: `Unexpected 'label' where the block is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } else { block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!); } this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', terminal: { kind: 'label', loc: terminal.loc, block, id: terminal.id, }, label: fallthroughId == null ? null : {id: fallthroughId, implicit: false}, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'sequence': case 'optional': case 'ternary': case 'logical': { const fallthroughId = terminal.fallthrough !== null && !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; if (fallthroughId !== null) { const scheduleId = this.cx.schedule(fallthroughId, 'if'); scheduleIds.push(scheduleId); } const {place, value} = this.visitValueBlockTerminal(terminal); this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'instruction', instruction: { id: terminal.id, lvalue: place, value, loc: terminal.loc, }, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'goto': { switch (terminal.variant) { case GotoVariant.Break: { const break_ = this.visitBreak( terminal.block, terminal.id, terminal.loc, ); if (break_ !== null) { blockValue.push(break_); } break; } case GotoVariant.Continue: { const continue_ = this.visitContinue( terminal.block, terminal.id, terminal.loc, ); if (continue_ !== null) { blockValue.push(continue_); } break; } case GotoVariant.Try: { break; } default: { assertExhaustive( terminal.variant, `Unexpected goto variant \`${terminal.variant}\``, ); } } break; } case 'maybe-throw': { /* * ReactiveFunction does not explicit model maybe-throw semantics, * so these terminals flatten away */ if (!this.cx.isScheduled(terminal.continuation)) { this.visitBlock( this.cx.ir.blocks.get(terminal.continuation)!, blockValue, ); } break; } case 'try': { const fallthroughId = this.cx.reachable(terminal.fallthrough) && !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; if (fallthroughId !== null) { const scheduleId = this.cx.schedule(fallthroughId, 'if'); scheduleIds.push(scheduleId); } this.cx.scheduleCatchHandler(terminal.handler); const block = this.traverseBlock( this.cx.ir.blocks.get(terminal.block)!, ); const handler = this.traverseBlock( this.cx.ir.blocks.get(terminal.handler)!, ); this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: 'terminal', label: fallthroughId == null ? null : {id: fallthroughId, implicit: false}, terminal: { kind: 'try', loc: terminal.loc, block, handlerBinding: terminal.handlerBinding, handler, id: terminal.id, }, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'pruned-scope': case 'scope': { const fallthroughId = !this.cx.isScheduled(terminal.fallthrough) ? terminal.fallthrough : null; if (fallthroughId !== null) { const scheduleId = this.cx.schedule(fallthroughId, 'if'); scheduleIds.push(scheduleId); this.cx.scopeFallthroughs.add(fallthroughId); } let block: ReactiveBlock; if (this.cx.isScheduled(terminal.block)) { CompilerError.invariant(false, { reason: `Unexpected 'scope' where the block is already scheduled`, description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], }); } else { block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!); } this.cx.unscheduleAll(scheduleIds); blockValue.push({ kind: terminal.kind, instructions: block, scope: terminal.scope, }); if (fallthroughId !== null) { this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); } break; } case 'unreachable': { // noop break; } case 'unsupported': { CompilerError.invariant(false, { reason: 'Unexpected unsupported terminal', description: null, details: [ { kind: 'error', loc: terminal.loc, message: null, }, ], suggestions: null, }); } default: { assertExhaustive(terminal, 'Unexpected terminal'); } } } visitValueBlock( id: BlockId, loc: SourceLocation, ): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} { const defaultBlock = this.cx.ir.blocks.get(id)!; if (defaultBlock.terminal.kind === 'branch') { const instructions = defaultBlock.instructions; if (instructions.length === 0) { return { block: defaultBlock.id, place: defaultBlock.terminal.test, value: { kind: 'LoadLocal', place: defaultBlock.terminal.test, loc: defaultBlock.terminal.test.loc, }, id: defaultBlock.terminal.id, }; } else if (defaultBlock.instructions.length === 1) { const instr = defaultBlock.instructions[0]!; CompilerError.invariant( instr.lvalue.identifier.id === defaultBlock.terminal.test.identifier.id, { reason: 'Expected branch block to end in an instruction that sets the test value', description: null, details: [ { kind: 'error', loc: instr.lvalue.loc, message: null, }, ], suggestions: null, }, ); return { block: defaultBlock.id, place: instr.lvalue!, value: instr.value, id: instr.id, }; } else { const instr = defaultBlock.instructions.at(-1)!; const sequence: ReactiveSequenceValue = { kind: 'SequenceExpression', instructions: defaultBlock.instructions.slice(0, -1), id: instr.id, value: instr.value, loc: loc, }; return { block: defaultBlock.id, place: defaultBlock.terminal.test, value: sequence, id: defaultBlock.terminal.id, }; } } else if (defaultBlock.terminal.kind === 'goto') { const instructions = defaultBlock.instructions; if (instructions.length === 0) { CompilerError.invariant(false, { reason: 'Expected goto value block to have at least one instruction', description: null, details: [ { kind: 'error', loc: null, message: null, }, ], suggestions: null, }); } else if (defaultBlock.instructions.length === 1) { const instr = defaultBlock.instructions[0]!; let place: Place = instr.lvalue; let value: ReactiveValue = instr.value; if ( /* * Value blocks generally end in a StoreLocal to assign the value of the * expression for this branch. These StoreLocal instructions can be pruned, * since we represent the value blocks as a compund value in ReactiveFunction * (no phis). However, it's also possible to have a value block that ends in * an AssignmentExpression, which we need to keep. So we only prune * StoreLocal for temporaries — any named/promoted values must be used * elsewhere and aren't safe to prune. */ value.kind === 'StoreLocal' && value.lvalue.place.identifier.name === null ) { place = value.lvalue.place; value = { kind: 'LoadLocal', place: value.value, loc: value.value.loc, }; } return { block: defaultBlock.id, place, value, id: instr.id, }; } else { const instr = defaultBlock.instructions.at(-1)!; let place: Place = instr.lvalue; let value: ReactiveValue = instr.value; if ( /* * Value blocks generally end in a StoreLocal to assign the value of the * expression for this branch. These StoreLocal instructions can be pruned, * since we represent the value blocks as a compund value in ReactiveFunction * (no phis). However, it's also possible to have a value block that ends in * an AssignmentExpression, which we need to keep. So we only prune * StoreLocal for temporaries — any named/promoted values must be used * elsewhere and aren't safe to prune. */ value.kind === 'StoreLocal' && value.lvalue.place.identifier.name === null ) { place = value.lvalue.place; value = { kind: 'LoadLocal', place: value.value, loc: value.value.loc, }; } const sequence: ReactiveSequenceValue = { kind: 'SequenceExpression', instructions: defaultBlock.instructions.slice(0, -1), id: instr.id, value, loc: loc, }; return { block: defaultBlock.id, place, value: sequence, id: instr.id, }; } } else { /* * The value block ended in a value terminal, recurse to get the value * of that terminal */ const init = this.visitValueBlockTerminal(defaultBlock.terminal); // Code following the logical terminal const final = this.visitValueBlock(init.fallthrough, loc); // Stitch the two together... const sequence: ReactiveSequenceValue = { kind: 'SequenceExpression', instructions: [ ...defaultBlock.instructions, { id: init.id, loc, lvalue: init.place, value: init.value, }, ], id: final.id, value: final.value, loc, }; return { block: init.fallthrough, value: sequence, place: final.place, id: final.id, }; } } visitValueBlockTerminal(terminal: Terminal): { value: ReactiveValue; place: Place; fallthrough: BlockId; id: InstructionId; } { switch (terminal.kind) { case 'sequence': { const block = this.visitValueBlock(terminal.block, terminal.loc); return { value: block.value, place: block.place, fallthrough: terminal.fallthrough, id: terminal.id, }; } case 'optional': { const test = this.visitValueBlock(terminal.test, terminal.loc); const testBlock = this.cx.ir.blocks.get(test.block)!; if (testBlock.terminal.kind !== 'branch') { CompilerError.throwTodo({ reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional test block`, description: null, loc: testBlock.terminal.loc, suggestions: null, }); } const consequent = this.visitValueBlock( testBlock.terminal.consequent, terminal.loc, ); const call: ReactiveSequenceValue = { kind: 'SequenceExpression', instructions: [ { id: test.id, loc: testBlock.terminal.loc, lvalue: test.place, value: test.value, }, ], id: consequent.id, value: consequent.value, loc: terminal.loc, }; return { place: {...consequent.place}, value: { kind: 'OptionalExpression', optional: terminal.optional, value: call, id: terminal.id, loc: terminal.loc, }, fallthrough: terminal.fallthrough, id: terminal.id, }; } case 'logical': { const test = this.visitValueBlock(terminal.test, terminal.loc); const testBlock = this.cx.ir.blocks.get(test.block)!; if (testBlock.terminal.kind !== 'branch') { CompilerError.throwTodo({ reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for logical test block`, description: null, loc: testBlock.terminal.loc, suggestions: null, }); } const leftFinal = this.visitValueBlock( testBlock.terminal.consequent, terminal.loc, ); const left: ReactiveSequenceValue = { kind: 'SequenceExpression', instructions: [ { id: test.id, loc: terminal.loc, lvalue: test.place, value: test.value, }, ], id: leftFinal.id, value: leftFinal.value, loc: terminal.loc, }; const right = this.visitValueBlock( testBlock.terminal.alternate, terminal.loc, ); const value: ReactiveLogicalValue = { kind: 'LogicalExpression', operator: terminal.operator, left: left, right: right.value, loc: terminal.loc, }; return { place: {...leftFinal.place}, value, fallthrough: terminal.fallthrough, id: terminal.id, }; } case 'ternary': { const test = this.visitValueBlock(terminal.test, terminal.loc); const testBlock = this.cx.ir.blocks.get(test.block)!; if (testBlock.terminal.kind !== 'branch') { CompilerError.throwTodo({ reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ternary test block`, description: null, loc: testBlock.terminal.loc, suggestions: null, }); } const consequent = this.visitValueBlock( testBlock.terminal.consequent, terminal.loc, ); const alternate = this.visitValueBlock( testBlock.terminal.alternate, terminal.loc, ); const value: ReactiveTernaryValue = { kind: 'ConditionalExpression', test: test.value, consequent: consequent.value, alternate: alternate.value, loc: terminal.loc, }; return { place: {...consequent.place}, value, fallthrough: terminal.fallthrough, id: terminal.id, }; } case 'maybe-throw': { CompilerError.throwTodo({ reason: `Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`, description: null, loc: terminal.loc, suggestions: null, }); } case 'label': { CompilerError.throwTodo({ reason: `Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc)`, description: null, loc: terminal.loc, suggestions: null, }); } default: { CompilerError.throwTodo({ reason: `Support \`${terminal.kind}\` as a value block terminal (conditional, logical, optional chaining, etc)`, description: null, loc: terminal.loc, suggestions: null, }); } } } emptyBlock(): ReactiveBlock { return []; } visitBreak( block: BlockId, id: InstructionId, loc: SourceLocation, ): ReactiveTerminalStatement | null { const target = this.cx.getBreakTarget(block); if (target === null) { CompilerError.invariant(false, { reason: 'Expected a break target', description: null, details: [ { kind: 'error', loc: null, message: null, }, ], suggestions: null, }); } if (this.cx.scopeFallthroughs.has(target.block)) { CompilerError.invariant(target.type === 'implicit', { reason: 'Expected reactive scope to implicitly break to fallthrough', description: null, details: [ { kind: 'error', loc, message: null, }, ], }); return null; } return { kind: 'terminal', terminal: { kind: 'break', loc, target: target.block, id, targetKind: target.type, }, label: null, }; } visitContinue( block: BlockId, id: InstructionId, loc: SourceLocation, ): ReactiveTerminalStatement { const target = this.cx.getContinueTarget(block); CompilerError.invariant(target !== null, { reason: `Expected continue target to be scheduled for bb${block}`, description: null, details: [ { kind: 'error', loc: null, message: null, }, ], suggestions: null, }); return { kind: 'terminal', terminal: { kind: 'continue', loc, target: target.block, id, targetKind: target.type, }, label: null, }; } } 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(); scopeFallthroughs: 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(); #catchHandlers: 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)!; } scheduleCatchHandler(block: BlockId): void { this.#catchHandlers.add(block); } reachable(id: BlockId): boolean { const block = this.ir.blocks.get(id)!; return block.terminal.kind !== 'unreachable'; } /* * Record that the given block will be emitted (eg by the codegen of a parent node) * so that child nodes can avoid re-emitting it. */ schedule(block: BlockId, type: 'if' | 'switch' | 'case'): number { const id = this.#nextScheduleId++; CompilerError.invariant(!this.#scheduled.has(block), { reason: `Break block is already scheduled: bb${block}`, description: null, details: [ { kind: 'error', loc: null, message: null, }, ], suggestions: null, }); this.#scheduled.add(block); this.#controlFlowStack.push({block, id, type}); return id; } scheduleLoop( fallthroughBlock: BlockId, continueBlock: BlockId, loopBlock: BlockId | null, ): number { const id = this.#nextScheduleId++; const ownsBlock = !this.#scheduled.has(fallthroughBlock); this.#scheduled.add(fallthroughBlock); CompilerError.invariant(!this.#scheduled.has(continueBlock), { reason: `Continue block is already scheduled: bb${continueBlock}`, description: null, details: [ { kind: 'error', loc: null, message: null, }, ], suggestions: null, }); this.#scheduled.add(continueBlock); let ownsLoop = false; if (loopBlock !== null) { ownsLoop = !this.#scheduled.has(loopBlock); this.#scheduled.add(loopBlock); } this.#controlFlowStack.push({ block: fallthroughBlock, ownsBlock, id, type: 'loop', continueBlock, loopBlock, ownsLoop, }); return id; } // Removes a block that was scheduled; must be called after that block is emitted. unschedule(scheduleId: number): void { const last = this.#controlFlowStack.pop(); CompilerError.invariant(last !== undefined && last.id === scheduleId, { reason: 'Can only unschedule the last target', description: null, details: [ { kind: 'error', loc: null, message: null, }, ], suggestions: null, }); if (last.type !== 'loop' || last.ownsBlock !== null) { this.#scheduled.delete(last.block); } if (last.type === 'loop') { this.#scheduled.delete(last.continueBlock); if (last.ownsLoop && last.loopBlock !== null) { this.#scheduled.delete(last.loopBlock); } } } /* * Helper to unschedule multiple scheduled blocks. The ids should be in * the order in which they were scheduled, ie most recently scheduled last. */ unscheduleAll(scheduleIds: Array): void { for (let i = scheduleIds.length - 1; i >= 0; i--) { this.unschedule(scheduleIds[i]!); } } // Check if the given @param block is scheduled or not. isScheduled(block: BlockId): boolean { return this.#scheduled.has(block) || this.#catchHandlers.has(block); } /* * Given the current control flow stack, determines how a `break` to the given @param block * must be emitted. Returns as follows: * - 'implicit' if control would implicitly transfer to that block * - 'labeled' if a labeled break is required to transfer control to that block * - 'unlabeled' if an unlabeled break would transfer to that block * - null if there is no information for this block * * The returned 'block' value should be used as the label if necessary. */ getBreakTarget(block: BlockId): { block: BlockId; type: ReactiveTerminalTargetKind; } { let hasPrecedingLoop = false; for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) { const target = this.#controlFlowStack[i]!; if (target.block === block) { let type: ReactiveTerminalTargetKind; if (target.type === 'loop') { /* * breaking out of a loop requires an explicit break, * but only requires a label if breaking past the innermost loop. */ type = hasPrecedingLoop ? 'labeled' : 'unlabeled'; } else if (i === this.#controlFlowStack.length - 1) { /* * breaking to the last break point, which is where control will transfer * implicitly */ type = 'implicit'; } else { // breaking somewhere else requires an explicit break type = 'labeled'; } return { block: target.block, type, }; } hasPrecedingLoop ||= target.type === 'loop'; } CompilerError.invariant(false, { reason: 'Expected a break target', description: null, details: [ { kind: 'error', loc: null, message: null, }, ], suggestions: null, }); } /* * Given the current control flow stack, determines how a `continue` to the given @param block * must be emitted. Returns as follows: * - 'implicit' if control would implicitly continue to that block * - 'labeled' if a labeled continue is required to continue to that block * - 'unlabeled' if an unlabeled continue would transfer to that block * - null if there is no information for this block * * The returned 'block' value should be used as the label if necessary. */ getContinueTarget( block: BlockId, ): {block: BlockId; type: ReactiveTerminalTargetKind} | null { let hasPrecedingLoop = false; for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) { const target = this.#controlFlowStack[i]!; if (target.type == 'loop' && target.continueBlock === block) { let type: ReactiveTerminalTargetKind; if (hasPrecedingLoop) { /* * continuing to a loop that is not the innermost loop always requires * a label */ type = 'labeled'; } else if (i === this.#controlFlowStack.length - 1) { /* * continuing to the last break point, which is where control will * transfer to naturally */ type = 'implicit'; } else { /* * the continue is inside some conditional logic, requires an explicit * continue */ type = 'unlabeled'; } return { block: target.block, type, }; } hasPrecedingLoop ||= target.type === 'loop'; } return null; } debugBreakTargets(): Array { return this.#controlFlowStack.map(target => ({...target})); } } type ControlFlowTarget = | {type: 'if'; block: BlockId; id: number} | {type: 'switch'; block: BlockId; id: number} | {type: 'case'; block: BlockId; id: number} | { type: 'loop'; block: BlockId; ownsBlock: boolean; continueBlock: BlockId; loopBlock: BlockId | null; ownsLoop: boolean; id: number; };