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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum RangeKey {
14 Rect {
15 sheet: SheetId,
16 start: AbsCoord,
17 end: AbsCoord, },
19 WholeRow {
20 sheet: SheetId,
21 row: u32,
22 },
23 WholeCol {
24 sheet: SheetId,
25 col: u32,
26 },
27 OpenRect {
29 sheet: SheetId,
30 start: Option<AbsCoord>,
31 end: Option<AbsCoord>,
32 },
33}
34
35pub 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>>, 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>>, pub offsets: Option<Vec<u32>>, }
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 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
235pub 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 let mut cell_index: HashMap<PackedSheetCell, u32, CoordBuildHasher> =
255 HashMap::with_hasher(CoordBuildHasher);
256 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
328pub 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 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}