From 71a2bb2dca0fccbf27950c402793a9bed8cab48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Sun, 22 Jun 2025 17:20:34 +0200 Subject: [PATCH] WIP --- Makefile | 7 ++- base/CALC.md | 61 +++++++++++++++++++ .../src/expressions/parser/static_analysis.rs | 1 + base/src/model.rs | 56 ++++++++++++++--- base/src/new_empty.rs | 4 ++ base/src/test/test_fn_or_xor.rs | 4 +- base/src/types.rs | 2 + bindings/wasm/src/lib.rs | 5 +- .../src/components/Worksheet/Worksheet.tsx | 2 - .../WorksheetCanvas/worksheetCanvas.ts | 2 - 10 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 base/CALC.md diff --git a/Makefile b/Makefile index 5f781d1..7337541 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,12 @@ clean: remove-artifacts rm -r -f base/target rm -r -f xlsx/target rm -r -f bindings/python/target - rm -r -f bindings/wasm/targets + rm -r -f bindings/wasm/target + rm -r -f bindings/wasm/pkg + rm -r -f webapp/IronCalc/node_modules + rm -r -f webapp/IronCalc/dist + rm -r -f webapp/app.ironcalc.com/frontend/node_modules + rm -r -f webapp/app.ironcalc.com/frontend/dist rm -f cargo-test-* rm -f base/cargo-test-* rm -f xlsx/cargo-test-* diff --git a/base/CALC.md b/base/CALC.md new file mode 100644 index 0000000..df39aff --- /dev/null +++ b/base/CALC.md @@ -0,0 +1,61 @@ +# Evaluation Strategy + + +We have a list of the spill cells: + +``` +// Checks if the array starting at cell will cover cells whose values +// has been requested +def CheckSpill(cell, array): + for c in cell+array: + support CellHasBeenRequested(c): + if support is not empty: + return support + return [] + +// Fills cells with the result (an array) +def FillCells(cell, result): + + +def EvaluateNodeInContext(node, context): + match node: + case OP(left, right, op): + l = EvaluateNodeInContext(left, context)? + r = EvaluateNodeInContext(left, context)? + return op(l, r) + case FUNCTION(args, fn): + ... + case CELL(cell): + EvaluateCell(cell) + case RANGE(start, end): + ... + + + +def EvaluateCell(cell): + if IsCellEvaluating(cell): + return CIRC + MarkEvaluating(cell) + result = EvaluateNodeInContext(cell.formula, cell) + if isSpill(result): + CheckSpill(cell, array)? + FillCells(result) + + +def EvaluateWorkbook(): + spill_cells = [cell_1, ...., cell_n]; + + + for cell in spill_cells: + result = evaluate(cell) +``` + + + + + + + +# When updating a cell value + +If it was a spill cell we nee \ No newline at end of file diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 731c95a..e395120 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -186,6 +186,7 @@ pub fn add_implicit_intersection(node: &mut Node, add: bool) { }; } +#[derive(Clone)] pub enum StaticResult { Scalar, Array(i32, i32), diff --git a/base/src/model.rs b/base/src/model.rs index 9b61ec6..e8ba92b 100644 --- a/base/src/model.rs +++ b/base/src/model.rs @@ -107,8 +107,6 @@ pub struct Model { pub(crate) shared_strings: HashMap, /// An instance of the parser pub(crate) parser: Parser, - /// The list of cells with formulas that are evaluated or being evaluated - pub(crate) cells: HashMap<(u32, i32, i32), CellState>, /// The locale of the model pub(crate) locale: Locale, /// The language used @@ -117,6 +115,15 @@ pub struct Model { pub(crate) tz: Tz, /// The view id. A view consists of a selected sheet and ranges. pub(crate) view_id: u32, + /// ** Runtime *** + /// The list of cells with formulas that are evaluated or being evaluated + pub(crate) cells: HashMap<(u32, i32, i32), CellState>, + /// The support graph + pub(crate) support_graph: HashMap<(u32, i32, i32), Vec<(u32, i32, i32)>>, + /// If the model is in a switch state then spill cells in the indices should be switched and recalculation redone + pub(crate) switch_cells: Option<(i32, i32)>, + /// Stack of cells being evaluated + pub(crate) stack: Vec<(u32, i32, i32)>, } // FIXME: Maybe this should be the same as CellReference @@ -1240,6 +1247,9 @@ impl Model { locale, tz, view_id: 0, + support_graph: HashMap::new(), + switch_cells: None, + stack: Vec::new(), }; model.parse_formulas(); @@ -1418,7 +1428,8 @@ impl Model { Some(cell) => match cell.get_formula() { None => cell.get_text(&self.workbook.shared_strings, &self.language), Some(i) => { - let formula = &self.parsed_formulas[sheet as usize][i as usize].0; + let (formula, static_result) = + &self.parsed_formulas[sheet as usize][i as usize]; let cell_ref = CellReferenceRC { sheet: self.workbook.worksheets[sheet as usize].get_name(), row: target_row, @@ -1800,9 +1811,9 @@ impl Model { self.set_cell_with_formula(sheet, row, column, formula, new_style_index)?; // Update the style if needed let cell = CellReferenceIndex { sheet, row, column }; - let parsed_formula = - &self.parsed_formulas[sheet as usize][formula_index as usize].0; - if let Some(units) = self.compute_node_units(parsed_formula, &cell) { + let (parsed_formula, static_result) = + self.parsed_formulas[sheet as usize][formula_index as usize].clone(); + if let Some(units) = self.compute_node_units(&parsed_formula, &cell) { let new_style_index = self .workbook .styles @@ -1810,6 +1821,14 @@ impl Model { let style = self.workbook.styles.get_style(new_style_index)?; self.set_cell_style(sheet, row, column, &style)? } + match static_result { + StaticResult::Scalar => {} + StaticResult::Array(_, _) + | StaticResult::Range(_, _) + | StaticResult::Unknown => { + self.workbook.spill_cells.push((sheet, row, column)); + } + } } else { // The list of currencies is '$', '€' and the local currency let mut currencies = vec!["$", "€"]; @@ -2123,8 +2142,29 @@ impl Model { /// Evaluates the model with a top-down recursive algorithm pub fn evaluate(&mut self) { - // clear all computation artifacts - self.cells.clear(); + let mut computed = false; + while !computed { + computed = true; + // clear all computation artifacts + self.cells.clear(); + // Evaluate all the cells that might spill + let spill_cells = self.workbook.spill_cells.clone(); + for (sheet, row, column) in spill_cells { + self.evaluate_cell(CellReferenceIndex { sheet, row, column }); + if self.switch_cells.is_some() { + computed = false; + break; + } + } + if let Some((index1, index2)) = self.switch_cells { + computed = false; + // switch the cells indices in the spill_cells + let cell1 = self.workbook.spill_cells[index1 as usize]; + let cell2 = self.workbook.spill_cells[index2 as usize]; + self.workbook.spill_cells[index1 as usize] = cell2; + self.workbook.spill_cells[index2 as usize] = cell1; + } + } let cells = self.get_all_cells(); diff --git a/base/src/new_empty.rs b/base/src/new_empty.rs index f91840e..96bc51b 100644 --- a/base/src/new_empty.rs +++ b/base/src/new_empty.rs @@ -407,6 +407,7 @@ impl Model { }, tables: HashMap::new(), views, + spill_cells: Vec::new(), }; let parsed_formulas = Vec::new(); let worksheets = &workbook.worksheets; @@ -429,6 +430,9 @@ impl Model { language, tz, view_id: 0, + support_graph: HashMap::new(), + switch_cells: None, + stack: Vec::new(), }; model.parse_formulas(); Ok(model) diff --git a/base/src/test/test_fn_or_xor.rs b/base/src/test/test_fn_or_xor.rs index b4338c3..f37d1ff 100644 --- a/base/src/test/test_fn_or_xor.rs +++ b/base/src/test/test_fn_or_xor.rs @@ -91,12 +91,12 @@ fn fn_or_xor() { model._set("A10", &format!("={func}(X99:Z99")); // Reference to cell with reference to empty range - model._set("B11", "=X99:Z99"); + model._set("B11", "=@X99:Z99"); model._set("A11", &format!("={func}(B11)")); // Reference to cell with non-empty range model._set("X12", "1"); - model._set("B12", "=X12:Z12"); + model._set("B12", "=@X12:Z12"); model._set("A12", &format!("={func}(B12)")); // Reference to text cell diff --git a/base/src/types.rs b/base/src/types.rs index 00e9d7e..9f43b92 100644 --- a/base/src/types.rs +++ b/base/src/types.rs @@ -51,6 +51,8 @@ pub struct Workbook { pub metadata: Metadata, pub tables: HashMap, pub views: HashMap, + /// The list of cells that spill in the order of evaluation + pub spill_cells: Vec<(u32, i32, i32)>, } /// A defined name. The `sheet_id` is the sheet index in case the name is local diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index c46ae44..c128732 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -767,7 +767,10 @@ impl Model { .map_err(to_js_error) } - #[wasm_bindgen(js_name = "getCellArrayStructure")] + #[wasm_bindgen( + js_name = "getCellArrayStructure", + unchecked_return_type = "CellArrayStructure" + )] pub fn get_cell_array_structure( &self, sheet: u32, diff --git a/webapp/IronCalc/src/components/Worksheet/Worksheet.tsx b/webapp/IronCalc/src/components/Worksheet/Worksheet.tsx index 8f54eaf..0f56e37 100644 --- a/webapp/IronCalc/src/components/Worksheet/Worksheet.tsx +++ b/webapp/IronCalc/src/components/Worksheet/Worksheet.tsx @@ -60,7 +60,6 @@ const Worksheet = forwardRef( const spacerElement = useRef(null); const cellOutline = useRef(null); const areaOutline = useRef(null); - const cellOutlineHandle = useRef(null); const cellArrayStructure = useRef(null); const extendToOutline = useRef(null); const columnResizeGuide = useRef(null); @@ -120,7 +119,6 @@ const Worksheet = forwardRef( rowGuide: rowGuideRef, columnHeaders: columnHeadersRef, cellOutline: outline, - cellOutlineHandle: handle, cellArrayStructure: arrayStructure, areaOutline: area, extendToOutline: extendTo, diff --git a/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts b/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts index d2bde37..b1746e2 100644 --- a/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts +++ b/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts @@ -31,7 +31,6 @@ export interface CanvasSettings { canvas: HTMLCanvasElement; cellOutline: HTMLDivElement; areaOutline: HTMLDivElement; - cellOutlineHandle: HTMLDivElement; cellArrayStructure: HTMLDivElement; extendToOutline: HTMLDivElement; columnGuide: HTMLDivElement; @@ -128,7 +127,6 @@ export default class WorksheetCanvas { this.refresh = options.refresh; this.cellOutline = options.elements.cellOutline; - this.cellOutlineHandle = options.elements.cellOutlineHandle; this.cellArrayStructure = options.elements.cellArrayStructure; this.areaOutline = options.elements.areaOutline; this.extendToOutline = options.elements.extendToOutline;