1use crate::engine::VertexId;
2use crate::engine::VertexKind;
3use crate::engine::eval::Engine;
4use crate::engine::template::region::Region;
5use crate::engine::used_extent::{
6 ExtentPolicy, OpenRangeBounds, ResolvedExtent, resolve_used_extent_with_fallback,
7};
8use crate::traits::{
9 EvaluationContext, FunctionProvider, NamedRangeResolver, Range, RangeResolver,
10 ReferenceResolver, Resolver, SourceResolver, Table, TableResolver,
11};
12use formualizer_common::{ExcelError, LiteralValue};
13use formualizer_parse::parser::{ReferenceType, TableReference};
14use rustc_hash::FxHashSet;
15use std::sync::Mutex;
16
17use crate::interpreter::Interpreter;
18
19pub struct DynamicRefCollector<'a, R: EvaluationContext> {
20 pub engine: &'a Engine<R>,
21 pub current_sheet: &'a str,
22 pub(crate) collected: Mutex<FxHashSet<VertexId>>,
23 pub(crate) collected_regions: Mutex<FxHashSet<Region>>,
24}
25
26impl<'a, R: EvaluationContext> DynamicRefCollector<'a, R> {
27 pub fn new(engine: &'a Engine<R>, current_sheet: &'a str) -> Self {
28 Self {
29 engine,
30 current_sheet,
31 collected: Mutex::new(FxHashSet::default()),
32 collected_regions: Mutex::new(FxHashSet::default()),
33 }
34 }
35
36 fn collect_formula_vertices_in_rect(
37 &self,
38 sheet_name: &str,
39 sr: u32,
40 sc: u32,
41 er: u32,
42 ec: u32,
43 ) {
44 let Some(sheet_id) = self.engine.graph.sheet_id(sheet_name) else {
45 return;
46 };
47 let sr0 = sr.saturating_sub(1);
48 let er0 = er.saturating_sub(1);
49 let sc0 = sc.saturating_sub(1);
50 let ec0 = ec.saturating_sub(1);
51 self.collected_regions
52 .lock()
53 .unwrap()
54 .insert(Region::rect(sheet_id, sr0, er0, sc0, ec0).normalized());
55 if self.engine.graph.sheet_index(sheet_id).is_none() {
56 return;
57 }
58
59 let mut out = self.collected.lock().unwrap();
60 for u in self.engine.graph.vertices_in_cols(sheet_id, sc0, ec0) {
61 let Some(row0) = self.engine.graph.vertex_grid_addr(u).map(|addr| addr.row()) else {
62 continue;
63 };
64 if row0 < sr0 || row0 > er0 {
65 continue;
66 }
67 match self.engine.graph.get_vertex_kind(u) {
68 VertexKind::FormulaScalar | VertexKind::FormulaArray
69 if (self.engine.graph.is_dirty(u) || self.engine.graph.is_volatile(u)) =>
70 {
71 out.insert(u);
72 }
73 _ => {}
74 }
75 }
76 }
77
78 fn collect_formula_vertices_for_range(
79 &self,
80 sheet_name: &str,
81 start_row: Option<u32>,
82 start_col: Option<u32>,
83 end_row: Option<u32>,
84 end_col: Option<u32>,
85 ) {
86 let Some(extent) = resolve_used_extent_with_fallback(
87 OpenRangeBounds {
88 start_row,
89 start_column: start_col,
90 end_row,
91 end_column: end_col,
92 },
93 ExtentPolicy::EvaluationCompat {
94 fallback_row: None,
95 fallback_column: None,
96 },
97 || {
98 self.engine
99 .sheet_bounds(sheet_name)
100 .map(|_| self.engine.config.max_open_ended_rows)
101 },
102 || {
103 self.engine
104 .sheet_bounds(sheet_name)
105 .map(|_| self.engine.config.max_open_ended_cols)
106 },
107 |first, last| self.engine.used_rows_for_columns(sheet_name, first, last),
108 |first, last| self.engine.used_cols_for_rows(sheet_name, first, last),
109 ) else {
110 return;
111 };
112
113 self.collect_formula_vertices_in_rect(
114 sheet_name,
115 extent.start_row,
116 extent.start_column,
117 extent.end_row,
118 extent.end_column,
119 );
120 }
121}
122
123impl<'a, R: EvaluationContext> ReferenceResolver for DynamicRefCollector<'a, R> {
124 fn resolve_cell_reference(
125 &self,
126 sheet: Option<&str>,
127 row: u32,
128 col: u32,
129 ) -> Result<LiteralValue, ExcelError> {
130 let sheet_name = sheet.unwrap_or(self.current_sheet);
131 if let Some(sheet_id) = self.engine.graph.sheet_id(sheet_name) {
132 self.collected_regions.lock().unwrap().insert(Region::point(
133 sheet_id,
134 row.saturating_sub(1),
135 col.saturating_sub(1),
136 ));
137 }
138 if let Some(vid) = self
139 .engine
140 .graph
141 .get_vertex_id_for_address(&self.engine.graph.make_cell_ref(sheet_name, row, col))
142 {
143 self.collected.lock().unwrap().insert(vid);
144 }
145 self.engine.resolve_cell_reference(sheet, row, col)
146 }
147}
148
149impl<'a, R: EvaluationContext> RangeResolver for DynamicRefCollector<'a, R> {
150 fn resolve_range_reference(
151 &self,
152 sheet: Option<&str>,
153 sr: Option<u32>,
154 sc: Option<u32>,
155 er: Option<u32>,
156 ec: Option<u32>,
157 ) -> Result<Box<dyn Range>, ExcelError> {
158 let sheet_name = sheet.unwrap_or(self.current_sheet);
159 self.collect_formula_vertices_for_range(sheet_name, sr, sc, er, ec);
160 self.engine.resolve_range_reference(sheet, sr, sc, er, ec)
161 }
162}
163
164impl<'a, R: EvaluationContext> NamedRangeResolver for DynamicRefCollector<'a, R> {
165 fn resolve_named_range_reference(
166 &self,
167 name: &str,
168 ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
169 self.engine.resolve_named_range_reference(name)
170 }
171}
172
173impl<'a, R: EvaluationContext> TableResolver for DynamicRefCollector<'a, R> {
174 fn resolve_table_reference(&self, tref: &TableReference) -> Result<Box<dyn Table>, ExcelError> {
175 self.engine.resolve_table_reference(tref)
176 }
177}
178
179impl<'a, R: EvaluationContext> SourceResolver for DynamicRefCollector<'a, R> {
180 fn source_scalar_version(&self, name: &str) -> Option<u64> {
181 self.engine.source_scalar_version(name)
182 }
183 fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
184 self.engine.resolve_source_scalar(name)
185 }
186 fn source_table_version(&self, name: &str) -> Option<u64> {
187 self.engine.source_table_version(name)
188 }
189 fn resolve_source_table(&self, name: &str) -> Result<Box<dyn Table>, ExcelError> {
190 self.engine.resolve_source_table(name)
191 }
192}
193
194impl<'a, R: EvaluationContext> Resolver for DynamicRefCollector<'a, R> {}
195
196impl<'a, R: EvaluationContext> FunctionProvider for DynamicRefCollector<'a, R> {
197 fn planning_semantic_revision(&self) -> Option<u64> {
198 self.engine.planning_semantic_revision()
199 }
200
201 fn get_function(
202 &self,
203 ns: &str,
204 name: &str,
205 ) -> Option<std::sync::Arc<dyn crate::traits::Function>> {
206 self.engine.get_function(ns, name)
207 }
208
209 fn get_function_for_planning(
210 &self,
211 ns: &str,
212 name: &str,
213 ) -> Option<std::sync::Arc<dyn crate::traits::Function>> {
214 self.engine.get_function_for_planning(ns, name)
215 }
216}
217
218impl<'a, R: EvaluationContext> EvaluationContext for DynamicRefCollector<'a, R> {
219 fn operation_diagnostic_collector(
220 &self,
221 ) -> Option<&crate::operation_diagnostics::OperationDiagnosticCollector> {
222 self.engine.operation_diagnostic_collector()
223 }
224
225 fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
226 self.engine.cancellation_token()
227 }
228
229 fn materialized_array_cells(&self) -> u64 {
230 self.engine.materialized_array_cells()
231 }
232
233 fn resolve_cell_format(
234 &self,
235 sheet: Option<&str>,
236 row: u32,
237 col: u32,
238 current_sheet: &str,
239 ) -> Option<crate::format::FormatId> {
240 self.engine
241 .resolve_cell_format(sheet, row, col, current_sheet)
242 }
243
244 fn format_class(
245 &self,
246 format: crate::format::FormatId,
247 ) -> Option<formualizer_common::numfmt::FormatClass> {
248 self.engine.format_class(format)
249 }
250
251 fn record_cell_derived_format(
252 &self,
253 sheet: &str,
254 row: u32,
255 col: u32,
256 format: Option<crate::format::FormatId>,
257 ) {
258 self.engine
259 .record_cell_derived_format(sheet, row, col, format)
260 }
261
262 fn resolve_range_view<'c>(
263 &'c self,
264 reference: &ReferenceType,
265 current_sheet: &str,
266 ) -> Result<crate::engine::range_view::RangeView<'c>, ExcelError> {
267 match reference {
269 ReferenceType::Cell {
270 sheet, row, col, ..
271 } => {
272 let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
273 self.collect_formula_vertices_in_rect(sheet_name, *row, *col, *row, *col);
274 }
275 ReferenceType::Range {
276 sheet,
277 start_row,
278 start_col,
279 end_row,
280 end_col,
281 ..
282 } => {
283 let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
284 self.collect_formula_vertices_for_range(
285 sheet_name, *start_row, *start_col, *end_row, *end_col,
286 );
287 }
288 ReferenceType::NamedRange(name) => {
289 let sid = self.engine.sheet_id(current_sheet);
290 if let Some(s) = sid
291 && let Some(nr) = self.engine.graph.resolve_name_entry(name, s)
292 {
293 let vid = nr.vertex;
294 self.collected.lock().unwrap().insert(vid);
295 }
296 }
297 ReferenceType::Table(_) => {
298 }
300 _ => {}
301 }
302
303 self.engine.resolve_range_view(reference, current_sheet)
304 }
305
306 fn resolve_spill_reference(
307 &self,
308 anchor: &ReferenceType,
309 current_sheet: &str,
310 ) -> Result<ReferenceType, ExcelError> {
311 match anchor {
313 ReferenceType::Cell {
314 sheet, row, col, ..
315 } => {
316 let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
317 self.collect_formula_vertices_in_rect(sheet_name, *row, *col, *row, *col);
318 }
319 ReferenceType::NamedRange(name) => {
320 if let Some(sheet_id) = self.engine.sheet_id(current_sheet)
321 && let Some(named) = self.engine.graph.resolve_name_entry(name, sheet_id)
322 {
323 self.collected.lock().unwrap().insert(named.vertex);
324 }
325 }
326 _ => {}
327 }
328 self.engine.resolve_spill_reference(anchor, current_sheet)
329 }
330}
331
332pub struct RangeVirtualDepProvider;
333
334impl RangeVirtualDepProvider {
335 pub(crate) fn resolve_range<R: EvaluationContext>(
336 engine: &Engine<R>,
337 sheet_name: &str,
338 range: &formualizer_common::SheetRangeRef<'_>,
339 ) -> Option<ResolvedExtent> {
340 resolve_used_extent_with_fallback(
341 OpenRangeBounds {
342 start_row: range.start_row.map(|bound| bound.index + 1),
343 start_column: range.start_col.map(|bound| bound.index + 1),
344 end_row: range.end_row.map(|bound| bound.index + 1),
345 end_column: range.end_col.map(|bound| bound.index + 1),
346 },
347 ExtentPolicy::VirtualDependencyCompat {
348 fallback_row: None,
349 fallback_column: None,
350 },
351 || {
352 engine
353 .sheet_bounds(sheet_name)
354 .map(|_| engine.config.max_open_ended_rows)
355 },
356 || {
357 engine
358 .sheet_bounds(sheet_name)
359 .map(|_| engine.config.max_open_ended_cols)
360 },
361 |first, last| engine.used_rows_for_columns(sheet_name, first, last),
362 |first, last| engine.used_cols_for_rows(sheet_name, first, last),
363 )
364 }
365
366 #[cfg(any(test, feature = "legacy_oracle"))]
367 pub fn get_virtual_deps<R: EvaluationContext>(
368 engine: &Engine<R>,
369 v: VertexId,
370 ) -> Vec<VertexId> {
371 let mut deps = Vec::new();
372 if let Some(ranges) = engine.graph.get_range_dependencies(v) {
373 let current_sheet_id = engine.graph.get_vertex_sheet_id(v);
374 for r in ranges {
375 let sheet_id = match r.sheet {
376 formualizer_common::SheetLocator::Id(id) => id,
377 _ => current_sheet_id,
378 };
379 let sheet_name = engine.graph.sheet_name(sheet_id);
380
381 let Some(extent) = Self::resolve_range(engine, sheet_name, r) else {
382 continue;
383 };
384 let sr = extent.start_row;
385 let sc = extent.start_column;
386 let er = extent.end_row;
387 let ec = extent.end_column;
388
389 if engine.graph.sheet_index(sheet_id).is_some() {
390 let sr0 = sr.saturating_sub(1);
391 let er0 = er.saturating_sub(1);
392 let sc0 = sc.saturating_sub(1);
393 let ec0 = ec.saturating_sub(1);
394 for u in engine.graph.vertices_in_cols(sheet_id, sc0, ec0) {
395 let Some(pc) = engine.graph.vertex_grid_addr(u) else {
396 continue;
397 };
398 let row0 = pc.row();
399 if row0 < sr0 || row0 > er0 {
400 continue;
401 }
402 match engine.graph.get_vertex_kind(u) {
403 VertexKind::FormulaScalar | VertexKind::FormulaArray
404 if (engine.graph.is_dirty(u) || engine.graph.is_volatile(u))
405 && u != v =>
406 {
407 deps.push(u);
408 }
409 _ => {}
410 }
411 }
412 }
413 }
414 }
415 deps
416 }
417}
418
419pub struct VirtualDepBuilder<'a, R: EvaluationContext> {
420 engine: &'a Engine<R>,
421}
422
423impl<'a, R: EvaluationContext> VirtualDepBuilder<'a, R> {
424 pub fn new(engine: &'a Engine<R>) -> Self {
425 Self { engine }
426 }
427 pub fn build(
435 &self,
436 candidates: &[VertexId],
437 ) -> (
438 rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
439 Vec<VertexId>,
440 ) {
441 self.build_inner(candidates, false)
442 }
443
444 pub fn build_with_range_members(
447 &self,
448 candidates: &[VertexId],
449 ) -> (
450 rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
451 Vec<VertexId>,
452 ) {
453 self.build_inner(candidates, true)
454 }
455
456 fn build_inner(
457 &self,
458 candidates: &[VertexId],
459 range_members: bool,
460 ) -> (
461 rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
462 Vec<VertexId>,
463 ) {
464 let mut vdeps: rustc_hash::FxHashMap<VertexId, Vec<VertexId>> =
465 rustc_hash::FxHashMap::default();
466 let augmented_vertices: Vec<VertexId> = Vec::new(); for &v in candidates {
469 #[cfg(any(test, feature = "legacy_oracle"))]
472 let mut deps = if range_members {
473 RangeVirtualDepProvider::get_virtual_deps(self.engine, v)
474 } else {
475 Vec::new()
476 };
477 #[cfg(not(any(test, feature = "legacy_oracle")))]
478 let mut deps = Vec::new();
479 let observed =
483 !range_members && self.engine.graph.authority_host().observed(v).is_some();
484 let dynamic_deps = if observed {
485 Vec::new()
486 } else {
487 DynamicRefVirtualDepProvider::get_virtual_deps(self.engine, v)
488 };
489
490 deps.extend(dynamic_deps);
491 deps.sort_unstable();
492 deps.dedup();
493
494 if !deps.is_empty() {
495 vdeps.insert(v, deps);
496 }
497 }
498
499 (vdeps, augmented_vertices)
500 }
501}
502
503pub struct DynamicRefVirtualDepProvider;
504
505impl DynamicRefVirtualDepProvider {
506 fn collect<R: EvaluationContext>(
507 engine: &Engine<R>,
508 v: VertexId,
509 ) -> (Vec<VertexId>, Vec<Region>) {
510 if !engine.graph.is_dynamic(v) {
511 return (Vec::new(), Vec::new());
512 }
513 let Some(view) = engine.graph.formula_view(v) else {
514 return (Vec::new(), Vec::new());
515 };
516 let sheet_id = engine.graph.get_vertex_sheet_id(v);
517 let sheet_name = engine.graph.sheet_name(sheet_id);
518 let _diagnostics = engine.operation_diagnostic_collector().map(|collector| {
522 collector.scope(if engine.diagnostic_request_active() {
523 crate::operation_diagnostics::Mode::Speculative
524 } else {
525 crate::operation_diagnostics::Mode::Suppressed
526 })
527 });
528 let collector = DynamicRefCollector::new(engine, sheet_name);
529 let cell_ref = engine
530 .graph
531 .get_cell_ref(v)
532 .unwrap_or_else(|| engine.graph.make_cell_ref(sheet_name, 0, 0));
533 let interpreter = Interpreter::new_with_cell(&collector, sheet_name, cell_ref);
537 let interpreter = match engine.current_resolved_functions() {
538 Some(table) => interpreter.with_resolved_functions(table),
539 None => interpreter,
540 };
541 let _ = interpreter.evaluate_formula_view(
542 view,
543 engine.graph.data_store(),
544 engine.graph.sheet_reg(),
545 );
546 let mut deps = collector
547 .collected
548 .lock()
549 .unwrap()
550 .iter()
551 .copied()
552 .filter(|&dependency| dependency != v)
553 .collect::<Vec<_>>();
554 deps.sort_unstable();
555 deps.dedup();
556 let mut regions = collector
557 .collected_regions
558 .lock()
559 .unwrap()
560 .iter()
561 .copied()
562 .collect::<Vec<_>>();
563 regions.sort_by_key(|region| {
564 let (rows, cols) = region.axis_ranges();
565 (region.sheet_id(), rows.query_bounds(), cols.query_bounds())
566 });
567 regions.dedup();
568 (deps, regions)
569 }
570
571 pub fn get_virtual_deps<R: EvaluationContext>(
572 engine: &Engine<R>,
573 v: VertexId,
574 ) -> Vec<VertexId> {
575 Self::collect(engine, v).0
576 }
577
578 pub(crate) fn get_virtual_regions<R: EvaluationContext>(
579 engine: &Engine<R>,
580 v: VertexId,
581 ) -> Vec<Region> {
582 Self::collect(engine, v).1
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::DynamicRefVirtualDepProvider;
589 use crate::engine::{Engine, EvalConfig};
590 use crate::test_workbook::TestWorkbook;
591
592 #[test]
595 fn virtual_deps_before_any_evaluation_request() {
596 crate::builtins::load_builtins();
597 let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
598 engine.add_sheet("S").expect("sheet");
599 engine
600 .set_cell_formula("S", 1, 1, formualizer_parse::parse("=42").expect("parse"))
601 .expect("A1");
602 engine
603 .set_cell_formula(
604 "S",
605 1,
606 2,
607 formualizer_parse::parse("=INDIRECT(\"A1\")+0").expect("parse"),
608 )
609 .expect("B1");
610 let found: usize = engine
611 .evaluation_vertices()
612 .into_iter()
613 .map(|v| DynamicRefVirtualDepProvider::get_virtual_deps(&engine, v).len())
614 .sum();
615 assert!(
616 found > 0,
617 "INDIRECT's target must be found before the first request"
618 );
619 }
620}