import {CompilerError} from '../CompilerError';
import {
DependencyPathEntry,
GeneratedSource,
Identifier,
ReactiveScopeDependency,
} from '../HIR';
import {printIdentifier} from '../HIR/PrintHIR';
import {ReactiveScopePropertyDependency} from '../ReactiveScopes/DeriveMinimalDependencies';
export class ReactiveScopeDependencyTreeHIR {
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
#deps: Map<Identifier, DependencyNode> = new Map();
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
for (const {path, identifier} of hoistableObjects) {
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
this.#hoistableObjects,
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
);
for (let i = 0; i < path.length; i++) {
const prevAccessType = currNode.properties.get(
path[i].property,
)?.accessType;
const accessType =
i + 1 < path.length && path[i + 1].optional ? 'Optional' : 'NonNull';
CompilerError.invariant(
prevAccessType == null || prevAccessType === accessType,
{
reason: 'Conflicting access types',
loc: GeneratedSource,
},
);
let nextNode = currNode.properties.get(path[i].property);
if (nextNode == null) {
nextNode = {
properties: new Map(),
accessType,
};
currNode.properties.set(path[i].property, nextNode);
}
currNode = nextNode;
}
}
}
static #getOrCreateRoot<T extends string>(
identifier: Identifier,
roots: Map<Identifier, TreeNode<T>>,
defaultAccessType: T,
): TreeNode<T> {
let rootNode = roots.get(identifier);
if (rootNode === undefined) {
rootNode = {
properties: new Map(),
accessType: defaultAccessType,
};
roots.set(identifier, rootNode);
}
return rootNode;
}
addDependency(dep: ReactiveScopePropertyDependency): void {
const {identifier, path} = dep;
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
this.#deps,
PropertyAccessType.UnconditionalAccess,
);
let hoistableCursor: HoistableNode | undefined =
this.#hoistableObjects.get(identifier);
for (const entry of path) {
let nextHoistableCursor: HoistableNode | undefined;
let nextDepCursor: DependencyNode;
if (entry.optional) {
if (hoistableCursor != null) {
nextHoistableCursor = hoistableCursor?.properties.get(entry.property);
}
let accessType;
if (
hoistableCursor != null &&
hoistableCursor.accessType === 'NonNull'
) {
accessType = PropertyAccessType.UnconditionalAccess;
} else {
accessType = PropertyAccessType.OptionalAccess;
}
nextDepCursor = makeOrMergeProperty(
depCursor,
entry.property,
accessType,
);
} else if (
hoistableCursor != null &&
hoistableCursor.accessType === 'NonNull'
) {
nextHoistableCursor = hoistableCursor.properties.get(entry.property);
nextDepCursor = makeOrMergeProperty(
depCursor,
entry.property,
PropertyAccessType.UnconditionalAccess,
);
} else {
break;
}
depCursor = nextDepCursor;
hoistableCursor = nextHoistableCursor;
}
depCursor.accessType = merge(
depCursor.accessType,
PropertyAccessType.OptionalDependency,
);
}
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#deps.entries()) {
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
}
return results;
}
printDeps(includeAccesses: boolean): string {
let res: Array<Array<string>> = [];
for (const [rootId, rootNode] of this.#deps.entries()) {
const rootResults = printSubtree(rootNode, includeAccesses).map(
result => `${printIdentifier(rootId)}.${result}`,
);
res.push(rootResults);
}
return res.flat().join('\n');
}
static debug<T extends string>(roots: Map<Identifier, TreeNode<T>>): string {
const buf: Array<string> = [`tree() [`];
for (const [rootId, rootNode] of roots) {
buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
this.#debugImpl(buf, rootNode, 1);
}
buf.push(']');
return buf.length > 2 ? buf.join('\n') : buf.join('');
}
static #debugImpl<T extends string>(
buf: Array<string>,
node: TreeNode<T>,
depth: number = 0,
): void {
for (const [property, childNode] of node.properties) {
buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
this.#debugImpl(buf, childNode, depth + 1);
}
}
}
enum PropertyAccessType {
OptionalAccess = 'OptionalAccess',
UnconditionalAccess = 'UnconditionalAccess',
OptionalDependency = 'OptionalDependency',
UnconditionalDependency = 'UnconditionalDependency',
}
function isOptional(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.OptionalAccess ||
access === PropertyAccessType.OptionalDependency
);
}
function isDependency(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.OptionalDependency ||
access === PropertyAccessType.UnconditionalDependency
);
}
function merge(
access1: PropertyAccessType,
access2: PropertyAccessType,
): PropertyAccessType {
const resultIsUnconditional = !(isOptional(access1) && isOptional(access2));
const resultIsDependency = isDependency(access1) || isDependency(access2);
if (resultIsUnconditional) {
if (resultIsDependency) {
return PropertyAccessType.UnconditionalDependency;
} else {
return PropertyAccessType.UnconditionalAccess;
}
} else {
if (resultIsDependency) {
return PropertyAccessType.OptionalDependency;
} else {
return PropertyAccessType.OptionalAccess;
}
}
}
type TreeNode<T extends string> = {
properties: Map<string, TreeNode<T>>;
accessType: T;
};
type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
type DependencyNode = TreeNode<PropertyAccessType>;
function collectMinimalDependenciesInSubtree(
node: DependencyNode,
rootIdentifier: Identifier,
path: Array<DependencyPathEntry>,
results: Set<ReactiveScopeDependency>,
): void {
if (isDependency(node.accessType)) {
results.add({identifier: rootIdentifier, path});
} else {
for (const [childName, childNode] of node.properties) {
collectMinimalDependenciesInSubtree(
childNode,
rootIdentifier,
[
...path,
{
property: childName,
optional: isOptional(childNode.accessType),
},
],
results,
);
}
}
}
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 makeOrMergeProperty(
node: DependencyNode,
property: string,
accessType: PropertyAccessType,
): DependencyNode {
let child = node.properties.get(property);
if (child == null) {
child = {
properties: new Map(),
accessType,
};
node.properties.set(property, child);
} else {
child.accessType = merge(child.accessType, accessType);
}
return child;
}