import {CompilerError} from '../CompilerError';
import {Environment} from '../HIR';
import {
areEqualPaths,
BlockId,
DeclarationId,
GeneratedSource,
Identifier,
InstructionId,
InstructionKind,
isObjectMethodType,
isRefValueType,
isUseRefType,
makeInstructionId,
Place,
PrunedReactiveScopeBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveOptionalCallValue,
ReactiveScope,
ReactiveScopeBlock,
ReactiveScopeDependency,
ReactiveTerminalStatement,
ReactiveValue,
ScopeId,
} from '../HIR/HIR';
import {eachInstructionValueOperand, eachPatternOperand} from '../HIR/visitors';
import {empty, Stack} from '../Utils/Stack';
import {assertExhaustive, Iterable_some} from '../Utils/utils';
import {
ReactiveScopeDependencyTree,
ReactiveScopePropertyDependency,
} from './DeriveMinimalDependencies';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
export function propagateScopeDependencies(fn: ReactiveFunction): void {
const escapingTemporaries: TemporariesUsedOutsideDefiningScope = {
declarations: new Map(),
usedOutsideDeclaringScope: new Set(),
};
visitReactiveFunction(fn, new FindPromotedTemporaries(), escapingTemporaries);
const context = new Context(escapingTemporaries.usedOutsideDeclaringScope);
for (const param of fn.params) {
if (param.kind === 'Identifier') {
context.declare(param.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
} else {
context.declare(param.place.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
}
}
visitReactiveFunction(fn, new PropagationVisitor(fn.env), context);
}
type TemporariesUsedOutsideDefiningScope = {
declarations: Map<DeclarationId, ScopeId>;
usedOutsideDeclaringScope: Set<DeclarationId>;
};
class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOutsideDefiningScope> {
scopes: Array<ScopeId> = [];
override visitScope(
scope: ReactiveScopeBlock,
state: TemporariesUsedOutsideDefiningScope,
): void {
this.scopes.push(scope.scope.id);
this.traverseScope(scope, state);
this.scopes.pop();
}
override visitInstruction(
instruction: ReactiveInstruction,
state: TemporariesUsedOutsideDefiningScope,
): void {
this.traverseInstruction(instruction, state);
const scope = this.scopes.at(-1);
if (instruction.lvalue === null || scope === undefined) {
return;
}
switch (instruction.value.kind) {
case 'LoadLocal':
case 'LoadContext':
case 'PropertyLoad': {
state.declarations.set(
instruction.lvalue.identifier.declarationId,
scope,
);
break;
}
default: {
break;
}
}
}
override visitPlace(
_id: InstructionId,
place: Place,
state: TemporariesUsedOutsideDefiningScope,
): void {
const declaringScope = state.declarations.get(
place.identifier.declarationId,
);
if (declaringScope === undefined) {
return;
}
if (this.scopes.indexOf(declaringScope) === -1) {
state.usedOutsideDeclaringScope.add(place.identifier.declarationId);
}
}
}
type DeclMap = Map<DeclarationId, Decl>;
type Decl = {
id: InstructionId;
scope: Stack<ScopeTraversalState>;
};
type ScopeTraversalState = {
value: ReactiveScope;
ownBlocks: Stack<BlockId>;
};
class PoisonState {
poisonedBlocks: Set<BlockId> = new Set();
poisonedScopes: Set<ScopeId> = new Set();
isPoisoned: boolean = false;
constructor(
poisonedBlocks: Set<BlockId>,
poisonedScopes: Set<ScopeId>,
isPoisoned: boolean,
) {
this.poisonedBlocks = poisonedBlocks;
this.poisonedScopes = poisonedScopes;
this.isPoisoned = isPoisoned;
}
clone(): PoisonState {
return new PoisonState(
new Set(this.poisonedBlocks),
new Set(this.poisonedScopes),
this.isPoisoned,
);
}
take(other: PoisonState): PoisonState {
const copy = new PoisonState(
this.poisonedBlocks,
this.poisonedScopes,
this.isPoisoned,
);
this.poisonedBlocks = other.poisonedBlocks;
this.poisonedScopes = other.poisonedScopes;
this.isPoisoned = other.isPoisoned;
return copy;
}
merge(
others: Array<PoisonState>,
currentScope: ScopeTraversalState | null,
): void {
for (const other of others) {
for (const id of other.poisonedBlocks) {
this.poisonedBlocks.add(id);
}
for (const id of other.poisonedScopes) {
this.poisonedScopes.add(id);
}
}
this.#invalidate(currentScope);
}
#invalidate(currentScope: ScopeTraversalState | null): void {
if (currentScope != null) {
if (this.poisonedScopes.has(currentScope.value.id)) {
this.isPoisoned = true;
return;
} else if (
currentScope.ownBlocks.find(blockId => this.poisonedBlocks.has(blockId))
) {
this.isPoisoned = true;
return;
}
}
this.isPoisoned = false;
}
addPoisonTarget(
target: BlockId | null,
activeScopes: Stack<ScopeTraversalState>,
): void {
const currentScope = activeScopes.value;
if (target == null && currentScope != null) {
let cursor = activeScopes;
while (true) {
const next = cursor.pop();
if (next.value == null) {
const poisonedScope = cursor.value!.value.id;
this.poisonedScopes.add(poisonedScope);
if (poisonedScope === currentScope?.value.id) {
this.isPoisoned = true;
}
break;
} else {
cursor = next;
}
}
} else if (target != null) {
this.poisonedBlocks.add(target);
if (
!this.isPoisoned &&
currentScope?.ownBlocks.find(blockId => blockId === target)
) {
this.isPoisoned = true;
}
}
}
removeMaybePoisonedScope(
id: ScopeId,
currentScope: ScopeTraversalState | null,
): void {
this.poisonedScopes.delete(id);
this.#invalidate(currentScope);
}
removeMaybePoisonedBlock(
id: BlockId,
currentScope: ScopeTraversalState | null,
): void {
this.poisonedBlocks.delete(id);
this.#invalidate(currentScope);
}
}
class Context {
#temporariesUsedOutsideScope: Set<DeclarationId>;
#declarations: DeclMap = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
#dependencies: ReactiveScopeDependencyTree =
new ReactiveScopeDependencyTree();
#properties: Map<Identifier, ReactiveScopePropertyDependency> = new Map();
#temporaries: Map<Identifier, Place> = new Map();
#inConditionalWithinScope: boolean = false;
#depsInCurrentConditional: ReactiveScopeDependencyTree =
new ReactiveScopeDependencyTree();
#scopes: Stack<ScopeTraversalState> = empty();
poisonState: PoisonState = new PoisonState(new Set(), new Set(), false);
constructor(temporariesUsedOutsideScope: Set<DeclarationId>) {
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
}
enter(scope: ReactiveScope, fn: () => void): Set<ReactiveScopeDependency> {
const prevInConditional = this.#inConditionalWithinScope;
const previousDependencies = this.#dependencies;
const prevDepsInConditional: ReactiveScopeDependencyTree | null = this
.isPoisoned
? this.#depsInCurrentConditional
: null;
if (prevDepsInConditional != null) {
this.#depsInCurrentConditional = new ReactiveScopeDependencyTree();
}
const scopedDependencies = new ReactiveScopeDependencyTree();
this.#inConditionalWithinScope = false;
this.#dependencies = scopedDependencies;
this.#scopes = this.#scopes.push({
value: scope,
ownBlocks: empty(),
});
this.poisonState.isPoisoned = false;
fn();
this.#scopes = this.#scopes.pop();
this.poisonState.removeMaybePoisonedScope(scope.id, this.#scopes.value);
this.#dependencies = previousDependencies;
this.#inConditionalWithinScope = prevInConditional;
const minInnerScopeDependencies =
scopedDependencies.deriveMinimalDependencies();
this.#dependencies.addDepsFromInnerScope(
scopedDependencies,
this.#inConditionalWithinScope || this.isPoisoned,
this.#checkValidDependency.bind(this),
);
if (prevDepsInConditional != null) {
prevDepsInConditional.addDepsFromInnerScope(
this.#depsInCurrentConditional,
true,
this.#checkValidDependency.bind(this),
);
this.#depsInCurrentConditional = prevDepsInConditional;
}
return minInnerScopeDependencies;
}
isUsedOutsideDeclaringScope(place: Place): boolean {
return this.#temporariesUsedOutsideScope.has(
place.identifier.declarationId,
);
}
printDeps(includeAccesses: boolean = false): string {
return this.#dependencies.printDeps(includeAccesses);
}
enterConditional(fn: () => void): ReactiveScopeDependencyTree {
const prevInConditional = this.#inConditionalWithinScope;
const prevUncondAccessed = this.#depsInCurrentConditional;
this.#inConditionalWithinScope = true;
this.#depsInCurrentConditional = new ReactiveScopeDependencyTree();
fn();
const result = this.#depsInCurrentConditional;
this.#inConditionalWithinScope = prevInConditional;
this.#depsInCurrentConditional = prevUncondAccessed;
return result;
}
promoteDepsFromExhaustiveConditionals(
depsInConditionals: Array<ReactiveScopeDependencyTree>,
): void {
this.#dependencies.promoteDepsFromExhaustiveConditionals(
depsInConditionals,
);
this.#depsInCurrentConditional.promoteDepsFromExhaustiveConditionals(
depsInConditionals,
);
}
declare(identifier: Identifier, decl: Decl): void {
if (!this.#declarations.has(identifier.declarationId)) {
this.#declarations.set(identifier.declarationId, decl);
}
this.#reassignments.set(identifier, decl);
}
declareTemporary(lvalue: Place, place: Place): void {
this.#temporaries.set(lvalue.identifier, place);
}
resolveTemporary(place: Place): Place {
return this.#temporaries.get(place.identifier) ?? place;
}
#getProperty(
object: Place,
property: string,
optional: boolean,
): ReactiveScopePropertyDependency {
const resolvedObject = this.resolveTemporary(object);
const resolvedDependency = this.#properties.get(resolvedObject.identifier);
let objectDependency: ReactiveScopePropertyDependency;
if (resolvedDependency === undefined) {
objectDependency = {
identifier: resolvedObject.identifier,
path: [],
};
} else {
objectDependency = {
identifier: resolvedDependency.identifier,
path: [...resolvedDependency.path],
};
}
objectDependency.path.push({property, optional});
return objectDependency;
}
declareProperty(
lvalue: Place,
object: Place,
property: string,
optional: boolean,
): void {
const nextDependency = this.#getProperty(object, property, optional);
this.#properties.set(lvalue.identifier, nextDependency);
}
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
if (
isUseRefType(maybeDependency.identifier) &&
maybeDependency.path.at(0)?.property === 'current'
) {
return false;
}
if (isRefValueType(maybeDependency.identifier)) {
return false;
}
if (isObjectMethodType(maybeDependency.identifier)) {
return false;
}
const identifier = maybeDependency.identifier;
const currentDeclaration =
this.#reassignments.get(identifier) ??
this.#declarations.get(identifier.declarationId);
const currentScope = this.currentScope.value?.value;
return (
currentScope != null &&
currentDeclaration !== undefined &&
currentDeclaration.id < currentScope.range.start &&
(currentDeclaration.scope == null ||
currentDeclaration.scope.value?.value !== currentScope)
);
}
#isScopeActive(scope: ReactiveScope): boolean {
if (this.#scopes === null) {
return false;
}
return this.#scopes.find(state => state.value === scope);
}
get currentScope(): Stack<ScopeTraversalState> {
return this.#scopes;
}
get isPoisoned(): boolean {
return this.poisonState.isPoisoned;
}
visitOperand(place: Place): void {
const resolved = this.resolveTemporary(place);
let dependency: ReactiveScopePropertyDependency = {
identifier: resolved.identifier,
path: [],
};
if (resolved.identifier.name === null) {
const propertyDependency = this.#properties.get(resolved.identifier);
if (propertyDependency !== undefined) {
dependency = {...propertyDependency};
}
}
this.visitDependency(dependency);
}
visitProperty(object: Place, property: string, optional: boolean): void {
const nextDependency = this.#getProperty(object, property, optional);
this.visitDependency(nextDependency);
}
visitDependency(maybeDependency: ReactiveScopePropertyDependency): void {
const originalDeclaration = this.#declarations.get(
maybeDependency.identifier.declarationId,
);
if (
originalDeclaration !== undefined &&
originalDeclaration.scope.value !== null
) {
originalDeclaration.scope.each(scope => {
if (
!this.#isScopeActive(scope.value) &&
!Iterable_some(
scope.value.declarations.values(),
decl =>
decl.identifier.declarationId ===
maybeDependency.identifier.declarationId,
)
) {
scope.value.declarations.set(maybeDependency.identifier.id, {
identifier: maybeDependency.identifier,
scope: originalDeclaration.scope.value!.value,
});
}
});
}
if (this.#checkValidDependency(maybeDependency)) {
const isPoisoned = this.isPoisoned;
this.#depsInCurrentConditional.add(maybeDependency, isPoisoned);
this.#dependencies.add(
maybeDependency,
this.#inConditionalWithinScope || isPoisoned,
);
}
}
visitReassignment(place: Place): void {
const currentScope = this.currentScope.value?.value;
if (
currentScope != null &&
!Iterable_some(
currentScope.reassignments,
identifier =>
identifier.declarationId === place.identifier.declarationId,
) &&
this.#checkValidDependency({identifier: place.identifier, path: []})
) {
currentScope.reassignments.add(place.identifier);
}
}
pushLabeledBlock(id: BlockId): void {
const currentScope = this.#scopes.value;
if (currentScope != null) {
currentScope.ownBlocks = currentScope.ownBlocks.push(id);
}
}
popLabeledBlock(id: BlockId): void {
const currentScope = this.#scopes.value;
if (currentScope != null) {
const last = currentScope.ownBlocks.value;
currentScope.ownBlocks = currentScope.ownBlocks.pop();
CompilerError.invariant(last != null && last === id, {
reason: '[PropagateScopeDependencies] Misformed block stack',
loc: GeneratedSource,
});
}
this.poisonState.removeMaybePoisonedBlock(id, currentScope);
}
}
class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
env: Environment;
constructor(env: Environment) {
super();
this.env = env;
}
override visitScope(scope: ReactiveScopeBlock, context: Context): void {
const scopeDependencies = context.enter(scope.scope, () => {
this.visitBlock(scope.instructions, context);
});
for (const candidateDep of scopeDependencies) {
if (
!Iterable_some(
scope.scope.dependencies,
existingDep =>
existingDep.identifier.declarationId ===
candidateDep.identifier.declarationId &&
areEqualPaths(existingDep.path, candidateDep.path),
)
) {
scope.scope.dependencies.add(candidateDep);
}
}
}
override visitPrunedScope(
scopeBlock: PrunedReactiveScopeBlock,
context: Context,
): void {
const _scopeDepdencies = context.enter(scopeBlock.scope, () => {
this.visitBlock(scopeBlock.instructions, context);
});
}
override visitInstruction(
instruction: ReactiveInstruction,
context: Context,
): void {
const {id, value, lvalue} = instruction;
this.visitInstructionValue(context, id, value, lvalue);
if (lvalue == null) {
return;
}
context.declare(lvalue.identifier, {
id,
scope: context.currentScope,
});
}
extractOptionalProperty(
context: Context,
optionalValue: ReactiveOptionalCallValue,
lvalue: Place,
): {
lvalue: Place;
object: Place;
property: string;
optional: boolean;
} | null {
const sequence = optionalValue.value;
CompilerError.invariant(sequence.kind === 'SequenceExpression', {
reason: 'Expected OptionalExpression value to be a SequenceExpression',
description: `Found a \`${sequence.kind}\``,
loc: sequence.loc,
});
if (
sequence.instructions.length === 1 &&
sequence.instructions[0].lvalue !== null &&
sequence.instructions[0].value.kind === 'LoadLocal' &&
sequence.instructions[0].value.place.identifier.name !== null &&
!context.isUsedOutsideDeclaringScope(sequence.instructions[0].lvalue) &&
sequence.value.kind === 'SequenceExpression' &&
sequence.value.instructions.length === 1 &&
sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
sequence.value.instructions[0].value.object.identifier.id ===
sequence.instructions[0].lvalue.identifier.id &&
sequence.value.instructions[0].lvalue !== null &&
sequence.value.value.kind === 'LoadLocal' &&
sequence.value.value.place.identifier.id ===
sequence.value.instructions[0].lvalue.identifier.id
) {
context.declareTemporary(
sequence.instructions[0].lvalue,
sequence.instructions[0].value.place,
);
const propertyLoad = sequence.value.instructions[0].value;
return {
lvalue,
object: propertyLoad.object,
property: propertyLoad.property,
optional: optionalValue.optional,
};
}
if (
sequence.instructions.length === 1 &&
sequence.instructions[0].lvalue !== null &&
sequence.instructions[0].value.kind === 'SequenceExpression' &&
sequence.instructions[0].value.instructions.length === 1 &&
sequence.instructions[0].value.instructions[0].lvalue !== null &&
sequence.instructions[0].value.instructions[0].value.kind ===
'LoadLocal' &&
sequence.instructions[0].value.instructions[0].value.place.identifier
.name !== null &&
!context.isUsedOutsideDeclaringScope(
sequence.instructions[0].value.instructions[0].lvalue,
) &&
sequence.instructions[0].value.value.kind === 'PropertyLoad' &&
sequence.instructions[0].value.value.object.identifier.id ===
sequence.instructions[0].value.instructions[0].lvalue.identifier.id &&
sequence.value.kind === 'SequenceExpression' &&
sequence.value.instructions.length === 1 &&
sequence.value.instructions[0].lvalue !== null &&
sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
sequence.value.instructions[0].value.object.identifier.id ===
sequence.instructions[0].lvalue.identifier.id &&
sequence.value.value.kind === 'LoadLocal' &&
sequence.value.value.place.identifier.id ===
sequence.value.instructions[0].lvalue.identifier.id
) {
context.declareTemporary(
sequence.instructions[0].value.instructions[0].lvalue,
sequence.instructions[0].value.instructions[0].value.place,
);
context.declareProperty(
sequence.instructions[0].lvalue,
sequence.instructions[0].value.value.object,
sequence.instructions[0].value.value.property,
false,
);
const propertyLoad = sequence.value.instructions[0].value;
return {
lvalue,
object: propertyLoad.object,
property: propertyLoad.property,
optional: optionalValue.optional,
};
}
if (
sequence.instructions.length === 1 &&
sequence.instructions[0].value.kind === 'SequenceExpression' &&
sequence.instructions[0].value.instructions.length === 1 &&
sequence.instructions[0].value.instructions[0].lvalue !== null &&
sequence.instructions[0].value.instructions[0].value.kind ===
'OptionalExpression' &&
sequence.instructions[0].value.value.kind === 'LoadLocal' &&
sequence.instructions[0].value.value.place.identifier.id ===
sequence.instructions[0].value.instructions[0].lvalue.identifier.id &&
sequence.value.kind === 'SequenceExpression' &&
sequence.value.instructions.length === 1 &&
sequence.value.instructions[0].lvalue !== null &&
sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
sequence.value.instructions[0].value.object.identifier.id ===
sequence.instructions[0].value.value.place.identifier.id &&
sequence.value.value.kind === 'LoadLocal' &&
sequence.value.value.place.identifier.id ===
sequence.value.instructions[0].lvalue.identifier.id
) {
const {lvalue: innerLvalue, value: innerOptional} =
sequence.instructions[0].value.instructions[0];
const innerProperty = this.extractOptionalProperty(
context,
innerOptional,
innerLvalue,
);
if (innerProperty === null) {
return null;
}
context.declareProperty(
innerProperty.lvalue,
innerProperty.object,
innerProperty.property,
innerProperty.optional,
);
const propertyLoad = sequence.value.instructions[0].value;
return {
lvalue,
object: propertyLoad.object,
property: propertyLoad.property,
optional: optionalValue.optional,
};
}
return null;
}
visitOptionalExpression(
context: Context,
id: InstructionId,
value: ReactiveOptionalCallValue,
lvalue: Place | null,
): void {
if (
lvalue !== null &&
value.optional &&
this.env.config.enableOptionalDependencies
) {
const inner = this.extractOptionalProperty(context, value, lvalue);
if (inner !== null) {
context.visitProperty(inner.object, inner.property, inner.optional);
return;
}
}
const inner = value.value;
CompilerError.invariant(inner.kind === 'SequenceExpression', {
reason: 'Expected OptionalExpression value to be a SequenceExpression',
description: `Found a \`${value.kind}\``,
loc: value.loc,
suggestions: null,
});
for (const instr of inner.instructions) {
this.visitInstruction(instr, context);
}
context.enterConditional(() => {
this.visitReactiveValue(context, id, inner.value, null);
});
}
visitReactiveValue(
context: Context,
id: InstructionId,
value: ReactiveValue,
lvalue: Place | null,
): void {
switch (value.kind) {
case 'OptionalExpression': {
this.visitOptionalExpression(context, id, value, lvalue);
break;
}
case 'LogicalExpression': {
this.visitReactiveValue(context, id, value.left, null);
context.enterConditional(() => {
this.visitReactiveValue(context, id, value.right, null);
});
break;
}
case 'ConditionalExpression': {
this.visitReactiveValue(context, id, value.test, null);
const consequentDeps = context.enterConditional(() => {
this.visitReactiveValue(context, id, value.consequent, null);
});
const alternateDeps = context.enterConditional(() => {
this.visitReactiveValue(context, id, value.alternate, null);
});
context.promoteDepsFromExhaustiveConditionals([
consequentDeps,
alternateDeps,
]);
break;
}
case 'SequenceExpression': {
for (const instr of value.instructions) {
this.visitInstruction(instr, context);
}
this.visitInstructionValue(context, id, value.value, null);
break;
}
case 'FunctionExpression': {
if (this.env.config.enableTreatFunctionDepsAsConditional) {
context.enterConditional(() => {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
});
} else {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
}
break;
}
case 'ReactiveFunctionValue': {
CompilerError.invariant(false, {
reason: `Unexpected ReactiveFunctionValue`,
loc: value.loc,
description: null,
suggestions: null,
});
}
default: {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
}
}
}
visitInstructionValue(
context: Context,
id: InstructionId,
value: ReactiveValue,
lvalue: Place | null,
): void {
if (value.kind === 'LoadLocal' && lvalue !== null) {
if (
value.place.identifier.name !== null &&
lvalue.identifier.name === null &&
!context.isUsedOutsideDeclaringScope(lvalue)
) {
context.declareTemporary(lvalue, value.place);
} else {
context.visitOperand(value.place);
}
} else if (value.kind === 'PropertyLoad') {
if (lvalue !== null && !context.isUsedOutsideDeclaringScope(lvalue)) {
context.declareProperty(lvalue, value.object, value.property, false);
} else {
context.visitProperty(value.object, value.property, false);
}
} else if (value.kind === 'StoreLocal') {
context.visitOperand(value.value);
if (value.lvalue.kind === InstructionKind.Reassign) {
context.visitReassignment(value.lvalue.place);
}
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
} else if (
value.kind === 'DeclareLocal' ||
value.kind === 'DeclareContext'
) {
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
} else if (value.kind === 'Destructure') {
context.visitOperand(value.value);
for (const place of eachPatternOperand(value.lvalue.pattern)) {
if (value.lvalue.kind === InstructionKind.Reassign) {
context.visitReassignment(place);
}
context.declare(place.identifier, {
id,
scope: context.currentScope,
});
}
} else {
this.visitReactiveValue(context, id, value, lvalue);
}
}
enterTerminal(stmt: ReactiveTerminalStatement, context: Context): void {
if (stmt.label != null) {
context.pushLabeledBlock(stmt.label.id);
}
const terminal = stmt.terminal;
switch (terminal.kind) {
case 'continue':
case 'break': {
context.poisonState.addPoisonTarget(
terminal.target,
context.currentScope,
);
break;
}
case 'throw':
case 'return': {
context.poisonState.addPoisonTarget(null, context.currentScope);
break;
}
}
}
exitTerminal(stmt: ReactiveTerminalStatement, context: Context): void {
if (stmt.label != null) {
context.popLabeledBlock(stmt.label.id);
}
}
override visitTerminal(
stmt: ReactiveTerminalStatement,
context: Context,
): void {
this.enterTerminal(stmt, context);
const terminal = stmt.terminal;
switch (terminal.kind) {
case 'break':
case 'continue': {
break;
}
case 'return': {
context.visitOperand(terminal.value);
break;
}
case 'throw': {
context.visitOperand(terminal.value);
break;
}
case 'for': {
this.visitReactiveValue(context, terminal.id, terminal.init, null);
this.visitReactiveValue(context, terminal.id, terminal.test, null);
context.enterConditional(() => {
this.visitBlock(terminal.loop, context);
if (terminal.update !== null) {
this.visitReactiveValue(
context,
terminal.id,
terminal.update,
null,
);
}
});
break;
}
case 'for-of': {
this.visitReactiveValue(context, terminal.id, terminal.init, null);
context.enterConditional(() => {
this.visitBlock(terminal.loop, context);
});
break;
}
case 'for-in': {
this.visitReactiveValue(context, terminal.id, terminal.init, null);
context.enterConditional(() => {
this.visitBlock(terminal.loop, context);
});
break;
}
case 'do-while': {
this.visitBlock(terminal.loop, context);
context.enterConditional(() => {
this.visitReactiveValue(context, terminal.id, terminal.test, null);
});
break;
}
case 'while': {
this.visitReactiveValue(context, terminal.id, terminal.test, null);
context.enterConditional(() => {
this.visitBlock(terminal.loop, context);
});
break;
}
case 'if': {
context.visitOperand(terminal.test);
const {consequent, alternate} = terminal;
const prevPoisonState = context.poisonState.clone();
const depsInIf = context.enterConditional(() => {
this.visitBlock(consequent, context);
});
if (alternate !== null) {
const ifPoisonState = context.poisonState.take(prevPoisonState);
const depsInElse = context.enterConditional(() => {
this.visitBlock(alternate, context);
});
context.poisonState.merge(
[ifPoisonState],
context.currentScope.value,
);
context.promoteDepsFromExhaustiveConditionals([depsInIf, depsInElse]);
}
break;
}
case 'switch': {
context.visitOperand(terminal.test);
const isDefaultOnly =
terminal.cases.length === 1 && terminal.cases[0].test == null;
if (isDefaultOnly) {
const case_ = terminal.cases[0];
if (case_.block != null) {
this.visitBlock(case_.block, context);
break;
}
}
const depsInCases = [];
let foundDefault = false;
const prevPoisonState = context.poisonState.clone();
const mutExPoisonStates: Array<PoisonState> = [];
for (const {test, block} of terminal.cases) {
if (test !== null) {
context.visitOperand(test);
} else {
foundDefault = true;
}
if (block !== undefined) {
mutExPoisonStates.push(
context.poisonState.take(prevPoisonState.clone()),
);
depsInCases.push(
context.enterConditional(() => {
this.visitBlock(block, context);
}),
);
}
}
if (foundDefault) {
context.promoteDepsFromExhaustiveConditionals(depsInCases);
}
context.poisonState.merge(
mutExPoisonStates,
context.currentScope.value,
);
break;
}
case 'label': {
this.visitBlock(terminal.block, context);
break;
}
case 'try': {
this.visitBlock(terminal.block, context);
this.visitBlock(terminal.handler, context);
break;
}
default: {
assertExhaustive(
terminal,
`Unexpected terminal kind \`${(terminal as any).kind}\``,
);
}
}
this.exitTerminal(stmt, context);
}
}