Skip to main content

formualizer_eval/engine/
target_preparation.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::time::{Duration, Instant};
3
4use formualizer_common::RangeAddress;
5
6use super::{EvaluationBudgets, VertexId};
7use crate::formula_plane::region_index::Region;
8use crate::formula_plane::runtime::FormulaSpanRef;
9use crate::reference::CellRef;
10
11pub type RequestId = u64;
12
13#[cfg(test)]
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub(crate) enum TargetPreparationFault {
16    #[default]
17    None,
18    AfterDiscovery,
19    FinalRevisionValidation,
20    FinalGraphValidation,
21    Admission,
22    Reservation,
23    BeforeFirstMutation,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Hash)]
27pub enum EvaluationTarget {
28    Cell {
29        sheet: String,
30        row: u32,
31        col: u32,
32    },
33    Range(RangeAddress),
34    Name {
35        name: String,
36        scope_sheet: Option<String>,
37    },
38    Table {
39        name: String,
40        selection: TableSelection,
41    },
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub(crate) enum TargetProducer {
46    Legacy(VertexId),
47    Span {
48        span_ref: FormulaSpanRef,
49        demanded: Region,
50    },
51    Symbol(VertexId),
52    ValueOnly(CellRef),
53}
54
55#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
56pub enum TableSelection {
57    #[default]
58    Whole,
59    Headers,
60    Data,
61    Totals,
62    Column(String),
63    Columns {
64        start: String,
65        end: String,
66    },
67}
68
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
70pub enum OpaquePreparePolicy {
71    #[default]
72    Widen,
73    Error,
74}
75
76#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub enum PrepareScope {
79    #[default]
80    Exact,
81    Sheets(Vec<String>),
82    Workbook,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
86#[non_exhaustive]
87pub enum OpaqueReason {
88    DynamicReference,
89    RuntimeTextReference,
90    UnknownFunction,
91    UnknownCustomFunction,
92    UnresolvedCrossSheetBinding,
93    UnresolvedName,
94    UnresolvedTable,
95    FormulaName,
96    DeferredSourcePackage,
97    UnsupportedSourceSemantics,
98    UncertainDefaultSheetBinding,
99}
100
101/// Controls that apply to a whole target evaluation: preparation *and*
102/// evaluation. Cancellation and the deadline are hoisted onto the engine for the
103/// duration of the call, so every checkpoint in both phases observes them.
104#[derive(Clone, Debug)]
105pub struct TargetEvalOptions<'a> {
106    pub request_id: Option<RequestId>,
107    pub cancel: Option<crate::engine::CancelToken>,
108    pub deadline: Option<Instant>,
109    pub budgets: Option<&'a EvaluationBudgets>,
110    pub opaque_policy: OpaquePreparePolicy,
111}
112
113impl Default for TargetEvalOptions<'_> {
114    fn default() -> Self {
115        Self {
116            request_id: None,
117            cancel: None,
118            deadline: None,
119            budgets: None,
120            opaque_policy: OpaquePreparePolicy::Widen,
121        }
122    }
123}
124
125#[derive(Clone, Debug, Default, PartialEq, Eq)]
126#[non_exhaustive]
127pub struct PreparationRevision {
128    pub graph: u64,
129    /// Raw FormulaPlane epoch. Kept separate from each authority-index counter so
130    /// unrelated component revisions cannot collide through arithmetic folding.
131    pub authority: u64,
132    pub authority_indexes: u64,
133    pub authority_indexed_plane: u64,
134    pub staged: u64,
135    pub symbols: u64,
136    pub semantic: u64,
137    pub provider: Option<u64>,
138}
139
140#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
141#[non_exhaustive]
142pub enum PreparationOutcome {
143    #[default]
144    Prepared,
145    CompatibilityPrepared,
146}
147
148#[derive(Clone, Debug, Default, PartialEq, Eq)]
149#[non_exhaustive]
150pub struct PreparedTargetGraphReport {
151    pub request_id: RequestId,
152    pub requested_targets: usize,
153    pub normalized_regions: usize,
154    pub normalized_target_list: Vec<EvaluationTarget>,
155    pub selected_staged_cells: usize,
156    /// Total family proposals owned by deferred packages selected by this request.
157    /// Family-bearing packages remain package-atomic; indexed ordinary-only
158    /// packages can be selected and consumed by coordinate.
159    pub selected_source_families: usize,
160    pub retained_staged_cells: usize,
161    pub selected_cells: Vec<RangeAddress>,
162    pub retained_cells: Vec<RangeAddress>,
163    pub widened_scope: PrepareScope,
164    pub widening_reasons: Vec<OpaqueReason>,
165    pub revisions: PreparationRevision,
166    pub commit_window: Duration,
167    pub estimated_scratch_bytes: u64,
168    pub observed_scratch_bytes: u64,
169    pub estimated_commit_work: u64,
170    pub actual_commit_work: u64,
171    pub outcome: PreparationOutcome,
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175pub(crate) struct StagedFormulaLease {
176    pub(crate) row: u32,
177    pub(crate) col: u32,
178    pub(crate) generation: u64,
179    pub(crate) insertion_order: u64,
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183struct StagedFormulaPresence {
184    generation: u64,
185    insertion_order: u64,
186}
187
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189pub(crate) struct StagedPackageLease {
190    pub(crate) generation: u64,
191    pub(crate) family_count: usize,
192}
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195struct StagedPackageRect {
196    start_row: u32,
197    start_col: u32,
198    end_row: u32,
199    end_col: u32,
200}
201
202impl StagedPackageRect {
203    fn intersects(self, start_row: u32, start_col: u32, end_row: u32, end_col: u32) -> bool {
204        self.start_row <= end_row
205            && start_row <= self.end_row
206            && self.start_col <= end_col
207            && start_col <= self.end_col
208    }
209}
210
211#[derive(Clone, Debug)]
212struct StagedPackagePresence {
213    generation: u64,
214    family_count: usize,
215    geometry: Vec<StagedPackageRect>,
216    // Exact formula-bearing geometry, unlike discovery's declared partition bounds.
217    occupancy_geometry: Vec<StagedPackageRect>,
218    fallback_points: BTreeSet<(u32, u32)>,
219    geometry_complete: bool,
220}
221
222#[derive(Clone, Debug, Default)]
223pub(crate) struct StagedFormulaIndex {
224    revision: u64,
225    next_generation: u64,
226    next_insertion_order: u64,
227    sheets: BTreeMap<String, BTreeMap<(u32, u32), StagedFormulaPresence>>,
228    packages: BTreeMap<String, StagedPackagePresence>,
229}
230
231impl StagedFormulaIndex {
232    fn bump(&mut self) {
233        self.revision = self
234            .revision
235            .checked_add(1)
236            .expect("staged formula index revision exhausted");
237    }
238
239    pub(crate) fn revision(&self) -> u64 {
240        self.revision
241    }
242
243    pub(crate) fn stage(&mut self, sheet: &str, row: u32, col: u32) {
244        let generation = self.next_generation;
245        self.next_generation = self
246            .next_generation
247            .checked_add(1)
248            .expect("staged formula generation exhausted");
249        let entries = self.sheets.entry(sheet.to_string()).or_default();
250        let insertion_order = entries.get(&(row, col)).map_or_else(
251            || {
252                let order = self.next_insertion_order;
253                self.next_insertion_order = self
254                    .next_insertion_order
255                    .checked_add(1)
256                    .expect("staged formula insertion order exhausted");
257                order
258            },
259            |entry| entry.insertion_order,
260        );
261        entries.insert(
262            (row, col),
263            StagedFormulaPresence {
264                generation,
265                insertion_order,
266            },
267        );
268        self.bump();
269    }
270
271    pub(crate) fn remove(&mut self, sheet: &str, row: u32, col: u32) -> bool {
272        let removed = self
273            .sheets
274            .get_mut(sheet)
275            .is_some_and(|entries| entries.remove(&(row, col)).is_some());
276        if self.sheets.get(sheet).is_some_and(BTreeMap::is_empty) {
277            self.sheets.remove(sheet);
278        }
279        if removed {
280            self.bump();
281        }
282        removed
283    }
284
285    pub(crate) fn clear_sheet(&mut self, sheet: &str) {
286        let changed = self.sheets.remove(sheet).is_some() | self.packages.remove(sheet).is_some();
287        if changed {
288            self.bump();
289        }
290    }
291
292    pub(crate) fn clear_all(&mut self) {
293        if !self.sheets.is_empty() || !self.packages.is_empty() {
294            self.sheets.clear();
295            self.packages.clear();
296            self.bump();
297        }
298    }
299
300    pub(crate) fn set_package(
301        &mut self,
302        sheet: &str,
303        package: Option<&super::DeferredFormulaPackage>,
304    ) {
305        let changed = if let Some(package) = package {
306            let generation = self.next_generation;
307            self.next_generation = self
308                .next_generation
309                .checked_add(1)
310                .expect("staged package generation exhausted");
311            let mut geometry = Vec::new();
312            for family in &package.families {
313                match &family.members {
314                    super::SourceFamilyMembers::CompleteDomain(domain) => {
315                        let rect = domain.rect();
316                        geometry.push(StagedPackageRect {
317                            start_row: rect.start.row.saturating_add(1),
318                            start_col: rect.start.col.saturating_add(1),
319                            end_row: rect.end.row.saturating_add(1),
320                            end_col: rect.end.col.saturating_add(1),
321                        });
322                    }
323                    super::SourceFamilyMembers::ExplicitMembers(members) => {
324                        geometry.extend(members.as_slice().iter().map(|coord| StagedPackageRect {
325                            start_row: coord.row.saturating_add(1),
326                            start_col: coord.col.saturating_add(1),
327                            end_row: coord.row.saturating_add(1),
328                            end_col: coord.col.saturating_add(1),
329                        }));
330                    }
331                }
332            }
333            let mut occupancy_geometry = geometry.clone();
334            for family in &package.partitioned_families {
335                occupancy_geometry.extend(family.fragments.iter().map(|fragment| {
336                    let rect = fragment.rect();
337                    StagedPackageRect {
338                        start_row: rect.start.row + 1,
339                        start_col: rect.start.col + 1,
340                        end_row: rect.end.row + 1,
341                        end_col: rect.end.col + 1,
342                    }
343                }));
344                // Both shared members and ordinary exceptions are formulas; holes
345                // have no legacy member and must never acquire occupancy.
346                occupancy_geometry.extend(family.legacy_members.as_slice().iter().map(|member| {
347                    StagedPackageRect {
348                        start_row: member.coord.row + 1,
349                        start_col: member.coord.col + 1,
350                        end_row: member.coord.row + 1,
351                        end_col: member.coord.col + 1,
352                    }
353                }));
354            }
355            geometry.extend(
356                package
357                    .partitioned_families
358                    .iter()
359                    .map(|family| StagedPackageRect {
360                        start_row: family.declared.start.row.saturating_add(1),
361                        start_col: family.declared.start.col.saturating_add(1),
362                        end_row: family.declared.end.row.saturating_add(1),
363                        end_col: family.declared.end.col.saturating_add(1),
364                    }),
365            );
366            let fallback_points = package
367                .source_coordinates
368                .iter()
369                .map(|coord| (coord.row.saturating_add(1), coord.col.saturating_add(1)))
370                .filter(|point| !package.source_accounted || !package.suppressed.contains(point))
371                .collect();
372            let geometry_complete = package.source_geometry_complete
373                || package.report.source_formula_records_spooled == 0;
374            self.packages.insert(
375                sheet.to_string(),
376                StagedPackagePresence {
377                    generation,
378                    family_count: package.families.len() + package.partitioned_families.len(),
379                    geometry,
380                    occupancy_geometry,
381                    fallback_points,
382                    geometry_complete,
383                },
384            );
385            true
386        } else {
387            self.packages.remove(sheet).is_some()
388        };
389        if changed {
390            self.bump();
391        }
392    }
393
394    /// Test occupancy without acquiring a source lease, parsing text, or opening
395    /// the replay spool. Coordinates are Excel (one-based). Current ordinary
396    /// entries take precedence over source suppression tombstones.
397    pub(crate) fn occupies_spill(
398        &self,
399        sheet: &str,
400        anchor: (u32, u32),
401        end: (u32, u32),
402        suppressed: impl Fn((u32, u32)) -> bool,
403    ) -> bool {
404        if self.sheets.get(sheet).is_some_and(|entries| {
405            entries
406                .range((anchor.0, 0)..=(end.0, u32::MAX))
407                .any(|(&point, _)| point != anchor && point.1 >= anchor.1 && point.1 <= end.1)
408        }) {
409            return true;
410        }
411        self.package_occupies_spill(sheet, anchor, end, suppressed)
412    }
413
414    pub(crate) fn package_occupies_spill(
415        &self,
416        sheet: &str,
417        anchor: (u32, u32),
418        end: (u32, u32),
419        suppressed: impl Fn((u32, u32)) -> bool,
420    ) -> bool {
421        let Some(package) = self.packages.get(sheet) else {
422            return false;
423        };
424        if package
425            .fallback_points
426            .range((anchor.0, 0)..=(end.0, u32::MAX))
427            .any(|&point| {
428                point != anchor && point.1 >= anchor.1 && point.1 <= end.1 && !suppressed(point)
429            })
430        {
431            return true;
432        }
433        package.occupancy_geometry.iter().any(|rect| {
434            if !rect.intersects(anchor.0, anchor.1, end.0, end.1) {
435                return false;
436            }
437            // Only suppressed coordinates (plus the anchor) can be skipped:
438            // a large unsuppressed domain returns on its first candidate.
439            for row in rect.start_row.max(anchor.0)..=rect.end_row.min(end.0) {
440                for col in rect.start_col.max(anchor.1)..=rect.end_col.min(end.1) {
441                    let point = (row, col);
442                    if point != anchor && !suppressed(point) {
443                        return true;
444                    }
445                }
446            }
447            false
448        })
449    }
450
451    pub(crate) fn package_points_in_region(
452        &self,
453        sheet: &str,
454        start_row: u32,
455        start_col: u32,
456        end_row: u32,
457        end_col: u32,
458    ) -> Vec<(u32, u32)> {
459        self.packages
460            .get(sheet)
461            .into_iter()
462            .flat_map(|package| {
463                package
464                    .fallback_points
465                    .range((start_row, 0)..=(end_row, u32::MAX))
466            })
467            .filter(|&&(_, col)| col >= start_col && col <= end_col)
468            .copied()
469            .collect()
470    }
471
472    pub(crate) fn consume_package_points(&mut self, sheet: &str, points: &BTreeSet<(u32, u32)>) {
473        if let Some(package) = self.packages.get_mut(sheet) {
474            for point in points {
475                package.fallback_points.remove(point);
476            }
477        }
478        self.touch_package(sheet);
479    }
480
481    pub(crate) fn update_package_family_count(&mut self, sheet: &str, count: usize) {
482        if let Some(package) = self.packages.get_mut(sheet) {
483            package.family_count = count;
484        }
485    }
486
487    pub(crate) fn touch_package(&mut self, sheet: &str) {
488        if let Some(package) = self.packages.get_mut(sheet) {
489            package.generation = self.next_generation;
490            self.next_generation = self
491                .next_generation
492                .checked_add(1)
493                .expect("staged package generation exhausted");
494            self.bump();
495        }
496    }
497
498    pub(crate) fn has_packages(&self) -> bool {
499        !self.packages.is_empty()
500    }
501
502    pub(crate) fn package_sheets(&self) -> impl Iterator<Item = &str> {
503        self.packages.keys().map(String::as_str)
504    }
505
506    pub(crate) fn package_for_region(
507        &self,
508        sheet: &str,
509        start_row: u32,
510        start_col: u32,
511        end_row: u32,
512        end_col: u32,
513    ) -> Option<Result<StagedPackageLease, ()>> {
514        let package = self.packages.get(sheet)?;
515        let intersects = package
516            .geometry
517            .iter()
518            .copied()
519            .any(|rect| rect.intersects(start_row, start_col, end_row, end_col))
520            || package
521                .fallback_points
522                .range((start_row, 0)..=(end_row, u32::MAX))
523                .any(|&(row, col)| col >= start_col && col <= end_col);
524        if intersects {
525            Some(Ok(StagedPackageLease {
526                generation: package.generation,
527                family_count: package.family_count,
528            }))
529        } else if package.geometry_complete {
530            None
531        } else {
532            Some(Err(()))
533        }
534    }
535
536    pub(crate) fn package_lease_for_sheet(&self, sheet: &str) -> Option<StagedPackageLease> {
537        self.packages.get(sheet).map(|package| StagedPackageLease {
538            generation: package.generation,
539            family_count: package.family_count,
540        })
541    }
542
543    pub(crate) fn package_lease_matches(&self, sheet: &str, lease: StagedPackageLease) -> bool {
544        self.packages.get(sheet).is_some_and(|package| {
545            package.generation == lease.generation && package.family_count == lease.family_count
546        })
547    }
548
549    pub(crate) fn leases_in_region(
550        &self,
551        sheet: &str,
552        start_row: u32,
553        start_col: u32,
554        end_row: u32,
555        end_col: u32,
556    ) -> Vec<StagedFormulaLease> {
557        let mut leases = self
558            .sheets
559            .get(sheet)
560            .into_iter()
561            .flat_map(|entries| entries.range((start_row, 0)..=(end_row, u32::MAX)))
562            .filter_map(|(&(row, col), entry)| {
563                (col >= start_col && col <= end_col).then_some(StagedFormulaLease {
564                    row,
565                    col,
566                    generation: entry.generation,
567                    insertion_order: entry.insertion_order,
568                })
569            })
570            .collect::<Vec<_>>();
571        leases.sort_by_key(|lease| lease.insertion_order);
572        leases
573    }
574
575    pub(crate) fn leases_for_sheet(&self, sheet: &str) -> Vec<StagedFormulaLease> {
576        self.leases_in_region(sheet, 1, 1, u32::MAX, u32::MAX)
577    }
578
579    pub(crate) fn all_leases(&self) -> Vec<(String, StagedFormulaLease)> {
580        let mut leases = self
581            .sheets
582            .iter()
583            .flat_map(|(sheet, entries)| {
584                entries.iter().map(move |(&(row, col), entry)| {
585                    (
586                        sheet.clone(),
587                        StagedFormulaLease {
588                            row,
589                            col,
590                            generation: entry.generation,
591                            insertion_order: entry.insertion_order,
592                        },
593                    )
594                })
595            })
596            .collect::<Vec<_>>();
597        leases.sort_by_key(|(_, lease)| lease.insertion_order);
598        leases
599    }
600
601    pub(crate) fn lease_matches(&self, sheet: &str, lease: StagedFormulaLease) -> bool {
602        self.sheets
603            .get(sheet)
604            .and_then(|entries| entries.get(&(lease.row, lease.col)))
605            .is_some_and(|entry| {
606                entry.generation == lease.generation
607                    && entry.insertion_order == lease.insertion_order
608            })
609    }
610
611    pub(crate) fn ordinary_count(&self) -> usize {
612        self.sheets.values().map(BTreeMap::len).sum()
613    }
614
615    #[cfg(test)]
616    pub(crate) fn package_count(&self) -> usize {
617        self.packages.len()
618    }
619}
620
621/// Former name of [`TargetEvalOptions`]. The struct governs the whole call, not
622/// only preparation, so it was renamed before the name was frozen by a release.
623#[deprecated(since = "0.8.0", note = "renamed to TargetEvalOptions")]
624pub type PrepareTargetsOptions<'a> = TargetEvalOptions<'a>;