From 36617143ba3b902f40f81ddcd406ae2c35142fb0 Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 23 Feb 2026 23:18:27 +0800 Subject: [PATCH] refactor: split LeafExecutionPath into prefix/leaf types for type-level guarantees Introduce ExecutionPathPrefix for incremental path building during graph traversal and restructure LeafExecutionPath with separate ancestors/leaf fields, making empty paths unrepresentable. Key changes: - ExecutionPathPrefix: new type for building paths, tracks all_containing_graphs_single_node incrementally during construction - LeafExecutionPath: now struct with ancestors + leaf fields (always non-empty by construction), precomputed single-node-chain check - execute_expanded_graph: takes &ExecutionPathPrefix, uses type-appropriate to_leaf_path/extended methods instead of clone+push - Remove Index<&LeafExecutionPath> impl (unused in production) Reduces unreachable! assertions from 3 to 1 (the sole remaining one guards the ancestor-is-Expanded invariant in resolve_leaf_graph). --- crates/vite_task/src/session/execute/mod.rs | 21 +-- .../vite_task/src/session/reporter/labeled.rs | 9 +- crates/vite_task/src/session/reporter/mod.rs | 176 ++++++++++++------ 3 files changed, 133 insertions(+), 73 deletions(-) diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs index ed6b9141..eefb0370 100644 --- a/crates/vite_task/src/session/execute/mod.rs +++ b/crates/vite_task/src/session/execute/mod.rs @@ -21,8 +21,8 @@ use super::{ ExecutionError, }, reporter::{ - ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, LeafExecutionPath, - LeafExecutionReporter, StdioSuggestion, + ExecutionPathPrefix, ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, + LeafExecutionPath, LeafExecutionReporter, StdioSuggestion, }, }; use crate::{Session, session::execute::spawn::SpawnTrackResult}; @@ -73,7 +73,7 @@ impl ExecutionContext<'_> { async fn execute_expanded_graph( &mut self, graph: &ExecutionGraph, - path_prefix: &LeafExecutionPath, + prefix: &ExecutionPathPrefix, ) { // `compute_topological_order()` returns nodes in topological order: for every // edge A→B, A appears before B. Since our edges mean "A depends on B", @@ -87,17 +87,16 @@ impl ExecutionContext<'_> { let task_execution = &graph[node_ix]; for (item_idx, item) in task_execution.items.iter().enumerate() { - // Build the path for this item by appending to the prefix - let mut item_path = path_prefix.clone(); - item_path.push(node_ix, item_idx); - match &item.kind { ExecutionItemKind::Leaf(leaf_kind) => { - self.execute_leaf(&item_path, leaf_kind).boxed_local().await; + let path = prefix.to_leaf_path(node_ix, item_idx); + self.execute_leaf(&path, leaf_kind).boxed_local().await; } ExecutionItemKind::Expanded(nested_graph) => { - // Recurse into the nested graph, carrying the path prefix forward. - self.execute_expanded_graph(nested_graph, &item_path).boxed_local().await; + let nested_prefix = prefix.extended(node_ix, item_idx, nested_graph); + self.execute_expanded_graph(nested_graph, &nested_prefix) + .boxed_local() + .await; } } } @@ -414,7 +413,7 @@ impl Session<'_> { // Execute the graph. Leaf-level errors are reported through the reporter // and do not abort the graph. Cycle detection is handled at plan time. - execution_context.execute_expanded_graph(&graph, &LeafExecutionPath::default()).await; + execution_context.execute_expanded_graph(&graph, &ExecutionPathPrefix::new(&graph)).await; // Leaf-level errors and non-zero exit statuses are tracked internally // by the reporter. diff --git a/crates/vite_task/src/session/reporter/labeled.rs b/crates/vite_task/src/session/reporter/labeled.rs index e096dbc0..3140d6fc 100644 --- a/crates/vite_task/src/session/reporter/labeled.rs +++ b/crates/vite_task/src/session/reporter/labeled.rs @@ -573,13 +573,13 @@ fn format_summary( mod tests { use std::sync::Arc; - use vite_task_plan::ExecutionGraph; + use vite_task_plan::{ExecutionGraph, execution_graph::ExecutionNodeIndex}; use super::*; use crate::session::{ event::CacheDisabledReason, reporter::{ - LeafExecutionPath, LeafExecutionReporter, StdioSuggestion, + ExecutionPathPrefix, LeafExecutionReporter, StdioSuggestion, test_fixtures::{expanded_task, in_process_task, spawn_task, test_path}, }, }; @@ -651,16 +651,13 @@ mod tests { /// Build a `LabeledGraphReporter` for the given graph and return a leaf reporter /// for the first node's first item. fn build_labeled_leaf(graph: ExecutionGraph) -> Box { - use vite_task_plan::execution_graph::ExecutionNodeIndex; - let graph_arc = Arc::new(graph); let builder = Box::new(LabeledReporterBuilder::new(test_path(), Box::new(tokio::io::sink()))); let mut reporter = builder.build(&graph_arc); // Create a leaf reporter for the first node's first item - let mut path = LeafExecutionPath::default(); - path.push(ExecutionNodeIndex::new(0), 0); + let path = ExecutionPathPrefix::new(&graph_arc).to_leaf_path(ExecutionNodeIndex::new(0), 0); reporter.new_leaf_execution(&path) } diff --git a/crates/vite_task/src/session/reporter/mod.rs b/crates/vite_task/src/session/reporter/mod.rs index f66f1c04..7ec09112 100644 --- a/crates/vite_task/src/session/reporter/mod.rs +++ b/crates/vite_task/src/session/reporter/mod.rs @@ -37,9 +37,7 @@ use smallvec::SmallVec; use tokio::io::AsyncWrite; use vite_path::AbsolutePath; use vite_str::Str; -use vite_task_plan::{ - ExecutionGraph, ExecutionItem, ExecutionItemDisplay, ExecutionItemKind, LeafExecutionKind, -}; +use vite_task_plan::{ExecutionGraph, ExecutionItem, ExecutionItemDisplay, ExecutionItemKind}; use super::{ cache::format_cache_status_inline, @@ -105,7 +103,7 @@ pub struct StdioConfig { // Leaf execution path — identifies a leaf within a (potentially nested) execution graph // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -/// One step in a [`LeafExecutionPath`]: identifies a specific execution item +/// One step in the execution path: identifies a specific execution item /// within a single level of the execution graph. #[derive(Clone, Copy, Debug)] struct ExecutionPathItem { @@ -125,73 +123,139 @@ impl ExecutionPathItem { } } -/// A path through a (potentially nested) execution graph that identifies a specific -/// leaf execution. +/// A prefix path through nested `Expanded` execution graphs, used during graph +/// traversal to incrementally build up the path before reaching a leaf. +/// +/// Created at the root via [`new`](Self::new) and extended at each `Expanded` +/// level via [`extended`](Self::extended). Completed into a [`LeafExecutionPath`] +/// via [`to_leaf_path`](Self::to_leaf_path) when a `Leaf` item is reached. +/// +/// Tracks `all_containing_graphs_single_node` incrementally so that +/// [`LeafExecutionPath`] can expose it as a simple field accessor without +/// re-traversing the graph. +#[derive(Clone, Debug)] +pub struct ExecutionPathPrefix { + /// Steps through intermediate `Expanded` items (may be empty for root-level leaves). + steps: SmallVec, + /// Whether every graph encountered so far (root + all nested `Expanded` ancestors) + /// has exactly one node. + all_containing_graphs_single_node: bool, +} + +impl ExecutionPathPrefix { + /// Create a prefix for the root execution graph. + /// + /// Captures the root graph's node count for the single-node chain check. + pub fn new(root_graph: &ExecutionGraph) -> Self { + Self { + steps: SmallVec::new(), + all_containing_graphs_single_node: root_graph.node_count() == 1, + } + } + + /// Extend this prefix with a step into a nested `Expanded` graph. + /// + /// Called in the `ExecutionItemKind::Expanded` arm of the traversal loop. + /// The nested graph's node count is folded into the single-node chain check. + pub fn extended( + &self, + graph_node_ix: ExecutionNodeIndex, + task_execution_item_index: usize, + nested_graph: &ExecutionGraph, + ) -> Self { + let mut steps = self.steps.clone(); + steps.push(ExecutionPathItem { graph_node_ix, task_execution_item_index }); + Self { + steps, + all_containing_graphs_single_node: self.all_containing_graphs_single_node + && nested_graph.node_count() == 1, + } + } + + /// Complete this prefix into a [`LeafExecutionPath`] by appending the final + /// leaf step. + /// + /// Called in the `ExecutionItemKind::Leaf` arm of the traversal loop. + pub fn to_leaf_path( + &self, + graph_node_ix: ExecutionNodeIndex, + task_execution_item_index: usize, + ) -> LeafExecutionPath { + LeafExecutionPath { + ancestors: self.steps.clone(), + leaf: ExecutionPathItem { graph_node_ix, task_execution_item_index }, + all_containing_graphs_single_node: self.all_containing_graphs_single_node, + } + } +} + +/// A complete path from root graph to a leaf execution item. /// -/// Each element in the path represents a step deeper into a nested `Expanded` execution -/// graph. The last element identifies the actual leaf item. +/// Structurally guaranteed to be non-empty: always contains at least the `leaf` +/// step. The `ancestors` may be empty (for root-level leaves) or contain one or +/// more steps through intermediate `Expanded` graphs. /// -/// For example, a path of `[(node_0, item_1), (node_2, item_0)]` means: +/// For example, a path with `ancestors = [(node_0, item_1)]` and +/// `leaf = (node_2, item_0)` means: /// - In the root graph, node 0, item 1 (which is an `Expanded` containing a nested graph) /// - In that nested graph, node 2, item 0 (the actual leaf execution) /// -/// Uses `SmallVec` with inline capacity of 4 since most execution graphs are shallow -/// (typically 1-2 levels of nesting). -#[derive(Clone, Debug, Default)] -pub struct LeafExecutionPath(SmallVec); +/// Constructed exclusively via [`ExecutionPathPrefix::to_leaf_path`], which +/// ensures that `ancestors` correspond to `Expanded` items and precomputes +/// `all_containing_graphs_single_node`. +#[derive(Clone, Debug)] +pub struct LeafExecutionPath { + /// Steps through intermediate `Expanded` items (empty for root-level leaves). + /// + /// Inline capacity of 4 (matching [`ExecutionPathPrefix`]) since most execution + /// graphs are shallow (typically 1-2 levels of nesting, so 0-1 ancestors). + ancestors: SmallVec, + /// The final step identifying the leaf item. + leaf: ExecutionPathItem, + /// Whether every containing graph (root + all nested `Expanded` ancestors) + /// has exactly one node. Precomputed during path construction. + all_containing_graphs_single_node: bool, +} impl LeafExecutionPath { - /// Append a new step to this path, identifying an item at the given node and item indices. - pub fn push(&mut self, graph_node_ix: ExecutionNodeIndex, task_execution_item_index: usize) { - self.0.push(ExecutionPathItem { graph_node_ix, task_execution_item_index }); + /// Walk through ancestor steps, descending into nested `Expanded` graphs. + /// Returns the graph that directly contains the leaf item. + /// + /// This is the single traversal helper used by [`resolve_item`](Self::resolve_item). + /// Construction via [`ExecutionPathPrefix`] ensures all ancestors point to + /// `Expanded` items, so the `Leaf` arm is structurally unreachable. + fn resolve_leaf_graph<'a>(&self, root_graph: &'a ExecutionGraph) -> &'a ExecutionGraph { + let mut current = root_graph; + for (depth, step) in self.ancestors.iter().enumerate() { + match &step.resolve(current).kind { + ExecutionItemKind::Expanded(nested) => current = nested, + ExecutionItemKind::Leaf(_) => unreachable!( + "LeafExecutionPath: ancestor at depth {depth} is a Leaf, expected Expanded" + ), + } + } + current } /// Resolve this path against a root execution graph, returning the final /// [`ExecutionItem`] the path points to. /// - /// This is the shared traversal logic that walks through nested `Expanded` - /// graphs. Used by: - /// - `Index<&LeafExecutionPath> for ExecutionGraph` — extracts `&LeafExecutionKind` - /// - `new_leaf_execution` in `labeled.rs` — extracts `ExecutionItemDisplay` - /// - /// # Panics - /// - /// - If the path is empty (indicates a bug in path construction). - /// - If an intermediate path element points to a `Leaf` item instead of - /// `Expanded` (only `Expanded` items contain nested graphs to descend into). + /// Used by `new_leaf_execution` in `labeled.rs` to extract `ExecutionItemDisplay`. fn resolve_item<'a>(&self, root_graph: &'a ExecutionGraph) -> &'a ExecutionItem { - let mut current_graph = root_graph; - let last_depth = self.0.len() - 1; - for (depth, path_item) in self.0.iter().enumerate() { - let item = path_item.resolve(current_graph); - if depth == last_depth { - return item; - } - match &item.kind { - ExecutionItemKind::Expanded(nested_graph) => { - current_graph = nested_graph; - } - ExecutionItemKind::Leaf(_) => { - unreachable!( - "LeafExecutionPath: intermediate element at depth {depth} is a Leaf, expected Expanded" - ) - } - } - } - unreachable!("LeafExecutionPath: empty path") + self.leaf.resolve(self.resolve_leaf_graph(root_graph)) } -} - -impl std::ops::Index<&LeafExecutionPath> for ExecutionGraph { - type Output = LeafExecutionKind; - fn index(&self, path: &LeafExecutionPath) -> &Self::Output { - match &path.resolve_item(self).kind { - ExecutionItemKind::Leaf(kind) => kind, - ExecutionItemKind::Expanded(_) => { - unreachable!("LeafExecutionPath: final element is Expanded, expected Leaf") - } - } + /// Whether every containing graph (root + all nested `Expanded` ancestors) + /// has exactly one node. + /// + /// This is used by the labeled reporter to determine whether inherited stdio + /// can be suggested for spawned processes: when there is only one task at + /// every level of the graph, the spawned process can safely inherit the + /// parent's stdio without interleaving output from other tasks. + /// + /// Precomputed during path construction — no graph traversal needed. + pub const fn all_containing_graphs_single_node(&self) -> bool { + self.all_containing_graphs_single_node } }