/*
* This file is part of TREB.
*
* TREB is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with TREB. If not, see .
*
* Copyright 2022-2026 trebco, llc.
* info@treb.app
*
*/
import type { Vertex} from './vertex';
import { Color } from './vertex';
import { SpreadsheetVertex } from './spreadsheet_vertex';
import { ArrayVertex } from './array-vertex';
import type { SpreadsheetVertexBase, CalculationResult, GraphCallbacks } from './spreadsheet_vertex_base';
import type { StateLeafVertex } from './state_leaf_vertex';
import type { ICellAddress, ICellAddress2, IArea, UnionValue } from 'treb-base-types';
import { Area } from 'treb-base-types';
import type { DataModel } from 'treb-data-model';
import { CalculationLeafVertex } from './calculation_leaf_vertex';
import { AreaUtils } from 'treb-base-types';
export type LeafVertex = StateLeafVertex|CalculationLeafVertex;
export type { StateLeafVertex };
// FIXME: this is a bad habit if you're testing on falsy for OK.
export enum GraphStatus {
OK = 0,
Loop,
CalculationError,
}
/**
* graph is now abstract, as we are extending it with the calculator.
*/
export abstract class Graph implements GraphCallbacks {
/**
* list of vertices, indexed by address as [sheet id][column][row]
*/
public vertices: Array>> = [[]];
public volatile_list: SpreadsheetVertexBase[] = [];
public calculation_list: SpreadsheetVertexBase[] = [];
// list of spills we have created
// public spills: IArea[] = [];
public spill_data: { area: IArea, vertex: StateLeafVertex }[] = [];
// public cells_map: {[index: number]: Cells} = {};
protected abstract readonly model: DataModel;
/**
* where is the loop in the graph (or at least the first one we found)?
*/
public loop_hint?: string;
// special
// public leaf_vertices: LeafVertex[] = [];
public leaf_vertices: Set = new Set();
/** lock down access */
private dirty_list: SpreadsheetVertexBase[] = [];
/** flag set on add edge */
private loop_check_required = false;
/*
public IsArrayVertex(vertex: Vertex): vertex is ArrayVertex {
return vertex.type === ArrayVertex.type;
}
*/
public IsSpreadsheetVertex(vertex: Vertex): vertex is SpreadsheetVertex {
return vertex.type === SpreadsheetVertex.type;
}
/* *
* we used to attach the data model here, but it's now an instance
* property (and readonly). we map still need to rebuild the map,
* so we're retaining the method for the time being (but renamed and
* reparameterized).
*
* if model were a class we wouldn't have to do this...
* /
protected RebuildMap(): void {
this.cells_map = {};
for (const sheet of this.model.sheets.list) {
this.cells_map[sheet.id] = sheet.cells;
}
}
*/
/**
* flush the graph, calculation tree and cells reference
*/
public FlushTree(): void {
this.dirty_list = [];
this.volatile_list = [];
this.vertices = [[]];
this.leaf_vertices.clear();
// this.cells_map = {};
// can we flush spills here without cleaning up? (...)
// this.spills = [];
/** array vertex maintains its own list */
ArrayVertex.Clear();
}
public ResolveArrayHead(address: ICellAddress): ICellAddress {
if (!address.sheet_id) { throw new Error('resolve array head with no sheet id'); }
//const cells = this.cells_map[address.sheet_id];
const cells = this.model.sheets.Find(address.sheet_id)?.cells;
if (!cells) {
throw new Error('no cells? sheet id ' + address.sheet_id);
}
const row = cells.data[address.row];
if (row) {
const cell = row[address.column];
if (cell && cell.area) {
const resolved = { row: cell.area.start.row, column: cell.area.start.column, sheet_id: address.sheet_id };
console.info('array head', address, resolved);
return resolved;
}
}
return address;
}
/**
* iterate vertices
* @param area
*/
public *IterateVertices(area: IArea, create = false): Generator {
// this is wasteful because it repeatedly gets the cells, but
// for a contiguous area we know they're in the same sheet. we
// cal also skip the repeated tests. FIXME
for (const address of AreaUtils.Iterate(area)) {
const vertex = this.GetVertex(address, create);
if (vertex) {
yield vertex;
}
}
}
/** overload */
public GetVertex(address: ICellAddress, create: true): SpreadsheetVertex;
/** overload */
public GetVertex(address: ICellAddress, create?: boolean): SpreadsheetVertex | undefined;
/** returns the vertex at this address. creates it if necessary. */
public GetVertex(address: ICellAddress, create?: boolean): SpreadsheetVertex | undefined {
if (!address.sheet_id) {
console.info(JSON.stringify({address, create}));
console.trace();
throw new Error('getvertex with no sheet id');
}
// if (!this.cells) return undefined;
//const cells = this.cells_map[address.sheet_id];
const cells = this.model.sheets.Find(address.sheet_id)?.cells;
if (!cells) {
throw new Error('no cells? sheet id ' + address.sheet_id);
return undefined;
}
if (!this.vertices[address.sheet_id]) {
if (!create) {
return undefined;
}
this.vertices[address.sheet_id] = [];
}
if (!this.vertices[address.sheet_id][address.column]) {
if (!create) {
return undefined;
}
this.vertices[address.sheet_id][address.column] = [];
}
else {
const existing_vertex = this.vertices[address.sheet_id][address.column][address.row];
if (existing_vertex) {
return existing_vertex;
}
if (!create) return undefined;
}
const vertex = new SpreadsheetVertex();
// vertex.address = { ...address };
// because we are passing in something other than an address, we're
// collecting a lot of extraneous data here. I am worried that someone
// is relying on it, so we will force it to be just the address props.
// see if something breaks.
vertex.address = {
row: address.row,
column: address.column,
absolute_row: address.absolute_row,
absolute_column: address.absolute_column,
sheet_id: address.sheet_id,
};
// this breaks if the cell reference does not point to a cell; that
// happens if a formula references an empty cell, and we run through
// a serialize/unserialize pass.
// FIXME: ensuring the cell will work, but that seems like unecessary
// work; is there a way we can just let this reference dangle? the only
// thing we need to worry about is maintaining the dependency, so if the
// cell _is_ created later we get the update. (...)
// FIXME: the above is not working. recall the BSM model. we had
//
// =IF(C3, C3, C4 + x)
//
// with no value in C3. as a result if you type something in, it won't
// update because there's no bound reference. we can ensure the cell,
// but maybe there's way to get it to work without that.
// I think the reason is because the reference lookup is closed by
// the calc routine; so dirty doesn't do it. let's ensure cell, for now.
/*
// works ok, maybe a little verbose
const row = cells.data2[address.row];
if (row) {
const cell = row[address.column];
if (cell) {
vertex.reference = cell;
}
}
*/
vertex.reference = cells.EnsureCell(address);
this.vertices[address.sheet_id][address.column][address.row] = vertex;
// if there's an array that contains this cell, we need to create an edge
// this.CreateImplicitEdgeToArrays(vertex);
// this is back, in the new form
ArrayVertex.CreateImplicitEdges(vertex, address as ICellAddress2);
return vertex;
}
/** deletes the vertex at this address. */
public RemoveVertex(address: ICellAddress): void {
if (!address.sheet_id) { throw new Error('removevertex with no sheet id'); }
const vertex = this.GetVertex(address, false);
if (!vertex) return;
vertex.Reset();
this.vertices[address.sheet_id][address.column][address.row] = undefined;
// ArrayVertex2.CheckOutbound();
}
/** removes all edges, for rebuilding. leaves value/formula as-is. */
public ResetVertex(address: ICellAddress): void {
const vertex = this.GetVertex(address, false);
if (vertex) vertex.Reset();
}
public RIBcount = 0;
/**
* resets the vertex by removing inbound edges and clearing formula flag.
* we have an option to set dirty because they get called together
* frequently, saves a lookup.
*/
public ResetInbound(address: ICellAddress, set_dirty = false, create = true, remove = false): void {
this.RIBcount++;
const vertex = this.GetVertex(address, create);
// console.info("RIB", address.row, address.column, 'd?', set_dirty, vertex, 'R?', remove);
if (!vertex) {
if (set_dirty) {
const list = ArrayVertex.GetContainingArrays(address as ICellAddress2);
for (const entry of list) {
this.SetVertexDirty(entry);
}
}
return;
}
// this vertexes' dependencies might only have one outbound edge
// (to this); in that case, we could remove the dependency vertex,
// since it is essentially orphaned
let dependencies: Vertex[] = [];
// do this conditionally so we avoid the slice if unecessary
if (remove) {
// dependencies = vertex.edges_in.slice(0);
dependencies = Array.from(vertex.edges_in);
}
vertex.ClearDependencies();
if (set_dirty) {
// this.dirty_list.push(vertex);
// vertex.SetDirty();
this.SetVertexDirty(vertex);
}
// vertex.expression = { type: 'missing', id: -1 };
// vertex.expression_error = false;
// this probably should not happen unless there are no dependents/outbound edges? (...)
if (remove) {
if (!vertex.has_outbound_edges) {
this.RemoveVertex(address);
}
for (const dependency of dependencies) {
if (!dependency.has_inbound_edges && !dependency.has_outbound_edges) {
const target = (dependency as SpreadsheetVertex);
if (target.address) {
this.RemoveVertex(target.address);
}
}
}
/*
if (vertex?.has_outbound_edges) {
console.info('(NOT) removing a vertex with outbound edges...')
}
else {
console.info('removing a vertex')
this.RemoveVertex(address);
}
*/
}
}
/* * dev * /
public ForceClean() {
for (const l1 of this.vertices) {
if (l1) {
for (const l2 of l1) {
if (l2) {
for (const vertex of l2) {
if (vertex && vertex.dirty) {
vertex.dirty = false;
}
}
}
}
}
}
}
/ * * dev, check if any vertices are dirtices * /
public CheckDirty() {
for (const l1 of this.vertices) {
if (l1) {
for (const l2 of l1) {
if (l2) {
for (const vertex of l2) {
if (vertex && vertex.dirty) {
console.info("DIRTY", `R${vertex.address?.row} C${vertex.address?.column}`, vertex);
}
}
}
}
}
}
}
*/
/**
* reset all vertices. this method is used so we can run the loop check
* as part of the graph calculation, instead of requiring the separate call.
*/
public ResetLoopState(): void {
for (const l1 of this.vertices) {
if (l1) {
for (const l2 of l1) {
if (l2) {
for (const vertex of l2) {
if (vertex) {
// vertex.color = Color.white;
vertex.color = vertex.edges_out.size ? Color.white : Color.black;
}
}
}
}
}
}
// this is unecessary
for (const vertex of this.leaf_vertices) {
vertex.color = Color.black;
}
}
/**
* global check returns true if there is any loop. this is more efficient
* than detecting loops on every call to AddEdge. uses the color algorithm
* from CLRS.
*
* UPDATE we switched to a stack-based check because we were hitting
* recursion limits, although that seemed to only happen in workers --
* perhaps they have different stack [in the malloc sense] sizes? in any
* event, I think the version below is now stable.
*
* @param force force a check, for dev/debug
*/
public LoopCheck(force = false): boolean {
// this flag is only set on AddEdge, and only cleared when we successfully
// get through this function. so if there are no new edges, we can bypass.
if (!this.loop_check_required && !force) { return false; }
// vertices is array [][][]
// build a list so we can simplify the second loop (waste of space?)
const list: Vertex[] = [];
for (const l1 of this.vertices) {
if (l1) {
for (const l2 of l1) {
if (l2) {
for (const vertex of l2) {
if (vertex) {
vertex.color = vertex.edges_out.size ? Color.white : Color.black;
list.push(vertex);
}
}
}
}
}
}
// we were having problems with large calculation loops (basically long
// lists of x+1) using a recursive DFS. so we need to switch to a stack,
// just in case, hopefully it won't be too expensive.
// ---
// unwind recursion -> stack. seems to work OK. could we not just
// use the list as the initial stack? (...)
// NOTE: I think this method is bugged. I'm fixing it in the vertex
// version of the loop check routine (because we don't use this anymore)
// but if this ever comes back it needs to be fixed.
const stack: Vertex[] = [];
for (const vertex of list) {
if (vertex.color === Color.white) {
vertex.color = Color.gray; // testing
stack.push(vertex);
while (stack.length) {
// so leave it on the stack until we're done. we may "recurse", in
// which case we need to come back to this item when the children
// have been handled. we will wind up looping again, so there are
// some wasted checks, although I'm not sure how to deal with that
// without duplicating the edge list.
// concept: stack is a list of [edge, skip = 0]
// when processing an entry, do
//
// const x of (skip ? v.edges_out.slice(skip) : v.edges_out)
//
// or maybe be efficient and not fancy,
//
// for (let i = skip; i < v.edges_out.length; i++)
//
// or what you should actually do is use the stack field as the loop
// variable, so it persists. or put something in the vertex so it
// persists and applies to things that are placed on the stack more
// than once.
const v = stack[stack.length - 1];
let completed = true;
if (v.color !== Color.black) {
for (const edge of v.edges_out) {
if (edge.color === Color.gray) {
this.loop_hint = this.RenderAddress((vertex as SpreadsheetVertex).address);
console.info('loop detected @', this.loop_hint);
return true; // exit
}
else if (edge.color === Color.white) {
// here we're pushing onto the stack, so these will be handled
// next, but since v is still on the stack once those are done
// we will hit v again.
// edge.color = Color.gray;
stack.push(edge);
completed = false;
}
}
}
// if we have not pushed anything onto the stack (we have not
// recursed), then we can clean up; since the stack is still the
// same we can pop() now.
if (completed) {
stack.pop();
v.color = Color.black; // v is complete, just in case we test it again
}
}
// OK, tested and complete
vertex.color = Color.black;
}
}
/*
const tail = (vertex: Vertex): boolean => {
vertex.color = Color.gray;
for (const edge of vertex.edges_out) {
if (edge.color === Color.gray) {
this.loop_hint = this.RenderAddress((vertex as SpreadsheetVertex).address);
console.info('loop detected @', this.loop_hint);
return true; // loop
}
else if (edge.color === Color.white) {
if (tail(edge)) {
return true; // loop
}
}
}
vertex.color = Color.black;
return false;
};
for (const vertex of list) {
if (vertex.color === Color.white && tail(vertex)) { return true; }
}
*/
this.loop_check_required = false;
this.loop_hint = undefined;
return false;
}
/**
* render address as string; this is for reporting loops
*/
public RenderAddress(address?: ICellAddress): string {
if (!address) { return 'undefined'; }
let sheet_name = '';
if (address.sheet_id) {
const sheet = this.model.sheets.Find(address.sheet_id);
if (sheet) {
sheet_name = sheet.name + '!';
}
/*
for (const sheet of this.model.sheets.list) {
if (address.sheet_id === sheet.id) {
sheet_name = sheet.name + '!';
break;
}
}
*/
}
const area = new Area(address);
return sheet_name + area.spreadsheet_label;
}
/**
* new array vertices
*/
protected CompositeAddArrayEdge(u: Area, vertex: Vertex): void {
if (!u.start.sheet_id) {
throw new Error('AddArrayEdge called without sheet ID');
}
// create or use existing
const [array_vertex, created] = ArrayVertex.GetVertex(u);
// add an edge
vertex.DependsOn(array_vertex);
// force a check on next calculation pass
this.loop_check_required = true;
if (!created) {
// console.info('reusing, so not adding edges');
return;
}
// now add edges from/to nodes THAT ALREADY EXIST
// range can't span sheets, so we only need one set to look up
const map = this.vertices[u.start.sheet_id];
// console.info({u});
// this might happen on create, we can let it go because the
// references will be added when the relevant sheet is added
if (!map) {
return;
}
// ...
if (u.entire_row) {
// console.group('entire row(s)')
for (let column = 0; column < map.length; column++) {
if (map[column]) {
for (let row = u.start.row; row <= u.end.row; row++ ) {
const vertex = map[column][row];
if (vertex) {
// console.info('add', column, row);
array_vertex.DependsOn(vertex);
}
}
}
}
// console.groupEnd();
}
else if (u.entire_column) {
// console.group('entire column(s)');
for (let column = u.start.column; column <= u.end.column; column++) {
if(map[column]) {
for (const vertex of map[column]) {
if (vertex?.address) {
// console.info('add', vertex.address);
array_vertex.DependsOn(vertex);
}
}
}
}
// console.groupEnd();
}
else {
for (let row = u.start.row; row <= u.end.row; row++) {
for (let column = u.start.column; column <= u.end.column; column++) {
if (map[column]) {
const vertex = map[column][row];
if (vertex) {
array_vertex.DependsOn(vertex);
}
}
/*
else {
console.info("HERE", column, row);
}
*/
}
}
}
}
public AddLeafVertexArrayEdge(u: Area, vertex: LeafVertex) {
this.CompositeAddArrayEdge(u, vertex);
}
/**
* new array vertices
*/
public AddArrayEdge(u: Area, v: ICellAddress): void {
if (!u.start.sheet_id) {
throw new Error('AddArrayEdge called without sheet ID');
}
// this should have already been added...
const v_v = this.GetVertex(v, true);
this.CompositeAddArrayEdge(u, v_v);
}
/** adds an edge from u -> v */
public AddEdge(u: ICellAddress, v: ICellAddress, /* tag?: string */ ): void {
const v_u = this.GetVertex(u, true);
const v_v = this.GetVertex(v, true);
// seems pretty uncommon, not sure it's a useful optimization
// const already_connected = v_u.edges_out.includes(v_v);
// if (already_connected)
// console.info('add edge', u.sheet_id, u.row, u.column, '<-', v.sheet_id, v.row, v.column, tag||'')
// const status = this.LoopCheck(v_v, v_u);
// if (status === GraphStatus.Loop) { return status; }
v_v.DependsOn(v_u);
// add implicit edge to array head. this is required at start
// because the array isn't set implicitly (why not?)
// watch out for missing sheet ID!
if (v_u.reference && v_u.reference.area && !v_u.array_head) {
// console.info('add implicit edge -> array head (?), u', u, ', v', v);
// the old version added an implicit edge from array head -> array
// member, not sure why that was a good idea (or why it doesn't work);
// add an implicit edge -> v instead...
//
// maybe we thought it was a good idea because it would consolidate
// all the edges through the member? you still get edges, though...
this.AddEdge({
...u,
row: v_u.reference.area.start.row,
column: v_u.reference.area.start.column,
}, v); // , 'implicit');
}
this.loop_check_required = true; // because new edges
}
/** removes edge from u -> v */
public RemoveEdge(u: ICellAddress, v: ICellAddress): void {
const v_u = this.GetVertex(u, false);
const v_v = this.GetVertex(v, false);
if (!v_u || !v_v) return;
v_u.RemoveDependent(v_v);
v_v.RemoveDependency(v_u);
}
/**
* not used? remove
* @deprecated
*/
public SetAreaDirty(area: IArea): void {
// console.info("SAD");
if (area.start.column === Infinity
|| area.end.column === Infinity
|| area.start.row === Infinity
|| area.end.row === Infinity ){
throw new Error('don\'t iterate over infinite area');
}
const sheet_id = area.start.sheet_id;
if (!sheet_id) {
throw new Error('invalid area, missing sheet id');
}
for (let column = area.start.column; column <= area.end.column; column++) {
for (let row = area.start.row; row <= area.end.row; row++) {
const address: ICellAddress = {row, column, sheet_id};
const vertex = this.GetVertex(address, false);
if (vertex) { this.SetDirty(address); }
// this.SetArraysDirty(address);
}
}
}
public SetVertexDirty(vertex: SpreadsheetVertexBase): void {
// console.info("SvD", vertex);
// see below re: concern about relying on this
if (vertex.dirty) { return; }
this.dirty_list.push(vertex);
vertex.dirty = true;
for (const edge of vertex.edges_out) {
this.SetVertexDirty(edge as SpreadsheetVertexBase);
}
}
/** sets dirty */
public SetDirty(address: ICellAddress): void {
// console.info("SD", address);
const vertex = this.GetVertex(address, true);
this.SetVertexDirty(vertex);
}
// --- leaf vertex api ---
/**
* adds a leaf vertex to the graph. this implies that someone else is
* managing and maintaining these vertices: we only need references.
*/
public AddLeafVertex(vertex: LeafVertex): void {
/*
if (this.leaf_vertices.has(vertex)) {
console.info("TLV already has", vertex);
}
*/
this.leaf_vertices.add(vertex);
}
/** removes vertex */
public RemoveLeafVertex(vertex: LeafVertex): void {
this.leaf_vertices.delete(vertex);
}
/**
* adds an edge from u -> v where v is a leaf vertex. this doesn't use
* the normal semantics, and you must pass in the actual vertex instead
* of an address.
*
* there is no loop check (leaves are not allowed to have outbound
* edges).
*/
public AddLeafVertexEdge(u: ICellAddress, v: LeafVertex): GraphStatus {
const v_u = this.GetVertex(u, true);
v.DependsOn(v_u);
return GraphStatus.OK;
}
/** removes edge from u -> v */
public RemoveLeafVertexEdge(u: ICellAddress, v: LeafVertex): void {
const v_u = this.GetVertex(u, false);
if (!v_u) return;
v_u.RemoveDependent(v);
v.RemoveDependency(v_u);
}
// --- for initial load ---
public InitializeGraph(): void {
for (const vertex of this.dirty_list) {
// take reference values for spreadsheet vertices
if (this.IsSpreadsheetVertex(vertex)) {
vertex.TakeReferenceValue();
if (this.CheckVolatile(vertex)) {
this.volatile_list.push(vertex);
}
}
// clear dirty flag on _all_ vertices
vertex.dirty = false;
}
// reset, essentially saying we're clean
this.dirty_list = [];
}
// --- calculation ---
/** runs calculation */
public Recalculate(): void {
/*
if (this.GlobalLoopCheck()) {
return GraphStatus.Loop;
}
*/
// FIXME: volatiles should proabbly be caclucated first,
// not last, because they're probably primary.
// for (const vertex of this.volatile_list) {
// vertex.SetDirty();
// }
// we do this using the local function so we can trace back arrays.
// be sure to do this _before_ checking spills
for (const vertex of this.volatile_list) {
this.SetVertexDirty(vertex as SpreadsheetVertex);
}
////////////////////////////////////////
// (moving this up so it comes before we slice the dirty list)
// the problem with flushing all the spills here
// is that if we're not calculating a range that
// intersects with the spill, it will disappear.
// options are (1) to dirty the spill root, so it
// calculates, or (2) to check for intersection.
// checking for intersection might work because
// we should have vertices and they should be marked
// as dirty...
// eh that's not going to work, because edges point
// the wrong way. if you edit a cell within a spill
// range, it won't dirty the spill source because
// the edge goes from spill source -> spill cell.
// we could create a special leaf vertex to watch the
// range. or we could just check here. vertices is
// more elegant (and more memory), this is clumsier (and
// more calc).
this.spill_data = this.spill_data.filter(({area, vertex}) => {
if (vertex.dirty) {
vertex.Reset();
const cells = area.start.sheet_id ? this.model.sheets.Find(area.start.sheet_id)?.cells : undefined;
if (cells) {
for (const {cell, row, column} of cells.IterateRC(new Area(area.start, area.end))) {
if (cell.spill) {
cell.spill = undefined;
if (typeof cell.value === 'undefined') {
cell.Reset();
}
}
// this is necessary for non-head cells in case the cell has deps
this.SetDirty({row, column, sheet_id: area.start.sheet_id});
}
// this.SetDirty(area.start);
}
return false; // drop
}
return true; // keep
});
/*
this.spills = this.spills.filter(spill => {
let dirty = false;
for (const vertex of this.IterateVertices(spill)) {
if (vertex.dirty) {
console.info("spill is dirty (it)");
dirty = true;
break;
}
}
if (dirty) {
const cells = spill.start.sheet_id ? this.model.sheets.Find(spill.start.sheet_id)?.cells : undefined;
if (cells) {
for (const {cell, row, column} of cells.IterateRC(new Area(spill.start, spill.end))) {
if (cell.spill) {
cell.spill = undefined;
if (typeof cell.value === 'undefined') {
cell.Reset();
}
else {
this.SetDirty({row, column, sheet_id: spill.start.sheet_id})
}
}
}
}
return false; // drop
}
return true; // keep
});
*/
//////////////////////////////////////////
this.calculation_list = this.dirty_list.slice(0);
// console.info("CL", this.calculation_list);
this.volatile_list = [];
this.dirty_list = [];
if (this.loop_check_required) {
// console.info('reset loop state');
this.ResetLoopState();
this.loop_check_required = false;
}
// console.info("CL", calculation_list)
// recalculate everything that's dirty. FIXME: optimize path
// so we do fewer wasted checks of "are all my deps clean"?
// for (const vertex of calculation_list) {
// vertex.Calculate(this);
//}
for (let i = 0; i < this.calculation_list.length; i++) {
this.calculation_list[i].Calculate(this);
}
this.calculation_list = [];
}
public abstract CalculationCallback(vertex: SpreadsheetVertexBase): CalculationResult;
public abstract SpreadCallback(vertex: SpreadsheetVertexBase, value: UnionValue): void;
public abstract SpillCallback(vertex: SpreadsheetVertexBase, value: UnionValue): void;
protected abstract CheckVolatile(vertex: SpreadsheetVertex): boolean;
}