import {CompilerError} from '../CompilerError';
import {DependencyPath, Identifier, ReactiveScopeDependency} from '../HIR';
import {printIdentifier} from '../HIR/PrintHIR';
import {assertExhaustive} from '../Utils/utils';
export type ReactiveScopePropertyDependency = ReactiveScopeDependency;
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} = dep;
let currNode = this.#getOrCreateRoot(dep.identifier);
for (const item of path) {
let currChild = getOrMakeProperty(currNode, item.property);
const accessType = inConditional
? PropertyAccessType.ConditionalAccess
: item.optional
? PropertyAccessType.OptionalAccess
: PropertyAccessType.UnconditionalAccess;
currChild.accessType = merge(currChild.accessType, accessType);
currNode = currChild;
}
const depType = inConditional
? PropertyAccessType.ConditionalDependency
: isOptional(currNode.accessType)
? PropertyAccessType.OptionalDependency
: PropertyAccessType.UnconditionalDependency;
currNode.accessType = merge(currNode.accessType, depType);
}
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#roots.entries()) {
const deps = deriveMinimalDependenciesInSubtree(rootNode, null);
CompilerError.invariant(
deps.every(
dep =>
dep.accessType === PropertyAccessType.UnconditionalDependency ||
dep.accessType == PropertyAccessType.OptionalDependency,
),
{
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');
}
debug(): string {
const buf: Array<string> = [`tree() [`];
for (const [rootId, rootNode] of this.#roots) {
buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
this.#debugImpl(buf, rootNode, 1);
}
buf.push(']');
return buf.length > 2 ? buf.join('\n') : buf.join('');
}
#debugImpl(
buf: Array<string>,
node: DependencyNode,
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 {
ConditionalAccess = 'ConditionalAccess',
OptionalAccess = 'OptionalAccess',
UnconditionalAccess = 'UnconditionalAccess',
ConditionalDependency = 'ConditionalDependency',
OptionalDependency = 'OptionalDependency',
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.OptionalDependency ||
access === PropertyAccessType.UnconditionalDependency
);
}
function isOptional(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.OptionalAccess ||
access === PropertyAccessType.OptionalDependency
);
}
function merge(
access1: PropertyAccessType,
access2: PropertyAccessType,
): PropertyAccessType {
const resultIsUnconditional =
isUnconditional(access1) || isUnconditional(access2);
const resultIsDependency = isDependency(access1) || isDependency(access2);
const resultIsOptional = isOptional(access1) || isOptional(access2);
if (resultIsUnconditional) {
if (resultIsDependency) {
return PropertyAccessType.UnconditionalDependency;
} else {
return PropertyAccessType.UnconditionalAccess;
}
} else if (resultIsOptional) {
if (resultIsDependency) {
return PropertyAccessType.OptionalDependency;
} else {
return PropertyAccessType.OptionalAccess;
}
} else {
if (resultIsDependency) {
return PropertyAccessType.ConditionalDependency;
} else {
return PropertyAccessType.ConditionalAccess;
}
}
}
type DependencyNode = {
properties: Map<string, DependencyNode>;
accessType: PropertyAccessType;
};
type ReduceResultNode = {
relativePath: DependencyPath;
accessType: PropertyAccessType;
};
function promoteResult(
accessType: PropertyAccessType,
path: {property: string; optional: boolean} | null,
): Array<ReduceResultNode> {
const result: ReduceResultNode = {
relativePath: [],
accessType,
};
if (path !== null) {
result.relativePath.push(path);
}
return [result];
}
function prependPath(
results: Array<ReduceResultNode>,
path: {property: string; optional: boolean} | null,
): Array<ReduceResultNode> {
if (path === null) {
return results;
}
return results.map(result => {
return {
accessType: result.accessType,
relativePath: [path, ...result.relativePath],
};
});
}
function deriveMinimalDependenciesInSubtree(
dep: DependencyNode,
property: string | null,
): Array<ReduceResultNode> {
const results: Array<ReduceResultNode> = [];
for (const [childName, childNode] of dep.properties) {
const childResult = deriveMinimalDependenciesInSubtree(
childNode,
childName,
);
results.push(...childResult);
}
switch (dep.accessType) {
case PropertyAccessType.UnconditionalDependency: {
return promoteResult(
PropertyAccessType.UnconditionalDependency,
property !== null ? {property, optional: false} : null,
);
}
case PropertyAccessType.UnconditionalAccess: {
if (
results.every(
({accessType}) =>
accessType === PropertyAccessType.UnconditionalDependency ||
accessType === PropertyAccessType.OptionalDependency,
)
) {
return prependPath(
results,
property !== null ? {property, optional: false} : null,
);
} else {
return promoteResult(
PropertyAccessType.UnconditionalDependency,
property !== null ? {property, optional: false} : null,
);
}
}
case PropertyAccessType.OptionalDependency: {
return promoteResult(
PropertyAccessType.OptionalDependency,
property !== null ? {property, optional: true} : null,
);
}
case PropertyAccessType.OptionalAccess: {
if (
results.every(
({accessType}) =>
accessType === PropertyAccessType.UnconditionalDependency ||
accessType === PropertyAccessType.OptionalDependency,
)
) {
return prependPath(
results,
property !== null ? {property, optional: true} : null,
);
} else {
return promoteResult(
PropertyAccessType.OptionalDependency,
property !== null ? {property, optional: true} : null,
);
}
}
case PropertyAccessType.ConditionalAccess:
case PropertyAccessType.ConditionalDependency: {
if (
results.every(
({accessType}) =>
accessType === PropertyAccessType.ConditionalDependency,
)
) {
return promoteResult(
PropertyAccessType.ConditionalDependency,
property !== null ? {property, optional: true} : null,
);
} else {
return promoteResult(
PropertyAccessType.UnconditionalDependency,
property !== null ? {property, optional: true} : null,
);
}
}
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;
}