Skip to main content

formualizer_eval/engine/
scheduler.rs

1#[cfg(any(test, feature = "legacy_oracle"))]
2use super::DependencyGraph;
3use super::vertex::VertexId;
4#[cfg(any(test, feature = "legacy_oracle"))]
5use formualizer_common::ExcelError;
6#[cfg(any(test, feature = "legacy_oracle"))]
7use rustc_hash::{FxHashMap, FxHashSet};
8
9#[cfg(any(test, feature = "legacy_oracle"))]
10pub struct Scheduler<'a> {
11    graph: &'a DependencyGraph,
12}
13
14#[derive(Debug, Clone)]
15pub struct Layer {
16    pub vertices: Vec<VertexId>,
17    /// Family runs of this layer (Program 2 execution units): index ranges
18    /// of `vertices` holding consecutive rows of one column of one family
19    /// node. Vertices outside every run execute one cell at a time.
20    pub(crate) runs: Vec<LayerRun>,
21    /// Program 3 chain unit: the vertices are one family's cells in row
22    /// order and each may read the ones before it (a recurrence such as
23    /// `=A1+1` filled down). They are evaluated in order, each written
24    /// before the next is read: never in parallel, never buffered.
25    pub(crate) sequential: bool,
26}
27
28/// A family run: `vertices[start..start + len]` are the cells
29/// `(sheet, row0 + i, col)` of the family owner `owner`, all at one layer.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub(crate) struct LayerRun {
32    pub start: u32,
33    pub len: u32,
34    pub sheet: u16,
35    pub col: u32,
36    pub row0: u32,
37    pub owner: u32,
38}
39
40impl Layer {
41    /// A layer without family runs (every vertex executes per cell).
42    pub fn new(vertices: Vec<VertexId>) -> Self {
43        Self {
44            vertices,
45            runs: Vec::new(),
46            sequential: false,
47        }
48    }
49
50    /// The vertices `lo..hi` as a layer, with the runs clipped to them.
51    pub(crate) fn sub_layer(&self, lo: usize, hi: usize) -> Layer {
52        let first = self
53            .runs
54            .partition_point(|r| (r.start + r.len) as usize <= lo);
55        let runs = self.runs[first..]
56            .iter()
57            .take_while(|r| (r.start as usize) < hi)
58            .map(|r| {
59                let s = (r.start as usize).max(lo);
60                let e = ((r.start + r.len) as usize).min(hi);
61                LayerRun {
62                    start: (s - lo) as u32,
63                    len: (e - s) as u32,
64                    row0: r.row0 + (s - r.start as usize) as u32,
65                    ..*r
66                }
67            })
68            .collect();
69        Layer {
70            vertices: self.vertices[lo..hi].to_vec(),
71            runs,
72            sequential: self.sequential,
73        }
74    }
75}
76
77/// One step of the canonical schedule walk: either an acyclic Kahn wave
78/// (`Layer`, an index into `Schedule::layers`) or a cyclic SCC treated as a
79/// single super-node (`Cycle`, an index into `Schedule::cycles`). Storing
80/// indices keeps `Schedule::layers`/`Schedule::cycles` the single owners of
81/// the vertex Vecs, so building or cloning a schedule never duplicates them.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum ScheduleUnit {
84    Layer(u32),
85    Cycle(u32),
86}
87
88#[derive(Debug, Clone)]
89pub struct Schedule {
90    /// Canonical walk order: condensation order over the dependency graph.
91    /// Every `Cycle` unit appears after all units containing its external
92    /// dependencies and before all units containing its external dependents.
93    pub units: Vec<ScheduleUnit>,
94    /// All cyclic SCCs (same contents as before `units` existed), for
95    /// consumers that only need the set of cycles.
96    pub cycles: Vec<Vec<VertexId>>,
97    /// The `Layer` units in order, for consumers that only walk layers.
98    pub layers: Vec<Layer>,
99}
100
101impl Schedule {
102    fn from_parts(layers: Vec<Layer>, cycles: Vec<Vec<VertexId>>) -> Self {
103        debug_assert!(
104            cycles.is_empty(),
105            "Schedule::from_parts is the cycle-free fast path"
106        );
107        let units = (0..layers.len() as u32).map(ScheduleUnit::Layer).collect();
108        Schedule {
109            units,
110            cycles,
111            layers,
112        }
113    }
114
115    /// This schedule restricted to the vertices in `keep` (Program 3 plan
116    /// reuse). A schedule orders every dependency among its vertices, so
117    /// its units restricted to a subset order every dependency among the
118    /// subset: each kept vertex stays in its layer, a family run keeps its
119    /// kept members as runs of consecutive rows (two or more members, as
120    /// the planner's adapter forms them), and a sequential (chain)
121    /// layer keeps its order. `None` when a cycle is only partly kept.
122    pub(crate) fn restrict(&self, keep: &super::idset::DenseIdSet) -> Option<(Schedule, usize)> {
123        let mut layers: Vec<Layer> = Vec::new();
124        let mut cycles: Vec<Vec<VertexId>> = Vec::new();
125        let mut units: Vec<ScheduleUnit> = Vec::new();
126        let mut kept = 0usize;
127        for unit in &self.units {
128            match *unit {
129                ScheduleUnit::Layer(i) => {
130                    let layer = &self.layers[i as usize];
131                    let mut vertices: Vec<VertexId> = Vec::new();
132                    let mut runs: Vec<LayerRun> = Vec::new();
133                    let mut next_run = 0usize;
134                    let mut idx = 0usize;
135                    while idx < layer.vertices.len() {
136                        // Inside a run: keep its members as sub-runs.
137                        if let Some(run) = layer.runs.get(next_run)
138                            && run.start as usize == idx
139                        {
140                            next_run += 1;
141                            let end = idx + run.len as usize;
142                            let mut open: Option<LayerRun> = None;
143                            for (k, &v) in layer.vertices[idx..end].iter().enumerate() {
144                                if keep.contains(&v) {
145                                    let row = run.row0 + k as u32;
146                                    match open.as_mut() {
147                                        Some(r) if r.row0 + r.len == row => r.len += 1,
148                                        _ => {
149                                            if let Some(r) = open.take().filter(|r| r.len >= 2) {
150                                                runs.push(r);
151                                            }
152                                            open = Some(LayerRun {
153                                                start: vertices.len() as u32,
154                                                len: 1,
155                                                row0: row,
156                                                ..*run
157                                            });
158                                        }
159                                    }
160                                    vertices.push(v);
161                                } else if let Some(r) = open.take().filter(|r| r.len >= 2) {
162                                    runs.push(r);
163                                }
164                            }
165                            if let Some(r) = open.take().filter(|r| r.len >= 2) {
166                                runs.push(r);
167                            }
168                            idx = end;
169                            continue;
170                        }
171                        let v = layer.vertices[idx];
172                        if keep.contains(&v) {
173                            vertices.push(v);
174                        }
175                        idx += 1;
176                    }
177                    if !vertices.is_empty() {
178                        kept += vertices.len();
179                        units.push(ScheduleUnit::Layer(layers.len() as u32));
180                        layers.push(Layer {
181                            vertices,
182                            runs,
183                            sequential: layer.sequential,
184                        });
185                    }
186                }
187                ScheduleUnit::Cycle(i) => {
188                    let cycle = &self.cycles[i as usize];
189                    let n = cycle.iter().filter(|v| keep.contains(v)).count();
190                    if n == 0 {
191                        continue;
192                    }
193                    if n != cycle.len() {
194                        return None;
195                    }
196                    kept += n;
197                    units.push(ScheduleUnit::Cycle(cycles.len() as u32));
198                    cycles.push(cycle.clone());
199                }
200            }
201        }
202        Some((
203            Schedule {
204                units,
205                cycles,
206                layers,
207            },
208            kept,
209        ))
210    }
211
212    /// Resolve a `ScheduleUnit::Layer` index.
213    pub fn unit_layer(&self, i: u32) -> &Layer {
214        &self.layers[i as usize]
215    }
216
217    /// Resolve a `ScheduleUnit::Cycle` index.
218    pub fn unit_cycle(&self, i: u32) -> &[VertexId] {
219        &self.cycles[i as usize]
220    }
221}
222
223#[cfg(any(test, feature = "legacy_oracle"))]
224impl<'a> Scheduler<'a> {
225    pub fn new(graph: &'a DependencyGraph) -> Self {
226        Self { graph }
227    }
228
229    pub fn create_schedule(&self, vertices: &[VertexId]) -> Result<Schedule, ExcelError> {
230        let _span = crate::engine::trace::fz_span!(
231            tracing::Level::INFO,
232            "schedule",
233            "schedule.legacy",
234            vertices = vertices.len()
235        );
236        // 1. Find strongly connected components using Tarjan's algorithm
237        let _scc_span =
238            crate::engine::trace::fz_span!(tracing::Level::INFO, "schedule", "schedule.tarjan");
239        let sccs = self.tarjan_scc(vertices)?;
240        drop(_scc_span);
241
242        // 2. Separate cyclic from acyclic components
243        let (cycles, acyclic_sccs) = self.separate_cycles(sccs);
244
245        // 3. Topologically sort acyclic components into layers
246        if self.graph.dynamic_topo_enabled() {
247            // Dynamic-topo (PK) branch: pk_layers_for orders only the acyclic
248            // subset, so we cannot interleave Cycle units by condensation
249            // position here. Stay conservative on this experimental path and
250            // emit ALL Cycle units first (matching today's stamp-cycles-first
251            // semantics), then the pk layers as Layer units.
252            let subset: Vec<VertexId> = acyclic_sccs.into_iter().flatten().collect();
253            let layers = if subset.is_empty() {
254                Vec::new()
255            } else {
256                self.graph
257                    .pk_layers_for(&subset)
258                    .unwrap_or(self.build_layers(vec![subset])?)
259            };
260            let mut units = Vec::with_capacity(cycles.len() + layers.len());
261            let mut cycle_order: Vec<usize> = (0..cycles.len()).collect();
262            cycle_order.sort_by_key(|&i| cycles[i].iter().copied().min());
263            for i in cycle_order {
264                units.push(ScheduleUnit::Cycle(i as u32));
265            }
266            units.extend((0..layers.len() as u32).map(ScheduleUnit::Layer));
267            return Ok(Schedule {
268                units,
269                cycles,
270                layers,
271            });
272        }
273
274        if cycles.is_empty() {
275            // Fast path: byte-for-byte today's layer construction.
276            let layers = self.build_layers(acyclic_sccs)?;
277            return Ok(Schedule::from_parts(layers, cycles));
278        }
279
280        // Cycle path: Kahn over the condensation (SCC-as-node).
281        let (units, layers) = self.build_condensation_units(&cycles, acyclic_sccs, None)?;
282        Ok(Schedule {
283            units,
284            cycles,
285            layers,
286        })
287    }
288
289    /// Create a schedule considering additional ephemeral (virtual) dependencies just for this pass.
290    /// `vdeps` maps a vertex to extra dependency vertices that should be considered as incoming edges.
291    pub fn create_schedule_with_virtual(
292        &self,
293        vertices: &[VertexId],
294        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
295    ) -> Result<Schedule, ExcelError> {
296        let _span = crate::engine::trace::fz_span!(
297            tracing::Level::INFO,
298            "schedule",
299            "schedule.virtual",
300            vertices = vertices.len(),
301            vdeps = vdeps.len()
302        );
303        // 1. SCC detection with virtual deps
304        let _scc_span = crate::engine::trace::fz_span!(
305            tracing::Level::INFO,
306            "schedule",
307            "schedule.tarjan_virtual"
308        );
309        let sccs = self.tarjan_scc_with_virtual(vertices, vdeps)?;
310        drop(_scc_span);
311        // 2. Separate cycles and acyclic components
312        let (cycles, acyclic_sccs) = self.separate_cycles(sccs);
313        // 3. Build layers over combined adjacency (graph + vdeps)
314        let _layers_span = crate::engine::trace::fz_span!(
315            tracing::Level::INFO,
316            "schedule",
317            "schedule.layers_virtual"
318        );
319        if cycles.is_empty() {
320            // Fast path: byte-for-byte today's layer construction.
321            let layers = self.build_layers_with_virtual(acyclic_sccs, vdeps)?;
322            return Ok(Schedule::from_parts(layers, cycles));
323        }
324        // Cycle path: Kahn over the condensation (SCC-as-node), honoring
325        // virtual deps as extra edges.
326        let (units, layers) = self.build_condensation_units(&cycles, acyclic_sccs, Some(vdeps))?;
327        Ok(Schedule {
328            units,
329            cycles,
330            layers,
331        })
332    }
333
334    /// Tarjan's strongly connected components algorithm
335    pub fn tarjan_scc(&self, vertices: &[VertexId]) -> Result<Vec<Vec<VertexId>>, ExcelError> {
336        self.tarjan_scc_impl(vertices, None)
337    }
338
339    /// Tarjan with virtual deps
340    fn tarjan_scc_with_virtual(
341        &self,
342        vertices: &[VertexId],
343        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
344    ) -> Result<Vec<Vec<VertexId>>, ExcelError> {
345        self.tarjan_scc_impl(vertices, Some(vdeps))
346    }
347
348    /// Iterative Tarjan over the scheduled subgraph, optionally honoring
349    /// per-pass virtual dependency edges.
350    ///
351    /// The DFS uses an explicit frame stack (vertex, dependency list, next
352    /// edge offset) instead of recursion, so depth is bounded by heap, not
353    /// the thread stack — the recursive predecessor SIGABRTed around depth
354    /// ~1500 in debug (2 MiB test stacks) on large SCCs AND on plain acyclic
355    /// chains whose dependencies point at higher vertex ids (see
356    /// `engine/tests/iterate_corpus_scale.rs`).
357    ///
358    /// Determinism contract: SCC emission order and within-SCC member order
359    /// are byte-identical to the recursive version (pinned by the
360    /// differential test in `engine/tests/tarjan_differential.rs` and the
361    /// ordering invariants in `engine/tests/schedule_units.rs`):
362    /// * dependencies are walked in adjacency order (base slice/Vec, then
363    ///   the vertex's virtual deps, exactly as the recursive code iterated);
364    /// * a vertex's SCC is emitted when its frame is exhausted and
365    ///   `lowlink == index` (the same program point as the recursive
366    ///   post-order emission);
367    /// * SCC members are popped off the Tarjan stack in the same order.
368    fn tarjan_scc_impl(
369        &self,
370        vertices: &[VertexId],
371        vdeps: Option<&FxHashMap<VertexId, Vec<VertexId>>>,
372    ) -> Result<Vec<Vec<VertexId>>, ExcelError> {
373        /// One vertex's dependency list, materialized once per DFS frame.
374        enum DepList<'g> {
375            Slice(&'g [VertexId]),
376            Owned(Vec<VertexId>),
377        }
378        impl DepList<'_> {
379            #[inline]
380            fn get(&self, i: usize) -> Option<VertexId> {
381                match self {
382                    DepList::Slice(s) => s.get(i).copied(),
383                    DepList::Owned(v) => v.get(i).copied(),
384                }
385            }
386        }
387
388        let deps_of = |vertex: VertexId| -> DepList<'_> {
389            // Edge order must match the recursive implementation: the base
390            // adjacency (zero-copy slice when available), with the vertex's
391            // virtual deps appended when present.
392            if let Some(extra) = vdeps.and_then(|m| m.get(&vertex)) {
393                let mut combined: Vec<VertexId> =
394                    if let Some(base) = self.graph.dependencies_slice(vertex) {
395                        base.to_vec()
396                    } else {
397                        self.graph.get_dependencies(vertex)
398                    };
399                combined.extend(extra.iter().copied());
400                DepList::Owned(combined)
401            } else if let Some(base) = self.graph.dependencies_slice(vertex) {
402                DepList::Slice(base)
403            } else {
404                DepList::Owned(self.graph.get_dependencies(vertex))
405            }
406        };
407
408        // Compact the candidate set into dense local ids once, up front. All
409        // per-edge / per-frame bookkeeping (index, lowlink, on-stack) then
410        // becomes a plain array op instead of an FxHashMap probe; the only
411        // remaining hash per edge is the `VertexId -> local id` membership
412        // lookup, which doubles as the old `vertex_set.contains` filter.
413        // `VertexId` is a `u32` newtype, so `u32` local ids cannot overflow.
414        const UNVISITED: u32 = u32::MAX;
415        let mut local_of: FxHashMap<VertexId, u32> =
416            FxHashMap::with_capacity_and_hasher(vertices.len(), Default::default());
417        let mut vertex_of_local: Vec<VertexId> = Vec::with_capacity(vertices.len());
418        for &v in vertices {
419            if let std::collections::hash_map::Entry::Vacant(slot) = local_of.entry(v) {
420                slot.insert(vertex_of_local.len() as u32);
421                vertex_of_local.push(v);
422            }
423        }
424        let n = vertex_of_local.len();
425
426        let mut index_counter: u32 = 0;
427        let mut indices: Vec<u32> = vec![UNVISITED; n];
428        let mut lowlinks: Vec<u32> = vec![0; n];
429        let mut on_stack: Vec<bool> = vec![false; n];
430        let mut stack: Vec<u32> = Vec::with_capacity(n);
431        let mut sccs: Vec<Vec<VertexId>> = Vec::new();
432
433        // Explicit DFS frames: (local id, its dependency list, next edge
434        // offset). Pre-sized to the worst case (one frame per vertex, e.g. a
435        // single deep chain) so deep schedules never re-grow the stack.
436        let mut frames: Vec<(u32, DepList<'_>, u32)> = Vec::with_capacity(n);
437
438        for &root_vertex in vertices {
439            let root = local_of[&root_vertex];
440            if indices[root as usize] != UNVISITED {
441                continue;
442            }
443            indices[root as usize] = index_counter;
444            lowlinks[root as usize] = index_counter;
445            index_counter += 1;
446            stack.push(root);
447            on_stack[root as usize] = true;
448            frames.push((root, deps_of(root_vertex), 0));
449
450            while let Some(frame) = frames.last_mut() {
451                let vertex = frame.0 as usize;
452                if let Some(dep_vertex) = frame.1.get(frame.2 as usize) {
453                    frame.2 += 1;
454                    // Only consider dependencies that are part of the current
455                    // scheduling task.
456                    let Some(&dep) = local_of.get(&dep_vertex) else {
457                        continue;
458                    };
459                    let d = dep as usize;
460                    if indices[d] == UNVISITED {
461                        // Not yet visited: descend (the recursive call).
462                        indices[d] = index_counter;
463                        lowlinks[d] = index_counter;
464                        index_counter += 1;
465                        stack.push(dep);
466                        on_stack[d] = true;
467                        frames.push((dep, deps_of(dep_vertex), 0));
468                    } else if on_stack[d] {
469                        // On the Tarjan stack: in the current SCC.
470                        let dep_index = indices[d];
471                        if dep_index < lowlinks[vertex] {
472                            lowlinks[vertex] = dep_index;
473                        }
474                    }
475                } else {
476                    // Vertex exhausted: emit its SCC if it is a root, then
477                    // return to the parent frame, folding the child lowlink in
478                    // (the post-recursion `min(lowlink[v], lowlink[dep])`).
479                    let vertex_lowlink = lowlinks[vertex];
480                    if vertex_lowlink == indices[vertex] {
481                        let mut scc = Vec::new();
482                        loop {
483                            let w = stack.pop().unwrap();
484                            on_stack[w as usize] = false;
485                            scc.push(vertex_of_local[w as usize]);
486                            if w as usize == vertex {
487                                break;
488                            }
489                        }
490                        sccs.push(scc);
491                    }
492                    frames.pop();
493                    if let Some(parent) = frames.last() {
494                        let p = parent.0 as usize;
495                        if vertex_lowlink < lowlinks[p] {
496                            lowlinks[p] = vertex_lowlink;
497                        }
498                    }
499                }
500            }
501        }
502
503        Ok(sccs)
504    }
505
506    /// Test-only visibility shim over the private virtual-deps entry, used by
507    /// the differential test in `engine/tests/tarjan_differential.rs`.
508    #[cfg(test)]
509    pub(crate) fn tarjan_scc_with_virtual_for_tests(
510        &self,
511        vertices: &[VertexId],
512        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
513    ) -> Result<Vec<Vec<VertexId>>, ExcelError> {
514        self.tarjan_scc_with_virtual(vertices, vdeps)
515    }
516
517    /// Recursive reference implementation, retained ONLY for the differential
518    /// test (`engine/tests/tarjan_differential.rs`) that proves the iterative
519    /// rewrite emits byte-identical SCC output. Never call on deep graphs
520    /// without a large stack: it overflows around depth ~1500 in debug.
521    #[cfg(test)]
522    pub(crate) fn tarjan_scc_recursive_reference(
523        &self,
524        vertices: &[VertexId],
525    ) -> Result<Vec<Vec<VertexId>>, ExcelError> {
526        let mut index_counter = 0;
527        let mut stack = Vec::new();
528        let mut indices = FxHashMap::default();
529        let mut lowlinks = FxHashMap::default();
530        let mut on_stack = FxHashSet::default();
531        let mut sccs = Vec::new();
532        let vertex_set: FxHashSet<VertexId> = vertices.iter().copied().collect();
533
534        for &vertex in vertices {
535            if !indices.contains_key(&vertex) {
536                self.tarjan_visit(
537                    vertex,
538                    &mut index_counter,
539                    &mut stack,
540                    &mut indices,
541                    &mut lowlinks,
542                    &mut on_stack,
543                    &mut sccs,
544                    &vertex_set,
545                )?;
546            }
547        }
548
549        Ok(sccs)
550    }
551
552    /// Recursive reference with virtual deps; see
553    /// [`Self::tarjan_scc_recursive_reference`].
554    #[cfg(test)]
555    pub(crate) fn tarjan_scc_with_virtual_recursive_reference(
556        &self,
557        vertices: &[VertexId],
558        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
559    ) -> Result<Vec<Vec<VertexId>>, ExcelError> {
560        let mut index_counter = 0;
561        let mut stack = Vec::new();
562        let mut indices = FxHashMap::default();
563        let mut lowlinks = FxHashMap::default();
564        let mut on_stack = FxHashSet::default();
565        let mut sccs = Vec::new();
566        let vertex_set: FxHashSet<VertexId> = vertices.iter().copied().collect();
567
568        for &vertex in vertices {
569            if !indices.contains_key(&vertex) {
570                self.tarjan_visit_with_virtual(
571                    vertex,
572                    &mut index_counter,
573                    &mut stack,
574                    &mut indices,
575                    &mut lowlinks,
576                    &mut on_stack,
577                    &mut sccs,
578                    &vertex_set,
579                    vdeps,
580                )?;
581            }
582        }
583
584        Ok(sccs)
585    }
586
587    #[cfg(test)]
588    #[allow(clippy::too_many_arguments)]
589    fn tarjan_visit(
590        &self,
591        vertex: VertexId,
592        index_counter: &mut usize,
593        stack: &mut Vec<VertexId>,
594        indices: &mut FxHashMap<VertexId, usize>,
595        lowlinks: &mut FxHashMap<VertexId, usize>,
596        on_stack: &mut FxHashSet<VertexId>,
597        sccs: &mut Vec<Vec<VertexId>>,
598        vertex_set: &FxHashSet<VertexId>,
599    ) -> Result<(), ExcelError> {
600        // Set the depth index for vertex to the smallest unused index
601        indices.insert(vertex, *index_counter);
602        lowlinks.insert(vertex, *index_counter);
603        *index_counter += 1;
604        stack.push(vertex);
605        on_stack.insert(vertex);
606
607        // Consider successors of vertex (dependencies)
608        if let Some(dependencies) = self.graph.dependencies_slice(vertex) {
609            for &dependency in dependencies {
610                // Only consider dependencies that are part of the current scheduling task
611                if !vertex_set.contains(&dependency) {
612                    continue;
613                }
614
615                if !indices.contains_key(&dependency) {
616                    // Successor dependency has not yet been visited; recurse on it
617                    self.tarjan_visit(
618                        dependency,
619                        index_counter,
620                        stack,
621                        indices,
622                        lowlinks,
623                        on_stack,
624                        sccs,
625                        vertex_set,
626                    )?;
627                    let dep_lowlink = lowlinks[&dependency];
628                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_lowlink));
629                } else if on_stack.contains(&dependency) {
630                    // Successor dependency is in stack and hence in the current SCC
631                    let dep_index = indices[&dependency];
632                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_index));
633                }
634            }
635        } else {
636            let dependencies = self.graph.get_dependencies(vertex);
637            for dependency in dependencies {
638                // Only consider dependencies that are part of the current scheduling task
639                if !vertex_set.contains(&dependency) {
640                    continue;
641                }
642
643                if !indices.contains_key(&dependency) {
644                    // Successor dependency has not yet been visited; recurse on it
645                    self.tarjan_visit(
646                        dependency,
647                        index_counter,
648                        stack,
649                        indices,
650                        lowlinks,
651                        on_stack,
652                        sccs,
653                        vertex_set,
654                    )?;
655                    let dep_lowlink = lowlinks[&dependency];
656                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_lowlink));
657                } else if on_stack.contains(&dependency) {
658                    // Successor dependency is in stack and hence in the current SCC
659                    let dep_index = indices[&dependency];
660                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_index));
661                }
662            }
663        }
664
665        // If vertex is a root node, pop the stack and print an SCC
666        if lowlinks[&vertex] == indices[&vertex] {
667            let mut scc = Vec::new();
668            loop {
669                let w = stack.pop().unwrap();
670                on_stack.remove(&w);
671                scc.push(w);
672                if w == vertex {
673                    break;
674                }
675            }
676            sccs.push(scc);
677        }
678
679        Ok(())
680    }
681
682    #[cfg(test)]
683    #[allow(clippy::too_many_arguments)]
684    fn tarjan_visit_with_virtual(
685        &self,
686        vertex: VertexId,
687        index_counter: &mut usize,
688        stack: &mut Vec<VertexId>,
689        indices: &mut FxHashMap<VertexId, usize>,
690        lowlinks: &mut FxHashMap<VertexId, usize>,
691        on_stack: &mut FxHashSet<VertexId>,
692        sccs: &mut Vec<Vec<VertexId>>,
693        vertex_set: &FxHashSet<VertexId>,
694        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
695    ) -> Result<(), ExcelError> {
696        // Set the depth index for vertex to the smallest unused index
697        indices.insert(vertex, *index_counter);
698        lowlinks.insert(vertex, *index_counter);
699        *index_counter += 1;
700        stack.push(vertex);
701        on_stack.insert(vertex);
702
703        // Consider successors of vertex (dependencies) including virtual deps
704        if let Some(extra) = vdeps.get(&vertex) {
705            let mut dependencies: Vec<VertexId> =
706                if let Some(base) = self.graph.dependencies_slice(vertex) {
707                    base.to_vec()
708                } else {
709                    self.graph.get_dependencies(vertex)
710                };
711            dependencies.extend(extra.iter().copied());
712
713            for dependency in dependencies {
714                // Only consider dependencies that are part of the current scheduling task
715                if !vertex_set.contains(&dependency) {
716                    continue;
717                }
718
719                if !indices.contains_key(&dependency) {
720                    // Successor dependency has not yet been visited; recurse on it
721                    self.tarjan_visit_with_virtual(
722                        dependency,
723                        index_counter,
724                        stack,
725                        indices,
726                        lowlinks,
727                        on_stack,
728                        sccs,
729                        vertex_set,
730                        vdeps,
731                    )?;
732                    let dep_lowlink = lowlinks[&dependency];
733                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_lowlink));
734                } else if on_stack.contains(&dependency) {
735                    // Successor dependency is in stack and hence in the current SCC
736                    let dep_index = indices[&dependency];
737                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_index));
738                }
739            }
740        } else if let Some(dependencies) = self.graph.dependencies_slice(vertex) {
741            for &dependency in dependencies {
742                // Only consider dependencies that are part of the current scheduling task
743                if !vertex_set.contains(&dependency) {
744                    continue;
745                }
746
747                if !indices.contains_key(&dependency) {
748                    // Successor dependency has not yet been visited; recurse on it
749                    self.tarjan_visit_with_virtual(
750                        dependency,
751                        index_counter,
752                        stack,
753                        indices,
754                        lowlinks,
755                        on_stack,
756                        sccs,
757                        vertex_set,
758                        vdeps,
759                    )?;
760                    let dep_lowlink = lowlinks[&dependency];
761                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_lowlink));
762                } else if on_stack.contains(&dependency) {
763                    // Successor dependency is in stack and hence in the current SCC
764                    let dep_index = indices[&dependency];
765                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_index));
766                }
767            }
768        } else {
769            let dependencies = self.graph.get_dependencies(vertex);
770            for dependency in dependencies {
771                // Only consider dependencies that are part of the current scheduling task
772                if !vertex_set.contains(&dependency) {
773                    continue;
774                }
775
776                if !indices.contains_key(&dependency) {
777                    // Successor dependency has not yet been visited; recurse on it
778                    self.tarjan_visit_with_virtual(
779                        dependency,
780                        index_counter,
781                        stack,
782                        indices,
783                        lowlinks,
784                        on_stack,
785                        sccs,
786                        vertex_set,
787                        vdeps,
788                    )?;
789                    let dep_lowlink = lowlinks[&dependency];
790                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_lowlink));
791                } else if on_stack.contains(&dependency) {
792                    // Successor dependency is in stack and hence in the current SCC
793                    let dep_index = indices[&dependency];
794                    lowlinks.insert(vertex, lowlinks[&vertex].min(dep_index));
795                }
796            }
797        }
798
799        // If vertex is a root node, pop the stack and produce an SCC
800        if lowlinks[&vertex] == indices[&vertex] {
801            let mut scc = Vec::new();
802            loop {
803                let w = stack.pop().unwrap();
804                on_stack.remove(&w);
805                scc.push(w);
806                if w == vertex {
807                    break;
808                }
809            }
810            sccs.push(scc);
811        }
812
813        Ok(())
814    }
815
816    pub(crate) fn separate_cycles(
817        &self,
818        sccs: Vec<Vec<VertexId>>,
819    ) -> (Vec<Vec<VertexId>>, Vec<Vec<VertexId>>) {
820        let mut cycles = Vec::new();
821        let mut acyclic = Vec::new();
822
823        for scc in sccs {
824            if scc.len() > 1 || (scc.len() == 1 && self.has_self_loop(scc[0])) {
825                cycles.push(scc);
826            } else {
827                acyclic.push(scc);
828            }
829        }
830
831        (cycles, acyclic)
832    }
833
834    fn has_self_loop(&self, vertex: VertexId) -> bool {
835        self.graph.has_self_loop(vertex)
836    }
837
838    pub(crate) fn build_layers(
839        &self,
840        acyclic_sccs: Vec<Vec<VertexId>>,
841    ) -> Result<Vec<Layer>, ExcelError> {
842        let vertices: Vec<VertexId> = acyclic_sccs.into_iter().flatten().collect();
843        if vertices.is_empty() {
844            return Ok(Vec::new());
845        }
846        let vertex_set: FxHashSet<VertexId> = vertices.iter().copied().collect();
847
848        // Calculate in-degrees for all vertices in the acyclic subgraph
849        let mut in_degrees: FxHashMap<VertexId, usize> = vertices.iter().map(|&v| (v, 0)).collect();
850        for &vertex_id in &vertices {
851            if let Some(dependencies) = self.graph.dependencies_slice(vertex_id) {
852                for &dep_id in dependencies {
853                    if vertex_set.contains(&dep_id)
854                        && let Some(in_degree) = in_degrees.get_mut(&vertex_id)
855                    {
856                        *in_degree += 1;
857                    }
858                }
859            } else {
860                let dependencies = self.graph.get_dependencies(vertex_id);
861                for dep_id in dependencies {
862                    if vertex_set.contains(&dep_id)
863                        && let Some(in_degree) = in_degrees.get_mut(&vertex_id)
864                    {
865                        *in_degree += 1;
866                    }
867                }
868            }
869        }
870
871        // Initialize the queue with all nodes having an in-degree of 0
872        let mut queue: std::collections::VecDeque<VertexId> = in_degrees
873            .iter()
874            .filter(|&(_, &in_degree)| in_degree == 0)
875            .map(|(&v, _)| v)
876            .collect();
877
878        let mut layers = Vec::new();
879        let mut processed_count = 0;
880
881        while !queue.is_empty() {
882            let mut current_layer_vertices = Vec::new();
883            for _ in 0..queue.len() {
884                let u = queue.pop_front().unwrap();
885                current_layer_vertices.push(u);
886                processed_count += 1;
887
888                // For each dependent of u, reduce its in-degree
889                if let Some(dependents) = self.graph.dependents_slice(u) {
890                    for &v_dep in dependents {
891                        if let Some(in_degree) = in_degrees.get_mut(&v_dep) {
892                            *in_degree -= 1;
893                            if *in_degree == 0 {
894                                queue.push_back(v_dep);
895                            }
896                        }
897                    }
898                } else {
899                    for v_dep in self.graph.get_dependents(u) {
900                        if let Some(in_degree) = in_degrees.get_mut(&v_dep) {
901                            *in_degree -= 1;
902                            if *in_degree == 0 {
903                                queue.push_back(v_dep);
904                            }
905                        }
906                    }
907                }
908            }
909            // Sort for deterministic output in tests
910            current_layer_vertices.sort();
911            layers.push(Layer::new(current_layer_vertices));
912        }
913
914        if processed_count != vertices.len() {
915            return Err(
916                ExcelError::new(formualizer_common::ExcelErrorKind::Circ).with_message(
917                    "Unexpected cycle detected in acyclic components during layer construction"
918                        .to_string(),
919                ),
920            );
921        }
922
923        Ok(layers)
924    }
925
926    /// Kahn's algorithm over the condensation of the scheduled subgraph:
927    /// each cyclic SCC is a super-node, each acyclic vertex a singleton node.
928    ///
929    /// Per Kahn wave we emit first the wave's `Cycle` units (ordered by
930    /// smallest member `VertexId` for determinism), then one `Layer` unit with
931    /// the wave's singleton vertices (sorted, as in `build_layers`). Within a
932    /// wave there are no inter-node edges, so this ordering is semantically
933    /// free. The result guarantees every `Cycle` unit appears after all units
934    /// containing its external dependencies and before all units containing
935    /// its external dependents (pinned by tests in
936    /// `engine/tests/schedule_units.rs`).
937    ///
938    /// Returns the unit walk plus the `Layer` units in order (the
939    /// compatibility `Schedule::layers` view).
940    fn build_condensation_units(
941        &self,
942        cycles: &[Vec<VertexId>],
943        acyclic_sccs: Vec<Vec<VertexId>>,
944        vdeps: Option<&FxHashMap<VertexId, Vec<VertexId>>>,
945    ) -> Result<(Vec<ScheduleUnit>, Vec<Layer>), ExcelError> {
946        let singletons: Vec<VertexId> = acyclic_sccs.into_iter().flatten().collect();
947        let cycle_node_count = cycles.len();
948        let node_count = cycle_node_count + singletons.len();
949
950        // Map every scheduled vertex to its condensation node.
951        let mut node_of: FxHashMap<VertexId, usize> = FxHashMap::default();
952        for (i, cycle) in cycles.iter().enumerate() {
953            for &v in cycle {
954                node_of.insert(v, i);
955            }
956        }
957        for (j, &v) in singletons.iter().enumerate() {
958            node_of.insert(v, cycle_node_count + j);
959        }
960
961        // Build in-degrees and the dependents adjacency from a single scan of
962        // the dependency direction, so both sides are guaranteed consistent.
963        // Edges to vertices outside the scheduled set are ignored
964        // (membership == presence in `node_of`); intra-node edges are ignored.
965        let mut in_degrees: Vec<usize> = vec![0; node_count];
966        let mut node_dependents: Vec<Vec<usize>> = vec![Vec::new(); node_count];
967        let scan_dep = |from_node: usize,
968                        dep: VertexId,
969                        in_degrees: &mut Vec<usize>,
970                        node_dependents: &mut Vec<Vec<usize>>| {
971            if let Some(&dep_node) = node_of.get(&dep)
972                && dep_node != from_node
973            {
974                in_degrees[from_node] += 1;
975                node_dependents[dep_node].push(from_node);
976            }
977        };
978        for (&v, &n_v) in node_of.iter() {
979            if let Some(deps) = self.graph.dependencies_slice(v) {
980                for &dep in deps {
981                    scan_dep(n_v, dep, &mut in_degrees, &mut node_dependents);
982                }
983            } else {
984                for dep in self.graph.get_dependencies(v) {
985                    scan_dep(n_v, dep, &mut in_degrees, &mut node_dependents);
986                }
987            }
988            if let Some(extra) = vdeps.and_then(|m| m.get(&v)) {
989                for &dep in extra {
990                    scan_dep(n_v, dep, &mut in_degrees, &mut node_dependents);
991                }
992            }
993        }
994
995        // Kahn by waves over condensation nodes.
996        let mut current: Vec<usize> = (0..node_count).filter(|&n| in_degrees[n] == 0).collect();
997        let mut units = Vec::new();
998        let mut layers = Vec::new();
999        let mut processed_count = 0usize;
1000        while !current.is_empty() {
1001            let mut wave_cycles: Vec<usize> = current
1002                .iter()
1003                .copied()
1004                .filter(|&n| n < cycle_node_count)
1005                .collect();
1006            wave_cycles.sort_by_key(|&n| cycles[n].iter().copied().min());
1007            for n in wave_cycles {
1008                units.push(ScheduleUnit::Cycle(n as u32));
1009            }
1010
1011            let mut wave_vertices: Vec<VertexId> = current
1012                .iter()
1013                .copied()
1014                .filter(|&n| n >= cycle_node_count)
1015                .map(|n| singletons[n - cycle_node_count])
1016                .collect();
1017            if !wave_vertices.is_empty() {
1018                // Sort for deterministic output, as in build_layers.
1019                wave_vertices.sort();
1020                units.push(ScheduleUnit::Layer(layers.len() as u32));
1021                layers.push(Layer::new(wave_vertices));
1022            }
1023
1024            processed_count += current.len();
1025            let mut next = Vec::new();
1026            for &n in &current {
1027                for &dependent in &node_dependents[n] {
1028                    in_degrees[dependent] -= 1;
1029                    if in_degrees[dependent] == 0 {
1030                        next.push(dependent);
1031                    }
1032                }
1033            }
1034            current = next;
1035        }
1036
1037        if processed_count != node_count {
1038            return Err(
1039                ExcelError::new(formualizer_common::ExcelErrorKind::Circ).with_message(
1040                    "Unexpected cycle detected in condensation during unit construction"
1041                        .to_string(),
1042                ),
1043            );
1044        }
1045
1046        Ok((units, layers))
1047    }
1048
1049    pub(crate) fn build_layers_with_virtual(
1050        &self,
1051        acyclic_sccs: Vec<Vec<VertexId>>,
1052        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
1053    ) -> Result<Vec<Layer>, ExcelError> {
1054        use std::collections::VecDeque;
1055        let vertices: Vec<VertexId> = acyclic_sccs.into_iter().flatten().collect();
1056        if vertices.is_empty() {
1057            return Ok(Vec::new());
1058        }
1059        let vertex_set: FxHashSet<VertexId> = vertices.iter().copied().collect();
1060
1061        // Build combined adjacency (dependencies and dependents) within the subset
1062        let mut combined_deps: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default();
1063        let mut combined_out: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default();
1064        for &v in &vertices {
1065            let mut deps: Vec<VertexId> = Vec::new();
1066            if let Some(base) = self.graph.dependencies_slice(v) {
1067                deps.extend(base.iter().copied().filter(|d| vertex_set.contains(d)));
1068            } else {
1069                deps.extend(
1070                    self.graph
1071                        .get_dependencies(v)
1072                        .into_iter()
1073                        .filter(|d| vertex_set.contains(d)),
1074                );
1075            }
1076            if let Some(extra) = vdeps.get(&v) {
1077                deps.extend(extra.iter().copied().filter(|d| vertex_set.contains(d)));
1078            }
1079            deps.sort_unstable();
1080            deps.dedup();
1081            combined_deps.insert(v, deps);
1082        }
1083        // invert
1084        for (&v, deps) in combined_deps.iter() {
1085            for &d in deps {
1086                combined_out.entry(d).or_default().push(v);
1087            }
1088        }
1089        // in-degrees
1090        let mut in_degrees: FxHashMap<VertexId, usize> = FxHashMap::default();
1091        for &v in &vertices {
1092            let indeg = combined_deps.get(&v).map(|v| v.len()).unwrap_or(0);
1093            in_degrees.insert(v, indeg);
1094        }
1095        // queue of 0 in-degree
1096        let mut queue: VecDeque<VertexId> = in_degrees
1097            .iter()
1098            .filter(|&(_, &deg)| deg == 0)
1099            .map(|(&v, _)| v)
1100            .collect();
1101
1102        let mut layers = Vec::new();
1103        let mut processed_count = 0;
1104        while !queue.is_empty() {
1105            let mut cur = Vec::new();
1106            for _ in 0..queue.len() {
1107                let u = queue.pop_front().unwrap();
1108                cur.push(u);
1109                processed_count += 1;
1110                if let Some(dependents) = combined_out.get(&u) {
1111                    for &w in dependents {
1112                        if let Some(ind) = in_degrees.get_mut(&w) {
1113                            *ind = ind.saturating_sub(1);
1114                            if *ind == 0 {
1115                                queue.push_back(w);
1116                            }
1117                        }
1118                    }
1119                }
1120            }
1121            cur.sort_unstable();
1122            layers.push(Layer::new(cur));
1123        }
1124        if processed_count != vertices.len() {
1125            return Err(
1126                ExcelError::new(formualizer_common::ExcelErrorKind::Circ).with_message(
1127                    "Unexpected cycle detected in acyclic components during layer construction (virtual)"
1128                        .to_string(),
1129                ),
1130            );
1131        }
1132        Ok(layers)
1133    }
1134}