import { CompilerError } from "../CompilerError";
import {
BlockId,
GeneratedSource,
Identifier,
IdentifierId,
InstructionId,
InstructionKind,
isObjectMethodType,
isRefValueType,
isUseRefType,
makeInstructionId,
Place,
PrunedReactiveScopeBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveScope,
ReactiveScopeBlock,
ReactiveScopeDependency,
ReactiveTerminalStatement,
ReactiveValue,
ScopeId,
} from "../HIR/HIR";
import {
eachInstructionValueOperand,
eachPatternOperand,
} from "../HIR/visitors";
import { empty, Stack } from "../Utils/Stack";
import { assertExhaustive } 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.config.enableTreatFunctionDepsAsConditional),
context
);
}
type TemporariesUsedOutsideDefiningScope = {
declarations: Map<IdentifierId, ScopeId>;
usedOutsideDeclaringScope: Set<IdentifierId>;
};
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.id, scope);
break;
}
default: {
break;
}
}
}
override visitPlace(
_id: InstructionId,
place: Place,
state: TemporariesUsedOutsideDefiningScope
): void {
const declaringScope = state.declarations.get(place.identifier.id);
if (declaringScope === undefined) {
return;
}
if (this.scopes.indexOf(declaringScope) === -1) {
state.usedOutsideDeclaringScope.add(place.identifier.id);
}
}
}
type DeclMap = Map<IdentifierId, 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<IdentifierId>;
#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<IdentifierId>) {
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.id);
}
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.id)) {
this.#declarations.set(identifier.id, 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,
isConditional: 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: [],
optionalPath: [],
};
} else {
objectDependency = {
identifier: resolvedDependency.identifier,
path: [...resolvedDependency.path],
optionalPath: [...resolvedDependency.optionalPath],
};
}
if (objectDependency.optionalPath.length > 0) {
objectDependency.optionalPath.push(property);
} else if (isConditional) {
objectDependency.optionalPath.push(property);
} else {
objectDependency.path.push(property);
}
return objectDependency;
}
declareProperty(lvalue: Place, object: Place, property: string): void {
const nextDependency = this.#getProperty(object, property, false);
this.#properties.set(lvalue.identifier, nextDependency);
}
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
if (
isUseRefType(maybeDependency.identifier) &&
maybeDependency.path.at(0) === "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.id);
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: [],
optionalPath: [],
};
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): void {
const nextDependency = this.#getProperty(object, property, false);
this.visitDependency(nextDependency);
}
visitDependency(maybeDependency: ReactiveScopePropertyDependency): void {
const originalDeclaration = this.#declarations.get(
maybeDependency.identifier.id
);
if (
originalDeclaration !== undefined &&
originalDeclaration.scope.value !== null
) {
originalDeclaration.scope.each((scope) => {
if (!this.#isScopeActive(scope.value)) {
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 &&
!Array.from(currentScope.reassignments).some(
(identifier) => identifier.id === place.identifier.id
) &&
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> {
enableTreatFunctionDepsAsConditional = false;
constructor(enableTreatFunctionDepsAsConditional: boolean) {
super();
this.enableTreatFunctionDepsAsConditional =
enableTreatFunctionDepsAsConditional;
}
override visitScope(scope: ReactiveScopeBlock, context: Context): void {
const scopeDependencies = context.enter(scope.scope, () => {
this.visitBlock(scope.instructions, context);
});
scope.scope.dependencies = scopeDependencies;
}
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,
});
}
visitReactiveValue(
context: Context,
id: InstructionId,
value: ReactiveValue
): void {
switch (value.kind) {
case "OptionalExpression": {
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);
});
break;
}
case "LogicalExpression": {
this.visitReactiveValue(context, id, value.left);
context.enterConditional(() => {
this.visitReactiveValue(context, id, value.right);
});
break;
}
case "ConditionalExpression": {
this.visitReactiveValue(context, id, value.test);
const consequentDeps = context.enterConditional(() => {
this.visitReactiveValue(context, id, value.consequent);
});
const alternateDeps = context.enterConditional(() => {
this.visitReactiveValue(context, id, value.alternate);
});
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.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);
} else {
context.visitProperty(value.object, value.property);
}
} 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);
}
}
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);
this.visitReactiveValue(context, terminal.id, terminal.test);
context.enterConditional(() => {
this.visitBlock(terminal.loop, context);
if (terminal.update !== null) {
this.visitReactiveValue(context, terminal.id, terminal.update);
}
});
break;
}
case "for-of": {
this.visitReactiveValue(context, terminal.id, terminal.init);
context.enterConditional(() => {
this.visitBlock(terminal.loop, context);
});
break;
}
case "for-in": {
this.visitReactiveValue(context, terminal.id, terminal.init);
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);
});
break;
}
case "while": {
this.visitReactiveValue(context, terminal.id, terminal.test);
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);
}
}