Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 283 additions & 50 deletions hyperformula/src/DependencyGraph/DependencyGraph.ts

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion hyperformula/src/DependencyGraph/FormulaVertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

import {AbsoluteCellRange} from '../AbsoluteCellRange'
import {AbsoluteCellRange, SimpleCellRange} from '../AbsoluteCellRange'
import {ArraySize} from '../ArraySize'
import {ArrayValue, ErroredArray, CellArray, NotComputedArray} from '../ArrayValue'
import {CellError, equalSimpleCellAddress, ErrorType, SimpleCellAddress} from '../Cell'
Expand All @@ -15,7 +15,18 @@ import {Maybe} from '../Maybe'
import {Ast} from '../parser'
import {ColumnsSpan, RowsSpan} from '../Span'
import {CellVertex} from './CellVertex'
import {RangeVertex} from './RangeVertex'

/**
* A dependency discovered while evaluating a formula (e.g., the target of INDIRECT), as opposed to one collected
* from the formula's AST.
*/
export interface RuntimeDependency {
/** Address or range read by the formula. Informational: the vertex is the source of truth. */
address: SimpleCellAddress | SimpleCellRange,
/** Vertex that was read: a cell, or the range vertex of a range. The graph has an edge from it to the formula vertex. */
vertex: CellVertex | RangeVertex,
}

/**
* Abstract base class for vertices that contain formulas in the dependency graph.
Expand All @@ -25,6 +36,11 @@ import {CellVertex} from './CellVertex'
* and {@link ArrayFormulaVertex} for array formulas that span multiple cells.
*/
export abstract class FormulaVertex extends CellVertex {
/**
* Dependencies recorded during the last evaluation of the formula. Undefined for formulas that have none.
*/
public runtimeDependencies?: RuntimeDependency[]

protected constructor(
protected formula: Ast,
protected cellAddress: SimpleCellAddress,
Expand Down Expand Up @@ -87,6 +103,20 @@ export abstract class FormulaVertex extends CellVertex {
public abstract valueOrUndef(): Maybe<InterpreterValue>

public abstract isComputed(): boolean

/**
* Points runtime dependencies on oldVertex to newVertex, when a cell vertex is replaced by another one.
*/
public replaceRuntimeDependencyVertex(oldVertex: CellVertex, newVertex: CellVertex): void {
if (this.runtimeDependencies === undefined) {
return
}
for (const dependency of this.runtimeDependencies) {
if (dependency.vertex === oldVertex) {
dependency.vertex = newVertex
}
}
}
}

/**
Expand Down
44 changes: 38 additions & 6 deletions hyperformula/src/DependencyGraph/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { SimpleCellAddress } from '../Cell'
import { SimpleCellRange } from '../AbsoluteCellRange'
import { TopSort, TopSortResult } from './TopSort'
import { NodeProcessingResult, TopSort, TopSortResult } from './TopSort'
import { ProcessableValue } from './ProcessableValue'

export type NodeId = number
Expand All @@ -15,6 +15,21 @@ export type DependencyQuery<Node> = (vertex: Node) => [(SimpleCellAddress | Simp
* Interface for nodes that can be stored in the Graph.
* The idInGraph property is managed by the Graph class and should not be set externally.
*/
/**
* View of a running top-sort pass, passed to the operating function together with the node.
*/
export interface TopSortPass<Node> {
/**
* Returns true if the node was reached by the pass and its value may still change later in it.
*/
isPending(node: Node): boolean,
/**
* Requests that a pending node is computed before the node being processed is retried.
* Call it right before returning {@link NodeProcessingResult.DEFERRED}.
*/
requestPull(node: Node): void,
}

export interface GraphNode {
idInGraph?: NodeId,
}
Expand Down Expand Up @@ -258,24 +273,41 @@ export class Graph<Node extends GraphNode> {
* Sorts the whole graph topologically. Nodes that are on cycles are kept separate.
*/
public topSortWithScc(): TopSortResult<Node> {
return this.getTopSortedWithSccSubgraphFrom(this.getNodes(), () => true, () => { })
return this.getTopSortedWithSccSubgraphFrom(this.getNodes(), () => NodeProcessingResult.CHANGED, () => { })
}

/**
* Sorts the graph topologically. Nodes that are on cycles are kept separate.
*
* @param {Node[]} modifiedNodes - seed for computation. The algorithm assumes that only these nodes have changed since the last run.
* @param {(node: Node) => boolean} operatingFunction - recomputes value of a node, and returns whether a change occurred
* @param {(node: Node, pass: TopSortPass<Node>) => NodeProcessingResult} operatingFunction - recomputes value of a node, and returns the outcome
* @param {(node: Node) => void} onCycle - action to be performed when node is on cycle
*/
public getTopSortedWithSccSubgraphFrom(
modifiedNodes: Node[],
operatingFunction: (node: Node) => boolean,
operatingFunction: (node: Node, pass: TopSortPass<Node>) => NodeProcessingResult,
onCycle: (node: Node) => void
): TopSortResult<Node> {
const topSortAlgorithm = new TopSort<Node>(this.nodesSparseArray, this.edgesSparseArray)
const precedentIds = (id: NodeId): NodeId[] => {
const node = this.nodesSparseArray[id]
if (node === undefined) {
return []
}
return this.dependencyQuery(node)
.map(([, dependency]) => dependency.idInGraph)
.filter((dependencyId): dependencyId is NodeId => dependencyId !== undefined)
}
const topSortAlgorithm = new TopSort<Node>(this.nodesSparseArray, this.edgesSparseArray, precedentIds)
const pass: TopSortPass<Node> = {
isPending: (node: Node) => node.idInGraph !== undefined && topSortAlgorithm.isPending(node.idInGraph),
requestPull: (node: Node) => {
if (node.idInGraph !== undefined) {
topSortAlgorithm.requestPull(node.idInGraph)
}
},
}
const modifiedNodesIds = modifiedNodes.map(node => node.idInGraph).filter(id => id !== undefined) as NodeId[]
return topSortAlgorithm.getTopSortedWithSccSubgraphFrom(modifiedNodesIds, operatingFunction, onCycle)
return topSortAlgorithm.getTopSortedWithSccSubgraphFrom(modifiedNodesIds, (node: Node) => operatingFunction(node, pass), onCycle)
}

/**
Expand Down
Loading
Loading