Skip to main content

formualizer_eval/engine/
live_edges.rs

1//! Live-edge collection for statically-cyclic SCC evaluation (Stage 1 of the
2//! runtime-cycle-verdicts work; pre-work for RFC #112).
3//!
4//! When a statically-cyclic SCC is evaluated member-by-member (Stage 2), we
5//! must record which reads *actually occurred* targeting other SCC members
6//! ("live edges"). Untaken short-circuit branches (`IF`/`IFS`/`CHOOSE`/
7//! `SWITCH`, ...) never execute their reads, so they contribute no live edges
8//! for free. After a pass, Stage 2 classifies the live subgraph: acyclic means
9//! the cycle was phantom (values stand); cyclic means `#CIRC!` or iterative
10//! evaluation.
11//!
12//! Stage 1 ships only the collection machinery:
13//!
14//! * [`LiveEdgeCollector`] — a per-SCC set of member cells plus the live edges
15//!   observed so far.
16//! * [`RecordingContext`] — a delegating [`EvaluationContext`] wrapper around
17//!   `&Engine<R>` that records reads as they resolve and forwards everything
18//!   else verbatim.
19//!
20//! # Inertness (binding constraint)
21//!
22//! Nothing in this module is wired into any production evaluation path. The
23//! acyclic/hot evaluation path never constructs a `RecordingContext`; no
24//! `Engine` field, flag, or branch was added. The wrapper is only exercised by
25//! Stage-2 SCC tasks (future) and by tests, so its cost is strictly zero for
26//! ordinary recalculation.
27//!
28//! # Threading
29//!
30//! SCC members are evaluated **sequentially on a single thread**; the
31//! collector is never contended. Interior mutability is required because the
32//! resolver traits take `&self`, and the `Send + Sync` super-bounds on
33//! [`crate::traits::ReferenceResolver`] et al. rule out `RefCell`, so we use a
34//! `Mutex`. It is uncontended by construction (single-threaded SCC pass), so
35//! the lock is a fast path (uncontested futex acquire) and never blocks.
36//!
37//! # Coordinates
38//!
39//! The collector API uses the engine's internal convention: 0-based row and
40//! column indices, rectangles **inclusive** of both corners (matching
41//! [`RangeView::start_row`]/[`RangeView::end_row`] and `CellRef`'s `Coord`).
42//! Resolver-level call sites (1-based Excel coordinates) convert before
43//! recording.
44
45use std::sync::Mutex;
46
47use formualizer_common::{ExcelError, LiteralValue};
48use formualizer_parse::parser::{ReferenceType, TableReference};
49use rustc_hash::{FxHashMap, FxHashSet};
50
51use crate::engine::eval::Engine;
52use crate::engine::range_view::RangeView;
53use crate::reference::{CellRef, SheetId};
54use crate::traits::{
55    EvaluationContext, FunctionProvider, NamedRangeResolver, Range, RangeResolver, ReferenceInfo,
56    ReferenceResolver, Resolver, SourceResolver, Table, TableResolver,
57};
58
59/* ───────────────────────── LiveEdgeCollector ───────────────────────── */
60
61/// One SCC member cell in collector-internal form (0-based coordinates).
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63struct MemberCell {
64    sheet_id: SheetId,
65    row: u32,
66    col: u32,
67}
68
69#[derive(Default)]
70struct CollectorState {
71    /// Index (into `members`) of the member currently being evaluated.
72    /// `None` until `set_current` is called; reads observed while `None` are
73    /// not attributable and are dropped.
74    current: Option<u32>,
75    /// Live edges as `(from_member_idx, to_member_idx)`. Self-edges `(i, i)`
76    /// are recorded (e.g. a member whose range argument includes itself).
77    edges: FxHashSet<(u32, u32)>,
78}
79
80/// Records which reads actually occurred targeting SCC members during a
81/// sequential member-by-member evaluation pass.
82///
83/// * Scalar reads are O(1) (hash lookup keyed by `(sheet, row, col)`).
84/// * Rectangle reads are recorded **once per resolved rect** and intersected
85///   with the membership in O(|SCC|) — never per cell of the rect.
86/// * Name reads (named-formula SCC members, spec §7.13) are O(1) lookups by
87///   the engine-folded name key.
88///
89/// Member indices are split: cell members occupy `0..cell_count`, name
90/// members occupy `cell_count..cell_count + name_count` (matching the spec
91/// §7.13 member ordering used by SCC tasks: cells first, then names).
92pub struct LiveEdgeCollector {
93    /// Iterable membership for rect intersection.
94    members: Vec<MemberCell>,
95    /// O(1) scalar lookup: (sheet_id, row0, col0) -> member index.
96    index: FxHashMap<(SheetId, u32, u32), u32>,
97    /// O(1) name lookup: engine-folded name key -> member index (indices
98    /// start after the cell members).
99    name_index: FxHashMap<String, u32>,
100    /// Total member count (cells + names); valid `set_current` range.
101    total_members: usize,
102    /// See module docs: uncontended Mutex forced by `Send + Sync` bounds on
103    /// the resolver traits; SCC passes are single-threaded.
104    state: Mutex<CollectorState>,
105}
106
107impl LiveEdgeCollector {
108    /// Build a collector for the given SCC membership. Member order defines
109    /// the indices used in recorded edges.
110    pub fn new(members: &[CellRef]) -> Self {
111        Self::new_with_names(members, &[])
112    }
113
114    /// Build a collector over cell members plus name-vertex members. Cell
115    /// members get indices `0..cells.len()`; name member `j` gets index
116    /// `cells.len() + j`. `names` must already be folded with the engine's
117    /// name-folding rule (see [`Engine::fold_name_key`]).
118    pub fn new_with_names(cells: &[CellRef], names: &[String]) -> Self {
119        let members: Vec<MemberCell> = cells
120            .iter()
121            .map(|c| MemberCell {
122                sheet_id: c.sheet_id,
123                row: c.coord.row(),
124                col: c.coord.col(),
125            })
126            .collect();
127        let mut index = FxHashMap::default();
128        index.reserve(members.len());
129        for (i, m) in members.iter().enumerate() {
130            index.insert((m.sheet_id, m.row, m.col), i as u32);
131        }
132        let mut name_index = FxHashMap::default();
133        name_index.reserve(names.len());
134        for (j, name) in names.iter().enumerate() {
135            name_index.insert(name.clone(), (members.len() + j) as u32);
136        }
137        let total_members = members.len() + names.len();
138        Self {
139            members,
140            index,
141            name_index,
142            total_members,
143            state: Mutex::new(CollectorState::default()),
144        }
145    }
146
147    pub fn member_count(&self) -> usize {
148        self.total_members
149    }
150
151    /// Set the member whose formula is about to be evaluated; subsequent
152    /// recorded reads are attributed to it.
153    pub fn set_current(&self, member_idx: u32) {
154        debug_assert!((member_idx as usize) < self.total_members);
155        self.state.lock().unwrap().current = Some(member_idx);
156    }
157
158    /// Stop attributing reads to any member (used between passes so that
159    /// out-of-band reads — snapshots, deltas — never record edges).
160    pub fn clear_current(&self) {
161        self.state.lock().unwrap().current = None;
162    }
163
164    /// Record a scalar read of `(sheet_id, row, col)` (0-based).
165    pub fn record_scalar(&self, sheet_id: SheetId, row: u32, col: u32) {
166        let Some(&to) = self.index.get(&(sheet_id, row, col)) else {
167            return;
168        };
169        let mut st = self.state.lock().unwrap();
170        if let Some(from) = st.current {
171            st.edges.insert((from, to));
172        }
173    }
174
175    /// Record a rectangle read (0-based, inclusive corners). Intersection is
176    /// O(|SCC|): each member is tested against the rect once; the rect is
177    /// never enumerated per cell.
178    pub fn record_rect(&self, sheet_id: SheetId, sr: u32, sc: u32, er: u32, ec: u32) {
179        let mut st = self.state.lock().unwrap();
180        let Some(from) = st.current else {
181            return;
182        };
183        for (i, m) in self.members.iter().enumerate() {
184            if m.sheet_id == sheet_id && m.row >= sr && m.row <= er && m.col >= sc && m.col <= ec {
185                st.edges.insert((from, i as u32));
186            }
187        }
188    }
189
190    /// Record a read of a named entity by folded name key (e.g. a formula
191    /// referencing a named-formula SCC member).
192    pub fn record_name(&self, folded_name: &str) {
193        let Some(&to) = self.name_index.get(folded_name) else {
194            return;
195        };
196        let mut st = self.state.lock().unwrap();
197        if let Some(from) = st.current {
198            st.edges.insert((from, to));
199        }
200    }
201
202    /// Drain the collected edges, leaving the collector empty (current member
203    /// attribution is preserved).
204    pub fn take_edges(&self) -> FxHashSet<(u32, u32)> {
205        std::mem::take(&mut self.state.lock().unwrap().edges)
206    }
207}
208
209/* ───────────────────────── ReadSink ───────────────────────── */
210
211/// Where a [`RecordingContext`] sends observed reads (0-based, inclusive).
212pub trait ReadSink: Send + Sync {
213    fn record_scalar(&self, sheet_id: SheetId, row: u32, col: u32);
214    fn record_rect(&self, sheet_id: SheetId, sr: u32, sc: u32, er: u32, ec: u32);
215    fn record_name(&self, folded_name: &str);
216}
217
218impl ReadSink for LiveEdgeCollector {
219    fn record_scalar(&self, sheet_id: SheetId, row: u32, col: u32) {
220        LiveEdgeCollector::record_scalar(self, sheet_id, row, col)
221    }
222    fn record_rect(&self, sheet_id: SheetId, sr: u32, sc: u32, er: u32, ec: u32) {
223        LiveEdgeCollector::record_rect(self, sheet_id, sr, sc, er, ec)
224    }
225    fn record_name(&self, folded_name: &str) {
226        LiveEdgeCollector::record_name(self, folded_name)
227    }
228}
229
230/// Every read of one evaluation, as rectangles `(sheet, r0, c0, r1, c1)`
231/// (0-based, inclusive; a cell read is a 1×1 rectangle). Names are not
232/// recorded: a name's reads flow through the range view of its definition.
233/// Used by the freshness recorder for dynamic readers (design §8.2).
234#[derive(Default)]
235pub struct ReadLog {
236    reads: Mutex<Vec<ReadRect>>,
237}
238
239/// `(sheet, r0, c0, r1, c1)`, 0-based inclusive.
240pub type ReadRect = (SheetId, u32, u32, u32, u32);
241
242impl ReadLog {
243    pub fn take(&self) -> Vec<ReadRect> {
244        std::mem::take(&mut self.reads.lock().unwrap())
245    }
246}
247
248impl ReadSink for ReadLog {
249    fn record_scalar(&self, sheet_id: SheetId, row: u32, col: u32) {
250        self.reads
251            .lock()
252            .unwrap()
253            .push((sheet_id, row, col, row, col));
254    }
255    fn record_rect(&self, sheet_id: SheetId, sr: u32, sc: u32, er: u32, ec: u32) {
256        self.reads.lock().unwrap().push((sheet_id, sr, sc, er, ec));
257    }
258    fn record_name(&self, _folded_name: &str) {}
259}
260
261/* ───────────────────────── RecordingContext ───────────────────────── */
262
263/// Delegating [`EvaluationContext`] that wraps `&Engine<R>` and records reads
264/// into a [`LiveEdgeCollector`].
265///
266/// Interception points (everything else is pure delegation):
267///
268/// * `EvaluationContext::resolve_cell_reference_value` — the interpreter's
269///   scalar read path (current-sheet aware).
270/// * `EvaluationContext::resolve_range_view` — the single choke point for
271///   range, named-range, table and dynamic (`INDIRECT`/`OFFSET`) reads. The
272///   engine resolves un/partially-bounded references to concrete used-region
273///   bounds, and the returned view carries the resolved sheet + rect, so we
274///   record exactly that rect once. Views materialised from owned rows (array
275///   literals, named literals/formulas) carry the synthetic `"__tmp"` sheet,
276///   which has no `SheetId`, so they are skipped automatically.
277/// * `ReferenceResolver::resolve_cell_reference` — sheet-qualified scalar
278///   reads (e.g. implicit intersection).
279/// * `RangeResolver::resolve_range_reference` — legacy boxed-range path; the
280///   rect is resolved via the engine's own `resolve_range_view` normalisation
281///   so unbounded references record their used-region bounds.
282///
283/// Not recordable at this layer (Stage 2 follow-ups, noted in tests):
284///
285/// * `NamedRangeResolver::resolve_named_range_reference` — values-only API
286///   with no sheet/region context. The engine-level named-range path flows
287///   through `resolve_range_view` (intercepted); only the external-resolver
288///   fallback is invisible.
289/// * `TableResolver::resolve_table_reference` — returns an opaque `Table`.
290///   Engine-registered tables flow through `resolve_range_view` (intercepted).
291pub struct RecordingContext<'a, R: EvaluationContext, S: ReadSink = LiveEdgeCollector> {
292    engine: &'a Engine<R>,
293    collector: &'a S,
294}
295
296impl<'a, R: EvaluationContext, S: ReadSink> RecordingContext<'a, R, S> {
297    pub fn new(engine: &'a Engine<R>, collector: &'a S) -> Self {
298        Self { engine, collector }
299    }
300
301    /// Record a read of a named entity, folding the raw reference text with
302    /// the engine's name-folding rule so it matches collector name keys.
303    fn record_name(&self, raw_name: &str) {
304        let key = self.engine.graph.name_lookup_key(raw_name);
305        self.collector.record_name(&key);
306    }
307
308    /// Record a scalar read given Excel 1-based coordinates.
309    fn record_cell_1based(&self, sheet_name: &str, row: u32, col: u32) {
310        if row == 0 || col == 0 {
311            return;
312        }
313        if let Some(sid) = self.engine.sheet_id(sheet_name) {
314            self.collector.record_scalar(sid, row - 1, col - 1);
315        }
316    }
317
318    /// Record the resolved rect of a `RangeView`. View bounds are absolute,
319    /// 0-based and inclusive. Owned/temporary views (sheet `"__tmp"`) have no
320    /// registered `SheetId` and are skipped.
321    fn record_view(&self, view: &RangeView<'_>) {
322        if view.is_empty() {
323            return;
324        }
325        if let Some(sid) = self.engine.sheet_id(view.sheet_name()) {
326            self.collector.record_rect(
327                sid,
328                view.start_row() as u32,
329                view.start_col() as u32,
330                view.end_row() as u32,
331                view.end_col() as u32,
332            );
333        }
334    }
335}
336
337impl<'a, R: EvaluationContext, S: ReadSink> ReferenceResolver for RecordingContext<'a, R, S> {
338    fn resolve_cell_reference(
339        &self,
340        sheet: Option<&str>,
341        row: u32,
342        col: u32,
343    ) -> Result<LiteralValue, ExcelError> {
344        // Unqualified (`None`) references are rejected by the engine itself
345        // (no current-sheet context at this trait level), so there is nothing
346        // attributable to record in that case.
347        if let Some(sheet_name) = sheet {
348            self.record_cell_1based(sheet_name, row, col);
349        }
350        self.engine.resolve_cell_reference(sheet, row, col)
351    }
352}
353
354impl<'a, R: EvaluationContext, S: ReadSink> RangeResolver for RecordingContext<'a, R, S> {
355    fn resolve_range_reference(
356        &self,
357        sheet: Option<&str>,
358        sr: Option<u32>,
359        sc: Option<u32>,
360        er: Option<u32>,
361        ec: Option<u32>,
362    ) -> Result<Box<dyn Range>, ExcelError> {
363        // Resolve the rect through the engine's own bound normalisation
364        // (used-region for unbounded axes) rather than duplicating it here.
365        if let Some(sheet_name) = sheet {
366            let reference = ReferenceType::Range {
367                sheet: Some(sheet_name.to_string()),
368                start_row: sr,
369                start_col: sc,
370                end_row: er,
371                end_col: ec,
372                start_row_abs: true,
373                start_col_abs: true,
374                end_row_abs: true,
375                end_col_abs: true,
376            };
377            if let Ok(view) = self.engine.resolve_range_view(&reference, sheet_name) {
378                self.record_view(&view);
379            }
380        }
381        self.engine.resolve_range_reference(sheet, sr, sc, er, ec)
382    }
383}
384
385impl<'a, R: EvaluationContext, S: ReadSink> NamedRangeResolver for RecordingContext<'a, R, S> {
386    fn resolve_named_range_reference(
387        &self,
388        name: &str,
389    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
390        // Values-only API without sheet/region context; record the *name*
391        // member edge (if the name itself is an SCC member) — region-level
392        // reads flow through `resolve_range_view` instead.
393        self.record_name(name);
394        self.engine.resolve_named_range_reference(name)
395    }
396}
397
398impl<'a, R: EvaluationContext, S: ReadSink> TableResolver for RecordingContext<'a, R, S> {
399    fn resolve_table_reference(&self, tref: &TableReference) -> Result<Box<dyn Table>, ExcelError> {
400        // Opaque `Table` without region context; engine-registered tables are
401        // intercepted in `resolve_range_view` instead.
402        self.engine.resolve_table_reference(tref)
403    }
404}
405
406impl<'a, R: EvaluationContext, S: ReadSink> SourceResolver for RecordingContext<'a, R, S> {
407    fn source_scalar_version(&self, name: &str) -> Option<u64> {
408        self.engine.source_scalar_version(name)
409    }
410    fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
411        self.engine.resolve_source_scalar(name)
412    }
413    fn source_table_version(&self, name: &str) -> Option<u64> {
414        self.engine.source_table_version(name)
415    }
416    fn resolve_source_table(&self, name: &str) -> Result<Box<dyn Table>, ExcelError> {
417        self.engine.resolve_source_table(name)
418    }
419}
420
421impl<'a, R: EvaluationContext, S: ReadSink> Resolver for RecordingContext<'a, R, S> {}
422
423impl<'a, R: EvaluationContext, S: ReadSink> FunctionProvider for RecordingContext<'a, R, S> {
424    fn planning_semantic_revision(&self) -> Option<u64> {
425        self.engine.planning_semantic_revision()
426    }
427
428    fn get_function(
429        &self,
430        ns: &str,
431        name: &str,
432    ) -> Option<std::sync::Arc<dyn crate::traits::Function>> {
433        self.engine.get_function(ns, name)
434    }
435
436    fn get_function_for_planning(
437        &self,
438        ns: &str,
439        name: &str,
440    ) -> Option<std::sync::Arc<dyn crate::traits::Function>> {
441        self.engine.get_function_for_planning(ns, name)
442    }
443}
444
445impl<'a, R: EvaluationContext, S: ReadSink> EvaluationContext for RecordingContext<'a, R, S> {
446    /* ── intercept-and-record ── */
447
448    fn resolve_range_view<'c>(
449        &'c self,
450        reference: &ReferenceType,
451        current_sheet: &str,
452    ) -> Result<RangeView<'c>, ExcelError> {
453        // Named reads can target a name *vertex* that is itself an SCC member
454        // (a named formula, spec §7.13). Those resolve to owned-row views with
455        // no sheet rect, so they must be recorded by name here in addition to
456        // the rect recording below (which covers Cell/Range definitions).
457        if let ReferenceType::NamedRange(name) = reference {
458            self.record_name(name);
459        }
460        let view = self.engine.resolve_range_view(reference, current_sheet)?;
461        self.record_view(&view);
462        Ok(view)
463    }
464
465    fn resolve_cell_reference_value(
466        &self,
467        sheet: Option<&str>,
468        row: u32,
469        col: u32,
470        current_sheet: &str,
471    ) -> Result<LiteralValue, ExcelError> {
472        self.record_cell_1based(sheet.unwrap_or(current_sheet), row, col);
473        self.engine
474            .resolve_cell_reference_value(sheet, row, col, current_sheet)
475    }
476
477    fn resolve_cell_format(
478        &self,
479        sheet: Option<&str>,
480        row: u32,
481        col: u32,
482        current_sheet: &str,
483    ) -> Option<crate::format::FormatId> {
484        self.engine
485            .resolve_cell_format(sheet, row, col, current_sheet)
486    }
487
488    fn format_class(
489        &self,
490        format: crate::format::FormatId,
491    ) -> Option<formualizer_common::numfmt::FormatClass> {
492        self.engine.format_class(format)
493    }
494
495    fn record_cell_derived_format(
496        &self,
497        sheet: &str,
498        row: u32,
499        col: u32,
500        format: Option<crate::format::FormatId>,
501    ) {
502        self.engine
503            .record_cell_derived_format(sheet, row, col, format)
504    }
505
506    /* ── pure delegation ── */
507
508    fn thread_pool(&self) -> Option<&std::sync::Arc<rayon::ThreadPool>> {
509        self.engine.thread_pool()
510    }
511    fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
512        self.engine.cancellation_token()
513    }
514    fn chunk_hint(&self) -> Option<usize> {
515        self.engine.chunk_hint()
516    }
517    fn locale(&self) -> crate::locale::Locale {
518        self.engine.locale()
519    }
520    fn workbook_sheet_count(&self) -> Option<usize> {
521        self.engine.workbook_sheet_count()
522    }
523    fn sheet_index_by_name(&self, sheet: &str) -> Option<usize> {
524        self.engine.sheet_index_by_name(sheet)
525    }
526    fn current_sheet_index(&self, current_sheet: &str) -> Option<usize> {
527        self.engine.current_sheet_index(current_sheet)
528    }
529    fn inspect_reference(
530        &self,
531        reference: &ReferenceType,
532        current_sheet: &str,
533    ) -> Result<Option<ReferenceInfo>, ExcelError> {
534        self.engine.inspect_reference(reference, current_sheet)
535    }
536    fn formula_text_at_cell(&self, cell: CellRef) -> Result<Option<String>, ExcelError> {
537        self.engine.formula_text_at_cell(cell)
538    }
539    fn resolve_spill_reference(
540        &self,
541        anchor: &ReferenceType,
542        current_sheet: &str,
543    ) -> Result<ReferenceType, ExcelError> {
544        // The extent is a read of the anchor: record it even when the anchor
545        // has no current spill (the rectangle itself is recorded when the
546        // caller reads it through `resolve_range_view`).
547        match anchor {
548            ReferenceType::Cell {
549                sheet, row, col, ..
550            } => self.record_cell_1based(sheet.as_deref().unwrap_or(current_sheet), *row, *col),
551            ReferenceType::NamedRange(name) => self.record_name(name),
552            _ => {}
553        }
554        self.engine.resolve_spill_reference(anchor, current_sheet)
555    }
556    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
557        self.engine.clock()
558    }
559    fn timezone(&self) -> &crate::timezone::TimeZoneSpec {
560        self.engine.timezone()
561    }
562    fn volatile_level(&self) -> crate::traits::VolatileLevel {
563        self.engine.volatile_level()
564    }
565    fn workbook_seed(&self) -> u64 {
566        self.engine.workbook_seed()
567    }
568    fn recalc_epoch(&self) -> u64 {
569        self.engine.recalc_epoch()
570    }
571    fn used_rows_for_columns(
572        &self,
573        sheet: &str,
574        start_col: u32,
575        end_col: u32,
576    ) -> Option<(u32, u32)> {
577        self.engine.used_rows_for_columns(sheet, start_col, end_col)
578    }
579    fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
580        self.engine.used_cols_for_rows(sheet, start_row, end_row)
581    }
582    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
583        self.engine.sheet_bounds(sheet)
584    }
585    fn data_snapshot_id(&self) -> u64 {
586        self.engine.data_snapshot_id()
587    }
588    fn backend_caps(&self) -> crate::traits::BackendCaps {
589        self.engine.backend_caps()
590    }
591    fn date_system(&self) -> crate::engine::DateSystem {
592        self.engine.date_system()
593    }
594    fn build_lookup_index(
595        &self,
596        view: &RangeView<'_>,
597        axis: crate::engine::lookup_index_cache::LookupAxis,
598    ) -> Option<std::sync::Arc<crate::engine::lookup_index_cache::LookupIndex>> {
599        self.engine.build_lookup_index(view, axis)
600    }
601    fn build_criteria_mask(
602        &self,
603        view: &RangeView<'_>,
604        col_in_view: usize,
605        pred: &crate::args::CriteriaPredicate,
606    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
607        self.engine.build_criteria_mask(view, col_in_view, pred)
608    }
609    fn build_row_visibility_mask(
610        &self,
611        view: &RangeView<'_>,
612        mode: crate::engine::row_visibility::VisibilityMaskMode,
613    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
614        self.engine.build_row_visibility_mask(view, mode)
615    }
616    fn nested_subtotal_cells(
617        &self,
618        view: &RangeView<'_>,
619        include_aggregate: bool,
620    ) -> Option<Vec<(usize, usize, usize)>> {
621        self.engine.nested_subtotal_cells(view, include_aggregate)
622    }
623}