Skip to main content

formualizer_eval/engine/graph/
range_deps.rs

1use super::*;
2use crate::engine::used_extent::{ExtentPolicy, OpenRangeBounds, resolve_used_extent};
3use formualizer_common::LiteralValue;
4use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
5
6#[derive(Clone, Copy, PartialEq, Eq)]
7enum RangeSelfUse {
8    NoMatch,
9    Excluded,
10    IncludedOrUnknown,
11}
12
13impl RangeSelfUse {
14    fn merge(self, other: Self) -> Self {
15        match (self, other) {
16            (Self::IncludedOrUnknown, _) | (_, Self::IncludedOrUnknown) => Self::IncludedOrUnknown,
17            (Self::Excluded, _) | (_, Self::Excluded) => Self::Excluded,
18            _ => Self::NoMatch,
19        }
20    }
21}
22
23impl DependencyGraph {
24    /// Visit compressed-range formula dependents covering one cell without
25    /// materializing the stripe union used by dirty propagation.
26    ///
27    /// This path is intentionally parallel to
28    /// `collect_range_dependents_for_rect`: scheduling keeps its existing
29    /// behavior, while inspection can stop before a pathological stripe has
30    /// been copied into an unbounded candidate set. Work is charged for every
31    /// stripe candidate and every compressed range exact-check.
32    pub(crate) fn visit_range_dependents_covering_bounded(
33        &self,
34        sheet_id: SheetId,
35        row0: u32,
36        col0: u32,
37        remaining_work: &mut u64,
38        visitor: &mut dyn FnMut(VertexId) -> bool,
39    ) -> bool {
40        if self.stripe_to_dependents.is_empty() {
41            return true;
42        }
43
44        let mut seen = FxHashSet::default();
45        let keys = [
46            StripeKey {
47                sheet_id,
48                stripe_type: StripeType::Column,
49                index: col0,
50            },
51            StripeKey {
52                sheet_id,
53                stripe_type: StripeType::Row,
54                index: row0,
55            },
56            StripeKey {
57                sheet_id,
58                stripe_type: StripeType::Block,
59                index: block_index(row0, col0),
60            },
61        ];
62
63        for key in keys {
64            if key.stripe_type == StripeType::Block && !self.config.enable_block_stripes {
65                continue;
66            }
67            let Some(candidates) = self.stripe_to_dependents.get(&key) else {
68                continue;
69            };
70            for &dependent in candidates {
71                if *remaining_work == 0 {
72                    return false;
73                }
74                *remaining_work -= 1;
75                if !seen.insert(dependent) {
76                    continue;
77                }
78                let Some(ranges) = self.formula_to_range_deps.get(&dependent) else {
79                    continue;
80                };
81                let mut covered = false;
82                for range in ranges {
83                    if *remaining_work == 0 {
84                        return false;
85                    }
86                    *remaining_work -= 1;
87                    // Match collect_range_dependents_for_rect: unresolved
88                    // non-Id locators are interpreted on the query sheet.
89                    let range_sheet = match range.sheet {
90                        SharedSheetLocator::Id(id) => id,
91                        _ => sheet_id,
92                    };
93                    if range_sheet != sheet_id {
94                        continue;
95                    }
96                    let start_row = range.start_row.map(|bound| bound.index).unwrap_or(0);
97                    let end_row = range.end_row.map(|bound| bound.index).unwrap_or(u32::MAX);
98                    let start_col = range.start_col.map(|bound| bound.index).unwrap_or(0);
99                    let end_col = range.end_col.map(|bound| bound.index).unwrap_or(u32::MAX);
100                    if start_row <= row0 && row0 <= end_row && start_col <= col0 && col0 <= end_col
101                    {
102                        covered = true;
103                        break;
104                    }
105                }
106                if covered && !visitor(dependent) {
107                    return false;
108                }
109            }
110        }
111        true
112    }
113
114    /// Public wrapper to add range-dependent edges.
115    pub fn add_range_edges(
116        &mut self,
117        dependent: VertexId,
118        ranges: &[SharedRangeRef<'static>],
119        current_sheet_id: SheetId,
120    ) {
121        self.add_range_dependent_edges(dependent, ranges, current_sheet_id);
122    }
123
124    /// Return the compressed range dependencies recorded for a formula vertex, if any.
125    /// These are `SharedRangeRef` entries that were not expanded into explicit
126    /// cell edges due to `range_expansion_limit` or due to infinite/partial bounds.
127    pub fn get_range_dependencies(
128        &self,
129        vertex: VertexId,
130    ) -> Option<&Vec<SharedRangeRef<'static>>> {
131        self.formula_to_range_deps.get(&vertex)
132    }
133
134    #[cfg(test)]
135    pub(crate) fn formula_to_range_deps(
136        &self,
137    ) -> &FxHashMap<VertexId, Vec<SharedRangeRef<'static>>> {
138        &self.formula_to_range_deps
139    }
140
141    #[cfg(test)]
142    pub(crate) fn stripe_to_dependents(&self) -> &FxHashMap<StripeKey, FxHashSet<VertexId>> {
143        &self.stripe_to_dependents
144    }
145
146    /// True when a (possibly open-ended) range region on `sheet_id` covers
147    /// the formula vertex's own cell. Used to record a self-loop for
148    /// stripe-compressed / whole-axis self-inclusion (#120): such references
149    /// never produce explicit cell edges, so the ingest self-reference check
150    /// (which scans expanded cell deps) misses them. `None` bounds mean the
151    /// axis is unbounded (whole column/row), which always covers the cell.
152    fn range_region_contains_self(
153        &self,
154        dependent: VertexId,
155        sheet_id: SheetId,
156        s_row: Option<u32>,
157        e_row: Option<u32>,
158        s_col: Option<u32>,
159        e_col: Option<u32>,
160    ) -> bool {
161        if self.store.sheet_id(dependent) != sheet_id {
162            return false;
163        }
164        let coord = self.store.coord(dependent);
165        let r0 = coord.row();
166        let c0 = coord.col();
167        s_row.is_none_or(|s| r0 >= s)
168            && e_row.is_none_or(|e| r0 <= e)
169            && s_col.is_none_or(|s| c0 >= s)
170            && e_col.is_none_or(|e| c0 <= e)
171    }
172
173    /// Record a self-loop edge (vertex → itself). The edge store and Tarjan
174    /// both treat self-loops as cycles (`separate_cycles` via `has_self_loop`).
175    fn record_self_loop(&mut self, vertex: VertexId) {
176        if !self.has_self_loop(vertex) {
177            self.edges.add_edge(vertex, vertex);
178        }
179    }
180
181    pub(crate) fn compressed_range_resolved_bounds(
182        &self,
183        sheet: SheetId,
184        range: (Option<u32>, Option<u32>, Option<u32>, Option<u32>),
185    ) -> Option<(u32, u32, u32, u32)> {
186        let (start_row, end_row, start_col, end_col) = range;
187        let extent = resolve_used_extent(
188            OpenRangeBounds {
189                start_row,
190                start_column: start_col,
191                end_row,
192                end_column: end_col,
193            },
194            ExtentPolicy::GraphCompat {
195                fallback_row: self.config.max_open_ended_rows.saturating_sub(1),
196                fallback_column: self.config.max_open_ended_cols.saturating_sub(1),
197            },
198            |first, last| self.used_row_bounds_for_columns(sheet, first, last),
199            |first, last| self.used_col_bounds_for_rows(sheet, first, last),
200        )?;
201        Some((
202            extent.start_row,
203            extent.end_row,
204            extent.start_column,
205            extent.end_column,
206        ))
207    }
208
209    /// Classify whether every occurrence of one compressed range that covers
210    /// the formula cell is narrowed away from that cell by a statically
211    /// resolvable `INDEX`. The range dependency itself remains conservative so
212    /// used-bound growth still invalidates the formula; only the synthetic #120
213    /// self-loop is omitted when the selected reference cannot contain the
214    /// formula cell.
215    fn compressed_range_self_use(
216        &self,
217        dependent: VertexId,
218        range_sheet: SheetId,
219        range: (Option<u32>, Option<u32>, Option<u32>, Option<u32>),
220    ) -> RangeSelfUse {
221        let Some(ast) = self.get_formula(dependent) else {
222            return RangeSelfUse::IncludedOrUnknown;
223        };
224
225        fn static_index(node: &ASTNode) -> Option<i64> {
226            match &node.node_type {
227                ASTNodeType::Literal(LiteralValue::Int(value)) => Some(*value),
228                ASTNodeType::Literal(LiteralValue::Number(value)) if value.is_finite() => {
229                    Some(*value as i64)
230                }
231                ASTNodeType::UnaryOp { op, expr } if op == "+" => static_index(expr),
232                ASTNodeType::UnaryOp { op, expr } if op == "-" => static_index(expr)?.checked_neg(),
233                _ => None,
234            }
235        }
236
237        fn matching_range(
238            graph: &DependencyGraph,
239            node: &ASTNode,
240            dependent: VertexId,
241            range_sheet: SheetId,
242            range: (Option<u32>, Option<u32>, Option<u32>, Option<u32>),
243        ) -> bool {
244            let ASTNodeType::Reference {
245                reference:
246                    ReferenceType::Range {
247                        sheet,
248                        start_row,
249                        start_col,
250                        end_row,
251                        end_col,
252                        ..
253                    },
254                ..
255            } = &node.node_type
256            else {
257                return false;
258            };
259            let sheet_id = match sheet.as_deref() {
260                Some(name) => match graph.sheet_id(name) {
261                    Some(id) => id,
262                    None => return false,
263                },
264                None => graph.get_vertex_sheet_id(dependent),
265            };
266            sheet_id == range_sheet
267                && start_row.map(|index| index.saturating_sub(1)) == range.0
268                && end_row.map(|index| index.saturating_sub(1)) == range.1
269                && start_col.map(|index| index.saturating_sub(1)) == range.2
270                && end_col.map(|index| index.saturating_sub(1)) == range.3
271        }
272
273        fn selected_region_contains_self(
274            graph: &DependencyGraph,
275            dependent: VertexId,
276            range_sheet: SheetId,
277            range: (Option<u32>, Option<u32>, Option<u32>, Option<u32>),
278            position: i64,
279            explicit_col: Option<i64>,
280        ) -> Option<bool> {
281            let (sr, er, sc, ec) = graph.compressed_range_resolved_bounds(range_sheet, range)?;
282            let (row, col) = match explicit_col {
283                Some(col) => (position, col),
284                None if sr == er => (1, position),
285                None => (position, 1),
286            };
287            if row < 0 || col < 0 {
288                return Some(false);
289            }
290            let coord = graph.store.coord(dependent);
291            let contains = if row == 0 && col == 0 {
292                coord.row() >= sr && coord.row() <= er && coord.col() >= sc && coord.col() <= ec
293            } else if col == 0 {
294                let selected_row = sr.checked_add(u32::try_from(row).ok()?.saturating_sub(1))?;
295                selected_row <= er
296                    && coord.row() == selected_row
297                    && coord.col() >= sc
298                    && coord.col() <= ec
299            } else if row == 0 {
300                let selected_col = sc.checked_add(u32::try_from(col).ok()?.saturating_sub(1))?;
301                selected_col <= ec
302                    && coord.col() == selected_col
303                    && coord.row() >= sr
304                    && coord.row() <= er
305            } else {
306                let selected_row = sr.checked_add(u32::try_from(row).ok()?.saturating_sub(1))?;
307                let selected_col = sc.checked_add(u32::try_from(col).ok()?.saturating_sub(1))?;
308                selected_row <= er
309                    && selected_col <= ec
310                    && coord.row() == selected_row
311                    && coord.col() == selected_col
312            };
313            Some(contains)
314        }
315
316        fn visit(
317            graph: &DependencyGraph,
318            node: &ASTNode,
319            dependent: VertexId,
320            range_sheet: SheetId,
321            range: (Option<u32>, Option<u32>, Option<u32>, Option<u32>),
322            index: Option<(i64, Option<i64>)>,
323        ) -> RangeSelfUse {
324            if matching_range(graph, node, dependent, range_sheet, range) {
325                return match index.and_then(|(row, col)| {
326                    selected_region_contains_self(graph, dependent, range_sheet, range, row, col)
327                }) {
328                    Some(false) => RangeSelfUse::Excluded,
329                    Some(true) | None => RangeSelfUse::IncludedOrUnknown,
330                };
331            }
332            match &node.node_type {
333                ASTNodeType::Function { name, args }
334                    if name.eq_ignore_ascii_case("INDEX") && (2..=3).contains(&args.len()) =>
335                {
336                    let row = static_index(&args[1]);
337                    let col = args.get(2).and_then(static_index);
338                    let selection = row.and_then(|row| {
339                        if args.len() == 2 || col.is_some() {
340                            Some((row, col))
341                        } else {
342                            None
343                        }
344                    });
345                    let mut use_kind =
346                        visit(graph, &args[0], dependent, range_sheet, range, selection);
347                    for arg in &args[1..] {
348                        use_kind =
349                            use_kind.merge(visit(graph, arg, dependent, range_sheet, range, None));
350                    }
351                    use_kind
352                }
353                ASTNodeType::Function { args, .. } => {
354                    args.iter().fold(RangeSelfUse::NoMatch, |kind, arg| {
355                        kind.merge(visit(graph, arg, dependent, range_sheet, range, None))
356                    })
357                }
358                ASTNodeType::UnaryOp { expr, .. } => {
359                    visit(graph, expr, dependent, range_sheet, range, None)
360                }
361                ASTNodeType::BinaryOp { left, right, .. } => visit(
362                    graph,
363                    left,
364                    dependent,
365                    range_sheet,
366                    range,
367                    None,
368                )
369                .merge(visit(graph, right, dependent, range_sheet, range, None)),
370                ASTNodeType::Call { callee, args } => {
371                    let mut kind = visit(graph, callee, dependent, range_sheet, range, None);
372                    for arg in args {
373                        kind = kind.merge(visit(graph, arg, dependent, range_sheet, range, None));
374                    }
375                    kind
376                }
377                ASTNodeType::Array(rows) => {
378                    rows.iter()
379                        .flatten()
380                        .fold(RangeSelfUse::NoMatch, |kind, item| {
381                            kind.merge(visit(graph, item, dependent, range_sheet, range, None))
382                        })
383                }
384                ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => {
385                    RangeSelfUse::NoMatch
386                }
387            }
388        }
389
390        visit(self, &ast, dependent, range_sheet, range, None)
391    }
392
393    pub(super) fn add_range_dependent_edges(
394        &mut self,
395        dependent: VertexId,
396        ranges: &[SharedRangeRef<'static>],
397        current_sheet_id: SheetId,
398    ) {
399        if ranges.is_empty() {
400            return;
401        }
402
403        self.formula_to_range_deps
404            .insert(dependent, ranges.to_vec());
405
406        for range in ranges {
407            let sheet_id = match range.sheet {
408                SharedSheetLocator::Id(id) => id,
409                _ => current_sheet_id,
410            };
411
412            let s_row = range.start_row.map(|b| b.index);
413            let e_row = range.end_row.map(|b| b.index);
414            let s_col = range.start_col.map(|b| b.index);
415            let e_col = range.end_col.map(|b| b.index);
416
417            // #120: a compressed range whose region covers this formula's own
418            // cell is a self-reference. Record a self-loop so SCC detection
419            // flags the cycle (the ingest self-ref check only sees expanded
420            // cell edges, which compressed ranges do not produce).
421            if self.range_region_contains_self(dependent, sheet_id, s_row, e_row, s_col, e_col)
422                && self.compressed_range_self_use(dependent, sheet_id, (s_row, e_row, s_col, e_col))
423                    != RangeSelfUse::Excluded
424            {
425                self.record_self_loop(dependent);
426            }
427
428            let col_stripes = (s_row.is_none() && e_row.is_none())
429                || (s_col.is_some() && e_col.is_some() && (s_row.is_none() || e_row.is_none()));
430            let row_stripes = (s_col.is_none() && e_col.is_none())
431                || (s_row.is_some() && e_row.is_some() && (s_col.is_none() || e_col.is_none()));
432
433            if col_stripes && !row_stripes {
434                let sc = s_col.unwrap_or(0);
435                let ec = e_col.unwrap_or(sc);
436                for col in sc..=ec {
437                    let key = StripeKey {
438                        sheet_id,
439                        stripe_type: StripeType::Column,
440                        index: col,
441                    };
442                    self.stripe_to_dependents
443                        .entry(key.clone())
444                        .or_default()
445                        .insert(dependent);
446                    #[cfg(test)]
447                    {
448                        if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1)
449                            && let Ok(mut g) = self.instr.lock()
450                        {
451                            g.stripe_inserts += 1;
452                        }
453                    }
454                }
455                continue;
456            }
457
458            if row_stripes && !col_stripes {
459                let sr = s_row.unwrap_or(0);
460                let er = e_row.unwrap_or(sr);
461                for row in sr..=er {
462                    let key = StripeKey {
463                        sheet_id,
464                        stripe_type: StripeType::Row,
465                        index: row,
466                    };
467                    self.stripe_to_dependents
468                        .entry(key.clone())
469                        .or_default()
470                        .insert(dependent);
471                    #[cfg(test)]
472                    {
473                        if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1)
474                            && let Ok(mut g) = self.instr.lock()
475                        {
476                            g.stripe_inserts += 1;
477                        }
478                    }
479                }
480                continue;
481            }
482
483            let start_row = s_row.unwrap_or(0);
484            let start_col = s_col.unwrap_or(0);
485            let end_row = e_row.unwrap_or(start_row);
486            let end_col = e_col.unwrap_or(start_col);
487
488            let height = end_row.saturating_sub(start_row) + 1;
489            let width = end_col.saturating_sub(start_col) + 1;
490
491            if self.config.enable_block_stripes && height > 1 && width > 1 {
492                let start_block_row = start_row / BLOCK_H;
493                let end_block_row = end_row / BLOCK_H;
494                let start_block_col = start_col / BLOCK_W;
495                let end_block_col = end_col / BLOCK_W;
496
497                for block_row in start_block_row..=end_block_row {
498                    for block_col in start_block_col..=end_block_col {
499                        let key = StripeKey {
500                            sheet_id,
501                            stripe_type: StripeType::Block,
502                            index: block_index(block_row * BLOCK_H, block_col * BLOCK_W),
503                        };
504                        self.stripe_to_dependents
505                            .entry(key.clone())
506                            .or_default()
507                            .insert(dependent);
508                        #[cfg(test)]
509                        {
510                            if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1)
511                                && let Ok(mut g) = self.instr.lock()
512                            {
513                                g.stripe_inserts += 1;
514                            }
515                        }
516                    }
517                }
518            } else if height > width {
519                for col in start_col..=end_col {
520                    let key = StripeKey {
521                        sheet_id,
522                        stripe_type: StripeType::Column,
523                        index: col,
524                    };
525                    self.stripe_to_dependents
526                        .entry(key.clone())
527                        .or_default()
528                        .insert(dependent);
529                    #[cfg(test)]
530                    {
531                        if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1)
532                            && let Ok(mut g) = self.instr.lock()
533                        {
534                            g.stripe_inserts += 1;
535                        }
536                    }
537                }
538            } else {
539                for row in start_row..=end_row {
540                    let key = StripeKey {
541                        sheet_id,
542                        stripe_type: StripeType::Row,
543                        index: row,
544                    };
545                    self.stripe_to_dependents
546                        .entry(key.clone())
547                        .or_default()
548                        .insert(dependent);
549                    #[cfg(test)]
550                    {
551                        if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1)
552                            && let Ok(mut g) = self.instr.lock()
553                        {
554                            g.stripe_inserts += 1;
555                        }
556                    }
557                }
558            }
559        }
560    }
561
562    /// Fast-path: add range dependencies using compact RangeKey.
563    pub fn add_range_deps_from_keys(
564        &mut self,
565        dependent: VertexId,
566        keys: &[crate::engine::plan::RangeKey],
567        current_sheet_id: SheetId,
568    ) {
569        use crate::engine::plan::RangeKey as RK;
570        if keys.is_empty() {
571            return;
572        }
573
574        let mut shared_ranges: Vec<SharedRangeRef<'static>> = Vec::with_capacity(keys.len());
575        for k in keys {
576            let sheet_loc = SharedSheetLocator::Id(match k {
577                RK::Rect { sheet, .. }
578                | RK::WholeRow { sheet, .. }
579                | RK::WholeCol { sheet, .. }
580                | RK::OpenRect { sheet, .. } => *sheet,
581            });
582
583            let mk_axis = |idx0: u32| formualizer_common::AxisBound::new(idx0, false);
584
585            let built = match k {
586                RK::Rect { start, end, .. } => {
587                    let sr = mk_axis(start.row());
588                    let sc = mk_axis(start.col());
589                    let er = mk_axis(end.row());
590                    let ec = mk_axis(end.col());
591                    SharedRangeRef::from_parts(sheet_loc, Some(sr), Some(sc), Some(er), Some(ec))
592                        .ok()
593                }
594                RK::WholeRow { row, .. } => {
595                    let r0 = row.saturating_sub(1);
596                    let b = mk_axis(r0);
597                    SharedRangeRef::from_parts(sheet_loc, Some(b), None, Some(b), None).ok()
598                }
599                RK::WholeCol { col, .. } => {
600                    let c0 = col.saturating_sub(1);
601                    let b = mk_axis(c0);
602                    SharedRangeRef::from_parts(sheet_loc, None, Some(b), None, Some(b)).ok()
603                }
604                RK::OpenRect { start, end, .. } => {
605                    let (sr, sc) = match start {
606                        Some(p) => (Some(mk_axis(p.row())), Some(mk_axis(p.col()))),
607                        None => (None, None),
608                    };
609                    let (er, ec) = match end {
610                        Some(p) => (Some(mk_axis(p.row())), Some(mk_axis(p.col()))),
611                        None => (None, None),
612                    };
613                    SharedRangeRef::from_parts(sheet_loc, sr, sc, er, ec).ok()
614                }
615            };
616
617            if let Some(r) = built {
618                shared_ranges.push(r.into_owned());
619            }
620        }
621
622        if shared_ranges.is_empty() {
623            return;
624        }
625
626        self.formula_to_range_deps
627            .insert(dependent, shared_ranges.clone());
628
629        for range in &shared_ranges {
630            let sheet_id = match range.sheet {
631                SharedSheetLocator::Id(id) => id,
632                _ => current_sheet_id,
633            };
634
635            let s_row = range.start_row.map(|b| b.index);
636            let e_row = range.end_row.map(|b| b.index);
637            let s_col = range.start_col.map(|b| b.index);
638            let e_col = range.end_col.map(|b| b.index);
639
640            // #120: see add_range_dependent_edges — compressed range covering
641            // the formula's own cell records a self-loop for SCC detection.
642            if self.range_region_contains_self(dependent, sheet_id, s_row, e_row, s_col, e_col)
643                && self.compressed_range_self_use(dependent, sheet_id, (s_row, e_row, s_col, e_col))
644                    != RangeSelfUse::Excluded
645            {
646                self.record_self_loop(dependent);
647            }
648
649            let col_stripes = (s_row.is_none() && e_row.is_none())
650                || (s_col.is_some() && e_col.is_some() && (s_row.is_none() || e_row.is_none()));
651            let row_stripes = (s_col.is_none() && e_col.is_none())
652                || (s_row.is_some() && e_row.is_some() && (s_col.is_none() || e_col.is_none()));
653
654            if col_stripes && !row_stripes {
655                let sc = s_col.unwrap_or(0);
656                let ec = e_col.unwrap_or(sc);
657                for col in sc..=ec {
658                    let key = StripeKey {
659                        sheet_id,
660                        stripe_type: StripeType::Column,
661                        index: col,
662                    };
663                    self.stripe_to_dependents
664                        .entry(key)
665                        .or_default()
666                        .insert(dependent);
667                }
668                continue;
669            }
670
671            if row_stripes && !col_stripes {
672                let sr = s_row.unwrap_or(0);
673                let er = e_row.unwrap_or(sr);
674                for row in sr..=er {
675                    let key = StripeKey {
676                        sheet_id,
677                        stripe_type: StripeType::Row,
678                        index: row,
679                    };
680                    self.stripe_to_dependents
681                        .entry(key)
682                        .or_default()
683                        .insert(dependent);
684                }
685                continue;
686            }
687
688            let start_row = s_row.unwrap_or(0);
689            let start_col = s_col.unwrap_or(0);
690            let end_row = e_row.unwrap_or(start_row);
691            let end_col = e_col.unwrap_or(start_col);
692
693            let height = end_row.saturating_sub(start_row) + 1;
694            let width = end_col.saturating_sub(start_col) + 1;
695
696            if self.config.enable_block_stripes && height > 1 && width > 1 {
697                let start_block_row = start_row / BLOCK_H;
698                let end_block_row = end_row / BLOCK_H;
699                let start_block_col = start_col / BLOCK_W;
700                let end_block_col = end_col / BLOCK_W;
701
702                for block_row in start_block_row..=end_block_row {
703                    for block_col in start_block_col..=end_block_col {
704                        let key = StripeKey {
705                            sheet_id,
706                            stripe_type: StripeType::Block,
707                            index: block_index(block_row * BLOCK_H, block_col * BLOCK_W),
708                        };
709                        self.stripe_to_dependents
710                            .entry(key)
711                            .or_default()
712                            .insert(dependent);
713                    }
714                }
715            } else if height > width {
716                for col in start_col..=end_col {
717                    let key = StripeKey {
718                        sheet_id,
719                        stripe_type: StripeType::Column,
720                        index: col,
721                    };
722                    self.stripe_to_dependents
723                        .entry(key)
724                        .or_default()
725                        .insert(dependent);
726                }
727            } else {
728                for row in start_row..=end_row {
729                    let key = StripeKey {
730                        sheet_id,
731                        stripe_type: StripeType::Row,
732                        index: row,
733                    };
734                    self.stripe_to_dependents
735                        .entry(key)
736                        .or_default()
737                        .insert(dependent);
738                }
739            }
740        }
741    }
742}