formualizer_eval/engine/
plan.rs

1use crate::SheetId;
2use crate::engine::packed_coord::PackedCoord;
3use crate::engine::sheet_registry::SheetRegistry;
4use formualizer_common::ExcelError;
5use formualizer_parse::parser::{CollectPolicy, ReferenceType};
6use rustc_hash::FxHashMap;
7
8/// Compact range descriptor used during planning (engine-only)
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub enum RangeKey {
11    Rect {
12        sheet: SheetId,
13        start: PackedCoord,
14        end: PackedCoord, // inclusive
15    },
16    WholeRow {
17        sheet: SheetId,
18        row: u32,
19    },
20    WholeCol {
21        sheet: SheetId,
22        col: u32,
23    },
24    /// Partially bounded rectangle; None means unbounded in that direction
25    OpenRect {
26        sheet: SheetId,
27        start: Option<PackedCoord>,
28        end: Option<PackedCoord>,
29    },
30}
31
32/// Bitflags conveying per-formula traits
33pub type FormulaFlags = u8;
34pub const F_VOLATILE: FormulaFlags = 0b0000_0001;
35pub const F_HAS_RANGES: FormulaFlags = 0b0000_0010;
36pub const F_HAS_NAMES: FormulaFlags = 0b0000_0100;
37pub const F_LIKELY_ARRAY: FormulaFlags = 0b0000_1000;
38
39#[derive(Debug, Default, Clone)]
40pub struct DependencyPlan {
41    pub formula_targets: Vec<(SheetId, PackedCoord)>,
42    pub global_cells: Vec<(SheetId, PackedCoord)>,
43    pub per_formula_cells: Vec<Vec<u32>>, // indices into global_cells
44    pub per_formula_ranges: Vec<Vec<RangeKey>>,
45    pub per_formula_flags: Vec<FormulaFlags>,
46    pub edges_flat: Option<Vec<u32>>, // optional flat adjacency (indices into global_cells)
47    pub offsets: Option<Vec<u32>>,    // len = num_formulas + 1 when edges_flat is Some
48}
49
50/// Build a compact dependency plan from ASTs without mutating the graph.
51/// Sheets referenced by name are resolved/created through SheetRegistry at plan time.
52pub fn build_dependency_plan<'a, I>(
53    sheet_reg: &mut SheetRegistry,
54    formulas: I,
55    policy: &CollectPolicy,
56    volatile_flags: Option<&[bool]>,
57) -> Result<DependencyPlan, ExcelError>
58where
59    I: Iterator<Item = (&'a str, u32, u32, &'a formualizer_parse::parser::ASTNode)>,
60{
61    let mut plan = DependencyPlan::default();
62
63    // Global cell pool: (sheet, coord) -> index
64    let mut cell_index: FxHashMap<(SheetId, PackedCoord), u32> = FxHashMap::default();
65
66    for (i, (sheet_name, row, col, ast)) in formulas.enumerate() {
67        let sheet_id = sheet_reg.id_for(sheet_name);
68        let target = (sheet_id, PackedCoord::new(row, col));
69        plan.formula_targets.push(target);
70
71        let mut flags: FormulaFlags = 0;
72        if let Some(v) = volatile_flags.and_then(|v| v.get(i)).copied() {
73            if v {
74                flags |= F_VOLATILE;
75            }
76        }
77
78        let mut per_cells: Vec<u32> = Vec::new();
79        let mut per_ranges: Vec<RangeKey> = Vec::new();
80
81        // Collect references using core collector (may expand small ranges per policy)
82        let refs = ast.collect_references(policy);
83        for r in refs {
84            match r {
85                ReferenceType::Cell { sheet, row, col } => {
86                    let dep_sheet = sheet
87                        .as_deref()
88                        .map(|name| sheet_reg.id_for(name))
89                        .unwrap_or(sheet_id);
90                    let key = (dep_sheet, PackedCoord::new(row, col));
91                    let idx = match cell_index.get(&key) {
92                        Some(&idx) => idx,
93                        None => {
94                            let new_idx = plan.global_cells.len() as u32;
95                            plan.global_cells.push(key);
96                            cell_index.insert(key, new_idx);
97                            new_idx
98                        }
99                    };
100                    per_cells.push(idx);
101                }
102                ReferenceType::Range {
103                    sheet,
104                    start_row,
105                    start_col,
106                    end_row,
107                    end_col,
108                } => {
109                    let dep_sheet = sheet
110                        .as_deref()
111                        .map(|name| sheet_reg.id_for(name))
112                        .unwrap_or(sheet_id);
113                    match (start_row, start_col, end_row, end_col) {
114                        (Some(sr), Some(sc), Some(er), Some(ec)) => {
115                            per_ranges.push(RangeKey::Rect {
116                                sheet: dep_sheet,
117                                start: PackedCoord::new(sr, sc),
118                                end: PackedCoord::new(er, ec),
119                            })
120                        }
121                        (None, Some(c), None, Some(ec)) if c == ec => {
122                            per_ranges.push(RangeKey::WholeCol {
123                                sheet: dep_sheet,
124                                col: c,
125                            })
126                        }
127                        (Some(r), None, Some(er), None) if r == er => {
128                            per_ranges.push(RangeKey::WholeRow {
129                                sheet: dep_sheet,
130                                row: r,
131                            })
132                        }
133                        _ => per_ranges.push(RangeKey::OpenRect {
134                            sheet: dep_sheet,
135                            start: start_row
136                                .zip(start_col)
137                                .map(|(r, c)| PackedCoord::new(r, c)),
138                            end: end_row.zip(end_col).map(|(r, c)| PackedCoord::new(r, c)),
139                        }),
140                    }
141                }
142                ReferenceType::NamedRange(_name) => {
143                    // Resolution handled later; mark via flags if caller cares
144                }
145                ReferenceType::Table(_tbl) => {
146                    // Treat like named entity; handled later
147                }
148            }
149        }
150
151        plan.per_formula_cells.push(per_cells);
152        plan.per_formula_ranges.push(per_ranges);
153        plan.per_formula_flags.push(flags);
154    }
155
156    Ok(plan)
157}