import {
ScopeId,
HIRFunction,
Place,
Instruction,
ReactiveScopeDependency,
Identifier,
ReactiveScope,
isObjectMethodType,
isRefValueType,
isUseRefType,
makeInstructionId,
InstructionId,
InstructionKind,
GeneratedSource,
DeclarationId,
areEqualPaths,
IdentifierId,
} from './HIR';
import {
collectHoistablePropertyLoads,
keyByScopeId,
} from './CollectHoistablePropertyLoads';
import {
ScopeBlockTraversal,
eachInstructionOperand,
eachInstructionValueOperand,
eachPatternOperand,
eachTerminalOperand,
} from './visitors';
import {Stack, empty} from '../Utils/Stack';
import {CompilerError} from '../CompilerError';
import {Iterable_some} from '../Utils/utils';
import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
const usedOutsideDeclaringScope =
findTemporariesUsedOutsideDeclaringScope(fn);
const temporaries = collectTemporariesSidemap(fn, usedOutsideDeclaringScope);
const {
temporariesReadInOptional,
processedInstrsInOptional,
hoistableObjects,
} = collectOptionalChainSidemap(fn);
const hoistablePropertyLoads = keyByScopeId(
fn,
collectHoistablePropertyLoads(fn, temporaries, hoistableObjects, null),
);
const scopeDeps = collectDependencies(
fn,
usedOutsideDeclaringScope,
new Map([...temporaries, ...temporariesReadInOptional]),
processedInstrsInOptional,
);
for (const [scope, deps] of scopeDeps) {
if (deps.length === 0) {
continue;
}
const hoistables = hoistablePropertyLoads.get(scope.id);
CompilerError.invariant(hoistables != null, {
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
loc: GeneratedSource,
});
const tree = new ReactiveScopeDependencyTreeHIR(
[...hoistables.assumedNonNullObjects].map(o => o.fullPath),
);
for (const dep of deps) {
tree.addDependency({...dep});
}
const candidates = tree.deriveMinimalDependencies();
for (const candidateDep of candidates) {
if (
!Iterable_some(
scope.dependencies,
existingDep =>
existingDep.identifier.declarationId ===
candidateDep.identifier.declarationId &&
areEqualPaths(existingDep.path, candidateDep.path),
)
)
scope.dependencies.add(candidateDep);
}
}
}
function findTemporariesUsedOutsideDeclaringScope(
fn: HIRFunction,
): ReadonlySet<DeclarationId> {
const declarations = new Map<DeclarationId, ScopeId>();
const prunedScopes = new Set<ScopeId>();
const scopeTraversal = new ScopeBlockTraversal();
const usedOutsideDeclaringScope = new Set<DeclarationId>();
function handlePlace(place: Place): void {
const declaringScope = declarations.get(place.identifier.declarationId);
if (
declaringScope != null &&
!scopeTraversal.isScopeActive(declaringScope) &&
!prunedScopes.has(declaringScope)
) {
usedOutsideDeclaringScope.add(place.identifier.declarationId);
}
}
function handleInstruction(instr: Instruction): void {
const scope = scopeTraversal.currentScope;
if (scope == null || prunedScopes.has(scope)) {
return;
}
switch (instr.value.kind) {
case 'LoadLocal':
case 'LoadContext':
case 'PropertyLoad': {
declarations.set(instr.lvalue.identifier.declarationId, scope);
break;
}
default: {
break;
}
}
}
for (const [blockId, block] of fn.body.blocks) {
scopeTraversal.recordScopes(block);
const scopeStartInfo = scopeTraversal.blockInfos.get(blockId);
if (scopeStartInfo?.kind === 'begin' && scopeStartInfo.pruned) {
prunedScopes.add(scopeStartInfo.scope.id);
}
for (const instr of block.instructions) {
for (const place of eachInstructionOperand(instr)) {
handlePlace(place);
}
handleInstruction(instr);
}
for (const place of eachTerminalOperand(block.terminal)) {
handlePlace(place);
}
}
return usedOutsideDeclaringScope;
}
export function collectTemporariesSidemap(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const {value, lvalue} = instr;
const usedOutside = usedOutsideDeclaringScope.has(
lvalue.identifier.declarationId,
);
if (value.kind === 'PropertyLoad' && !usedOutside) {
const property = getProperty(
value.object,
value.property,
false,
temporaries,
);
temporaries.set(lvalue.identifier.id, property);
} else if (
value.kind === 'LoadLocal' &&
lvalue.identifier.name == null &&
value.place.identifier.name !== null &&
!usedOutside
) {
temporaries.set(lvalue.identifier.id, {
identifier: value.place.identifier,
path: [],
});
}
}
}
return temporaries;
}
function getProperty(
object: Place,
propertyName: string,
optional: boolean,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
): ReactiveScopeDependency {
const resolvedDependency = temporaries.get(object.identifier.id);
let property: ReactiveScopeDependency;
if (resolvedDependency == null) {
property = {
identifier: object.identifier,
path: [{property: propertyName, optional}],
};
} else {
property = {
identifier: resolvedDependency.identifier,
path: [...resolvedDependency.path, {property: propertyName, optional}],
};
}
return property;
}
type Decl = {
id: InstructionId;
scope: Stack<ReactiveScope>;
};
class Context {
#declarations: Map<DeclarationId, Decl> = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
#scopes: Stack<ReactiveScope> = empty();
#dependencies: Stack<Array<ReactiveScopeDependency>> = empty();
deps: Map<ReactiveScope, Array<ReactiveScopeDependency>> = new Map();
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
constructor(
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
) {
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
this.#temporaries = temporaries;
}
enterScope(scope: ReactiveScope): void {
this.#dependencies = this.#dependencies.push([]);
this.#scopes = this.#scopes.push(scope);
}
exitScope(scope: ReactiveScope, pruned: boolean): void {
const scopedDependencies = this.#dependencies.value;
CompilerError.invariant(scopedDependencies != null, {
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
loc: scope.loc,
});
this.#scopes = this.#scopes.pop();
this.#dependencies = this.#dependencies.pop();
for (const dep of scopedDependencies) {
if (this.#checkValidDependency(dep)) {
this.#dependencies.value?.push(dep);
}
}
if (!pruned) {
this.deps.set(scope, scopedDependencies);
}
}
isUsedOutsideDeclaringScope(place: Place): boolean {
return this.#temporariesUsedOutsideScope.has(
place.identifier.declarationId,
);
}
declare(identifier: Identifier, decl: Decl): void {
if (!this.#declarations.has(identifier.declarationId)) {
this.#declarations.set(identifier.declarationId, decl);
}
this.#reassignments.set(identifier, decl);
}
#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;
return (
currentScope != null &&
currentDeclaration !== undefined &&
currentDeclaration.id < currentScope.range.start
);
}
#isScopeActive(scope: ReactiveScope): boolean {
if (this.#scopes === null) {
return false;
}
return this.#scopes.find(state => state === scope);
}
get currentScope(): Stack<ReactiveScope> {
return this.#scopes;
}
visitOperand(place: Place): void {
this.visitDependency(
this.#temporaries.get(place.identifier.id) ?? {
identifier: place.identifier,
path: [],
},
);
}
visitProperty(object: Place, property: string, optional: boolean): void {
const nextDependency = getProperty(
object,
property,
optional,
this.#temporaries,
);
this.visitDependency(nextDependency);
}
visitDependency(maybeDependency: ReactiveScopeDependency): void {
const originalDeclaration = this.#declarations.get(
maybeDependency.identifier.declarationId,
);
if (
originalDeclaration !== undefined &&
originalDeclaration.scope.value !== null
) {
originalDeclaration.scope.each(scope => {
if (
!this.#isScopeActive(scope) &&
!Iterable_some(
scope.declarations.values(),
decl =>
decl.identifier.declarationId ===
maybeDependency.identifier.declarationId,
)
) {
scope.declarations.set(maybeDependency.identifier.id, {
identifier: maybeDependency.identifier,
scope: originalDeclaration.scope.value!,
});
}
});
}
if (this.#checkValidDependency(maybeDependency)) {
this.#dependencies.value!.push(maybeDependency);
}
}
visitReassignment(place: Place): void {
const currentScope = this.currentScope.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);
}
}
}
function handleInstruction(instr: Instruction, context: Context): void {
const {id, value, lvalue} = instr;
if (value.kind === 'LoadLocal') {
if (
value.place.identifier.name === null ||
lvalue.identifier.name !== null ||
context.isUsedOutsideDeclaringScope(lvalue)
) {
context.visitOperand(value.place);
}
} else if (value.kind === 'PropertyLoad') {
if (context.isUsedOutsideDeclaringScope(lvalue)) {
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 {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
}
context.declare(lvalue.identifier, {
id,
scope: context.currentScope,
});
}
function collectDependencies(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<InstructionId>,
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
const context = new Context(usedOutsideDeclaringScope, temporaries);
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(),
});
}
}
const scopeTraversal = new ScopeBlockTraversal();
for (const [blockId, block] of fn.body.blocks) {
scopeTraversal.recordScopes(block);
const scopeBlockInfo = scopeTraversal.blockInfos.get(blockId);
if (scopeBlockInfo?.kind === 'begin') {
context.enterScope(scopeBlockInfo.scope);
} else if (scopeBlockInfo?.kind === 'end') {
context.exitScope(scopeBlockInfo.scope, scopeBlockInfo?.pruned);
}
for (const phi of block.phis) {
for (const operand of phi.operands) {
const maybeOptionalChain = temporaries.get(operand[1].id);
if (maybeOptionalChain) {
context.visitDependency(maybeOptionalChain);
}
}
}
for (const instr of block.instructions) {
if (!processedInstrsInOptional.has(instr.id)) {
handleInstruction(instr, context);
}
}
if (!processedInstrsInOptional.has(block.terminal.id)) {
for (const place of eachTerminalOperand(block.terminal)) {
context.visitOperand(place);
}
}
}
return context.deps;
}