import {CompilerError} from '../CompilerError';
import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
import {Set_intersect, Set_union, getOrInsertDefault} from '../Utils/utils';
import {
BasicBlock,
BlockId,
DependencyPathEntry,
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
ReactiveScopeDependency,
ScopeId,
} from './HIR';
export function collectHoistablePropertyLoads(
fn: HIRFunction,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
): ReadonlyMap<ScopeId, BlockInfo> {
const registry = new PropertyPathRegistry();
const nodes = collectNonNullsInBlocks(fn, temporaries, registry);
propagateNonNull(fn, nodes);
const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
for (const [_, block] of fn.body.blocks) {
if (block.terminal.kind === 'scope') {
nodesKeyedByScopeId.set(
block.terminal.scope.id,
nodes.get(block.terminal.block)!,
);
}
}
return nodesKeyedByScopeId;
}
export type BlockInfo = {
block: BasicBlock;
assumedNonNullObjects: ReadonlySet<PropertyPathNode>;
};
type RootNode = {
properties: Map<string, PropertyPathNode>;
parent: null;
fullPath: ReactiveScopeDependency;
root: IdentifierId;
};
type PropertyPathNode =
| {
properties: Map<string, PropertyPathNode>;
parent: PropertyPathNode;
fullPath: ReactiveScopeDependency;
}
| RootNode;
class PropertyPathRegistry {
roots: Map<IdentifierId, RootNode> = new Map();
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
let rootNode = this.roots.get(identifier.id);
if (rootNode === undefined) {
rootNode = {
root: identifier.id,
properties: new Map(),
fullPath: {
identifier,
path: [],
},
parent: null,
};
this.roots.set(identifier.id, rootNode);
}
return rootNode;
}
static getOrCreatePropertyEntry(
parent: PropertyPathNode,
entry: DependencyPathEntry,
): PropertyPathNode {
if (entry.optional) {
CompilerError.throwTodo({
reason: 'handle optional nodes',
loc: GeneratedSource,
});
}
let child = parent.properties.get(entry.property);
if (child == null) {
child = {
properties: new Map(),
parent: parent,
fullPath: {
identifier: parent.fullPath.identifier,
path: parent.fullPath.path.concat(entry),
},
};
parent.properties.set(entry.property, child);
}
return child;
}
getOrCreateProperty(n: ReactiveScopeDependency): PropertyPathNode {
let currNode = this.getOrCreateIdentifier(n.identifier);
if (n.path.length === 0) {
return currNode;
}
for (let i = 0; i < n.path.length - 1; i++) {
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
currNode,
n.path[i],
);
}
return PropertyPathRegistry.getOrCreatePropertyEntry(
currNode,
n.path.at(-1)!,
);
}
}
function addNonNullPropertyPath(
source: Identifier,
sourceNode: PropertyPathNode,
instrId: InstructionId,
knownImmutableIdentifiers: Set<IdentifierId>,
result: Set<PropertyPathNode>,
): void {
const isMutableAtInstr =
source.mutableRange.end > source.mutableRange.start + 1 &&
source.scope != null &&
inRange({id: instrId}, source.scope.range);
if (
!isMutableAtInstr ||
knownImmutableIdentifiers.has(sourceNode.fullPath.identifier.id)
) {
result.add(sourceNode);
}
}
function collectNonNullsInBlocks(
fn: HIRFunction,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
registry: PropertyPathRegistry,
): ReadonlyMap<BlockId, BlockInfo> {
const knownImmutableIdentifiers = new Set<IdentifierId>();
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
for (const p of fn.params) {
if (p.kind === 'Identifier') {
knownImmutableIdentifiers.add(p.identifier.id);
}
}
}
const knownNonNullIdentifiers = new Set<PropertyPathNode>();
if (
fn.fnType === 'Component' &&
fn.params.length > 0 &&
fn.params[0].kind === 'Identifier'
) {
const identifier = fn.params[0].identifier;
knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier));
}
const nodes = new Map<BlockId, BlockInfo>();
for (const [_, block] of fn.body.blocks) {
const assumedNonNullObjects = new Set<PropertyPathNode>(
knownNonNullIdentifiers,
);
for (const instr of block.instructions) {
if (instr.value.kind === 'PropertyLoad') {
const source = temporaries.get(instr.value.object.identifier.id) ?? {
identifier: instr.value.object.identifier,
path: [],
};
addNonNullPropertyPath(
instr.value.object.identifier,
registry.getOrCreateProperty(source),
instr.id,
knownImmutableIdentifiers,
assumedNonNullObjects,
);
} else if (instr.value.kind === 'Destructure') {
const source = instr.value.value.identifier.id;
const sourceNode = temporaries.get(source);
if (sourceNode != null) {
addNonNullPropertyPath(
instr.value.value.identifier,
registry.getOrCreateProperty(sourceNode),
instr.id,
knownImmutableIdentifiers,
assumedNonNullObjects,
);
}
} else if (instr.value.kind === 'ComputedLoad') {
const source = instr.value.object.identifier.id;
const sourceNode = temporaries.get(source);
if (sourceNode != null) {
addNonNullPropertyPath(
instr.value.object.identifier,
registry.getOrCreateProperty(sourceNode),
instr.id,
knownImmutableIdentifiers,
assumedNonNullObjects,
);
}
}
}
nodes.set(block.id, {
block,
assumedNonNullObjects,
});
}
return nodes;
}
function propagateNonNull(
fn: HIRFunction,
nodes: ReadonlyMap<BlockId, BlockInfo>,
): void {
const blockSuccessors = new Map<BlockId, Set<BlockId>>();
const terminalPreds = new Set<BlockId>();
for (const [blockId, block] of fn.body.blocks) {
for (const pred of block.preds) {
getOrInsertDefault(blockSuccessors, pred, new Set()).add(blockId);
}
if (block.terminal.kind === 'throw' || block.terminal.kind === 'return') {
terminalPreds.add(blockId);
}
}
function recursivelyPropagateNonNull(
nodeId: BlockId,
direction: 'forward' | 'backward',
traversalState: Map<BlockId, 'active' | 'done'>,
): boolean {
if (traversalState.has(nodeId)) {
return false;
}
traversalState.set(nodeId, 'active');
const node = nodes.get(nodeId);
if (node == null) {
CompilerError.invariant(false, {
reason: `Bad node ${nodeId}, kind: ${direction}`,
loc: GeneratedSource,
});
}
const neighbors = Array.from(
direction === 'backward'
? (blockSuccessors.get(nodeId) ?? [])
: node.block.preds,
);
let changed = false;
for (const pred of neighbors) {
if (!traversalState.has(pred)) {
const neighborChanged = recursivelyPropagateNonNull(
pred,
direction,
traversalState,
);
changed ||= neighborChanged;
}
}
const neighborAccesses = Set_intersect(
Array.from(neighbors)
.filter(n => traversalState.get(n) === 'done')
.map(n => assertNonNull(nodes.get(n)).assumedNonNullObjects),
);
const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects;
const mergedObjects = Set_union(prevObjects, neighborAccesses);
assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects;
traversalState.set(nodeId, 'done');
changed ||= prevObjects.size !== mergedObjects.size;
return changed;
}
const traversalState = new Map<BlockId, 'done' | 'active'>();
const reversedBlocks = [...fn.body.blocks];
reversedBlocks.reverse();
let changed;
let i = 0;
do {
CompilerError.invariant(i++ < 100, {
reason:
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
loc: GeneratedSource,
});
changed = false;
for (const [blockId] of fn.body.blocks) {
const forwardChanged = recursivelyPropagateNonNull(
blockId,
'forward',
traversalState,
);
changed ||= forwardChanged;
}
traversalState.clear();
for (const [blockId] of reversedBlocks) {
const backwardChanged = recursivelyPropagateNonNull(
blockId,
'backward',
traversalState,
);
changed ||= backwardChanged;
}
traversalState.clear();
} while (changed);
}
export function assertNonNull<T extends NonNullable<U>, U>(
value: T | null | undefined,
source?: string,
): T {
CompilerError.invariant(value != null, {
reason: 'Unexpected null',
description: source != null ? `(from ${source})` : null,
loc: GeneratedSource,
});
return value;
}