Skip to main content

formualizer_eval/engine/
recalc_job.rs

1//! The single held-schedule driver for explicit jobs and evaluate_all_until.
2use super::*;
3use crate::engine::resumable::*;
4
5pub(in crate::engine) struct HeldRecalcSchedule {
6    full: Walk,
7    priority: Option<Walk>,
8    candidates: Vec<VertexId>,
9    old_vdeps: FxHashMap<VertexId, Vec<VertexId>>,
10    aux_retained_bytes: u64,
11}
12struct Walk {
13    schedule: EvaluationSchedule,
14    unit: usize,
15    /// Vertex offset within the current layer, always at an atomic run boundary.
16    layer_offset: usize,
17    completed: usize,
18    total: usize,
19    retained_bytes: u64,
20}
21impl Walk {
22    fn new(schedule: EvaluationSchedule) -> Self {
23        let total = schedule
24            .layers
25            .iter()
26            .map(|l| l.vertices.len())
27            .sum::<usize>()
28            + schedule.cycles.iter().map(Vec::len).sum::<usize>();
29        let retained_bytes = match &schedule {
30            EvaluationSchedule::Shared(_) => 0,
31            EvaluationSchedule::Owned(s) => {
32                (s.units.capacity() * std::mem::size_of::<ScheduleUnit>()
33                    + s.layers.capacity() * std::mem::size_of::<crate::engine::scheduler::Layer>()
34                    + s.cycles.capacity() * std::mem::size_of::<Vec<VertexId>>()
35                    + s.cycles
36                        .iter()
37                        .map(|c| c.capacity() * std::mem::size_of::<VertexId>())
38                        .sum::<usize>()
39                    + s.layers
40                        .iter()
41                        .map(|l| {
42                            l.vertices.capacity() * std::mem::size_of::<VertexId>()
43                                + l.runs.capacity()
44                                    * std::mem::size_of::<crate::engine::scheduler::LayerRun>()
45                        })
46                        .sum::<usize>()) as u64
47            }
48        };
49        Self {
50            schedule,
51            unit: 0,
52            layer_offset: 0,
53            completed: 0,
54            total,
55            retained_bytes,
56        }
57    }
58    fn done(&self) -> bool {
59        self.unit == self.schedule.units.len()
60    }
61}
62impl<R: EvaluationContext> Engine<R> {
63    /// Start an explicit job in O(1). Planning and clock sampling happen in
64    /// the first step. Returns AlreadyPending for either kind of continuation.
65    pub fn begin_recalc(&mut self, options: RecalcJobOptions) -> Result<(), RecalcJobError> {
66        if self.is_poisoned() {
67            return Err(RecalcJobError::Poisoned);
68        }
69        if self.has_active_recalc_job() || self.resumable.is_some() {
70            return Err(RecalcJobError::AlreadyPending {
71                explicit: self.has_active_recalc_job(),
72            });
73        }
74        self.recalc_options = Some(options);
75        Ok(())
76    }
77    pub(super) fn active_recalc_job_error() -> ExcelError {
78        ExcelError::new(ExcelErrorKind::Value)
79            .with_message(
80                "recalculation job is active; use step_recalc, prioritize_recalc, or cancel_recalc",
81            )
82            .with_extra(formualizer_common::ExcelErrorExtra::RecalcJobActive)
83    }
84    /// True only for an explicit job. Implicit Phase 1 continuations are
85    /// reported by has_pending_resumable_evaluation.
86    pub fn has_active_recalc_job(&self) -> bool {
87        self.recalc_options.is_some()
88    }
89
90    /// Last request wins; materialization is deferred until the next step.
91    pub fn prioritize_recalc(
92        &mut self,
93        targets: &[crate::engine::EvaluationTarget],
94    ) -> Result<(), RecalcJobError> {
95        if self.is_poisoned() {
96            return Err(RecalcJobError::Poisoned);
97        }
98        let options = self
99            .recalc_options
100            .as_mut()
101            .ok_or(RecalcJobError::NoActiveJob)?;
102        options.initial_priority = targets.to_vec();
103        if let Some(held) = self.resumable.as_mut().and_then(|s| s.held.as_mut()) {
104            held.priority = None;
105        }
106        Ok(())
107    }
108
109    /// Preserve installed values and dirtiness; cancellation never advances the epoch.
110    /// Returns None without touching state when the engine is poisoned.
111    pub fn cancel_recalc(&mut self) -> Option<StepReport> {
112        if self.is_poisoned() {
113            return None;
114        }
115        self.recalc_options.take()?;
116        let progress = self.recalc_progress();
117        self.resumable = None;
118        self.redirty_for_next_recalc();
119        self.freshness_begin_request();
120        Some(StepReport {
121            phase: StepPhase::Cancelled,
122            progress,
123            step_computed_vertices: 0,
124            step_elapsed: std::time::Duration::ZERO,
125            changed: Default::default(),
126            longest_unit: None,
127            result: None,
128        })
129    }
130    /// Execute the next soft-budgeted step. Only installed units become clean.
131    ///
132    /// A returned evaluation error ends the job and leaves unfinished work dirty.
133    /// Function panics become typed cell errors and operation diagnostics.
134    /// Engine-internal unwinds propagate and permanently poison the engine (see
135    /// [the module documentation](crate::engine::resumable)).
136    pub fn step_recalc(&mut self, stop: EvaluationStop) -> Result<StepReport, RecalcJobError> {
137        if self.is_poisoned() {
138            return Err(RecalcJobError::Poisoned);
139        }
140        if !self.has_active_recalc_job() {
141            return Err(RecalcJobError::NoActiveJob);
142        }
143        let result = self.drive_recalc_step(stop);
144        if result.as_ref().map_or(true, |r| {
145            matches!(r.phase, StepPhase::Finished | StepPhase::Cancelled)
146        }) {
147            self.recalc_options = None;
148            if result
149                .as_ref()
150                .is_ok_and(|r| r.phase == StepPhase::Cancelled)
151            {
152                self.resumable = None;
153                self.redirty_for_next_recalc();
154                self.freshness_begin_request();
155            }
156        }
157        result.map_err(Into::into)
158    }
159
160    /// Drive an explicit job, invoking the callback after every successful step,
161    /// including the terminal report. The callback may read, edit, prioritize or
162    /// cancel the engine. Break pauses an active job; step errors end it.
163    ///
164    /// Finished/Cancelled take precedence over Break on a terminal step.
165    /// Do not begin a replacement job inside the callback. Callback panics
166    /// propagate without additional lifecycle cleanup: the job keeps the state
167    /// it reached before the panic, so an untouched non-terminal job stays
168    /// active while a finished or cancelled job has already ended.
169    /// Function panics become typed cell errors as described in
170    /// [resumable](crate::engine::resumable).
171    pub fn recalc_with(
172        &mut self,
173        options: RecalcJobOptions,
174        stop_per_step: EvaluationStop,
175        mut on_step: impl FnMut(&mut Self, &StepReport) -> std::ops::ControlFlow<()>,
176    ) -> Result<RecalcOutcome, RecalcJobError> {
177        if self.is_poisoned() {
178            return Err(RecalcJobError::Poisoned);
179        }
180        self.begin_recalc(options)?;
181        loop {
182            let report = self.step_recalc(stop_per_step.clone())?;
183            let flow = on_step(self, &report);
184            if let Some(result) = report.result {
185                return Ok(RecalcOutcome::Finished(result));
186            }
187            if report.phase == StepPhase::Cancelled || !self.has_active_recalc_job() {
188                return Ok(RecalcOutcome::Cancelled);
189            }
190            if flow.is_break() {
191                return Ok(RecalcOutcome::Paused);
192            }
193        }
194    }
195
196    pub(super) fn drive_recalc_step(
197        &mut self,
198        stop: EvaluationStop,
199    ) -> Result<StepReport, ExcelError> {
200        self.ensure_not_poisoned()?;
201        let _unwind_poison = crate::engine::poison::UnwindPoisonGuard(self.poisoned.clone());
202        self.resumable_call = true;
203        self.active_cancel_flag = stop.cancel.clone();
204        let result = self.observe_evaluation_resource_request(
205            EvaluationRequestKind::FullCancellable,
206            |engine| engine.run_recalc_step(stop),
207        );
208        self.active_cancel_flag = None;
209        self.resumable_call = false;
210        if result.is_err() {
211            self.resumable = None;
212            self.redirty_for_next_recalc();
213        }
214        result
215    }
216
217    fn recalc_progress(&self) -> StepProgress {
218        let Some(s) = &self.resumable else {
219            return StepProgress::default();
220        };
221        let held = s.held.as_ref();
222        StepProgress {
223            pass: s.pass.max(1),
224            completed_in_pass: s.pass_completed,
225            scheduled_in_pass: s.pass_scheduled,
226            cumulative_computed_vertices: s.computed_vertices,
227            total_exact: s.pass_exact
228                && !self.graph.has_spill_anchors()
229                && !self.has_staged_formulas(),
230            staged_formulas_remaining: self.staged_formula_count(),
231            priority_pending: s.priority_deferred
232                || held.is_some_and(|h| h.priority.as_ref().is_some_and(|p| !p.done()))
233                || self
234                    .recalc_options
235                    .as_ref()
236                    .is_some_and(|o| !o.initial_priority.is_empty()),
237        }
238    }
239
240    fn run_recalc_step(&mut self, stop: EvaluationStop) -> Result<StepReport, ExcelError> {
241        debug_assert!(!self.graph.deferred_dirty_active());
242        let start = crate::instant::FzInstant::now();
243        let stop_at = stop.soft_deadline.and_then(|d| start.checked_add(d));
244        let explicit = self.has_active_recalc_job();
245        let function_epoch = crate::function_registry::semantic_epoch();
246        let provider_revision = self.resolver.planning_semantic_revision();
247        self.observe_function_semantic_epoch()?;
248        self.lookup_index_cache.reset_counters();
249        let _source_cache = self.source_cache_session();
250        self.validate_deterministic_mode()?;
251        self.account_recalc_retained()?;
252        let initial = self.resumable.is_none();
253        if initial {
254            self.begin_evaluation_request();
255            let mut state = LogicalRecalculation::default();
256            let mut retained: FxHashMap<u64, Vec<VertexId>> = FxHashMap::default();
257            for (&vertex, &scc) in &self.retained_scc_members {
258                retained.entry(scc).or_default().push(vertex);
259            }
260            for mut key in retained.into_values() {
261                key.sort_unstable();
262                state
263                    .cycle_seeds
264                    .insert(key.clone(), self.capture_original_cycle_seeds(&key));
265                state.original_cycles.insert(key);
266            }
267            state.refresh_seed_retained_bytes();
268            self.resumable = Some(state);
269        }
270        let mutation_revision = self.inspection_mutation_revision();
271        let state = self.resumable.as_ref().unwrap();
272        let semantics_changed = !initial
273            && (state.function_epoch != function_epoch
274                || state.provider_revision != provider_revision);
275        let mutated = !initial
276            && (state.mutation_revision != mutation_revision
277                || state.source_mutation_generation != self.source_mutation_generation);
278        let invalidated = semantics_changed
279            || mutated
280            || (!initial
281                && (state.topology_epoch != self.topology_epoch
282                    || state.authority_revision != self.schedule_cache_authority_revision()));
283        if semantics_changed || mutated {
284            let vertices: Vec<_> = state
285                .installed
286                .iter()
287                .filter(|&v| {
288                    self.graph.is_live_formula_vertex(v)
289                        && (semantics_changed
290                            || self.graph.is_volatile(v)
291                            || self.graph.is_dynamic(v))
292                })
293                .collect();
294            self.graph.mark_dirty_many(&vertices);
295        }
296        if invalidated {
297            // Supported edits and semantic changes publish one of these keys.
298            // Stable steps must not scan the resolution snapshot's sheet names.
299            self.refresh_resolution_snapshots();
300            let state = self.resumable.as_mut().unwrap();
301            state.held = None;
302            if let Some(options) = &mut self.recalc_options
303                && options.initial_priority.is_empty()
304            {
305                options.initial_priority = state.priority_targets.clone();
306            }
307        }
308        let mut computed = 0;
309        let mut cycle_errors = 0;
310        let mut units = 0;
311        let mut phase = StepPhase::Evaluating;
312        let mut longest: Option<SlowUnitReport> = None;
313        let collect = self
314            .recalc_options
315            .as_ref()
316            .is_some_and(|o| o.collect_deltas);
317        let mut delta = DeltaCollector::new(if collect {
318            DeltaMode::Cells
319        } else {
320            DeltaMode::Off
321        });
322        let cancelled =
323            |stop: &EvaluationStop| stop.cancel.as_ref().is_some_and(|t| t.is_cancelled());
324        let expired = || stop_at.is_some_and(|t| crate::instant::FzInstant::now() >= t);
325        if explicit && cancelled(&stop) {
326            phase = StepPhase::Cancelled;
327        }
328        if phase != StepPhase::Cancelled
329            && self.config.defer_graph_building
330            && self.has_staged_formulas()
331        {
332            let t = crate::instant::FzInstant::now();
333            // Graph construction is one atomic unit in v1. A soft token
334            // cannot discard it mid-build; hard ledger checks stay armed.
335            let saved_cancel = self.active_cancel_flag.take();
336            let built = self.build_graph_all();
337            self.active_cancel_flag = saved_cancel;
338            built?;
339            // Parsing staged formulas introduces function names after admission.
340            // Refresh dispatch here without restarting the logical clock/collector.
341            self.refresh_resolution_snapshots();
342            Self::note_slow_unit(
343                &mut longest,
344                SlowUnitKind::GraphBuild,
345                &[],
346                t.elapsed(),
347                &self.graph,
348            );
349            self.resumable.as_mut().unwrap().held = None;
350            if explicit && cancelled(&stop) {
351                phase = StepPhase::Cancelled;
352            } else if explicit && expired() {
353                phase = StepPhase::BuildingGraph;
354            }
355        }
356        if phase != StepPhase::Cancelled {
357            self.require_unified_authority()?;
358        }
359        if phase == StepPhase::Evaluating {
360            loop {
361                if self.resumable.as_ref().unwrap().held.is_none() {
362                    let candidates: Vec<_> = self
363                        .graph
364                        .get_evaluation_vertices()
365                        .into_iter()
366                        .filter(|&v| {
367                            initial && self.resumable.as_ref().unwrap().pass == 0
368                                || self.graph.is_dirty(v)
369                        })
370                        .collect();
371                    if candidates.is_empty() {
372                        phase = StepPhase::Finished;
373                        break;
374                    }
375                    if cancelled(&stop) && self.resumable.as_ref().unwrap().original_cycles_recorded
376                    {
377                        phase = StepPhase::Cancelled;
378                        break;
379                    }
380                    // Pre-cancelled initial planning still captures original SCC
381                    // seeds before edits can dissolve and recreate them (R1).
382                    let saved = if cancelled(&stop) {
383                        self.active_cancel_flag.take()
384                    } else {
385                        None
386                    };
387                    let t = crate::instant::FzInstant::now();
388                    let planned = self.create_evaluation_schedule(&candidates);
389                    if saved.is_some() {
390                        self.active_cancel_flag = saved;
391                    }
392                    let (schedule, old_vdeps, _) = planned?;
393                    self.record_original_cycle_seeds(&schedule);
394                    // Conservative: function calls and array/reference expressions
395                    // may introduce a first spill even with no current anchor.
396                    let exact = self.recalc_total_exact(&candidates);
397                    Self::note_slow_unit(
398                        &mut longest,
399                        SlowUnitKind::ScheduleBuild,
400                        &[],
401                        t.elapsed(),
402                        &self.graph,
403                    );
404                    let authority_revision = self.schedule_cache_authority_revision();
405                    let aux_retained_bytes = (candidates.capacity()
406                        * std::mem::size_of::<VertexId>()
407                        + old_vdeps.capacity() * std::mem::size_of::<(VertexId, Vec<VertexId>)>()
408                        + old_vdeps
409                            .values()
410                            .map(|v| v.capacity() * std::mem::size_of::<VertexId>())
411                            .sum::<usize>()) as u64;
412                    let full = Walk::new(schedule);
413                    let s = self.resumable.as_mut().unwrap();
414                    s.pass += 1;
415                    s.pass_completed = 0;
416                    s.pass_scheduled = full.total;
417                    s.pass_exact = exact;
418                    s.authority_revision = authority_revision;
419                    s.held = Some(HeldRecalcSchedule {
420                        full,
421                        priority: None,
422                        candidates,
423                        old_vdeps,
424                        aux_retained_bytes,
425                    });
426                    self.account_recalc_retained()?;
427                    if cancelled(&stop) {
428                        phase = StepPhase::Cancelled;
429                        break;
430                    }
431                    if explicit && expired() {
432                        phase = StepPhase::Scheduling;
433                        break;
434                    }
435                }
436                if cancelled(&stop) {
437                    phase = StepPhase::Cancelled;
438                    break;
439                }
440                let mut held = self.resumable.as_mut().unwrap().held.take().unwrap();
441                if let Some(options) = self.recalc_options.as_mut()
442                    && !options.initial_priority.is_empty()
443                {
444                    let targets = std::mem::take(&mut options.initial_priority);
445                    self.resumable.as_mut().unwrap().priority_targets = targets.clone();
446                    let t = crate::instant::FzInstant::now();
447                    held.priority = self.build_recalc_priority(&held.full.schedule, &targets)?;
448                    self.resumable.as_mut().unwrap().priority_deferred = held.priority.is_none();
449                    Self::note_slow_unit(
450                        &mut longest,
451                        SlowUnitKind::ScheduleBuild,
452                        &[],
453                        t.elapsed(),
454                        &self.graph,
455                    );
456                    if explicit && expired() {
457                        self.resumable.as_mut().unwrap().held = Some(held);
458                        phase = StepPhase::Scheduling;
459                        break;
460                    }
461                }
462                self.begin_pass(&held.full.schedule);
463                let mut completed_cycles = Vec::new();
464                let mut barrier = false;
465                let mut stopped = false;
466                loop {
467                    let priority = held.priority.as_ref().is_some_and(|p| !p.done());
468                    let walk = if priority {
469                        held.priority.as_mut().unwrap()
470                    } else {
471                        &mut held.full
472                    };
473                    if walk.done() {
474                        break;
475                    }
476                    let index = walk.unit;
477                    match walk.schedule.units[index] {
478                        ScheduleUnit::Cycle(i) => {
479                            let vertices = walk.schedule.unit_cycle(i);
480                            let skip = vertices.iter().all(|v| {
481                                self.resumable.as_ref().unwrap().installed.contains(v)
482                                    && !self.graph.is_dirty(*v)
483                            });
484                            let t = crate::instant::FzInstant::now();
485                            if !skip {
486                                match self.handle_cycle_unit(
487                                    vertices,
488                                    collect.then_some(&mut delta),
489                                    None,
490                                    stop.cancel.as_ref().map(|t| &**t.as_flag()),
491                                ) {
492                                    Ok(n) => {
493                                        computed += vertices.len();
494                                        cycle_errors += usize::from(n > 0);
495                                        completed_cycles.extend_from_slice(vertices);
496                                    }
497                                    Err(e) if e.kind == ExcelErrorKind::Cancelled => {
498                                        self.restore_cancelled_recalc_cycle(vertices);
499                                        phase = StepPhase::Cancelled;
500                                        stopped = true;
501                                        break;
502                                    }
503                                    Err(e) => return Err(e),
504                                }
505                                Self::note_slow_unit(
506                                    &mut longest,
507                                    SlowUnitKind::Cycle,
508                                    vertices,
509                                    t.elapsed(),
510                                    &self.graph,
511                                );
512                                units += 1;
513                            }
514                            walk.completed += vertices.len();
515                            walk.unit += 1;
516                        }
517                        ScheduleUnit::Layer(i) => {
518                            let layer = walk.schedule.unit_layer(i);
519                            let lo = walk.layer_offset;
520                            let run_idx = layer.runs.partition_point(|r| (r.start as usize) < lo);
521                            let run = layer.runs.get(run_idx).filter(|r| r.start as usize == lo);
522                            let hi = run.map_or(lo + 1, |r| (r.start + r.len) as usize);
523                            let vertices = &layer.vertices[lo..hi];
524                            let skip = vertices.iter().all(|v| {
525                                self.resumable.as_ref().unwrap().installed.contains(v)
526                                    && !self.graph.is_dirty(*v)
527                            });
528                            if !skip {
529                                let part = layer.sub_layer(lo, hi);
530                                self.resource_checkpoint(part.vertices.len() as u64)?;
531                                self.resumable
532                                    .as_mut()
533                                    .unwrap()
534                                    .forget_dissolved_new_cycles(&part.vertices);
535                                let t = crate::instant::FzInstant::now();
536                                // Asking the ordinary evaluator for per-cell deltas
537                                // disables its scalar bulk commit. Keep that path
538                                // for scalar arithmetic families and conservatively
539                                // report the whole installed run instead.
540                                let run_delta = collect
541                                    && run.is_some()
542                                    && !self.graph.has_spill_anchors()
543                                    && (self.resumable.as_ref().unwrap().pass_exact
544                                        || self
545                                            .graph
546                                            .get_formula(vertices[0])
547                                            .is_some_and(|ast| Self::recalc_scalar_ast(&ast)));
548                                // Local run proof is independent of the bounded
549                                // whole-pass progress proof on heterogeneous books.
550                                match self.evaluate_layer_units_until(
551                                    &part,
552                                    (collect && !run_delta).then_some(&mut delta),
553                                    None,
554                                    None,
555                                    buffer_layer_writes(&part),
556                                    None,
557                                ) {
558                                    Ok(n) => computed += n,
559                                    Err(e) if e.kind == ExcelErrorKind::Cancelled => {
560                                        phase = StepPhase::Cancelled;
561                                        stopped = true;
562                                        break;
563                                    }
564                                    Err(e) => return Err(e),
565                                }
566                                if run_delta {
567                                    let first = self.graph.get_cell_ref(vertices[0]).unwrap();
568                                    let last =
569                                        self.graph.get_cell_ref(*vertices.last().unwrap()).unwrap();
570                                    delta.record_region(
571                                        first.sheet_id,
572                                        first.coord.row(),
573                                        first.coord.col(),
574                                        last.coord.row(),
575                                        last.coord.col(),
576                                    );
577                                    // A reference can expose an array-valued input.
578                                    // No old spill existed on admission to this
579                                    // optimization; cover any newly installed extent.
580                                    if self.graph.has_spill_anchors() {
581                                        for &vertex in vertices {
582                                            if let Some((a, b)) =
583                                                self.graph.spill_extent_for_anchor(vertex)
584                                            {
585                                                delta.record_region(
586                                                    a.sheet_id,
587                                                    a.coord.row(),
588                                                    a.coord.col(),
589                                                    b.coord.row(),
590                                                    b.coord.col(),
591                                                );
592                                            }
593                                        }
594                                    }
595                                }
596                                Self::note_slow_unit(
597                                    &mut longest,
598                                    if run.is_some() {
599                                        SlowUnitKind::FamilyRun
600                                    } else {
601                                        SlowUnitKind::SingleCell
602                                    },
603                                    vertices,
604                                    t.elapsed(),
605                                    &self.graph,
606                                );
607                                units += 1;
608                            }
609                            walk.completed += hi - lo;
610                            walk.layer_offset = hi;
611                            if hi == layer.vertices.len() {
612                                walk.layer_offset = 0;
613                                walk.unit += 1;
614                            }
615                        }
616                    }
617                    if self.stop_after_unit(&walk.schedule, index) {
618                        barrier = true;
619                        break;
620                    }
621                    if self.resumable_should_stop(units, stop_at) {
622                        stopped = true;
623                        break;
624                    }
625                }
626                let changed = self.changed_virtual_dep_vertices(&held.candidates, &held.old_vdeps);
627                self.resource_checkpoint(0)?;
628                let mut installed = std::mem::take(&mut self.resumable.as_mut().unwrap().installed);
629                let pending =
630                    self.freshness_finish_interrupted_pass(&completed_cycles, &mut installed);
631                self.resumable.as_mut().unwrap().installed = installed;
632                let (pending, fr5_replan) = pending?;
633                barrier |= fr5_replan;
634                for v in changed {
635                    self.graph.set_dirty(v, true);
636                }
637                let full_done = held.full.done();
638                self.resumable.as_mut().unwrap().pass_completed = held.full.completed;
639                self.resumable.as_mut().unwrap().held = Some(held);
640                if !pending && !self.graph.has_dirty_evaluation_vertices() {
641                    phase = StepPhase::Finished;
642                    break;
643                }
644                if phase == StepPhase::Cancelled {
645                    break;
646                }
647                if barrier || full_done {
648                    let state = self.resumable.as_mut().unwrap();
649                    if state.replans >= 5 {
650                        return Err(self.replan_exhausted_error(5, "dynamic dependency evaluation"));
651                    }
652                    state.replans += 1;
653                    state.held = None;
654                    if let Some(options) = &mut self.recalc_options {
655                        options.initial_priority = state.priority_targets.clone();
656                    }
657                }
658                if stopped {
659                    break;
660                }
661            }
662        }
663        self.resource_checkpoint(0)?;
664        let authority_revision = self.schedule_cache_authority_revision();
665        let mutation_revision = self.inspection_mutation_revision();
666        let s = self.resumable.as_mut().unwrap();
667        s.function_epoch = function_epoch;
668        s.provider_revision = provider_revision;
669        s.mutation_revision = mutation_revision;
670        s.source_mutation_generation = self.source_mutation_generation;
671        s.topology_epoch = self.topology_epoch;
672        if s.authority_revision != authority_revision && s.held.is_some() {
673            s.held = None;
674            if let Some(options) = &mut self.recalc_options
675                && options.initial_priority.is_empty()
676            {
677                options.initial_priority = s.priority_targets.clone();
678            }
679        }
680        s.authority_revision = authority_revision;
681        if phase == StepPhase::Finished {
682            s.priority_deferred = false;
683            s.pass_completed = s.pass_scheduled;
684        }
685        s.computed_vertices += computed;
686        s.cycle_errors += cycle_errors;
687        s.elapsed += start.elapsed();
688        let progress = self.recalc_progress();
689        self.account_recalc_retained()?;
690        let changed = Self::finish_step_delta(
691            delta,
692            self.recalc_options
693                .as_ref()
694                .map_or(1024, |o| o.max_delta_records),
695        );
696        let result = if phase == StepPhase::Finished {
697            let state = self.resumable.take().unwrap();
698            self.redirty_for_next_recalc();
699            self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
700            Some(EvalResult {
701                computed_vertices: state.computed_vertices,
702                cycle_errors: state.cycle_errors,
703                elapsed: state.elapsed,
704            })
705        } else {
706            None
707        };
708        Ok(StepReport {
709            phase,
710            progress,
711            step_computed_vertices: computed,
712            step_elapsed: start.elapsed(),
713            changed,
714            longest_unit: longest,
715            result,
716        })
717    }
718
719    fn restore_cancelled_recalc_cycle(&mut self, vertices: &[VertexId]) {
720        let mut key = vertices.to_vec();
721        key.sort_unstable();
722        let seeds = self
723            .resumable
724            .as_ref()
725            .and_then(|s| s.cycle_seeds.get(&key))
726            .cloned()
727            .unwrap_or_default();
728        for (vertex, value) in seeds {
729            self.graph.update_vertex_value_ref(vertex, &value);
730            self.mirror_vertex_value_to_overlay(vertex, &value);
731            self.graph.set_dirty(vertex, true);
732        }
733    }
734
735    fn account_recalc_retained(&mut self) -> Result<(), ExcelError> {
736        let bytes = self.resumable.as_ref().map_or(0, |s| {
737            let held = s.held.as_ref().map_or(0, |h| {
738                h.full.retained_bytes
739                    + h.priority.as_ref().map_or(0, |p| p.retained_bytes)
740                    + h.aux_retained_bytes
741            });
742            held + s.installed.heap_bytes() as u64
743                + s.new_cycle_members.heap_bytes() as u64
744                + s.seed_retained_bytes
745        });
746        if let Some(ledger) = self.active_resource_ledger.as_mut() {
747            ledger
748                .account_recalc_retained(bytes)
749                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
750        }
751        Ok(())
752    }
753
754    fn recalc_total_exact(&self, candidates: &[VertexId]) -> bool {
755        if self.graph.has_spill_anchors() {
756            return false;
757        }
758        let mut scalar_templates = FxHashSet::default();
759        for &vertex in candidates {
760            if self.graph.is_dynamic(vertex) {
761                return false;
762            }
763            let Some(view) = self.graph.formula_view(vertex) else {
764                return false;
765            };
766            if scalar_templates.contains(&view.template) {
767                continue;
768            }
769            // Exactness is optional, so bound proof work on heterogeneous
770            // workbooks rather than clone one AST per formula at admission.
771            if scalar_templates.len() >= 1024
772                || !self
773                    .graph
774                    .get_formula(vertex)
775                    .is_some_and(|ast| Self::recalc_scalar_ast(&ast))
776            {
777                return false;
778            }
779            scalar_templates.insert(view.template);
780        }
781        true
782    }
783
784    fn recalc_scalar_ast(ast: &ASTNode) -> bool {
785        match &ast.node_type {
786            ASTNodeType::Literal(v) => !matches!(v, LiteralValue::Array(_)),
787            ASTNodeType::Reference { reference, .. } => {
788                matches!(reference, ReferenceType::Cell { .. })
789            }
790            ASTNodeType::UnaryOp { expr, .. } => Self::recalc_scalar_ast(expr),
791            ASTNodeType::BinaryOp { left, right, .. } => {
792                Self::recalc_scalar_ast(left) && Self::recalc_scalar_ast(right)
793            }
794            _ => false,
795        }
796    }
797
798    fn note_slow_unit(
799        longest: &mut Option<SlowUnitReport>,
800        kind: SlowUnitKind,
801        vertices: &[VertexId],
802        elapsed: std::time::Duration,
803        graph: &DependencyGraph,
804    ) {
805        if longest.as_ref().is_none_or(|r| elapsed > r.elapsed) {
806            let representative_cell = vertices
807                .first()
808                .and_then(|v| graph.get_cell_ref(*v))
809                .and_then(|c| {
810                    formualizer_common::PackedSheetCell::try_new(
811                        c.sheet_id,
812                        c.coord.row(),
813                        c.coord.col(),
814                    )
815                });
816            *longest = Some(SlowUnitReport {
817                kind,
818                representative_cell,
819                vertex_count: vertices.len(),
820                elapsed,
821            });
822        }
823    }
824    fn finish_step_delta(delta: DeltaCollector, cap: usize) -> StepDelta {
825        let mut delta = delta.finish_target();
826        let mut overflow_sheets = Vec::new();
827        if delta.records.len() > cap {
828            overflow_sheets.extend(delta.records.iter().map(|r| r.sheet_id()));
829            overflow_sheets.sort_unstable();
830            overflow_sheets.dedup();
831            delta.records.clear();
832        }
833        StepDelta {
834            delta,
835            overflow_sheets,
836        }
837    }
838
839    fn build_recalc_priority(
840        &mut self,
841        full: &crate::engine::scheduler::Schedule,
842        targets: &[crate::engine::EvaluationTarget],
843    ) -> Result<Option<Walk>, ExcelError> {
844        use crate::engine::target_preparation::TargetProducer;
845        let roots = self.resolve_target_producers(targets)?;
846        let roots: Vec<_> = roots
847            .into_iter()
848            .filter_map(|r| match r {
849                TargetProducer::Legacy(v) | TargetProducer::Symbol(v) => Some(v),
850                TargetProducer::ValueOnly(_) => None,
851            })
852            .collect();
853        let (mut vertices, _) = self.demand_subgraph(&roots)?;
854        // Expand every touched family run and its precedent cone to a fixed
855        // point. Cap priority expansion at 65,536 formulas; exceeding it
856        // defers priority wholesale, never splits a run.
857        loop {
858            let mut selected = crate::engine::idset::DenseIdSet::default();
859            selected.extend(vertices.iter().copied());
860            let mut expanded = false;
861            for layer in &full.layers {
862                for run in &layer.runs {
863                    let members =
864                        &layer.vertices[run.start as usize..(run.start + run.len) as usize];
865                    if members.iter().any(|v| selected.contains(v)) {
866                        for &v in members {
867                            expanded |= selected.insert(v);
868                        }
869                    }
870                }
871            }
872            if selected.len() > 65_536 {
873                return Ok(None);
874            }
875            if !expanded {
876                return Ok(full
877                    .restrict(&selected)
878                    .map(|(s, _)| Walk::new(EvaluationSchedule::Owned(s))));
879            }
880            let seeds: Vec<_> = selected.iter().collect();
881            let (closure, _) = self.demand_subgraph(&seeds)?;
882            vertices = seeds;
883            vertices.extend(closure);
884        }
885    }
886
887    /// Zero-based inclusive viewport. Visits only coordinates inside it,
888    /// including staged formulas; no workbook-wide dirty-set scan.
889    pub fn has_pending_in_rect(&self, sheet: SheetId, r0: u32, c0: u32, r1: u32, c1: u32) -> bool {
890        if r0 > r1 || c0 > c1 {
891            return false;
892        }
893        for row in r0..=r1 {
894            for col in c0..=c1 {
895                if self.pending_at(sheet, row, col) {
896                    return true;
897                }
898            }
899        }
900        false
901    }
902    /// Pending formula cells in a zero-based inclusive viewport, including staged
903    /// formulas. Work is O(viewport area); reads may still return old values.
904    pub fn pending_cells_in_rect(
905        &self,
906        sheet: SheetId,
907        r0: u32,
908        c0: u32,
909        r1: u32,
910        c1: u32,
911    ) -> crate::engine::TargetEvalDelta {
912        let mut delta = DeltaCollector::new(DeltaMode::Cells);
913        if r0 <= r1 && c0 <= c1 {
914            for row in r0..=r1 {
915                for col in c0..=c1 {
916                    if self.pending_at(sheet, row, col) {
917                        delta.record_cell(sheet, row, col);
918                    }
919                }
920            }
921        }
922        delta.finish_target()
923    }
924    fn pending_at(&self, sheet: SheetId, row: u32, col: u32) -> bool {
925        let cell = self
926            .graph
927            .make_cell_ref(self.graph.sheet_name(sheet), row + 1, col + 1);
928        self.graph
929            .get_vertex_id_for_address(&cell)
930            .is_some_and(|v| self.graph.is_live_formula_vertex(v) && self.graph.is_dirty(v))
931            || self
932                .get_staged_formula_text(self.graph.sheet_name(sheet), row + 1, col + 1)
933                .is_some()
934    }
935}