import * as Equal from "../Equal.ts" import type * as Graph from "../Graph.ts" import * as Hash from "../Hash.ts" import { NodeInspectSymbol } from "../Inspectable.ts" import * as Option from "../Option.ts" import { pipeArguments } from "../Pipeable.ts" import { hasProperty } from "../Predicate.ts" /** @internal */ export const TypeId = "~effect/Graph" /** @internal */ export const isGraph = ( u: U | Graph.Graph | Graph.MutableGraph ): u is Graph.Graph | Graph.MutableGraph => hasProperty(u, TypeId) /** @internal */ export interface GraphImpl extends Iterable, Equal.Equal { readonly [TypeId]: unknown type: T mutable: boolean transforming: boolean nodes: Map edges: Map> adjacency: Map> reverseAdjacency: Map> nextNodeIndex: Graph.NodeIndex nextEdgeIndex: Graph.EdgeIndex acyclic: Option.Option toJSON(): unknown } /** @internal */ export const toImpl = ( graph: Graph.Graph | Graph.MutableGraph ): GraphImpl => graph as unknown as GraphImpl /** @internal */ export const isTransforming = ( graph: Graph.Graph | Graph.MutableGraph ): boolean => toImpl(graph).transforming /** @internal */ export const withTransformation = ( graph: Graph.MutableGraph, evaluate: () => A ): A => { const impl = toImpl(graph) const transforming = impl.transforming impl.transforming = true try { return evaluate() } finally { impl.transforming = transforming } } const edgeEquals = (type: Graph.Kind, self: Graph.Edge, that: Graph.Edge): boolean => (type === "directed" ? self.source === that.source && self.target === that.target : (self.source === that.source && self.target === that.target) || (self.source === that.target && self.target === that.source)) && Equal.equals(self.data, that.data) const edgeHash = (type: Graph.Kind, edge: Graph.Edge): number => type === "directed" ? Hash.hash(edge) : Hash.optimize(Hash.combine(Hash.hash(edge.data), endpointsHash(edge.source, edge.target))) /** * Addition is commutative and does not cancel self-loops. * * @internal */ export const endpointsHash = (source: unknown, target: unknown): number => Hash.combine(0, Hash.hash(source)) + Hash.combine(0, Hash.hash(target)) const graphSeed = Hash.string("Graph") const ProtoGraph = { [TypeId]: { _N: (_: never) => _, _E: (_: never) => _ }, [Symbol.iterator](this: GraphImpl) { return this.nodes[Symbol.iterator]() }, [NodeInspectSymbol](this: GraphImpl) { return this.toJSON() }, [Equal.symbol](this: GraphImpl, that: Equal.Equal): boolean { if (hasProperty(that, TypeId)) { const thatImpl = toImpl(that as Graph.Graph) if ( this.nodes.size !== thatImpl.nodes.size || this.edges.size !== thatImpl.edges.size || this.type !== thatImpl.type ) { return false } for (const [nodeIndex, nodeData] of this.nodes) { if (!thatImpl.nodes.has(nodeIndex) || !Equal.equals(nodeData, thatImpl.nodes.get(nodeIndex))) { return false } } for (const [edgeIndex, edgeData] of this.edges) { const otherEdge = thatImpl.edges.get(edgeIndex) if (otherEdge === undefined || !edgeEquals(this.type, edgeData, otherEdge)) { return false } } return true } return false }, [Hash.symbol](this: GraphImpl): number { let hash = graphSeed hash = hash ^ Hash.string(this.type) hash = hash ^ Hash.number(this.nodes.size) hash = hash ^ Hash.number(this.edges.size) for (const [nodeIndex, nodeData] of this.nodes) { hash ^= Hash.combine(Hash.hash(nodeIndex), Hash.hash(nodeData)) } for (const [edgeIndex, edgeData] of this.edges) { hash ^= Hash.combine(Hash.hash(edgeIndex), edgeHash(this.type, edgeData)) } return Hash.optimize(hash) }, toJSON(this: GraphImpl) { return { _id: "Graph", nodeCount: this.nodes.size, edgeCount: this.edges.size, type: this.type } }, toString(this: GraphImpl) { return `Graph(${this.type}, ${this.nodes.size}, ${this.edges.size})` }, pipe() { return pipeArguments(this, arguments) } } /** @internal */ export const make = (type: T, mutable: boolean): GraphImpl => { const graph: GraphImpl = Object.create(ProtoGraph) graph.type = type graph.mutable = mutable graph.transforming = false graph.nodes = new Map() graph.edges = new Map() graph.adjacency = new Map() graph.reverseAdjacency = new Map() graph.nextNodeIndex = 0 graph.nextEdgeIndex = 0 graph.acyclic = Option.some(true) return graph } const cloneAdjacency = ( adjacency: Map> ): Map> => { const cloned = new Map>() for (const [nodeIndex, edges] of adjacency) { cloned.set(nodeIndex, [...edges]) } return cloned } /** @internal */ export const clone = ( source: GraphImpl, mutable: boolean ): GraphImpl => { const graph = make(source.type, mutable) graph.nodes = new Map(source.nodes) graph.edges = new Map(source.edges) graph.adjacency = cloneAdjacency(source.adjacency) graph.reverseAdjacency = cloneAdjacency(source.reverseAdjacency) graph.nextNodeIndex = source.nextNodeIndex graph.nextEdgeIndex = source.nextEdgeIndex graph.acyclic = source.acyclic return graph } /** @internal */ export const finalize = (source: GraphImpl): GraphImpl => { const graph: GraphImpl = Object.create(ProtoGraph) graph.type = source.type graph.mutable = false graph.transforming = false graph.nodes = source.nodes graph.edges = source.edges graph.adjacency = source.adjacency graph.reverseAdjacency = source.reverseAdjacency graph.nextNodeIndex = source.nextNodeIndex graph.nextEdgeIndex = source.nextEdgeIndex graph.acyclic = source.acyclic return graph } /** @internal */ export const snapshot = (graph: Graph.Graph): Graph.Snapshot => { const impl = toImpl(graph) return { type: graph.type, nodes: Array.from(impl.nodes, ([index, data]) => ({ index, data })).sort((a, b) => a.index - b.index), edges: Array.from(impl.edges, ([index, edge]) => ({ index, source: edge.source, target: edge.target, data: edge.data })).sort((a, b) => a.index - b.index) } } /** @internal */ export const hydrate = ( snapshot: Graph.Snapshot ): Graph.Graph => { const graph = make(snapshot.type, false) for (const node of snapshot.nodes) { graph.nodes.set(node.index, node.data) graph.adjacency.set(node.index, []) graph.reverseAdjacency.set(node.index, []) } for (const edge of snapshot.edges) { graph.edges.set(edge.index, { source: edge.source, target: edge.target, data: edge.data }) graph.adjacency.get(edge.source)!.push(edge.index) graph.reverseAdjacency.get(edge.target)!.push(edge.index) if (snapshot.type === "undirected") { graph.adjacency.get(edge.target)!.push(edge.index) graph.reverseAdjacency.get(edge.source)!.push(edge.index) } } graph.nextNodeIndex = snapshot.nodes.length === 0 ? 0 : snapshot.nodes[snapshot.nodes.length - 1].index + 1 graph.nextEdgeIndex = snapshot.edges.length === 0 ? 0 : snapshot.edges[snapshot.edges.length - 1].index + 1 graph.acyclic = Option.none() return graph as unknown as Graph.Graph }