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