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