import {CompilerError} from '../CompilerError';
import {
Environment,
Identifier,
IdentifierId,
InstructionId,
Place,
PropertyLiteral,
ReactiveBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
ReactiveTerminalStatement,
getHookKind,
isUseRefType,
isUseStateType,
} from '../HIR';
import {eachCallArgument, eachInstructionLValue} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
type CreateUpdate = 'Create' | 'Update' | 'Unknown';
type KindMap = Map<IdentifierId, CreateUpdate>;
class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
map: KindMap = new Map();
aliases: DisjointSet<IdentifierId>;
paths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>;
env: Environment;
constructor(
env: Environment,
aliases: DisjointSet<IdentifierId>,
paths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
) {
super();
this.aliases = aliases;
this.paths = paths;
this.env = env;
}
join(values: Array<CreateUpdate>): CreateUpdate {
function join2(l: CreateUpdate, r: CreateUpdate): CreateUpdate {
if (l === 'Update' || r === 'Update') {
return 'Update';
} else if (l === 'Create' || r === 'Create') {
return 'Create';
} else if (l === 'Unknown' || r === 'Unknown') {
return 'Unknown';
}
assertExhaustive(r, `Unhandled variable kind ${r}`);
}
return values.reduce(join2, 'Unknown');
}
isCreateOnlyHook(id: Identifier): boolean {
return isUseStateType(id) || isUseRefType(id);
}
override visitPlace(
_: InstructionId,
place: Place,
state: CreateUpdate,
): void {
this.map.set(
place.identifier.id,
this.join([state, this.map.get(place.identifier.id) ?? 'Unknown']),
);
}
override visitBlock(block: ReactiveBlock, state: CreateUpdate): void {
super.visitBlock([...block].reverse(), state);
}
override visitInstruction(instruction: ReactiveInstruction): void {
const state = this.join(
[...eachInstructionLValue(instruction)].map(
operand => this.map.get(operand.identifier.id) ?? 'Unknown',
),
);
const visitCallOrMethodNonArgs = (): void => {
switch (instruction.value.kind) {
case 'CallExpression': {
this.visitPlace(instruction.id, instruction.value.callee, state);
break;
}
case 'MethodCall': {
this.visitPlace(instruction.id, instruction.value.property, state);
this.visitPlace(instruction.id, instruction.value.receiver, state);
break;
}
}
};
const isHook = (): boolean => {
let callee = null;
switch (instruction.value.kind) {
case 'CallExpression': {
callee = instruction.value.callee.identifier;
break;
}
case 'MethodCall': {
callee = instruction.value.property.identifier;
break;
}
}
return callee != null && getHookKind(this.env, callee) != null;
};
switch (instruction.value.kind) {
case 'CallExpression':
case 'MethodCall': {
if (
instruction.lvalue &&
this.isCreateOnlyHook(instruction.lvalue.identifier)
) {
[...eachCallArgument(instruction.value.args)].forEach(operand =>
this.visitPlace(instruction.id, operand, 'Create'),
);
visitCallOrMethodNonArgs();
} else {
this.traverseInstruction(instruction, isHook() ? 'Update' : state);
}
break;
}
default: {
this.traverseInstruction(instruction, state);
}
}
}
override visitScope(scope: ReactiveScopeBlock): void {
const state = this.join(
[
...scope.scope.declarations.keys(),
...[...scope.scope.reassignments.values()].map(ident => ident.id),
].map(id => this.map.get(id) ?? 'Unknown'),
);
super.visitScope(scope, state);
[...scope.scope.dependencies].forEach(ident => {
let target: undefined | IdentifierId =
this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
ident.path.forEach(token => {
target &&= this.paths.get(target)?.get(token.property);
});
if (target && this.map.get(target) === 'Create') {
scope.scope.dependencies.delete(ident);
}
});
}
override visitTerminal(
stmt: ReactiveTerminalStatement,
state: CreateUpdate,
): void {
CompilerError.invariant(state !== 'Create', {
reason: "Visiting a terminal statement with state 'Create'",
loc: stmt.terminal.loc,
});
super.visitTerminal(stmt, state);
}
override visitReactiveFunctionValue(
_id: InstructionId,
_dependencies: Array<Place>,
fn: ReactiveFunction,
state: CreateUpdate,
): void {
visitReactiveFunction(fn, this, state);
}
}
export default function pruneInitializationDependencies(
fn: ReactiveFunction,
): void {
const [aliases, paths] = getAliases(fn);
visitReactiveFunction(fn, new Visitor(fn.env, aliases, paths), 'Update');
}
function update(
map: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
key: IdentifierId,
path: PropertyLiteral,
value: IdentifierId,
): void {
const inner = map.get(key) ?? new Map();
inner.set(path, value);
map.set(key, inner);
}
class AliasVisitor extends ReactiveFunctionVisitor {
scopeIdentifiers: DisjointSet<IdentifierId> = new DisjointSet<IdentifierId>();
scopePaths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>> = new Map();
override visitInstruction(instr: ReactiveInstruction): void {
if (
instr.value.kind === 'StoreLocal' ||
instr.value.kind === 'StoreContext'
) {
this.scopeIdentifiers.union([
instr.value.lvalue.place.identifier.id,
instr.value.value.identifier.id,
]);
} else if (
instr.value.kind === 'LoadLocal' ||
instr.value.kind === 'LoadContext'
) {
instr.lvalue &&
this.scopeIdentifiers.union([
instr.lvalue.identifier.id,
instr.value.place.identifier.id,
]);
} else if (instr.value.kind === 'PropertyLoad') {
instr.lvalue &&
update(
this.scopePaths,
instr.value.object.identifier.id,
instr.value.property,
instr.lvalue.identifier.id,
);
} else if (instr.value.kind === 'PropertyStore') {
update(
this.scopePaths,
instr.value.object.identifier.id,
instr.value.property,
instr.value.value.identifier.id,
);
}
}
}
function getAliases(
fn: ReactiveFunction,
): [
DisjointSet<IdentifierId>,
Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
] {
const visitor = new AliasVisitor();
visitReactiveFunction(fn, visitor, null);
let disjoint = visitor.scopeIdentifiers;
let scopePaths = new Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>();
for (const [key, value] of visitor.scopePaths) {
for (const [path, id] of value) {
update(
scopePaths,
disjoint.find(key) ?? key,
path,
disjoint.find(id) ?? id,
);
}
}
return [disjoint, scopePaths];
}