Skip to main content

formualizer_eval/engine/
plan.rs

1use crate::SheetId;
2use crate::engine::arena::{AstNodeId, DataStore};
3use crate::engine::sheet_registry::SheetRegistry;
4use formualizer_common::Coord as AbsCoord;
5use formualizer_common::CoordBuildHasher;
6use formualizer_common::ExcelError;
7use formualizer_common::PackedSheetCell;
8use formualizer_parse::parser::CollectPolicy;
9use std::collections::HashMap;
10
11/// Compact range descriptor used during planning (engine-only)
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum RangeKey {
14    Rect {
15        sheet: SheetId,
16        start: AbsCoord,
17        end: AbsCoord, // inclusive
18    },
19    WholeRow {
20        sheet: SheetId,
21        row: u32,
22    },
23    WholeCol {
24        sheet: SheetId,
25        col: u32,
26    },
27    /// Partially bounded rectangle; None means unbounded in that direction
28    OpenRect {
29        sheet: SheetId,
30        start_row: Option<u32>,
31        start_col: Option<u32>,
32        end_row: Option<u32>,
33        end_col: Option<u32>,
34    },
35}
36
37/// Bitflags conveying per-formula traits
38pub type FormulaFlags = u8;
39pub const F_VOLATILE: FormulaFlags = 0b0000_0001;
40pub const F_HAS_RANGES: FormulaFlags = 0b0000_0010;
41pub const F_HAS_NAMES: FormulaFlags = 0b0000_0100;
42pub const F_HAS_TABLES: FormulaFlags = 0b0001_0000;
43pub const F_LIKELY_ARRAY: FormulaFlags = 0b0000_1000;
44
45#[derive(Debug, Default, Clone)]
46pub struct DependencyPlan {
47    pub formula_targets: Vec<(SheetId, AbsCoord)>,
48    pub global_cells: Vec<(SheetId, AbsCoord)>,
49    pub vertex_pool: Vec<(SheetId, AbsCoord)>,
50    pub vertex_pool_packed: Vec<PackedSheetCell>,
51    pub formula_target_pool_indices: Vec<u32>,
52    pub global_cell_pool_indices: Vec<u32>,
53    pub per_formula_cells: Vec<Vec<u32>>, // indices into global_cells
54    pub per_formula_ranges: Vec<Vec<RangeKey>>,
55    pub per_formula_names: Vec<Vec<String>>,
56    pub per_formula_tables: Vec<Vec<String>>,
57    pub per_formula_flags: Vec<FormulaFlags>,
58    pub edges_flat: Option<Vec<u32>>, // optional flat adjacency (indices into global_cells)
59    pub offsets: Option<Vec<u32>>,    // len = num_formulas + 1 when edges_flat is Some
60}
61
62#[derive(Debug, Clone, Copy)]
63pub enum DependencyPlanAst<'a> {
64    Tree(&'a formualizer_parse::parser::ASTNode),
65    Arena(AstNodeId),
66}
67
68type EnsureVertexPoolIndex<'a> = dyn FnMut(&mut DependencyPlan, (SheetId, AbsCoord)) -> u32 + 'a;
69
70struct PlanReferenceContext<'a> {
71    sheet_reg: &'a mut SheetRegistry,
72    data_store: Option<&'a DataStore>,
73    current_sheet: SheetId,
74    policy: &'a CollectPolicy,
75    plan: &'a mut DependencyPlan,
76    cell_index: &'a mut HashMap<PackedSheetCell, u32, CoordBuildHasher>,
77    ensure_vertex_pool_index: &'a mut EnsureVertexPoolIndex<'a>,
78    per_cells: &'a mut Vec<u32>,
79    per_ranges: &'a mut Vec<RangeKey>,
80    per_names: &'a mut Vec<String>,
81    per_tables: &'a mut Vec<String>,
82    flags: &'a mut FormulaFlags,
83}
84
85fn no_local_bindings(
86    _: &PlanReferenceContext<'_>,
87    _: &str,
88    _: usize,
89) -> crate::engine::refs::LocalBindingStyle {
90    crate::engine::refs::LocalBindingStyle::None
91}
92
93fn plan_data_store<'context>(context: &'context PlanReferenceContext<'_>) -> &'context DataStore {
94    context
95        .data_store
96        .expect("arena traversal requires a data store")
97}
98
99fn plan_sheet_registry<'context>(
100    context: &'context PlanReferenceContext<'_>,
101) -> &'context SheetRegistry {
102    context.sheet_reg
103}
104
105fn collect_plan_reference(
106    context: &mut PlanReferenceContext<'_>,
107    reference: crate::engine::refs::SemanticReference<'_>,
108) -> Result<(), ExcelError> {
109    use crate::engine::refs::SemanticReference;
110
111    match reference {
112        SemanticReference::Cell(cell) => {
113            let dep_sheet = cell
114                .sheet
115                .name()
116                .map(|name| context.sheet_reg.id_for(name))
117                .unwrap_or(context.current_sheet);
118            let key = (dep_sheet, AbsCoord::from_excel(cell.row, cell.col));
119            let packed = PackedSheetCell::try_new(dep_sheet, key.1.row(), key.1.col())
120                .expect("plan dependency coordinate must fit PackedSheetCell");
121            let idx = match context.cell_index.get(&packed) {
122                Some(&idx) => idx,
123                None => {
124                    let new_idx = context.plan.global_cells.len() as u32;
125                    context.plan.global_cells.push(key);
126                    context.cell_index.insert(packed, new_idx);
127                    let pool_idx = (context.ensure_vertex_pool_index)(context.plan, key);
128                    context.plan.global_cell_pool_indices.push(pool_idx);
129                    new_idx
130                }
131            };
132            context.per_cells.push(idx);
133        }
134        SemanticReference::FiniteRange(range) => {
135            let (sr, sc, er, ec) = range
136                .finite_bounds()
137                .expect("finite reference must have all bounds");
138            let area = range.saturating_area().expect("finite area");
139
140            // Planning's caller-owned policy (historically 16 in the standard
141            // planning setup) intentionally differs from graph ingest's default 64.
142            if context.policy.expand_small_ranges
143                && area <= context.policy.range_expansion_limit as u64
144            {
145                let row_abs = range.start_row_abs && range.end_row_abs;
146                let col_abs = range.start_col_abs && range.end_col_abs;
147                for row in sr..=er {
148                    for col in sc..=ec {
149                        collect_plan_reference(
150                            context,
151                            SemanticReference::Cell(crate::engine::refs::CellReference {
152                                original: range.original,
153                                sheet: range.sheet,
154                                row,
155                                col,
156                                row_abs,
157                                col_abs,
158                            }),
159                        )?;
160                    }
161                }
162            } else {
163                let dep_sheet = range
164                    .sheet
165                    .name()
166                    .map(|name| context.sheet_reg.id_for(name))
167                    .unwrap_or(context.current_sheet);
168                context.per_ranges.push(RangeKey::Rect {
169                    sheet: dep_sheet,
170                    start: AbsCoord::from_excel(sr, sc),
171                    end: AbsCoord::from_excel(er, ec),
172                });
173            }
174        }
175        SemanticReference::OpenRange(range) => {
176            let dep_sheet = range
177                .sheet
178                .name()
179                .map(|name| context.sheet_reg.id_for(name))
180                .unwrap_or(context.current_sheet);
181            match (
182                range.start_row,
183                range.start_col,
184                range.end_row,
185                range.end_col,
186            ) {
187                (None, Some(col), None, Some(end_col)) if col == end_col => {
188                    context.per_ranges.push(RangeKey::WholeCol {
189                        sheet: dep_sheet,
190                        col,
191                    })
192                }
193                (Some(row), None, Some(end_row), None) if row == end_row => {
194                    context.per_ranges.push(RangeKey::WholeRow {
195                        sheet: dep_sheet,
196                        row,
197                    })
198                }
199                _ => context.per_ranges.push(RangeKey::OpenRect {
200                    sheet: dep_sheet,
201                    start_row: range.start_row.map(|row| row.saturating_sub(1)),
202                    start_col: range.start_col.map(|col| col.saturating_sub(1)),
203                    end_row: range.end_row.map(|row| row.saturating_sub(1)),
204                    end_col: range.end_col.map(|col| col.saturating_sub(1)),
205                }),
206            }
207        }
208        SemanticReference::ExternalSource(external) => match external.kind {
209            formualizer_parse::parser::ExternalRefKind::Cell { .. } => {
210                *context.flags |= F_HAS_NAMES;
211                context.per_names.push(external.raw.clone());
212            }
213            formualizer_parse::parser::ExternalRefKind::Range { .. } => {
214                *context.flags |= F_HAS_TABLES;
215                context.per_tables.push(external.raw.clone());
216            }
217        },
218        SemanticReference::Name(name) => {
219            if context.policy.include_names {
220                *context.flags |= F_HAS_NAMES;
221                context.per_names.push(name.to_string());
222            }
223        }
224        SemanticReference::Table(table) => {
225            *context.flags |= F_HAS_TABLES;
226            context.per_tables.push(table.name.clone());
227        }
228        SemanticReference::ThreeDimensional(_) | SemanticReference::Unsupported(_) => {}
229    }
230    Ok(())
231}
232
233/// Build a compact dependency plan from ASTs without mutating the graph.
234/// Sheets referenced by name are resolved/created through SheetRegistry at plan time.
235pub fn build_dependency_plan<'a, I>(
236    sheet_reg: &mut SheetRegistry,
237    formulas: I,
238    policy: &CollectPolicy,
239    volatile_flags: Option<&[bool]>,
240) -> Result<DependencyPlan, ExcelError>
241where
242    I: Iterator<Item = (&'a str, u32, u32, &'a formualizer_parse::parser::ASTNode)>,
243{
244    let mut plan = DependencyPlan::default();
245
246    // Global cell pool: packed absolute cell -> index.
247    //
248    // Uses CoordBuildHasher because FxHasher's weak avalanche collides badly
249    // on structured packed keys (PackedSheetCell reserves bits 50..64 and has
250    // narrow dynamic range on row-major workloads), turning this O(N) loop
251    // into O(N^2). See formualizer_common::coord_hash.
252    let mut cell_index: HashMap<PackedSheetCell, u32, CoordBuildHasher> =
253        HashMap::with_hasher(CoordBuildHasher);
254    // Unified vertex pool for loader-specialized ensure.
255    let mut vertex_pool_index: HashMap<PackedSheetCell, u32, CoordBuildHasher> =
256        HashMap::with_hasher(CoordBuildHasher);
257
258    let mut ensure_vertex_pool_index =
259        |plan: &mut DependencyPlan, key: (SheetId, AbsCoord)| -> u32 {
260            let packed = PackedSheetCell::try_new(key.0, key.1.row(), key.1.col())
261                .expect("plan vertex pool coordinate must fit PackedSheetCell");
262            match vertex_pool_index.get(&packed) {
263                Some(&idx) => idx,
264                None => {
265                    let new_idx = plan.vertex_pool.len() as u32;
266                    plan.vertex_pool.push(key);
267                    plan.vertex_pool_packed.push(packed);
268                    vertex_pool_index.insert(packed, new_idx);
269                    new_idx
270                }
271            }
272        };
273
274    for (i, (sheet_name, row, col, ast)) in formulas.enumerate() {
275        let sheet_id = sheet_reg.id_for(sheet_name);
276        let target = (sheet_id, AbsCoord::from_excel(row, col));
277        plan.formula_targets.push(target);
278        let target_pool_idx = ensure_vertex_pool_index(&mut plan, target);
279        plan.formula_target_pool_indices.push(target_pool_idx);
280
281        let mut flags: FormulaFlags = 0;
282        if let Some(v) = volatile_flags.and_then(|v| v.get(i)).copied()
283            && v
284        {
285            flags |= F_VOLATILE;
286        }
287
288        let mut per_cells: Vec<u32> = Vec::new();
289        let mut per_ranges: Vec<RangeKey> = Vec::new();
290        let mut per_names: Vec<String> = Vec::new();
291        let mut per_tables: Vec<String> = Vec::new();
292
293        {
294            let mut context = PlanReferenceContext {
295                sheet_reg,
296                data_store: None,
297                current_sheet: sheet_id,
298                policy,
299                plan: &mut plan,
300                cell_index: &mut cell_index,
301                ensure_vertex_pool_index: &mut ensure_vertex_pool_index,
302                per_cells: &mut per_cells,
303                per_ranges: &mut per_ranges,
304                per_names: &mut per_names,
305                per_tables: &mut per_tables,
306                flags: &mut flags,
307            };
308            crate::engine::refs::visit_tree_references(
309                ast,
310                &mut context,
311                no_local_bindings,
312                collect_plan_reference,
313            )?;
314        }
315
316        plan.per_formula_cells.push(per_cells);
317        plan.per_formula_ranges.push(per_ranges);
318        plan.per_formula_names.push(per_names);
319        plan.per_formula_tables.push(per_tables);
320        plan.per_formula_flags.push(flags);
321    }
322
323    Ok(plan)
324}
325
326/// Build a compact dependency plan from a mix of tree and arena ASTs.
327pub fn build_dependency_plan_mixed<'a, I>(
328    sheet_reg: &mut SheetRegistry,
329    data_store: &DataStore,
330    formulas: I,
331    policy: &CollectPolicy,
332    volatile_flags: Option<&[bool]>,
333) -> Result<DependencyPlan, ExcelError>
334where
335    I: Iterator<Item = (&'a str, u32, u32, DependencyPlanAst<'a>)>,
336{
337    let mut plan = DependencyPlan::default();
338
339    let mut cell_index: HashMap<PackedSheetCell, u32, CoordBuildHasher> =
340        HashMap::with_hasher(CoordBuildHasher);
341    let mut vertex_pool_index: HashMap<PackedSheetCell, u32, CoordBuildHasher> =
342        HashMap::with_hasher(CoordBuildHasher);
343
344    let mut ensure_vertex_pool_index =
345        |plan: &mut DependencyPlan, key: (SheetId, AbsCoord)| -> u32 {
346            let packed = PackedSheetCell::try_new(key.0, key.1.row(), key.1.col())
347                .expect("plan vertex pool coordinate must fit PackedSheetCell");
348            match vertex_pool_index.get(&packed) {
349                Some(&idx) => idx,
350                None => {
351                    let new_idx = plan.vertex_pool.len() as u32;
352                    plan.vertex_pool.push(key);
353                    plan.vertex_pool_packed.push(packed);
354                    vertex_pool_index.insert(packed, new_idx);
355                    new_idx
356                }
357            }
358        };
359
360    for (i, (sheet_name, row, col, ast)) in formulas.enumerate() {
361        let sheet_id = sheet_reg.id_for(sheet_name);
362        let target = (sheet_id, AbsCoord::from_excel(row, col));
363        plan.formula_targets.push(target);
364        let target_pool_idx = ensure_vertex_pool_index(&mut plan, target);
365        plan.formula_target_pool_indices.push(target_pool_idx);
366
367        let mut flags: FormulaFlags = 0;
368        if let Some(v) = volatile_flags.and_then(|v| v.get(i)).copied()
369            && v
370        {
371            flags |= F_VOLATILE;
372        }
373
374        let mut per_cells: Vec<u32> = Vec::new();
375        let mut per_ranges: Vec<RangeKey> = Vec::new();
376        let mut per_names: Vec<String> = Vec::new();
377        let mut per_tables: Vec<String> = Vec::new();
378
379        {
380            let mut context = PlanReferenceContext {
381                sheet_reg,
382                data_store: Some(data_store),
383                current_sheet: sheet_id,
384                policy,
385                plan: &mut plan,
386                cell_index: &mut cell_index,
387                ensure_vertex_pool_index: &mut ensure_vertex_pool_index,
388                per_cells: &mut per_cells,
389                per_ranges: &mut per_ranges,
390                per_names: &mut per_names,
391                per_tables: &mut per_tables,
392                flags: &mut flags,
393            };
394            match ast {
395                DependencyPlanAst::Tree(ast) => crate::engine::refs::visit_tree_references(
396                    ast,
397                    &mut context,
398                    no_local_bindings,
399                    collect_plan_reference,
400                )?,
401                DependencyPlanAst::Arena(ast_id) => crate::engine::refs::visit_arena_references(
402                    ast_id,
403                    &mut context,
404                    plan_data_store,
405                    plan_sheet_registry,
406                    collect_plan_reference,
407                )?,
408            }
409        }
410
411        plan.per_formula_cells.push(per_cells);
412        plan.per_formula_ranges.push(per_ranges);
413        plan.per_formula_names.push(per_names);
414        plan.per_formula_tables.push(per_tables);
415        plan.per_formula_flags.push(flags);
416    }
417
418    Ok(plan)
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use crate::engine::arena::DataStore;
425    use crate::engine::sheet_registry::SheetRegistry;
426    use formualizer_parse::parse;
427
428    #[test]
429    fn overflow_sized_ranges_stay_compressed_in_plan() {
430        for formula in [
431            "=SUM(A1:FLA983055)",
432            "=SUM(A1:XFD262144)",
433            "=SUM(A1:XFD1048576)",
434        ] {
435            let ast = parse(formula).unwrap();
436            let policy = CollectPolicy {
437                expand_small_ranges: true,
438                range_expansion_limit: 64,
439                include_names: true,
440            };
441            let mut registry = SheetRegistry::new();
442            let plan = build_dependency_plan(
443                &mut registry,
444                std::iter::once(("Sheet1", 1, 1, &ast)),
445                &policy,
446                None,
447            )
448            .unwrap();
449            assert!(plan.per_formula_cells[0].is_empty(), "{formula}");
450            assert_eq!(plan.per_formula_ranges[0].len(), 1, "{formula}");
451        }
452    }
453
454    #[test]
455    fn tree_plan_handles_deep_left_associative_formula_without_call_stack_growth() {
456        let terms = if cfg!(debug_assertions) {
457            20_000
458        } else {
459            100_000
460        };
461        let formula = format!(
462            "={}",
463            std::iter::repeat_n("A1", terms)
464                .collect::<Vec<_>>()
465                .join("+")
466        );
467        let ast = parse(&formula).unwrap();
468        let policy = CollectPolicy {
469            expand_small_ranges: true,
470            range_expansion_limit: 16,
471            include_names: true,
472        };
473        let mut registry = SheetRegistry::new();
474        let plan = build_dependency_plan(
475            &mut registry,
476            std::iter::once(("Sheet1", 1, 1, &ast)),
477            &policy,
478            None,
479        )
480        .unwrap();
481        assert_eq!(plan.global_cells.len(), 1);
482        assert_eq!(plan.per_formula_cells[0].len(), terms);
483
484        // ASTNode owns a recursively boxed tree, so avoid making this traversal
485        // test depend on the standard library's recursive drop implementation.
486        std::mem::forget(ast);
487    }
488
489    #[test]
490    fn mixed_arena_plan_matches_tree_plan_for_basic_refs() {
491        let asts = [
492            parse("=A1+SUM(B2:C3)+NamedThing").unwrap(),
493            parse("=Sheet2!D4+Table1[#Data]").unwrap(),
494        ];
495        let policy = CollectPolicy {
496            expand_small_ranges: true,
497            range_expansion_limit: 16,
498            include_names: true,
499        };
500
501        let mut tree_reg = SheetRegistry::new();
502        let tree_plan = build_dependency_plan(
503            &mut tree_reg,
504            asts.iter()
505                .enumerate()
506                .map(|(i, ast)| ("Sheet1", (i + 1) as u32, 5, ast)),
507            &policy,
508            Some(&[false, true]),
509        )
510        .unwrap();
511
512        let mut arena_reg = SheetRegistry::new();
513        arena_reg.id_for("Sheet1");
514        let mut store = DataStore::new();
515        let ids: Vec<_> = asts
516            .iter()
517            .map(|ast| store.store_ast(ast, &arena_reg))
518            .collect();
519        let arena_plan = build_dependency_plan_mixed(
520            &mut arena_reg,
521            &store,
522            ids.iter()
523                .enumerate()
524                .map(|(i, id)| ("Sheet1", (i + 1) as u32, 5, DependencyPlanAst::Arena(*id))),
525            &policy,
526            Some(&[false, true]),
527        )
528        .unwrap();
529
530        assert_eq!(arena_plan.formula_targets, tree_plan.formula_targets);
531        assert_eq!(arena_plan.global_cells, tree_plan.global_cells);
532        assert_eq!(arena_plan.vertex_pool, tree_plan.vertex_pool);
533        assert_eq!(
534            arena_plan.formula_target_pool_indices,
535            tree_plan.formula_target_pool_indices
536        );
537        assert_eq!(
538            arena_plan.global_cell_pool_indices,
539            tree_plan.global_cell_pool_indices
540        );
541        assert_eq!(arena_plan.per_formula_cells, tree_plan.per_formula_cells);
542        assert_eq!(arena_plan.per_formula_ranges, tree_plan.per_formula_ranges);
543        assert_eq!(arena_plan.per_formula_names, tree_plan.per_formula_names);
544        assert_eq!(arena_plan.per_formula_tables, tree_plan.per_formula_tables);
545        assert_eq!(arena_plan.per_formula_flags, tree_plan.per_formula_flags);
546    }
547}