jinaga
Version:
Data management for web and mobile applications.
197 lines • 8.21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectDisconnectedSpecification = exports.DisconnectedSpecificationError = void 0;
class DisconnectedSpecificationError extends Error {
constructor(message) {
super(message);
this.name = "DisconnectedSpecificationError";
}
}
exports.DisconnectedSpecificationError = DisconnectedSpecificationError;
/**
* Union-Find (Disjoint Set) data structure for efficiently managing
* connected components of labels in a specification graph.
*
* This is an alternative to the graph + DFS approach that uses the equivalence
* relation property of connected graphs. Instead of maintaining explicit
* connections, we maintain discrete sets of connected labels and join them
* when connections are added.
*/
class UnionFind {
constructor() {
this.parent = new Map();
this.rank = new Map();
}
/**
* Ensure a label exists in the data structure.
* If it doesn't exist, initialize it as its own set.
*/
ensureLabel(label) {
if (!this.parent.has(label)) {
this.parent.set(label, label);
this.rank.set(label, 0);
}
}
/**
* Find the root of the set containing the given label.
* Uses path compression for optimization.
*/
find(label) {
const parent = this.parent.get(label);
if (!parent) {
throw new Error(`Label ${label} not found in UnionFind`);
}
if (parent !== label) {
// Path compression: make all nodes on the path point directly to the root
this.parent.set(label, this.find(parent));
}
return this.parent.get(label);
}
/**
* Union two sets containing the given labels.
* Uses union by rank for optimization.
*/
union(label1, label2) {
this.ensureLabel(label1);
this.ensureLabel(label2);
const root1 = this.find(label1);
const root2 = this.find(label2);
if (root1 === root2) {
return; // Already in the same set
}
const rank1 = this.rank.get(root1);
const rank2 = this.rank.get(root2);
// Union by rank: attach smaller tree under root of larger tree
if (rank1 < rank2) {
this.parent.set(root1, root2);
}
else if (rank1 > rank2) {
this.parent.set(root2, root1);
}
else {
this.parent.set(root2, root1);
this.rank.set(root1, rank1 + 1);
}
}
/**
* Get all connected components as separate arrays of labels.
*/
getConnectedComponents() {
const components = new Map();
for (const label of this.parent.keys()) {
const root = this.find(label);
if (!components.has(root)) {
components.set(root, []);
}
components.get(root).push(label);
}
return Array.from(components.values());
}
}
/**
* Add all bidirectional connections from the specification to the UnionFind structure.
* Also collects all labels encountered during traversal.
*/
function addConnectionsToUnionFind(specification, unionFind, allLabels, labelTypes) {
// Add connections from path conditions in matches
for (const match of specification.matches) {
addConnectionsFromMatchToUnionFind(unionFind, match, allLabels, labelTypes);
}
// Add connections from nested specification projections
addConnectionsFromSpecificationProjectionsToUnionFind(unionFind, specification.projection, allLabels, labelTypes);
}
function addConnectionsFromMatchToUnionFind(unionFind, match, allLabels, labelTypes) {
const unknownLabel = match.unknown.name;
allLabels.add(unknownLabel);
labelTypes.set(unknownLabel, match.unknown.type);
for (const condition of match.conditions) {
if (condition.type === "path") {
// Connect unknown to the label on the right side of the path
const rightLabel = condition.labelRight;
allLabels.add(rightLabel);
// Note: We don't know the type of rightLabel here, it should have been added elsewhere
unionFind.union(unknownLabel, rightLabel);
}
else if (condition.type === "existential") {
// Process nested matches in existential conditions
for (const nestedMatch of condition.matches) {
addConnectionsFromMatchToUnionFind(unionFind, nestedMatch, allLabels, labelTypes);
// Connect the parent unknown to labels referenced in existential conditions
const nestedConnections = getLabelsReferencedInMatch(nestedMatch);
for (const connectedLabel of nestedConnections) {
allLabels.add(connectedLabel);
unionFind.union(unknownLabel, connectedLabel);
}
}
}
}
}
function addConnectionsFromSpecificationProjectionsToUnionFind(unionFind, projection, allLabels, labelTypes) {
if (projection.type === "composite") {
for (const component of projection.components) {
if (component.type === "specification") {
// Process nested specification projections - these contain matches
for (const match of component.matches) {
addConnectionsFromMatchToUnionFind(unionFind, match, allLabels, labelTypes);
}
// Recursively process nested projections
addConnectionsFromSpecificationProjectionsToUnionFind(unionFind, component.projection, allLabels, labelTypes);
}
// Field, hash, and fact components reference labels but don't create connections
}
}
// Single projections (field, fact, hash) don't create connections
}
function detectDisconnectedSpecification(specification) {
// Collect all labels while building connections
const allLabels = new Set();
const labelTypes = new Map();
// Add given labels
for (const given of specification.given) {
allLabels.add(given.label.name);
labelTypes.set(given.label.name, given.label.type);
}
// Build connections and collect labels in one pass
const unionFind = new UnionFind();
addConnectionsToUnionFind(specification, unionFind, allLabels, labelTypes);
// Ensure all labels are in the UnionFind (including isolated ones)
for (const label of allLabels) {
unionFind.ensureLabel(label);
}
if (allLabels.size <= 1) {
// Single label or empty specification cannot be disconnected
return;
}
const connectedComponents = unionFind.getConnectedComponents();
if (connectedComponents.length > 1) {
const componentDescriptions = connectedComponents.map((component, index) => {
const labelDescriptions = component.map((label) => {
const type = labelTypes.get(label) || 'unknown';
return `'${label}:${type}'`;
}).join(", ");
return `Subgraph ${index + 1}: [${labelDescriptions}]`;
}).join("; ");
throw new DisconnectedSpecificationError(`Disconnected specification detected. The specification contains ${connectedComponents.length} ` +
`disconnected subgraphs: ${componentDescriptions}. ` +
`All labels must be connected through path conditions, existential conditions, or projections.`);
}
}
exports.detectDisconnectedSpecification = detectDisconnectedSpecification;
function getLabelsReferencedInMatch(match) {
const referencedLabels = new Set();
for (const condition of match.conditions) {
if (condition.type === "path") {
referencedLabels.add(condition.labelRight);
}
else if (condition.type === "existential") {
for (const nestedMatch of condition.matches) {
const nestedLabels = getLabelsReferencedInMatch(nestedMatch);
for (const label of nestedLabels) {
referencedLabels.add(label);
}
}
}
}
return Array.from(referencedLabels);
}
//# sourceMappingURL=UnionFind.js.map