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_row: Option<u32>,
31 start_col: Option<u32>,
32 end_row: Option<u32>,
33 end_col: Option<u32>,
34 },
35}
36
37pub 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>>, 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>>, pub offsets: Option<Vec<u32>>, }
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 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
233pub 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 let mut cell_index: HashMap<PackedSheetCell, u32, CoordBuildHasher> =
253 HashMap::with_hasher(CoordBuildHasher);
254 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
326pub 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 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}