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