/* * 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 { GraphCallbacks } from './spreadsheet_vertex_base'; import { SpreadsheetVertexBase } from './spreadsheet_vertex_base'; import type { Cell, CellValue, ICellAddress, UnionValue } from 'treb-base-types'; import { Area, Box, ValueType } from 'treb-base-types'; import type { ExpressionUnit } from 'treb-parser'; import { Color } from './vertex'; import { ErrorType } from '../function-error'; export enum SpreadsheetError { None, CalculationError, } /** * specialization of vertex with attached data and calculation metadata */ export class SpreadsheetVertex extends SpreadsheetVertexBase { public static type = 'spreadsheet-vertex'; // I wonder if we should drop this and look up on demand -- might // help in large blocks... public reference?: Cell; public error = SpreadsheetError.None; // why is this (?)? can't we use a default junk address? public address?: ICellAddress; //public result: UnionOrArray = UndefinedUnion(); public result: UnionValue = {type: ValueType.undefined}; public expression: ExpressionUnit = { type: 'missing', id: -1 }; public expression_error = false; public short_circuit = false; public type = SpreadsheetVertex.type; // for type guard /** * it seems like this could be cached, if it gets checked a lot * also what's with the crazy return signature? [fixed] */ get array_head(): boolean { if (!this.address) return false; return (!!this.reference) && (!!this.reference.area) && (this.reference.area.start.column === this.address.column) && (this.reference.area.start.row === this.address.row); } /** * to support restoring cached values (from file), we need a way to get * the value from the reference (cell). normally this is done during * calculation, and in reverse (we set the value). * * some additional implications of this: * * - does not set volatile/nonvolatile, which is usually managed as a * side-effect of the calculation. * * - does not remove the entry from the dirty list * * - does not clear the internal dirty flag. it used to do that, but we * took it out because we are now managing multple vertex types, and * we don't want to attach that behavior to a type-specific method. * * so the caller needs to explicitly address the dirty and volatile lists * for this vertex. */ public TakeReferenceValue(): void { if (this.reference) { this.result = Box(this.reference.GetValue()); } } /** * once we populate a spill array, we need to follow edges * to dirty nodes and recalculate. watch out for loops, though * * @returns expanded list, or false if we detect a loop */ public ExpandEdgeList(source: SpreadsheetVertex, list: SpreadsheetVertex[]): SpreadsheetVertex[] | false { const expanded: SpreadsheetVertex[] = [...list]; const queue: SpreadsheetVertex[] = [...list]; while (queue.length > 0) { const entry = queue.shift(); if (entry) { for (const edge of entry.edges_out.values()) { if (edge as SpreadsheetVertex === source) { console.info("== source"); return false; } expanded.push(edge as SpreadsheetVertex); queue.push(edge as SpreadsheetVertex); } } } return expanded; } /** * calculates the function, but only if all dependencies are clean. * if one or more dependencies are dirty, just exit. this should work out * so that when the last dependency is satisfied, the propagation will * succeed. FIXME: optimize order. * * FIXME: why is this in vertex, instead of graph? [a: dirty check?] * A: for overloading. leaf extends this class, and has a separate * calculation routine. */ public Calculate(graph: GraphCallbacks): void { if (!this.dirty) return; // it would be nice if we could get this out of the calculate routine, // but that's a problem because we can't calculate in the right order. // one solution might be to have two methods, one which includes it // and one which doesn't, and call the checked method only when necessary. // OTOH that means maintaining the internal calculation part twice (or // adding a method call). if (this.color === Color.white && this.LoopCheck()) { // console.info('LCB', `R${this.address?.row} C${this.address?.column}`, this); // if (this.LoopCheck()) { // throw new Error('loop loop 2') this.dirty = false; if (this.edges_in.size) { // console.info('set loop err', `R${this.address?.row} C${this.address?.column}`, this); // this should alwys be true, because it has edges so // it must be a formula (right?) // we don't have to do that test because now we only set // vertices -> white if they match if (this.reference && ( this.array_head || this.reference.type === ValueType.formula )) { this.reference.SetCalculationError(ErrorType.Loop); } //this.reference?.SetCalculationError('LOOP'); // intuitively this seems like a good idea but I'm not sure // that it is actually necessary (TODO: check) for (const edge of this.edges_out){ (edge as SpreadsheetVertex).Calculate(graph); } return; } /* else { console.info('SKIP loop err', `R${this.address?.row} C${this.address?.column}`, this); } */ // } } // this is done before checking if it's a formula for the case of // arrays: arrays are not formulae but they are dependent on the // array head. if the head is dirty we need to calculate that before // any dependents of _this_ cell are calculated. // the head calculation should take care of setting this value, that is, // we don't need to do the actual lookup. // this prevents a runaway if there's a loop (and we are not catching it), // but there's a side-effect: the dirty flag never gets cleared. if we want // to fix this we need to clean the dirty flag on vertices before a full // recalc, I guess... // that's also why page reload "fixes" the issue: because there's a global // cleaning of dirty flags. or maybe they don't survive serialization, I don't know. for (const edge of this.edges_in) { if ((edge as SpreadsheetVertexBase).dirty) { // console.info('exiting on dirty deps', `R${this.address?.row} C${this.address?.column}`, this); return; } } // console.info('OK calc', `R${this.address?.row} C${this.address?.column}`, this); // we won't have a reference if the reference is to an empty cell, // so check that. [Q: what?] if (this.reference) { if (this.reference.type === ValueType.formula) { this.short_circuit = false; const result = graph.CalculationCallback.call(graph, this); // console.info("RX", result); this.result = result.value; // this test is a waste for 99% of calls // // [FYI it has to do with dynamic dependencies, needs to be documented] // if (this.short_circuit) { return; } // what about setting dirty flag? (...) // and this one for ~75%? if (result.volatile) graph.volatile_list.push(this); } else this.result = this.reference.GetValue4(); // is this going to work properly if it's an error? (...) if (this.array_head) { graph.SpreadCallback.call(graph, this, this.result); } else if (this.reference.type === ValueType.formula) { // adding check for spill, not withstanding the below if (this.result.type === ValueType.array) { // note array of length 1 should not trigger spill behavior // (moved to callback method) // the return value here (recalc) is the list of updated cells. // not sure if we're properly handling cells that _were_ part // of the spill but are no longer. we might need to track the // original value for that (TODO/FIXME) const recalc = graph.SpillCallback.call(graph, this, this.result); if (recalc) { // set everyone dirty first, then recalculate. the aim is to // avoid extra recalcs if a cell is based on two inputs that // change (although that's unlikely here...) const recalc_list: SpreadsheetVertex[] = []; for (const entry of (recalc as SpreadsheetVertex[])) { const expanded = this.ExpandEdgeList(this, Array.from(entry.edges_out.values()) as SpreadsheetVertex[]); if (expanded === false) { throw new Error('loop'); } for (const edge of expanded) { edge.dirty = true; recalc_list.push(edge); } /* // will this work properly with loops? (...) for (const edge of entry.edges_out) { // I think this is the problem. we're setting out edges // on this vertex dirty but not following the graph after // that (edge as SpreadsheetVertex).dirty = true; (edge as SpreadsheetVertex).Calculate(graph); } */ } for (const edge of recalc_list) { if (edge.dirty) { edge.Calculate(graph); } } } } else { // --- // data should now be clean when it gets here (famous last words) // we're now sometimes getting 0-length arrays here. that's a // function of our new polynomial methods, BUT, we should probably // handle it properly regardless. // neven // const single = (this.result.type === ValueType.array) ? this.result.value[0][0] : this.result; // we are using object type in the returned value for sparklines... // so we can't drop it here. we could change rendering though. or // whitelist types. or blacklist types. or something. this.reference.SetCalculatedValue(this.result.value as CellValue, this.result.type); } } } else { console.info('skip dirty constant? [or dangling...]'); } this.dirty = false; // so this is causing problems in long chains. we need // to do this !recursively. there's a slight problem in // that we do it in the loop check as well... not sure // how this will play out. // some options: // (1) push (dirty) edges onto a global list (or list contained in graph) // (2) return boolean, with one state indicating our dependencies need calculating // (3) return a list of dirty dependencies, caller can push onto their list // // (4) because dirty vertices are on the list, you could just loop until // the list is clean (i.e. restart and exit if there are no dirty // vertices left)... that's kind of the same as pushing onto the back of // the list but it avoids extending the list (not sure if that that is // a useful optimization or not) // for (const edge of this.edges_out as Set){ if (edge.dirty) { graph.calculation_list.push(edge); } } } }