1use std::collections::{HashMap, VecDeque};
15use std::error::Error;
16use std::fmt;
17
18use formualizer_common::{
19 CellAddress, ExcelError, ExcelErrorKind, LiteralValue, RangeAddress, RangeArea, SheetId,
20};
21use formualizer_parse::parser::{
22 ASTNode, ReferenceType, SpecialItem, TableReference, TableSpecifier,
23};
24use rustc_hash::FxHashMap;
25
26use crate::engine::named_range::NamedDefinition;
27use crate::engine::refs;
28use crate::engine::used_extent::{
29 ExtentPolicy, OpenRangeBounds, ResolvedExtent, resolve_used_extent,
30};
31use crate::engine::{Engine, VertexId, VertexKind};
32use crate::reference::{CellRef, Coord};
33use crate::traits::EvaluationContext;
34
35const DEFAULT_MAX_LINKS: u32 = 256;
36const DEFAULT_MAX_WORK: u64 = 100_000;
37
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
42pub struct StateStamp {
43 pub mutation_revision: u64,
44 pub recalc_epoch: u64,
45}
46
47#[cfg_attr(feature = "serde", derive(serde::Serialize))]
48#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
49#[non_exhaustive]
50pub enum Staleness {
51 Current,
52 Dirty,
53 NeverEvaluated,
54}
55
56#[cfg_attr(feature = "serde", derive(serde::Serialize))]
62#[derive(Clone, Debug, Eq, PartialEq, Hash)]
63#[non_exhaustive]
64pub enum SpillRole {
65 Anchor { extent: RangeAddress },
66 Member { anchor: CellAddress },
67}
68
69#[cfg_attr(feature = "serde", derive(serde::Serialize))]
70#[derive(Clone, Debug, PartialEq)]
71#[non_exhaustive]
72pub struct CellSnapshot {
73 pub address: CellAddress,
74 pub formula: Option<String>,
75 pub value: Option<LiteralValue>,
76 pub value_included: bool,
77 pub staleness: Staleness,
78 pub volatile: bool,
79 pub spill: Option<SpillRole>,
82}
83
84#[cfg_attr(feature = "serde", derive(serde::Serialize))]
85#[derive(Clone, Debug, PartialEq)]
86#[non_exhaustive]
87pub struct CellSnapshotReport {
88 pub stamp: StateStamp,
89 pub cell: CellSnapshot,
90}
91
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93#[derive(Clone, Debug, PartialEq)]
94#[non_exhaustive]
95pub enum NameResolution {
96 Cell(CellAddress),
97 Range {
98 declared: RangeArea,
99 resolved: Option<RangeAddress>,
100 },
101 Literal(LiteralValue),
102 Formula {
103 formula: String,
104 value: Option<LiteralValue>,
105 },
106 Unresolved,
107}
108
109#[cfg_attr(feature = "serde", derive(serde::Serialize))]
110#[derive(Clone, Debug, PartialEq)]
111#[non_exhaustive]
112pub enum SemanticReference {
113 Cell(CellAddress),
114 Range {
115 declared: RangeArea,
116 resolved: Option<RangeAddress>,
117 cell_count: u64,
118 },
119 Name {
120 name: String,
121 resolution: NameResolution,
122 },
123 Table {
124 name: String,
125 specifier: String,
126 resolved: RangeAddress,
127 },
128 External {
129 raw: String,
130 },
131 Unsupported {
132 text: String,
133 reason: String,
134 },
135}
136
137#[cfg_attr(feature = "serde", derive(serde::Serialize))]
138#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
139#[non_exhaustive]
140pub enum Provenance {
141 Declared,
142 Observed,
143}
144
145#[cfg_attr(feature = "serde", derive(serde::Serialize))]
146#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
147#[non_exhaustive]
148pub enum TraceLinkKind {
149 Formula { provenance: Provenance },
150 SpillAnchor,
151 SpillReader,
152}
153
154#[cfg_attr(feature = "serde", derive(serde::Serialize))]
155#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
156#[non_exhaustive]
157pub enum LinkDisposition {
158 Expanded,
159 Convergent,
160 Cycle,
161 Elided,
162}
163
164#[cfg_attr(feature = "serde", derive(serde::Serialize))]
165#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
166#[non_exhaustive]
167pub enum OmittedCount {
168 Exact(u64),
169 AtLeast(u64),
170}
171
172#[cfg_attr(feature = "serde", derive(serde::Serialize))]
173#[derive(Clone, Debug, Default, Eq, PartialEq)]
174#[non_exhaustive]
175pub struct TruncationReport {
176 pub incomplete: bool,
177 pub omitted: Option<OmittedCount>,
181}
182
183#[cfg_attr(feature = "serde", derive(serde::Serialize))]
184#[derive(Clone, Debug, PartialEq)]
185#[non_exhaustive]
186pub struct Precedent {
187 pub reference: SemanticReference,
188 pub provenance: Provenance,
189}
190
191#[cfg_attr(feature = "serde", derive(serde::Serialize))]
192#[derive(Clone, Debug, PartialEq)]
193#[non_exhaustive]
194pub struct PrecedentReport {
195 pub stamp: StateStamp,
196 pub cell: CellAddress,
197 pub precedents: Vec<Precedent>,
198 pub truncation: TruncationReport,
199}
200
201#[cfg_attr(feature = "serde", derive(serde::Serialize))]
202#[derive(Clone, Debug, Eq, PartialEq)]
203#[non_exhaustive]
204pub struct Dependent {
205 pub cell: CellAddress,
206 pub via: Vec<CellAddress>,
210}
211
212#[cfg_attr(feature = "serde", derive(serde::Serialize))]
213#[derive(Clone, Debug, Eq, PartialEq)]
214#[non_exhaustive]
215pub struct DependentsReport {
216 pub stamp: StateStamp,
217 pub cell: CellAddress,
218 pub dependents: Vec<Dependent>,
219 pub truncation: TruncationReport,
220}
221
222#[cfg_attr(feature = "serde", derive(serde::Serialize))]
223#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
224pub struct TraceNodeId(pub u32);
225
226#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
227#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
228#[non_exhaustive]
229pub enum TraceDirection {
230 Precedents,
231 Dependents,
232}
233
234#[cfg_attr(feature = "serde", derive(serde::Serialize))]
235#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
236#[non_exhaustive]
237pub struct TraceLinkTarget {
238 pub node: TraceNodeId,
239 pub disposition: LinkDisposition,
240}
241
242#[cfg_attr(feature = "serde", derive(serde::Serialize))]
243#[derive(Clone, Debug, PartialEq)]
244#[non_exhaustive]
245pub struct TraceLink {
246 pub reference: SemanticReference,
247 pub kind: TraceLinkKind,
248 pub targets: Vec<TraceLinkTarget>,
249 pub omitted: Option<OmittedCount>,
250}
251
252#[cfg_attr(feature = "serde", derive(serde::Serialize))]
253#[derive(Clone, Debug, PartialEq)]
254#[non_exhaustive]
255pub struct TraceNode {
256 pub id: TraceNodeId,
257 pub cell: CellSnapshot,
258 pub links: Vec<TraceLink>,
259}
260
261#[cfg_attr(feature = "serde", derive(serde::Serialize))]
262#[derive(Clone, Debug, PartialEq)]
263#[non_exhaustive]
264pub struct TraceGraph {
265 pub stamp: StateStamp,
266 pub direction: TraceDirection,
267 pub roots: Vec<TraceNodeId>,
270 pub nodes: Vec<TraceNode>,
271 pub truncation: TruncationReport,
272}
273
274#[cfg_attr(feature = "serde", derive(serde::Serialize))]
275#[derive(Clone, Debug, PartialEq)]
276#[non_exhaustive]
277pub struct RangePage {
278 pub stamp: StateStamp,
279 pub declared: RangeArea,
280 pub resolved: Option<RangeAddress>,
281 pub total: u64,
282 pub offset: u64,
285 pub items: Vec<CellSnapshot>,
286 pub next_offset: Option<u64>,
287}
288
289#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
295#[derive(Clone, Copy, Debug, Eq, PartialEq)]
296pub struct SnapshotOptions {
297 pub include_values: bool,
298}
299
300impl Default for SnapshotOptions {
301 fn default() -> Self {
302 Self {
303 include_values: true,
304 }
305 }
306}
307
308impl SnapshotOptions {
309 pub fn with_include_values(mut self, include_values: bool) -> Self {
310 self.include_values = include_values;
311 self
312 }
313}
314
315#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
321#[derive(Clone, Copy, Debug, Eq, PartialEq)]
322pub struct PrecedentOptions {
323 pub max_links: u32,
324 pub max_work: u64,
325}
326
327impl Default for PrecedentOptions {
328 fn default() -> Self {
329 Self {
330 max_links: DEFAULT_MAX_LINKS,
331 max_work: DEFAULT_MAX_WORK,
332 }
333 }
334}
335
336impl PrecedentOptions {
337 pub fn with_max_links(mut self, max_links: u32) -> Self {
338 self.max_links = max_links;
339 self
340 }
341
342 pub fn with_max_work(mut self, max_work: u64) -> Self {
343 self.max_work = max_work;
344 self
345 }
346}
347
348#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354#[derive(Clone, Copy, Debug, Eq, PartialEq)]
355pub struct DependentsOptions {
356 pub max_results: u32,
361 pub max_work: u64,
362}
363
364impl Default for DependentsOptions {
365 fn default() -> Self {
366 Self {
367 max_results: 256,
368 max_work: DEFAULT_MAX_WORK,
369 }
370 }
371}
372
373impl DependentsOptions {
374 pub fn with_max_results(mut self, max_results: u32) -> Self {
375 self.max_results = max_results;
376 self
377 }
378
379 pub fn with_max_work(mut self, max_work: u64) -> Self {
380 self.max_work = max_work;
381 self
382 }
383}
384
385#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391#[derive(Clone, Copy, Debug, Eq, PartialEq)]
392pub struct TraceOptions {
393 pub direction: TraceDirection,
394 pub max_depth: u32,
397 pub max_nodes: u32,
400 pub max_links: u32,
401 pub max_work: u64,
402 pub range_member_budget: u32,
403 pub include_values: bool,
404}
405
406impl Default for TraceOptions {
407 fn default() -> Self {
408 Self {
409 direction: TraceDirection::Precedents,
410 max_depth: 6,
411 max_nodes: 512,
412 max_links: 1_024,
413 max_work: DEFAULT_MAX_WORK,
414 range_member_budget: 256,
415 include_values: true,
416 }
417 }
418}
419
420impl TraceOptions {
421 pub fn with_direction(mut self, direction: TraceDirection) -> Self {
422 self.direction = direction;
423 self
424 }
425
426 pub fn with_max_depth(mut self, max_depth: u32) -> Self {
427 self.max_depth = max_depth;
428 self
429 }
430
431 pub fn with_max_nodes(mut self, max_nodes: u32) -> Self {
432 self.max_nodes = max_nodes;
433 self
434 }
435
436 pub fn with_max_links(mut self, max_links: u32) -> Self {
437 self.max_links = max_links;
438 self
439 }
440
441 pub fn with_max_work(mut self, max_work: u64) -> Self {
442 self.max_work = max_work;
443 self
444 }
445
446 pub fn with_range_member_budget(mut self, range_member_budget: u32) -> Self {
447 self.range_member_budget = range_member_budget;
448 self
449 }
450
451 pub fn with_include_values(mut self, include_values: bool) -> Self {
452 self.include_values = include_values;
453 self
454 }
455}
456
457#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
463#[derive(Clone, Copy, Debug, Eq, PartialEq)]
464pub struct RangePageOptions {
465 pub offset: u64,
466 pub limit: u32,
467 pub include_values: bool,
468 pub expected_stamp: Option<StateStamp>,
469}
470
471impl Default for RangePageOptions {
472 fn default() -> Self {
473 Self {
474 offset: 0,
475 limit: 100,
476 include_values: true,
477 expected_stamp: None,
478 }
479 }
480}
481
482impl RangePageOptions {
483 pub fn with_offset(mut self, offset: u64) -> Self {
484 self.offset = offset;
485 self
486 }
487
488 pub fn with_limit(mut self, limit: u32) -> Self {
489 self.limit = limit;
490 self
491 }
492
493 pub fn with_include_values(mut self, include_values: bool) -> Self {
494 self.include_values = include_values;
495 self
496 }
497
498 pub fn with_expected_stamp(mut self, expected_stamp: StateStamp) -> Self {
499 self.expected_stamp = Some(expected_stamp);
500 self
501 }
502}
503
504#[cfg_attr(feature = "serde", derive(serde::Serialize))]
505#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
506#[non_exhaustive]
507pub enum InspectionUnavailableReason {
508 DeferredDependencyGraph,
509 DependencyAuthorityUnavailable,
512}
513
514#[cfg_attr(feature = "serde", derive(serde::Serialize))]
515#[derive(Clone, Debug, Eq, PartialEq)]
516#[non_exhaustive]
517pub enum InspectError {
518 SheetNotFound {
519 sheet: String,
520 },
521 InvalidAddress {
522 message: String,
523 },
524 InvalidOptions {
525 message: String,
526 },
527 DependencyStateUnavailable {
528 reason: InspectionUnavailableReason,
529 },
530 RevisionMismatch {
531 expected: StateStamp,
532 actual: StateStamp,
533 },
534 ResourceExhausted {
536 resource: &'static str,
537 },
538}
539
540impl fmt::Display for InspectError {
541 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542 match self {
543 Self::SheetNotFound { sheet } => write!(f, "sheet not found: {sheet}"),
544 Self::InvalidAddress { message } => write!(f, "invalid address: {message}"),
545 Self::InvalidOptions { message } => write!(f, "invalid inspection options: {message}"),
546 Self::DependencyStateUnavailable { reason } => {
547 write!(f, "dependency state unavailable: {reason:?}")
548 }
549 Self::RevisionMismatch { expected, actual } => write!(
550 f,
551 "inspection revision mismatch: expected {expected:?}, actual {actual:?}"
552 ),
553 Self::ResourceExhausted { resource } => {
554 write!(f, "inspection resource exhausted: {resource}")
555 }
556 }
557 }
558}
559
560impl Error for InspectError {}
561
562#[cfg_attr(feature = "serde", derive(serde::Serialize))]
563#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
564pub(crate) struct CellKey {
565 sheet_id: SheetId,
566 row0: u32,
567 col0: u32,
568}
569
570#[derive(Clone, Debug)]
571pub(crate) struct FormulaView {
572 ast: ASTNode,
573 volatile: bool,
574 dirty: bool,
575}
576
577#[derive(Clone, Debug)]
578pub(crate) enum InternalSpillRole {
579 Anchor { extent: RangeAddress },
580 Member { anchor: CellKey },
581}
582
583#[derive(Clone, Copy, Debug, Eq, PartialEq)]
584pub(crate) enum QueryCompleteness {
585 Complete,
586 Incomplete,
587}
588
589#[derive(Clone, Copy, Debug)]
590pub(crate) struct WorkBudget {
591 remaining: u64,
592}
593
594impl WorkBudget {
595 fn new(limit: u64) -> Self {
596 Self { remaining: limit }
597 }
598
599 fn charge(&mut self) -> bool {
600 if self.remaining == 0 {
601 false
602 } else {
603 self.remaining -= 1;
604 true
605 }
606 }
607}
608
609pub(crate) trait ReferenceVisitor {
610 fn visit(&mut self, reference: refs::SemanticReference<'_>) -> bool;
612}
613
614pub(crate) trait DependentVisitor {
615 fn visit(&mut self, dependent: CellKey) -> bool;
617}
618
619pub(crate) trait InspectSource {
623 fn formula_at(&self, cell: CellKey) -> Result<Option<FormulaView>, InspectError>;
624 fn visit_declared_references(
625 &self,
626 cell: CellKey,
627 visitor: &mut dyn ReferenceVisitor,
628 ) -> Result<(), InspectError>;
629 fn visit_dependents_covering(
630 &self,
631 cell: CellKey,
632 budget: &mut WorkBudget,
633 visitor: &mut dyn DependentVisitor,
634 ) -> Result<QueryCompleteness, InspectError>;
635 fn spill_role(&self, cell: CellKey) -> Option<InternalSpillRole>;
636}
637
638struct LegacyInspectSource<'a, R> {
639 engine: &'a Engine<R>,
640}
641
642impl<R: EvaluationContext> LegacyInspectSource<'_, R> {
643 fn cell_ref(&self, key: CellKey) -> CellRef {
644 CellRef::new(key.sheet_id, Coord::new(key.row0, key.col0, true, true))
645 }
646}
647
648fn visit_formula_ast_references(
649 ast: &ASTNode,
650 visitor: &mut dyn ReferenceVisitor,
651) -> Result<(), InspectError> {
652 struct Context<'a> {
653 visitor: &'a mut dyn ReferenceVisitor,
654 stopped: bool,
655 }
656 fn local_bindings(_: &Context<'_>, name: &str, _: usize) -> refs::LocalBindingStyle {
657 match name
658 .rsplit('.')
659 .next()
660 .unwrap_or(name)
661 .to_ascii_uppercase()
662 .as_str()
663 {
664 "LET" => refs::LocalBindingStyle::LocalBindingPairs,
665 "LAMBDA" => refs::LocalBindingStyle::LambdaParameters,
666 _ => refs::LocalBindingStyle::None,
667 }
668 }
669 fn consume(
670 context: &mut Context<'_>,
671 reference: refs::SemanticReference<'_>,
672 ) -> Result<(), ExcelError> {
673 if context.visitor.visit(reference) {
674 Ok(())
675 } else {
676 context.stopped = true;
677 Err(ExcelError::new(ExcelErrorKind::NImpl)
678 .with_message("inspection visitor requested stop"))
679 }
680 }
681
682 let mut context = Context {
683 visitor,
684 stopped: false,
685 };
686 let result = refs::visit_tree_references(ast, &mut context, local_bindings, consume);
687 if context.stopped {
688 Ok(())
689 } else {
690 result.map_err(|error| InspectError::InvalidAddress {
691 message: error.to_string(),
692 })
693 }
694}
695
696impl<R: EvaluationContext> InspectSource for LegacyInspectSource<'_, R> {
697 fn formula_at(&self, cell: CellKey) -> Result<Option<FormulaView>, InspectError> {
698 let sheet = self.engine.graph.sheet_name(cell.sheet_id);
699 let row = cell.row0 + 1;
700 let col = cell.col0 + 1;
701 if let Some(text) = self.engine.get_staged_formula_text(sheet, row, col) {
702 let text = if text.starts_with('=') {
705 text
706 } else {
707 format!("={text}")
708 };
709 let ast =
710 formualizer_parse::parse(&text).map_err(|error| InspectError::InvalidAddress {
711 message: format!("staged formula at {sheet}!R{row}C{col} is invalid: {error}"),
712 })?;
713 let volatile = self.engine.graph.fp8_parity_is_ast_volatile(&ast);
714 return Ok(Some(FormulaView {
715 ast,
716 volatile,
717 dirty: true,
718 }));
719 }
720
721 let cell_ref = self.cell_ref(cell);
722 let Some(vertex) = self.engine.graph.get_vertex_for_cell(&cell_ref) else {
723 return Ok(None);
724 };
725 let Some(ast) = self.engine.graph.get_formula(vertex) else {
726 return Ok(None);
727 };
728 Ok(Some(FormulaView {
729 ast,
730 volatile: self.engine.graph.is_volatile(vertex),
731 dirty: self.engine.graph.is_dirty(vertex),
732 }))
733 }
734
735 fn visit_declared_references(
736 &self,
737 cell: CellKey,
738 visitor: &mut dyn ReferenceVisitor,
739 ) -> Result<(), InspectError> {
740 let Some(formula) = self.formula_at(cell)? else {
741 return Ok(());
742 };
743
744 visit_formula_ast_references(&formula.ast, visitor)
745 }
746
747 fn visit_dependents_covering(
748 &self,
749 cell: CellKey,
750 budget: &mut WorkBudget,
751 visitor: &mut dyn DependentVisitor,
752 ) -> Result<QueryCompleteness, InspectError> {
753 #[cfg(not(any(test, feature = "legacy_oracle")))]
756 let complete = {
757 let _ = (cell, budget, visitor);
758 true
759 };
760 #[cfg(any(test, feature = "legacy_oracle"))]
761 let complete = self.engine.graph.visit_range_dependents_covering_bounded(
762 cell.sheet_id,
763 cell.row0,
764 cell.col0,
765 &mut budget.remaining,
766 &mut |vertex| {
767 self.engine
768 .graph
769 .get_cell_ref(vertex)
770 .is_none_or(|cell_ref| {
771 visitor.visit(CellKey {
772 sheet_id: cell_ref.sheet_id,
773 row0: cell_ref.coord.row(),
774 col0: cell_ref.coord.col(),
775 })
776 })
777 },
778 );
779 Ok(if complete {
780 QueryCompleteness::Complete
781 } else {
782 QueryCompleteness::Incomplete
783 })
784 }
785
786 fn spill_role(&self, cell: CellKey) -> Option<InternalSpillRole> {
787 let cell_ref = self.cell_ref(cell);
788 if let Some(vertex) = self.engine.graph.get_vertex_for_cell(&cell_ref)
789 && let Some(cells) = self.engine.graph.spill_cells_for_anchor(vertex)
790 {
791 let mut bounds: Option<(u32, u32, u32, u32)> = None;
792 for member in cells {
793 bounds = Some(match bounds {
794 None => (
795 member.coord.row(),
796 member.coord.col(),
797 member.coord.row(),
798 member.coord.col(),
799 ),
800 Some((sr, sc, er, ec)) => (
801 sr.min(member.coord.row()),
802 sc.min(member.coord.col()),
803 er.max(member.coord.row()),
804 ec.max(member.coord.col()),
805 ),
806 });
807 }
808 if let Some((sr, sc, er, ec)) = bounds {
809 let sheet = self.engine.graph.sheet_name(cell.sheet_id).to_string();
810 return Some(InternalSpillRole::Anchor {
811 extent: RangeAddress {
812 sheet,
813 start_row: sr + 1,
814 start_col: sc + 1,
815 end_row: er + 1,
816 end_col: ec + 1,
817 },
818 });
819 }
820 }
821 let anchor = self.engine.graph.spill_registry_anchor_for_cell(cell_ref)?;
822 let anchor_ref = self.engine.graph.get_cell_ref(anchor)?;
823 Some(InternalSpillRole::Member {
824 anchor: CellKey {
825 sheet_id: anchor_ref.sheet_id,
826 row0: anchor_ref.coord.row(),
827 col0: anchor_ref.coord.col(),
828 },
829 })
830 }
831}
832
833fn merge_omitted(target: &mut Option<OmittedCount>, addition: OmittedCount) {
834 *target = Some(match (target.take(), addition) {
835 (None, value) => value,
836 (Some(OmittedCount::Exact(a)), OmittedCount::Exact(b)) => {
837 OmittedCount::Exact(a.saturating_add(b))
838 }
839 (Some(OmittedCount::Exact(a)), OmittedCount::AtLeast(b))
840 | (Some(OmittedCount::AtLeast(a)), OmittedCount::Exact(b))
841 | (Some(OmittedCount::AtLeast(a)), OmittedCount::AtLeast(b)) => {
842 OmittedCount::AtLeast(a.saturating_add(b))
843 }
844 });
845}
846
847fn address_cmp(left: &CellAddress, right: &CellAddress) -> std::cmp::Ordering {
848 left.sheet
849 .cmp(&right.sheet)
850 .then_with(|| left.row.cmp(&right.row))
851 .then_with(|| left.column.cmp(&right.column))
852}
853
854impl<R: EvaluationContext> Engine<R> {
855 fn inspect_stamp(&self) -> StateStamp {
856 StateStamp {
857 mutation_revision: self.inspection_mutation_revision(),
858 recalc_epoch: self.recalc_epoch,
859 }
860 }
861
862 fn inspect_source(&self) -> LegacyInspectSource<'_, R> {
863 LegacyInspectSource { engine: self }
864 }
865
866 fn canonical_cell(
867 &self,
868 address: &CellAddress,
869 ) -> Result<(CellKey, CellAddress), InspectError> {
870 CellAddress::new(address.sheet.clone(), address.row, address.column).map_err(|error| {
871 InspectError::InvalidAddress {
872 message: error.to_string(),
873 }
874 })?;
875 let Some(sheet_id) = self.graph.sheet_id(&address.sheet) else {
876 return Err(InspectError::SheetNotFound {
877 sheet: address.sheet.clone(),
878 });
879 };
880 let canonical = CellAddress {
881 sheet: self.graph.sheet_name(sheet_id).to_string(),
882 row: address.row,
883 column: address.column,
884 };
885 Ok((
886 CellKey {
887 sheet_id,
888 row0: address.row - 1,
889 col0: address.column - 1,
890 },
891 canonical,
892 ))
893 }
894
895 fn canonical_area(&self, area: &RangeArea) -> Result<(SheetId, RangeArea), InspectError> {
896 RangeArea::new(
897 area.sheet.clone(),
898 area.start_row,
899 area.start_column,
900 area.end_row,
901 area.end_column,
902 )
903 .map_err(|error| InspectError::InvalidAddress {
904 message: error.to_string(),
905 })?;
906 let Some(sheet_id) = self.graph.sheet_id(&area.sheet) else {
907 return Err(InspectError::SheetNotFound {
908 sheet: area.sheet.clone(),
909 });
910 };
911 Ok((
912 sheet_id,
913 RangeArea {
914 sheet: self.graph.sheet_name(sheet_id).to_string(),
915 start_row: area.start_row,
916 start_column: area.start_column,
917 end_row: area.end_row,
918 end_column: area.end_column,
919 },
920 ))
921 }
922
923 fn address_for_key(&self, key: CellKey) -> CellAddress {
924 CellAddress {
925 sheet: self.graph.sheet_name(key.sheet_id).to_string(),
926 row: key.row0 + 1,
927 column: key.col0 + 1,
928 }
929 }
930
931 fn key_for_vertex(&self, vertex: VertexId) -> Option<CellKey> {
932 if !matches!(
933 self.graph.get_vertex_kind(vertex),
934 VertexKind::FormulaScalar | VertexKind::FormulaArray
935 ) {
936 return None;
937 }
938 let cell = self.graph.get_cell_ref(vertex)?;
939 Some(CellKey {
940 sheet_id: cell.sheet_id,
941 row0: cell.coord.row(),
942 col0: cell.coord.col(),
943 })
944 }
945
946 fn resolve_semantic_area(&self, area: &RangeArea) -> Option<RangeAddress> {
947 let extent = resolve_used_extent(
948 OpenRangeBounds {
949 start_row: area.start_row,
950 start_column: area.start_column,
951 end_row: area.end_row,
952 end_column: area.end_column,
953 },
954 ExtentPolicy::Semantic,
955 |first, last| self.semantic_used_rows_for_columns(&area.sheet, first, last),
956 |first, last| self.semantic_used_cols_for_rows(&area.sheet, first, last),
957 )?;
958 Some(Self::range_from_extent(&area.sheet, extent))
959 }
960
961 fn range_from_extent(sheet: &str, extent: ResolvedExtent) -> RangeAddress {
962 RangeAddress {
963 sheet: sheet.to_string(),
964 start_row: extent.start_row,
965 start_col: extent.start_column,
966 end_row: extent.end_row,
967 end_col: extent.end_column,
968 }
969 }
970
971 fn snapshot_for_key(
972 &self,
973 key: CellKey,
974 include_value: bool,
975 ) -> Result<CellSnapshot, InspectError> {
976 let source = self.inspect_source();
977 let formula = source.formula_at(key)?;
978 let address = self.address_for_key(key);
979 let cached_value = self.read_cell_value(&address.sheet, address.row, address.column);
980 let (canonical_formula, volatile, staleness) = match formula {
981 Some(view) => {
982 let staleness = if cached_value.is_none() {
987 Staleness::NeverEvaluated
988 } else if view.dirty {
989 Staleness::Dirty
990 } else {
991 Staleness::Current
992 };
993 (
994 Some(formualizer_parse::pretty::canonical_formula(&view.ast)),
995 view.volatile,
996 staleness,
997 )
998 }
999 None => (None, false, Staleness::Current),
1000 };
1001 let spill = source.spill_role(key).map(|role| match role {
1002 InternalSpillRole::Anchor { extent } => SpillRole::Anchor { extent },
1003 InternalSpillRole::Member { anchor } => SpillRole::Member {
1004 anchor: self.address_for_key(anchor),
1005 },
1006 });
1007 Ok(CellSnapshot {
1008 address,
1009 formula: canonical_formula,
1010 value: include_value.then_some(cached_value).flatten(),
1011 value_included: include_value,
1012 staleness,
1013 volatile,
1014 spill,
1015 })
1016 }
1017
1018 pub fn inspect_cell(
1020 &self,
1021 cell: &CellAddress,
1022 options: &SnapshotOptions,
1023 ) -> Result<CellSnapshotReport, InspectError> {
1024 let (key, _) = self.canonical_cell(cell)?;
1025 Ok(CellSnapshotReport {
1026 stamp: self.inspect_stamp(),
1027 cell: self.snapshot_for_key(key, options.include_values)?,
1028 })
1029 }
1030
1031 fn resolve_name(&self, key: CellKey, name: &str) -> NameResolution {
1032 let Some(named) = self.graph.resolve_name_entry(name, key.sheet_id) else {
1033 return NameResolution::Unresolved;
1034 };
1035 match &named.definition {
1036 NamedDefinition::Cell(cell) => NameResolution::Cell(CellAddress {
1037 sheet: self.graph.sheet_name(cell.sheet_id).to_string(),
1038 row: cell.coord.row() + 1,
1039 column: cell.coord.col() + 1,
1040 }),
1041 NamedDefinition::Range(range) => {
1042 let resolved = RangeAddress {
1043 sheet: self.graph.sheet_name(range.start.sheet_id).to_string(),
1044 start_row: range.start.coord.row() + 1,
1045 start_col: range.start.coord.col() + 1,
1046 end_row: range.end.coord.row() + 1,
1047 end_col: range.end.coord.col() + 1,
1048 };
1049 NameResolution::Range {
1050 declared: RangeArea::from_finite(&resolved),
1051 resolved: Some(resolved),
1052 }
1053 }
1054 NamedDefinition::Literal(value) => NameResolution::Literal(value.clone()),
1055 NamedDefinition::Formula { ast, .. } => NameResolution::Formula {
1056 formula: formualizer_parse::pretty::canonical_formula(ast),
1057 value: self.graph.get_value(named.vertex),
1058 },
1059 }
1060 }
1061
1062 fn resolve_table_area(
1063 &self,
1064 key: CellKey,
1065 table_ref: &TableReference,
1066 ) -> Option<(String, String, RangeAddress)> {
1067 let metadata = self.table_metadata(&table_ref.name)?;
1068 let canonical_name = metadata.name.clone();
1069 let specifier = table_ref
1070 .specifier
1071 .as_ref()
1072 .map(ToString::to_string)
1073 .unwrap_or_default();
1074 let mut start_row = metadata.start_row;
1075 let mut end_row = metadata.end_row;
1076 let mut start_col = metadata.start_col;
1077 let mut end_col = metadata.end_col;
1078 let data_start = start_row + u32::from(metadata.header_row);
1079 let data_end = end_row.saturating_sub(u32::from(metadata.totals_row));
1080
1081 fn col_index(headers: &[String], name: &str) -> Option<u32> {
1082 headers
1083 .iter()
1084 .position(|header| header.eq_ignore_ascii_case(name))
1085 .and_then(|index| u32::try_from(index).ok())
1086 }
1087
1088 match table_ref.specifier.as_ref()? {
1089 TableSpecifier::All | TableSpecifier::SpecialItem(SpecialItem::All) => {}
1090 TableSpecifier::Data | TableSpecifier::SpecialItem(SpecialItem::Data) => {
1091 start_row = data_start;
1092 end_row = data_end;
1093 }
1094 TableSpecifier::Headers | TableSpecifier::SpecialItem(SpecialItem::Headers) => {
1095 if !metadata.header_row {
1096 return None;
1097 }
1098 end_row = start_row;
1099 }
1100 TableSpecifier::Totals | TableSpecifier::SpecialItem(SpecialItem::Totals) => {
1101 if !metadata.totals_row {
1102 return None;
1103 }
1104 start_row = end_row;
1105 }
1106 TableSpecifier::Column(name) => {
1107 let index = col_index(&metadata.headers, name)?;
1108 start_col += index;
1109 end_col = start_col;
1110 start_row = data_start;
1111 end_row = data_end;
1112 }
1113 TableSpecifier::ColumnRange(first, last) => {
1114 let mut first = col_index(&metadata.headers, first)?;
1115 let mut last = col_index(&metadata.headers, last)?;
1116 if first > last {
1117 std::mem::swap(&mut first, &mut last);
1118 }
1119 start_col += first;
1120 end_col = metadata.start_col + last;
1121 start_row = data_start;
1122 end_row = data_end;
1123 }
1124 TableSpecifier::SpecialItem(SpecialItem::ThisRow)
1125 | TableSpecifier::Row(formualizer_parse::parser::TableRowSpecifier::Current) => {
1126 let row = key.row0 + 1;
1127 if row < data_start || row > data_end {
1128 return None;
1129 }
1130 start_row = row;
1131 end_row = row;
1132 }
1133 TableSpecifier::Row(_) => return None,
1134 TableSpecifier::Combination(parts) => {
1135 let mut this_row = false;
1136 let mut selected_column: Option<(u32, u32)> = None;
1137 for part in parts {
1138 match part.as_ref() {
1139 TableSpecifier::SpecialItem(SpecialItem::ThisRow) => this_row = true,
1140 TableSpecifier::Column(name) => {
1141 let column = col_index(&metadata.headers, name)?;
1142 selected_column = Some((column, column));
1143 }
1144 TableSpecifier::ColumnRange(first, last) => {
1145 let mut first = col_index(&metadata.headers, first)?;
1146 let mut last = col_index(&metadata.headers, last)?;
1147 if first > last {
1148 std::mem::swap(&mut first, &mut last);
1149 }
1150 selected_column = Some((first, last));
1151 }
1152 TableSpecifier::Data | TableSpecifier::SpecialItem(SpecialItem::Data) => {
1153 start_row = data_start;
1154 end_row = data_end;
1155 }
1156 TableSpecifier::Headers
1157 | TableSpecifier::SpecialItem(SpecialItem::Headers) => {
1158 if !metadata.header_row {
1159 return None;
1160 }
1161 end_row = start_row;
1162 }
1163 TableSpecifier::Totals
1164 | TableSpecifier::SpecialItem(SpecialItem::Totals) => {
1165 if !metadata.totals_row {
1166 return None;
1167 }
1168 start_row = end_row;
1169 }
1170 TableSpecifier::All | TableSpecifier::SpecialItem(SpecialItem::All) => {}
1171 TableSpecifier::Row(_) | TableSpecifier::Combination(_) => return None,
1172 }
1173 }
1174 if this_row {
1175 let row = key.row0 + 1;
1176 if row < data_start || row > data_end {
1177 return None;
1178 }
1179 start_row = row;
1180 end_row = row;
1181 }
1182 if let Some((first, last)) = selected_column {
1183 start_col = metadata.start_col + first;
1184 end_col = metadata.start_col + last;
1185 if !this_row {
1186 start_row = data_start;
1187 end_row = data_end;
1188 }
1189 }
1190 }
1191 }
1192 if start_row > end_row || start_col > end_col {
1193 return None;
1194 }
1195 Some((
1196 canonical_name,
1197 specifier,
1198 RangeAddress {
1199 sheet: metadata.sheet,
1200 start_row,
1201 start_col,
1202 end_row,
1203 end_col,
1204 },
1205 ))
1206 }
1207
1208 fn own_reference(
1209 &self,
1210 key: CellKey,
1211 reference: refs::SemanticReference<'_>,
1212 ) -> SemanticReference {
1213 match reference {
1214 refs::SemanticReference::Cell(cell) => {
1215 let sheet_id = cell
1216 .sheet
1217 .name()
1218 .and_then(|name| self.graph.sheet_id(name))
1219 .unwrap_or(key.sheet_id);
1220 SemanticReference::Cell(CellAddress {
1221 sheet: self.graph.sheet_name(sheet_id).to_string(),
1222 row: cell.row,
1223 column: cell.col,
1224 })
1225 }
1226 refs::SemanticReference::FiniteRange(range)
1227 | refs::SemanticReference::OpenRange(range) => {
1228 let sheet_id = range
1229 .sheet
1230 .name()
1231 .and_then(|name| self.graph.sheet_id(name))
1232 .unwrap_or(key.sheet_id);
1233 let declared = RangeArea {
1234 sheet: self.graph.sheet_name(sheet_id).to_string(),
1235 start_row: range.start_row,
1236 start_column: range.start_col,
1237 end_row: range.end_row,
1238 end_column: range.end_col,
1239 };
1240 let resolved = self.resolve_semantic_area(&declared);
1241 let cell_count = resolved.as_ref().map_or(0, |range| {
1242 u64::from(range.width()) * u64::from(range.height())
1243 });
1244 SemanticReference::Range {
1245 declared,
1246 resolved,
1247 cell_count,
1248 }
1249 }
1250 refs::SemanticReference::Name(name) => SemanticReference::Name {
1251 name: name.to_string(),
1252 resolution: self.resolve_name(key, name),
1253 },
1254 refs::SemanticReference::Table(table) => {
1255 if let Some((name, specifier, resolved)) = self.resolve_table_area(key, table) {
1256 SemanticReference::Table {
1257 name,
1258 specifier,
1259 resolved,
1260 }
1261 } else {
1262 SemanticReference::Unsupported {
1263 text: ReferenceType::Table(table.clone()).to_string(),
1264 reason: "structured reference could not be resolved at this placement"
1265 .to_string(),
1266 }
1267 }
1268 }
1269 refs::SemanticReference::ExternalSource(external) => SemanticReference::External {
1270 raw: external.raw.clone(),
1271 },
1272 refs::SemanticReference::ThreeDimensional(reference) => {
1273 SemanticReference::Unsupported {
1274 text: reference.to_string(),
1275 reason: "3D references are not supported by phase-1 introspection".to_string(),
1276 }
1277 }
1278 refs::SemanticReference::Unsupported(reference) => SemanticReference::Unsupported {
1279 text: reference.to_string(),
1280 reason: "reference form is unsupported by introspection".to_string(),
1281 },
1282 }
1283 }
1284
1285 fn collect_precedents(
1286 &self,
1287 key: CellKey,
1288 max_links: u32,
1289 work: &mut WorkBudget,
1290 ) -> Result<(Vec<Precedent>, TruncationReport), InspectError> {
1291 struct Collector<'a, R> {
1292 engine: &'a Engine<R>,
1293 key: CellKey,
1294 max_links: usize,
1295 work: &'a mut WorkBudget,
1296 precedents: Vec<Precedent>,
1297 truncated: bool,
1298 }
1299 impl<R: EvaluationContext> ReferenceVisitor for Collector<'_, R> {
1300 fn visit(&mut self, reference: refs::SemanticReference<'_>) -> bool {
1301 if !self.work.charge() {
1302 self.truncated = true;
1303 return false;
1304 }
1305 let reference = self.engine.own_reference(self.key, reference);
1306 if self
1307 .precedents
1308 .iter()
1309 .any(|existing| existing.reference == reference)
1310 {
1311 return true;
1312 }
1313 if self.precedents.len() >= self.max_links {
1314 self.truncated = true;
1315 return false;
1316 }
1317 self.precedents.push(Precedent {
1318 reference,
1319 provenance: Provenance::Declared,
1320 });
1321 true
1322 }
1323 }
1324
1325 let source = self.inspect_source();
1326 let mut collector = Collector {
1327 engine: self,
1328 key,
1329 max_links: max_links as usize,
1330 work,
1331 precedents: Vec::new(),
1332 truncated: false,
1333 };
1334 source.visit_declared_references(key, &mut collector)?;
1335 let truncation = if collector.truncated {
1336 TruncationReport {
1337 incomplete: true,
1338 omitted: Some(OmittedCount::AtLeast(1)),
1339 }
1340 } else {
1341 TruncationReport::default()
1342 };
1343 Ok((collector.precedents, truncation))
1344 }
1345
1346 pub fn precedents(
1349 &self,
1350 cell: &CellAddress,
1351 options: &PrecedentOptions,
1352 ) -> Result<PrecedentReport, InspectError> {
1353 let (key, canonical) = self.canonical_cell(cell)?;
1354 let mut work = WorkBudget::new(options.max_work);
1355 let (precedents, truncation) =
1356 self.collect_precedents(key, options.max_links, &mut work)?;
1357 Ok(PrecedentReport {
1358 stamp: self.inspect_stamp(),
1359 cell: canonical,
1360 precedents,
1361 truncation,
1362 })
1363 }
1364
1365 fn dependency_state_available(&self) -> Result<(), InspectError> {
1366 if self.has_staged_formulas() {
1367 Err(InspectError::DependencyStateUnavailable {
1368 reason: InspectionUnavailableReason::DeferredDependencyGraph,
1369 })
1370 } else {
1371 Ok(())
1372 }
1373 }
1374
1375 fn spill_query_members(&self, key: CellKey) -> Vec<CellKey> {
1376 let source = self.inspect_source();
1377 let Some(InternalSpillRole::Anchor { .. }) = source.spill_role(key) else {
1378 return vec![key];
1379 };
1380 let cell_ref = CellRef::new(key.sheet_id, Coord::new(key.row0, key.col0, true, true));
1381 let Some(vertex) = self.graph.get_vertex_for_cell(&cell_ref) else {
1382 return vec![key];
1383 };
1384 self.graph
1385 .spill_cells_for_anchor(vertex)
1386 .unwrap_or(&[])
1387 .iter()
1388 .map(|member| CellKey {
1389 sheet_id: member.sheet_id,
1390 row0: member.coord.row(),
1391 col0: member.coord.col(),
1392 })
1393 .collect()
1394 }
1395
1396 fn collect_dependents(
1397 &self,
1398 key: CellKey,
1399 max_results: u32,
1400 work: &mut WorkBudget,
1401 ) -> Result<(Vec<Dependent>, TruncationReport), InspectError> {
1402 self.dependency_state_available()?;
1403 let source = self.inspect_source();
1404 let mut found: FxHashMap<CellAddress, Vec<CellAddress>> = FxHashMap::default();
1405 let mut incomplete = false;
1406 let max_results = max_results as usize;
1407
1408 let spill_anchor_query = matches!(
1409 source.spill_role(key),
1410 Some(InternalSpillRole::Anchor { .. })
1411 );
1412 let members = self.spill_query_members(key);
1413 for member in members {
1414 if !work.charge() {
1415 incomplete = true;
1416 break;
1417 }
1418 let via = self.address_for_key(member);
1419 let mut record = |dependent_key: CellKey| {
1420 let address = self.address_for_key(dependent_key);
1421 if let Some(via_members) = found.get_mut(&address) {
1422 if spill_anchor_query && !via_members.contains(&via) {
1423 via_members.push(via.clone());
1424 }
1425 return true;
1426 }
1427 found.insert(
1428 address,
1429 if spill_anchor_query {
1430 vec![via.clone()]
1431 } else {
1432 Vec::new()
1433 },
1434 );
1435 true
1436 };
1437
1438 let member_ref = CellRef::new(
1439 member.sheet_id,
1440 Coord::new(member.row0, member.col0, true, true),
1441 );
1442 {
1446 let _ = &member_ref;
1447 let complete = self
1448 .graph
1449 .authority_visit_text_dependents(
1450 (member.sheet_id, member.row0, member.col0),
1451 &mut work.remaining,
1452 &mut |(sheet_id, row0, col0)| {
1453 record(CellKey {
1454 sheet_id,
1455 row0,
1456 col0,
1457 })
1458 },
1459 )
1460 .map_err(|_| InspectError::DependencyStateUnavailable {
1461 reason: InspectionUnavailableReason::DependencyAuthorityUnavailable,
1462 })?;
1463 if !complete {
1464 incomplete = true;
1465 break;
1466 }
1467 }
1468 }
1469
1470 let mut dependents: Vec<_> = found
1471 .into_iter()
1472 .map(|(cell, mut via)| {
1473 via.sort_by(address_cmp);
1474 via.dedup();
1475 Dependent { cell, via }
1476 })
1477 .collect();
1478 dependents.sort_by(|left, right| address_cmp(&left.cell, &right.cell));
1479 let known_omitted_dependent = dependents.len() > max_results;
1480 if known_omitted_dependent {
1481 dependents.truncate(max_results);
1482 incomplete = true;
1483 }
1484 Ok((
1485 dependents,
1486 if incomplete {
1487 TruncationReport {
1488 incomplete: true,
1489 omitted: known_omitted_dependent.then_some(OmittedCount::AtLeast(1)),
1490 }
1491 } else {
1492 TruncationReport::default()
1493 },
1494 ))
1495 }
1496
1497 pub fn dependents(
1500 &self,
1501 cell: &CellAddress,
1502 options: &DependentsOptions,
1503 ) -> Result<DependentsReport, InspectError> {
1504 let (key, canonical) = self.canonical_cell(cell)?;
1505 let mut work = WorkBudget::new(options.max_work);
1506 let (dependents, truncation) =
1507 self.collect_dependents(key, options.max_results, &mut work)?;
1508 Ok(DependentsReport {
1509 stamp: self.inspect_stamp(),
1510 cell: canonical,
1511 dependents,
1512 truncation,
1513 })
1514 }
1515
1516 fn target_addresses(reference: &SemanticReference) -> Option<&RangeAddress> {
1517 match reference {
1518 SemanticReference::Range { resolved, .. } => resolved.as_ref(),
1519 SemanticReference::Name {
1520 resolution: NameResolution::Range { resolved, .. },
1521 ..
1522 } => resolved.as_ref(),
1523 SemanticReference::Table { resolved, .. } => Some(resolved),
1524 _ => None,
1525 }
1526 }
1527
1528 fn target_cell(reference: &SemanticReference) -> Option<&CellAddress> {
1529 match reference {
1530 SemanticReference::Cell(cell) => Some(cell),
1531 SemanticReference::Name {
1532 resolution: NameResolution::Cell(cell),
1533 ..
1534 } => Some(cell),
1535 _ => None,
1536 }
1537 }
1538
1539 fn classify_cycle_dispositions(nodes: &mut [TraceNode]) {
1543 let adjacency: Vec<Vec<usize>> = nodes
1544 .iter()
1545 .map(|node| {
1546 node.links
1547 .iter()
1548 .flat_map(|link| link.targets.iter())
1549 .map(|target| target.node.0 as usize)
1550 .collect()
1551 })
1552 .collect();
1553 let mut reachability = vec![vec![false; nodes.len()]; nodes.len()];
1554 for start in 0..nodes.len() {
1555 let mut stack = adjacency[start].clone();
1556 while let Some(next) = stack.pop() {
1557 if reachability[start][next] {
1558 continue;
1559 }
1560 reachability[start][next] = true;
1561 stack.extend(adjacency[next].iter().copied());
1562 }
1563 }
1564
1565 for (source, node) in nodes.iter_mut().enumerate() {
1566 for target in node
1567 .links
1568 .iter_mut()
1569 .flat_map(|link| link.targets.iter_mut())
1570 {
1571 let target_index = target.node.0 as usize;
1572 if source == target_index || reachability[target_index][source] {
1573 target.disposition = LinkDisposition::Cycle;
1574 } else if target.disposition == LinkDisposition::Cycle {
1575 target.disposition = LinkDisposition::Convergent;
1576 }
1577 }
1578 }
1579 }
1580
1581 pub fn trace(
1583 &self,
1584 roots: &[CellAddress],
1585 options: &TraceOptions,
1586 ) -> Result<TraceGraph, InspectError> {
1587 if roots.is_empty() {
1588 return Err(InspectError::InvalidOptions {
1589 message: "trace requires at least one root".to_string(),
1590 });
1591 }
1592 let canonical_roots: Vec<_> = roots
1593 .iter()
1594 .map(|root| self.canonical_cell(root))
1595 .collect::<Result<_, _>>()?;
1596 let mut unique_roots = FxHashMap::default();
1597 for (key, canonical) in &canonical_roots {
1598 unique_roots.entry(canonical.clone()).or_insert(*key);
1599 }
1600 if unique_roots.len() > options.max_nodes as usize {
1601 return Err(InspectError::InvalidOptions {
1602 message: format!(
1603 "max_nodes ({}) must hold all {} unique roots",
1604 options.max_nodes,
1605 unique_roots.len()
1606 ),
1607 });
1608 }
1609 if options.direction == TraceDirection::Dependents {
1610 self.dependency_state_available()?;
1611 }
1612
1613 let mut nodes = Vec::new();
1614 let mut node_by_address: FxHashMap<CellAddress, TraceNodeId> = FxHashMap::default();
1615 let mut root_ids = Vec::new();
1616 let mut queue = VecDeque::new();
1617 let mut parents: HashMap<TraceNodeId, Option<TraceNodeId>> = HashMap::new();
1618 let mut truncation = TruncationReport::default();
1619
1620 for (key, canonical) in canonical_roots {
1623 if let Some(&id) = node_by_address.get(&canonical) {
1624 root_ids.push(id);
1625 continue;
1626 }
1627 let id = TraceNodeId(nodes.len() as u32);
1628 nodes.push(TraceNode {
1629 id,
1630 cell: self.snapshot_for_key(key, options.include_values)?,
1631 links: Vec::new(),
1632 });
1633 node_by_address.insert(canonical, id);
1634 root_ids.push(id);
1635 queue.push_back((id, key, 0u32));
1636 parents.insert(id, None);
1637 }
1638
1639 let mut work = WorkBudget::new(options.max_work);
1640 let mut links_used = 0u32;
1641 let mut range_members_used = 0u32;
1642
1643 while let Some((source_id, source_key, depth)) = queue.pop_front() {
1644 let can_follow = depth < options.max_depth;
1645 let mut links = Vec::new();
1646 if options.direction == TraceDirection::Precedents {
1647 if let Some(InternalSpillRole::Member { anchor }) =
1648 self.inspect_source().spill_role(source_key)
1649 {
1650 if links_used < options.max_links {
1651 links_used += 1;
1652 let anchor_address = self.address_for_key(anchor);
1653 let mut link = TraceLink {
1654 reference: SemanticReference::Cell(anchor_address.clone()),
1655 kind: TraceLinkKind::SpillAnchor,
1656 targets: Vec::new(),
1657 omitted: None,
1658 };
1659 self.attach_cell_target(
1660 anchor_address,
1661 source_id,
1662 anchor,
1663 can_follow,
1664 depth,
1665 options,
1666 false,
1667 &mut nodes,
1668 &mut node_by_address,
1669 &mut parents,
1670 &mut queue,
1671 &mut link,
1672 &mut truncation,
1673 )?;
1674 links.push(link);
1675 } else {
1676 truncation.incomplete = true;
1677 merge_omitted(&mut truncation.omitted, OmittedCount::AtLeast(1));
1678 }
1679 } else {
1680 let available_links = options.max_links.saturating_sub(links_used);
1681 let (precedents, local_truncation) =
1682 self.collect_precedents(source_key, available_links, &mut work)?;
1683 if local_truncation.incomplete {
1684 truncation.incomplete = true;
1685 if let Some(omitted) = local_truncation.omitted {
1686 merge_omitted(&mut truncation.omitted, omitted);
1687 }
1688 }
1689 for precedent in precedents {
1690 links_used += 1;
1691 let mut link = TraceLink {
1692 reference: precedent.reference,
1693 kind: TraceLinkKind::Formula {
1694 provenance: precedent.provenance,
1695 },
1696 targets: Vec::new(),
1697 omitted: None,
1698 };
1699 if let Some(cell) = Self::target_cell(&link.reference).cloned() {
1700 let (target_key, canonical) = self.canonical_cell(&cell)?;
1701 self.attach_cell_target(
1702 canonical,
1703 source_id,
1704 target_key,
1705 can_follow,
1706 depth,
1707 options,
1708 false,
1709 &mut nodes,
1710 &mut node_by_address,
1711 &mut parents,
1712 &mut queue,
1713 &mut link,
1714 &mut truncation,
1715 )?;
1716 } else if let Some(range) = Self::target_addresses(&link.reference).cloned()
1717 {
1718 self.attach_range_targets(
1719 &range,
1720 source_id,
1721 can_follow,
1722 depth,
1723 options,
1724 &mut range_members_used,
1725 &mut nodes,
1726 &mut node_by_address,
1727 &mut parents,
1728 &mut queue,
1729 &mut link,
1730 &mut truncation,
1731 )?;
1732 }
1733 links.push(link);
1734 }
1735 }
1736 } else {
1737 let available = options.max_links.saturating_sub(links_used);
1738 let (dependents, local_truncation) =
1739 self.collect_dependents(source_key, available, &mut work)?;
1740 if local_truncation.incomplete {
1741 truncation.incomplete = true;
1742 if let Some(omitted) = local_truncation.omitted {
1743 merge_omitted(&mut truncation.omitted, omitted);
1744 }
1745 }
1746 for dependent in dependents {
1747 links_used += 1;
1748 let spill_reader = dependent
1749 .via
1750 .iter()
1751 .any(|via| via != &self.address_for_key(source_key));
1752 let (target_key, canonical) = self.canonical_cell(&dependent.cell)?;
1753 let mut link = TraceLink {
1754 reference: SemanticReference::Cell(canonical.clone()),
1755 kind: if spill_reader {
1756 TraceLinkKind::SpillReader
1757 } else {
1758 TraceLinkKind::Formula {
1759 provenance: Provenance::Declared,
1760 }
1761 },
1762 targets: Vec::new(),
1763 omitted: None,
1764 };
1765 self.attach_cell_target(
1766 canonical,
1767 source_id,
1768 target_key,
1769 can_follow,
1770 depth,
1771 options,
1772 false,
1773 &mut nodes,
1774 &mut node_by_address,
1775 &mut parents,
1776 &mut queue,
1777 &mut link,
1778 &mut truncation,
1779 )?;
1780 links.push(link);
1781 }
1782 }
1783 nodes[source_id.0 as usize].links = links;
1784 }
1785
1786 Self::classify_cycle_dispositions(&mut nodes);
1787
1788 Ok(TraceGraph {
1789 stamp: self.inspect_stamp(),
1790 direction: options.direction,
1791 roots: root_ids,
1792 nodes,
1793 truncation,
1794 })
1795 }
1796
1797 #[allow(clippy::too_many_arguments)]
1798 fn attach_cell_target(
1799 &self,
1800 address: CellAddress,
1801 source_id: TraceNodeId,
1802 key: CellKey,
1803 can_follow: bool,
1804 depth: u32,
1805 options: &TraceOptions,
1806 defer_missing_omission: bool,
1807 nodes: &mut Vec<TraceNode>,
1808 node_by_address: &mut FxHashMap<CellAddress, TraceNodeId>,
1809 parents: &mut HashMap<TraceNodeId, Option<TraceNodeId>>,
1810 queue: &mut VecDeque<(TraceNodeId, CellKey, u32)>,
1811 link: &mut TraceLink,
1812 truncation: &mut TruncationReport,
1813 ) -> Result<(), InspectError> {
1814 if let Some(&target_id) = node_by_address.get(&address) {
1815 link.targets.push(TraceLinkTarget {
1816 node: target_id,
1817 disposition: LinkDisposition::Convergent,
1820 });
1821 return Ok(());
1822 }
1823 if nodes.len() >= options.max_nodes as usize {
1824 link.omitted = Some(OmittedCount::AtLeast(1));
1825 truncation.incomplete = true;
1826 if !defer_missing_omission {
1827 merge_omitted(&mut truncation.omitted, OmittedCount::AtLeast(1));
1828 }
1829 return Ok(());
1830 }
1831 let target_id = TraceNodeId(nodes.len() as u32);
1832 nodes.push(TraceNode {
1833 id: target_id,
1834 cell: self.snapshot_for_key(key, options.include_values)?,
1835 links: Vec::new(),
1836 });
1837 node_by_address.insert(address, target_id);
1838 parents.insert(target_id, Some(source_id));
1839 link.targets.push(TraceLinkTarget {
1840 node: target_id,
1841 disposition: if can_follow {
1842 LinkDisposition::Expanded
1843 } else {
1844 LinkDisposition::Elided
1845 },
1846 });
1847 if can_follow {
1848 queue.push_back((target_id, key, depth + 1));
1849 } else {
1850 truncation.incomplete = true;
1853 }
1854 Ok(())
1855 }
1856
1857 #[allow(clippy::too_many_arguments)]
1858 fn attach_range_targets(
1859 &self,
1860 range: &RangeAddress,
1861 source_id: TraceNodeId,
1862 can_follow: bool,
1863 depth: u32,
1864 options: &TraceOptions,
1865 range_members_used: &mut u32,
1866 nodes: &mut Vec<TraceNode>,
1867 node_by_address: &mut FxHashMap<CellAddress, TraceNodeId>,
1868 parents: &mut HashMap<TraceNodeId, Option<TraceNodeId>>,
1869 queue: &mut VecDeque<(TraceNodeId, CellKey, u32)>,
1870 link: &mut TraceLink,
1871 truncation: &mut TruncationReport,
1872 ) -> Result<(), InspectError> {
1873 let total = u64::from(range.width()) * u64::from(range.height());
1874 let mut attached = 0u64;
1875 let mut compressed_ancestors = Vec::new();
1876 let mut cursor = Some(source_id);
1877 while let Some(ancestor) = cursor {
1878 let address = &nodes[ancestor.0 as usize].cell.address;
1879 if range.sheet == address.sheet
1880 && range.start_row <= address.row
1881 && address.row <= range.end_row
1882 && range.start_col <= address.column
1883 && address.column <= range.end_col
1884 {
1885 compressed_ancestors.push((ancestor, address.row, address.column));
1886 }
1887 cursor = parents.get(&ancestor).copied().flatten();
1888 }
1889
1890 for (ancestor, _, _) in &compressed_ancestors {
1893 link.targets.push(TraceLinkTarget {
1894 node: *ancestor,
1895 disposition: LinkDisposition::Convergent,
1898 });
1899 attached += 1;
1900 }
1901
1902 'rows: for row in range.start_row..=range.end_row {
1903 for column in range.start_col..=range.end_col {
1904 if compressed_ancestors
1905 .iter()
1906 .any(|(_, ancestor_row, ancestor_column)| {
1907 row == *ancestor_row && column == *ancestor_column
1908 })
1909 {
1910 continue;
1911 }
1912 if *range_members_used >= options.range_member_budget {
1913 break 'rows;
1914 }
1915 *range_members_used += 1;
1916 let address = CellAddress {
1917 sheet: range.sheet.clone(),
1918 row,
1919 column,
1920 };
1921 let (key, canonical) = self.canonical_cell(&address)?;
1922 let before = link.targets.len();
1923 self.attach_cell_target(
1924 canonical,
1925 source_id,
1926 key,
1927 can_follow,
1928 depth,
1929 options,
1930 true,
1931 nodes,
1932 node_by_address,
1933 parents,
1934 queue,
1935 link,
1936 truncation,
1937 )?;
1938 if link.targets.len() > before {
1939 attached += 1;
1940 } else if link.omitted.is_some() {
1941 break 'rows;
1942 }
1943 }
1944 }
1945 if attached < total {
1946 let omitted = OmittedCount::Exact(total - attached);
1947 link.omitted = Some(omitted);
1948 truncation.incomplete = true;
1949 merge_omitted(&mut truncation.omitted, omitted);
1950 }
1951 Ok(())
1952 }
1953
1954 pub fn range_page(
1956 &self,
1957 area: &RangeArea,
1958 options: &RangePageOptions,
1959 ) -> Result<RangePage, InspectError> {
1960 if options.limit == 0 {
1961 return Err(InspectError::InvalidOptions {
1962 message: "range page limit must be at least one".to_string(),
1963 });
1964 }
1965 let stamp = self.inspect_stamp();
1966 if let Some(expected) = options.expected_stamp
1967 && expected != stamp
1968 {
1969 return Err(InspectError::RevisionMismatch {
1970 expected,
1971 actual: stamp,
1972 });
1973 }
1974 let (_, declared) = self.canonical_area(area)?;
1975 let resolved = self.resolve_semantic_area(&declared);
1976 let total = resolved.as_ref().map_or(0, |range| {
1977 u64::from(range.width()) * u64::from(range.height())
1978 });
1979 let start = options.offset.min(total);
1980 let end = start.saturating_add(u64::from(options.limit)).min(total);
1981 let mut items = Vec::new();
1982 items
1983 .try_reserve((end - start) as usize)
1984 .map_err(|_| InspectError::ResourceExhausted {
1985 resource: "range page items",
1986 })?;
1987 if let Some(range) = &resolved {
1988 let width = u64::from(range.width());
1989 for offset in start..end {
1990 let row = range.start_row + u32::try_from(offset / width).unwrap_or(u32::MAX);
1991 let column = range.start_col + u32::try_from(offset % width).unwrap_or(u32::MAX);
1992 let (key, _) = self.canonical_cell(&CellAddress {
1993 sheet: range.sheet.clone(),
1994 row,
1995 column,
1996 })?;
1997 items.push(self.snapshot_for_key(key, options.include_values)?);
1998 }
1999 }
2000 Ok(RangePage {
2001 stamp,
2002 declared,
2003 resolved,
2004 total,
2005 offset: options.offset,
2006 items,
2007 next_offset: (end < total).then_some(end),
2008 })
2009 }
2010}