import { CompilerError } from "../CompilerError";
import { Identifier, ReactiveScopeDependency } from "../HIR";
import { printIdentifier } from "../HIR/PrintHIR";
import { assertExhaustive } from "../Utils/utils";
export type ReactiveScopePropertyDependency = ReactiveScopeDependency & {
optionalPath: Array<string>;
};
export class ReactiveScopeDependencyTree {
#roots: Map<Identifier, DependencyNode> = new Map();
#getOrCreateRoot(identifier: Identifier): DependencyNode {
let rootNode = this.#roots.get(identifier);
if (rootNode === undefined) {
rootNode = {
properties: new Map(),
accessType: PropertyAccessType.UnconditionalAccess,
};
this.#roots.set(identifier, rootNode);
}
return rootNode;
}
add(dep: ReactiveScopePropertyDependency, inConditional: boolean): void {
const { path, optionalPath } = dep;
let currNode = this.#getOrCreateRoot(dep.identifier);
const accessType = inConditional
? PropertyAccessType.ConditionalAccess
: PropertyAccessType.UnconditionalAccess;
for (const property of path) {
let currChild = getOrMakeProperty(currNode, property);
currChild.accessType = merge(currChild.accessType, accessType);
currNode = currChild;
}
if (optionalPath.length === 0) {
const depType = inConditional
? PropertyAccessType.ConditionalDependency
: PropertyAccessType.UnconditionalDependency;
currNode.accessType = merge(currNode.accessType, depType);
} else {
for (const property of optionalPath) {
let currChild = getOrMakeProperty(currNode, property);
currChild.accessType = merge(
currChild.accessType,
PropertyAccessType.ConditionalAccess
);
currNode = currChild;
}
currNode.accessType = merge(
currNode.accessType,
PropertyAccessType.ConditionalDependency
);
}
}
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#roots.entries()) {
const deps = deriveMinimalDependenciesInSubtree(rootNode);
CompilerError.invariant(
deps.every(
(dep) => dep.accessType === PropertyAccessType.UnconditionalDependency
),
{
reason:
"[PropagateScopeDependencies] All dependencies must be reduced to unconditional dependencies.",
description: null,
loc: null,
suggestions: null,
}
);
for (const dep of deps) {
results.add({
identifier: rootId,
path: dep.relativePath,
});
}
}
return results;
}
addDepsFromInnerScope(
depsFromInnerScope: ReactiveScopeDependencyTree,
innerScopeInConditionalWithinParent: boolean,
checkValidDepIdFn: (dep: ReactiveScopeDependency) => boolean
): void {
for (const [id, otherRoot] of depsFromInnerScope.#roots) {
if (!checkValidDepIdFn({ identifier: id, path: [] })) {
continue;
}
let currRoot = this.#getOrCreateRoot(id);
addSubtree(currRoot, otherRoot, innerScopeInConditionalWithinParent);
if (!isUnconditional(currRoot.accessType)) {
currRoot.accessType = isDependency(currRoot.accessType)
? PropertyAccessType.UnconditionalDependency
: PropertyAccessType.UnconditionalAccess;
}
}
}
promoteDepsFromExhaustiveConditionals(
trees: Array<ReactiveScopeDependencyTree>
): void {
CompilerError.invariant(trees.length > 1, {
reason: "Expected trees to be at least 2 elements long.",
description: null,
loc: null,
suggestions: null,
});
for (const [id, root] of this.#roots) {
const nodesForRootId = mapNonNull(trees, (tree) => {
const node = tree.#roots.get(id);
if (node != null && isUnconditional(node.accessType)) {
return node;
} else {
return null;
}
});
if (nodesForRootId) {
addSubtreeIntersection(
root.properties,
nodesForRootId.map((root) => root.properties)
);
}
}
}
printDeps(includeAccesses: boolean): string {
let res = [];
for (const [rootId, rootNode] of this.#roots.entries()) {
const rootResults = printSubtree(rootNode, includeAccesses).map(
(result) => `${printIdentifier(rootId)}.${result}`
);
res.push(rootResults);
}
return res.flat().join("\n");
}
}
enum PropertyAccessType {
ConditionalAccess = "ConditionalAccess",
UnconditionalAccess = "UnconditionalAccess",
ConditionalDependency = "ConditionalDependency",
UnconditionalDependency = "UnconditionalDependency",
}
const MIN_ACCESS_TYPE = PropertyAccessType.ConditionalAccess;
function isUnconditional(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.UnconditionalAccess ||
access === PropertyAccessType.UnconditionalDependency
);
}
function isDependency(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.ConditionalDependency ||
access === PropertyAccessType.UnconditionalDependency
);
}
function merge(
access1: PropertyAccessType,
access2: PropertyAccessType
): PropertyAccessType {
const resultIsUnconditional =
isUnconditional(access1) || isUnconditional(access2);
const resultIsDependency = isDependency(access1) || isDependency(access2);
if (resultIsUnconditional) {
if (resultIsDependency) {
return PropertyAccessType.UnconditionalDependency;
} else {
return PropertyAccessType.UnconditionalAccess;
}
} else {
if (resultIsDependency) {
return PropertyAccessType.ConditionalDependency;
} else {
return PropertyAccessType.ConditionalAccess;
}
}
}
type DependencyNode = {
properties: Map<string, DependencyNode>;
accessType: PropertyAccessType;
};
type ReduceResultNode = {
relativePath: Array<string>;
accessType: PropertyAccessType;
};
const promoteUncondResult = [
{
relativePath: [],
accessType: PropertyAccessType.UnconditionalDependency,
},
];
const promoteCondResult = [
{
relativePath: [],
accessType: PropertyAccessType.ConditionalDependency,
},
];
function deriveMinimalDependenciesInSubtree(
dep: DependencyNode
): Array<ReduceResultNode> {
const results: Array<ReduceResultNode> = [];
for (const [childName, childNode] of dep.properties) {
const childResult = deriveMinimalDependenciesInSubtree(childNode).map(
({ relativePath, accessType }) => {
return {
relativePath: [childName, ...relativePath],
accessType,
};
}
);
results.push(...childResult);
}
switch (dep.accessType) {
case PropertyAccessType.UnconditionalDependency: {
return promoteUncondResult;
}
case PropertyAccessType.UnconditionalAccess: {
if (
results.every(
({ accessType }) =>
accessType === PropertyAccessType.UnconditionalDependency
)
) {
return results;
} else {
return promoteUncondResult;
}
}
case PropertyAccessType.ConditionalAccess:
case PropertyAccessType.ConditionalDependency: {
if (
results.every(
({ accessType }) =>
accessType === PropertyAccessType.ConditionalDependency
)
) {
return promoteCondResult;
} else {
return promoteUncondResult;
}
}
default: {
assertExhaustive(
dep.accessType,
"[PropgateScopeDependencies] Unhandled access type!"
);
}
}
}
function demoteSubtreeToConditional(subtree: DependencyNode): void {
const stack: Array<DependencyNode> = [subtree];
let node;
while ((node = stack.pop()) !== undefined) {
const { accessType, properties } = node;
if (!isUnconditional(accessType)) {
continue;
}
node.accessType = isDependency(accessType)
? PropertyAccessType.ConditionalDependency
: PropertyAccessType.ConditionalAccess;
for (const childNode of properties.values()) {
if (isUnconditional(accessType)) {
stack.push(childNode);
}
}
}
}
function addSubtree(
currNode: DependencyNode,
otherNode: DependencyNode,
demoteOtherNode: boolean
): void {
let otherType = otherNode.accessType;
if (demoteOtherNode) {
otherType = isDependency(otherType)
? PropertyAccessType.ConditionalDependency
: PropertyAccessType.ConditionalAccess;
}
currNode.accessType = merge(currNode.accessType, otherType);
for (const [propertyName, otherChild] of otherNode.properties) {
const currChild = currNode.properties.get(propertyName);
if (currChild) {
addSubtree(currChild, otherChild, demoteOtherNode);
} else {
if (demoteOtherNode) {
demoteSubtreeToConditional(otherChild);
}
currNode.properties.set(propertyName, otherChild);
}
}
}
function addSubtreeIntersection(
currProperties: Map<string, DependencyNode>,
otherProperties: Array<Map<string, DependencyNode>>
): void {
CompilerError.invariant(otherProperties.length > 1, {
reason:
"[DeriveMinimalDependencies] Expected otherProperties to be at least 2 elements long.",
description: null,
loc: null,
suggestions: null,
});
for (const [propertyName, currNode] of currProperties) {
const otherNodes = mapNonNull(otherProperties, (properties) => {
const node = properties.get(propertyName);
if (node != null && isUnconditional(node.accessType)) {
return node;
} else {
return null;
}
});
if (otherNodes) {
addSubtreeIntersection(
currNode.properties,
otherNodes.map((node) => node.properties)
);
const isDep = otherNodes.some((tree) => isDependency(tree.accessType));
const externalAccessType = isDep
? PropertyAccessType.UnconditionalDependency
: PropertyAccessType.UnconditionalAccess;
currNode.accessType = merge(externalAccessType, currNode.accessType);
}
}
}
function printSubtree(
node: DependencyNode,
includeAccesses: boolean
): Array<string> {
const results: Array<string> = [];
for (const [propertyName, propertyNode] of node.properties) {
if (includeAccesses || isDependency(propertyNode.accessType)) {
results.push(`${propertyName} (${propertyNode.accessType})`);
}
const propertyResults = printSubtree(propertyNode, includeAccesses);
results.push(
...propertyResults.map((result) => `${propertyName}.${result}`)
);
}
return results;
}
function getOrMakeProperty(
node: DependencyNode,
property: string
): DependencyNode {
let child = node.properties.get(property);
if (child == null) {
child = {
properties: new Map(),
accessType: MIN_ACCESS_TYPE,
};
node.properties.set(property, child);
}
return child;
}
function mapNonNull<T extends NonNullable<V>, V, U>(
arr: Array<U>,
fn: (arg0: U) => T | undefined | null
): Array<T> | null {
const result = [];
for (let i = 0; i < arr.length; i++) {
const element = fn(arr[i]);
if (element) {
result.push(element);
} else {
return null;
}
}
return result;
}