From e03698582cf727d3cc9cef4e8d26ed1a593e4f76 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 Jun 2026 22:47:03 -0400 Subject: [PATCH 1/8] Avoid stack overflows in resolver error reporting --- Cargo.lock | 42 ++++ Cargo.toml | 1 + crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/error.rs | 251 +++++++++++++++++------ crates/uv-resolver/src/pubgrub/mod.rs | 2 +- crates/uv-resolver/src/pubgrub/report.rs | 250 ++++++++++++++++++---- crates/uv-resolver/src/resolver/mod.rs | 8 +- crates/uv/tests/it/pip_compile.rs | 78 +++++++ 8 files changed, 522 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c31b42a751156..b5b34d2b08faa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + [[package]] name = "arcstr" version = "1.2.0" @@ -2992,6 +3001,15 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -3467,6 +3485,16 @@ dependencies = [ "hex", ] +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "ptr_meta" version = "0.3.1" @@ -4737,6 +4765,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.60.2", +] + [[package]] name = "statrs" version = "0.18.0" @@ -7094,6 +7135,7 @@ dependencies = [ "serde", "serde_json", "smallvec", + "stacker", "textwrap", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index bcc765debc7f0..1fe4adf771cae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -257,6 +257,7 @@ serde_json = { version = "1.0.128" } sha2 = { version = "0.10.8" } smallvec = { version = "1.13.2" } spdx = { version = "0.13.0" } +stacker = { version = "0.1.24" } syn = { version = "2.0.77" } target-lexicon = { version = "0.13.0" } tempfile = { version = "3.14.0" } diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 35a8396d1c40f..b65b4c3e8a5a7 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -69,6 +69,7 @@ schemars = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true } smallvec = { workspace = true } +stacker = { workspace = true } textwrap = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index d84b729a35999..032d10d16ff83 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -5,12 +5,11 @@ use std::sync::{Arc, OnceLock}; use indexmap::IndexSet; use itertools::Itertools; use owo_colors::OwoColorize; -use pubgrub::{ - DefaultStringReporter, DerivationTree, Derived, External, Range, Ranges, Reporter, Term, -}; +use pubgrub::{DerivationTree, Derived, External, Range, Ranges, Term}; use rustc_hash::FxHashMap; use tracing::trace; +use uv_configuration::min_stack_size; use uv_distribution_types::{ DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist, }; @@ -27,7 +26,10 @@ use crate::dependency_provider::UvDependencyProvider; use crate::fork_indexes::ForkIndexes; use crate::fork_urls::ForkUrls; use crate::prerelease::AllowPrerelease; -use crate::pubgrub::{PubGrubHint, PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter}; +use crate::pubgrub::{ + PubGrubHint, PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter, + report_derivation_tree, +}; use crate::python_requirement::PythonRequirement; use crate::resolution::ConflictingDistributionError; use crate::resolver::{ @@ -167,9 +169,60 @@ impl From> for ResolveError { pub type ErrorTree = DerivationTree, UnavailableReason>; +const DERIVATION_TREE_RED_ZONE: usize = 64 * 1024; + +/// Run one recursive derivation-tree step with enough stack to reach the next growth point. +pub(crate) fn with_growing_stack(callback: impl FnOnce() -> R) -> R { + stacker::maybe_grow(DERIVATION_TREE_RED_ZONE, min_stack_size(), callback) +} + +/// Visit the packages in a derivation tree without recursive calls. +pub(crate) fn derivation_tree_packages( + derivation_tree: &ErrorTree, +) -> impl Iterator { + let mut packages = Vec::new(); + let mut trees = vec![derivation_tree]; + + while let Some(tree) = trees.pop() { + match tree { + DerivationTree::External(external) => match external { + External::FromDependencyOf(package, _, dependency, _) => { + packages.push(package); + packages.push(dependency); + } + External::NoVersions(package, _) + | External::NotRoot(package, _) + | External::Custom(package, _, _) => packages.push(package), + }, + DerivationTree::Derived(derived) => { + packages.extend(derived.terms.keys()); + trees.push(&derived.cause1); + trees.push(&derived.cause2); + } + } + } + + packages.into_iter() +} + +fn drop_derivation_tree(derivation_tree: ErrorTree) { + let mut trees = vec![derivation_tree]; + + while let Some(tree) = trees.pop() { + if let DerivationTree::Derived(derived) = tree { + if let Ok(cause1) = Arc::try_unwrap(derived.cause1) { + trees.push(cause1); + } + if let Ok(cause2) = Arc::try_unwrap(derived.cause2) { + trees.push(cause2); + } + } + } +} + /// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report. pub struct NoSolutionError { - error: pubgrub::NoSolutionError, + error: Option>, index: InMemoryIndex, /// The versions that were available for each package after `exclude-newer` filtering. /// @@ -224,7 +277,7 @@ impl NoSolutionError { options: Options, ) -> Self { Self { - error, + error: Some(error), index, included_versions, available_versions, @@ -246,6 +299,12 @@ impl NoSolutionError { } } + fn error(&self) -> &ErrorTree { + self.error + .as_ref() + .expect("no-solution error is only taken during drop") + } + /// Get the cached report and hints, computing them on first access. fn cached(&self) -> &(String, IndexSet) { self.cached.get_or_init(|| self.compute_report_and_hints()) @@ -255,7 +314,7 @@ impl NoSolutionError { /// wrap an [`PubGrubPackageInner::Extra`] package. pub(crate) fn collapse_proxies(derivation_tree: ErrorTree) -> ErrorTree { fn collapse(derivation_tree: ErrorTree) -> Option { - match derivation_tree { + with_growing_stack(|| match derivation_tree { DerivationTree::Derived(derived) => { match (&*derived.cause1, &*derived.cause2) { ( @@ -288,7 +347,7 @@ impl NoSolutionError { } } DerivationTree::External(_) => Some(derivation_tree), - } + }) } collapse(derivation_tree) @@ -302,7 +361,7 @@ impl NoSolutionError { /// satisfy `==1.0.0`. pub(crate) fn collapse_local_version_segments(derivation_tree: ErrorTree) -> ErrorTree { fn strip(derivation_tree: ErrorTree) -> Option { - match derivation_tree { + with_growing_stack(|| match derivation_tree { DerivationTree::External(External::NotRoot(_, _)) => Some(derivation_tree), DerivationTree::External(External::NoVersions(package, versions)) => { if SentinelRange::from(&versions).is_complement() { @@ -359,7 +418,7 @@ impl NoSolutionError { _ => None, } } - } + }) } strip(derivation_tree).expect("derivation tree should contain at least one term") @@ -370,8 +429,8 @@ impl NoSolutionError { fn find(derivation_tree: &ErrorTree, minimum: &mut LowerBound) { match derivation_tree { DerivationTree::Derived(derived) => { - find(derived.cause1.as_ref(), minimum); - find(derived.cause2.as_ref(), minimum); + with_growing_stack(|| find(derived.cause1.as_ref(), minimum)); + with_growing_stack(|| find(derived.cause2.as_ref(), minimum)); } DerivationTree::External(External::FromDependencyOf(.., package, version)) => { if let PubGrubPackageInner::Python(_) = &**package { @@ -388,7 +447,7 @@ impl NoSolutionError { } let mut minimum = LowerBound::default(); - find(&self.error, &mut minimum); + find(self.error(), &mut minimum); minimum } @@ -399,9 +458,7 @@ impl NoSolutionError { /// Get the packages that are involved in this error. pub fn packages(&self) -> impl Iterator { - self.error - .packages() - .into_iter() + derivation_tree_packages(self.error()) .filter_map(|p| p.name()) .unique() } @@ -432,7 +489,7 @@ impl NoSolutionError { }; // Transform the error tree for reporting - let mut tree = self.error.clone(); + let mut tree = self.error().clone(); simplify_derivation_tree_markers(&self.python_requirement, &mut tree); let should_display_tree = std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE) .is_some() @@ -470,7 +527,7 @@ impl NoSolutionError { display_tree(&tree, "Resolver derivation tree after reduction"); } - let report = DefaultStringReporter::report_with_formatter(&tree, &formatter); + let report = report_derivation_tree(&tree, &formatter); let inherited_exclude_newer_ranges = FxHashMap::default(); let mut hints = IndexSet::default(); @@ -523,7 +580,12 @@ impl std::fmt::Debug for NoSolutionError { cached: _, } = self; f.debug_struct("NoSolutionError") - .field("error", error) + .field( + "error", + error + .as_ref() + .expect("no-solution error is only taken during drop"), + ) .field("included_versions", included_versions) .field("available_versions", available_versions) .field("available_indexes", available_indexes) @@ -544,6 +606,14 @@ impl std::fmt::Debug for NoSolutionError { } } +impl Drop for NoSolutionError { + fn drop(&mut self) { + if let Some(error) = self.error.take() { + drop_derivation_tree(error); + } + } +} + impl std::error::Error for NoSolutionError {} impl uv_errors::Hint for NoSolutionError { @@ -593,8 +663,8 @@ fn display_tree_inner( let prefix = " ".repeat(depth); match error { DerivationTree::Derived(derived) => { - display_tree_inner(&derived.cause1, lines, depth + 1); - display_tree_inner(&derived.cause2, lines, depth + 1); + with_growing_stack(|| display_tree_inner(&derived.cause1, lines, depth + 1)); + with_growing_stack(|| display_tree_inner(&derived.cause2, lines, depth + 1)); for (package, term) in &derived.terms { match term { Term::Positive(versions) => { @@ -650,7 +720,7 @@ fn collapse_redundant_no_versions( DerivationTree::External(External::NoVersions(package, versions)), ) => { // First, always recursively visit the other side of the tree - collapse_redundant_no_versions(other); + with_growing_stack(|| collapse_redundant_no_versions(other)); // Retrieve the nearest terms, either alongside this node or from the parent. let package_terms = if let DerivationTree::Derived(derived) = other { @@ -682,8 +752,12 @@ fn collapse_redundant_no_versions( } // If not, just recurse _ => { - collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause1)); - collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause2)); + with_growing_stack(|| { + collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause1)); + }); + with_growing_stack(|| { + collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause2)); + }); } } } @@ -757,8 +831,11 @@ fn collapse_redundant_no_versions_tree( } // If not, just recurse _ => { - collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause1)) - || collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause2)) + with_growing_stack(|| { + collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause1)) + }) || with_growing_stack(|| { + collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause2)) + }) } } } @@ -782,7 +859,9 @@ fn collapse_no_versions_of_workspace_members( (DerivationTree::External(External::NoVersions(package, _)), ref mut other) | (ref mut other, DerivationTree::External(External::NoVersions(package, _))) => { // First, always recursively visit the other side of the tree - collapse_no_versions_of_workspace_members(other, workspace_members); + with_growing_stack(|| { + collapse_no_versions_of_workspace_members(other, workspace_members); + }); // Then, if the package is a workspace member... let (PubGrubPackageInner::Package { name, .. } @@ -800,14 +879,18 @@ fn collapse_no_versions_of_workspace_members( } // If not, just recurse _ => { - collapse_no_versions_of_workspace_members( - Arc::make_mut(&mut derived.cause1), - workspace_members, - ); - collapse_no_versions_of_workspace_members( - Arc::make_mut(&mut derived.cause2), - workspace_members, - ); + with_growing_stack(|| { + collapse_no_versions_of_workspace_members( + Arc::make_mut(&mut derived.cause1), + workspace_members, + ); + }); + with_growing_stack(|| { + collapse_no_versions_of_workspace_members( + Arc::make_mut(&mut derived.cause2), + workspace_members, + ); + }); } } } @@ -859,8 +942,16 @@ fn collapse_redundant_depends_on_no_versions( } // If not, just recurse _ => { - collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1)); - collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2)); + with_growing_stack(|| { + collapse_redundant_depends_on_no_versions(Arc::make_mut( + &mut derived.cause1, + )); + }); + with_growing_stack(|| { + collapse_redundant_depends_on_no_versions(Arc::make_mut( + &mut derived.cause2, + )); + }); } } } @@ -911,8 +1002,16 @@ fn collapse_redundant_depends_on_no_versions_inner( } // If not, just recurse _ => { - collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1)); - collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2)); + with_growing_stack(|| { + collapse_redundant_depends_on_no_versions(Arc::make_mut( + &mut derived.cause1, + )); + }); + with_growing_stack(|| { + collapse_redundant_depends_on_no_versions(Arc::make_mut( + &mut derived.cause2, + )); + }); } } } @@ -953,14 +1052,18 @@ fn simplify_derivation_tree_markers( (pkg, term) }) .collect(); - simplify_derivation_tree_markers( - python_requirement, - Arc::make_mut(&mut derived.cause1), - ); - simplify_derivation_tree_markers( - python_requirement, - Arc::make_mut(&mut derived.cause2), - ); + with_growing_stack(|| { + simplify_derivation_tree_markers( + python_requirement, + Arc::make_mut(&mut derived.cause1), + ); + }); + with_growing_stack(|| { + simplify_derivation_tree_markers( + python_requirement, + Arc::make_mut(&mut derived.cause2), + ); + }); } } } @@ -988,7 +1091,7 @@ fn collapse_unavailable_versions( DerivationTree::External(External::Custom(package, versions, reason)), ) => { // First, recursively collapse the other side of the tree - collapse_unavailable_versions(other); + with_growing_stack(|| collapse_unavailable_versions(other)); // If it's not a derived tree, nothing to do. let DerivationTree::Derived(Derived { @@ -1060,8 +1163,12 @@ fn collapse_unavailable_versions( } // If not, just recurse _ => { - collapse_unavailable_versions(Arc::make_mut(&mut derived.cause1)); - collapse_unavailable_versions(Arc::make_mut(&mut derived.cause2)); + with_growing_stack(|| { + collapse_unavailable_versions(Arc::make_mut(&mut derived.cause1)); + }); + with_growing_stack(|| { + collapse_unavailable_versions(Arc::make_mut(&mut derived.cause2)); + }); } } } @@ -1109,15 +1216,25 @@ fn drop_root_dependency_on_project( } // Recursively collapse the other side of the tree - drop_root_dependency_on_project(other, project); + with_growing_stack(|| drop_root_dependency_on_project(other, project)); // Then, replace this node with the other tree *tree = other.clone(); } // If not, just recurse _ => { - drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause1), project); - drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause2), project); + with_growing_stack(|| { + drop_root_dependency_on_project( + Arc::make_mut(&mut derived.cause1), + project, + ); + }); + with_growing_stack(|| { + drop_root_dependency_on_project( + Arc::make_mut(&mut derived.cause2), + project, + ); + }); } } } @@ -1413,18 +1530,22 @@ fn simplify_derivation_tree_ranges( }, DerivationTree::Derived(derived) => { // Recursively simplify both sides of the tree - simplify_derivation_tree_ranges( - Arc::make_mut(&mut derived.cause1), - included_versions, - candidate_selector, - resolver_environment, - ); - simplify_derivation_tree_ranges( - Arc::make_mut(&mut derived.cause2), - included_versions, - candidate_selector, - resolver_environment, - ); + with_growing_stack(|| { + simplify_derivation_tree_ranges( + Arc::make_mut(&mut derived.cause1), + included_versions, + candidate_selector, + resolver_environment, + ); + }); + with_growing_stack(|| { + simplify_derivation_tree_ranges( + Arc::make_mut(&mut derived.cause2), + included_versions, + candidate_selector, + resolver_environment, + ); + }); // Simplify the terms derived.terms = std::mem::take(&mut derived.terms) diff --git a/crates/uv-resolver/src/pubgrub/mod.rs b/crates/uv-resolver/src/pubgrub/mod.rs index 6cbeaa729c22a..bb4fc85d2a1fb 100644 --- a/crates/uv-resolver/src/pubgrub/mod.rs +++ b/crates/uv-resolver/src/pubgrub/mod.rs @@ -2,7 +2,7 @@ pub(crate) use crate::pubgrub::dependencies::{DependencySource, PubGrubDependenc pub(crate) use crate::pubgrub::package::{PubGrubPackage, PubGrubPackageInner, PubGrubPython}; pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority, PubGrubTiebreaker}; pub use crate::pubgrub::report::PubGrubHint; -pub(crate) use crate::pubgrub::report::PubGrubReportFormatter; +pub(crate) use crate::pubgrub::report::{PubGrubReportFormatter, report as report_derivation_tree}; mod dependencies; mod package; diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index cb5abc4d4413a..4abbeba084939 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -20,7 +20,7 @@ use uv_pep508::{MarkerEnvironment, MarkerExpression, MarkerTree, MarkerValueVers use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, Tags}; use crate::candidate_selector::CandidateSelector; -use crate::error::{ErrorTree, PrefixMatch}; +use crate::error::{ErrorTree, PrefixMatch, with_growing_stack}; use crate::exclude_newer::EffectiveExcludeNewerSource; use crate::fork_indexes::ForkIndexes; use crate::fork_urls::ForkUrls; @@ -51,6 +51,170 @@ pub(crate) struct PubGrubReportFormatter<'a> { pub(crate) tags: Option<&'a Tags>, } +/// Render a PubGrub report while growing the stack between recursive steps. +/// +/// This mirrors [`pubgrub::DefaultStringReporter`], whose recursive entry point is private. +pub(crate) fn report( + derivation_tree: &ErrorTree, + formatter: &PubGrubReportFormatter<'_>, +) -> String { + match derivation_tree { + DerivationTree::External(external) => formatter.format_external(external), + DerivationTree::Derived(derived) => { + let mut reporter = StackSafeReporter::default(); + reporter.build_recursive(derived, formatter); + reporter.lines.join("\n") + } + } +} + +#[derive(Default)] +struct StackSafeReporter { + ref_count: usize, + shared_with_ref: Map, + lines: Vec, +} + +impl StackSafeReporter { + fn build_recursive( + &mut self, + derived: &Derived, UnavailableReason>, + formatter: &PubGrubReportFormatter<'_>, + ) { + with_growing_stack(|| self.build_recursive_inner(derived, formatter)); + } + + fn build_recursive_inner( + &mut self, + derived: &Derived, UnavailableReason>, + formatter: &PubGrubReportFormatter<'_>, + ) { + self.build_recursive_helper(derived, formatter); + if let Some(id) = derived.shared_id + && !self.shared_with_ref.contains_key(&id) + { + self.add_line_ref(); + self.shared_with_ref.insert(id, self.ref_count); + } + } + + fn build_recursive_helper( + &mut self, + current: &Derived, UnavailableReason>, + formatter: &PubGrubReportFormatter<'_>, + ) { + match (current.cause1.as_ref(), current.cause2.as_ref()) { + (DerivationTree::External(external1), DerivationTree::External(external2)) => { + self.lines.push(formatter.explain_both_external( + external1, + external2, + ¤t.terms, + )); + } + (DerivationTree::Derived(derived), DerivationTree::External(external)) + | (DerivationTree::External(external), DerivationTree::Derived(derived)) => { + self.report_one_each(derived, external, ¤t.terms, formatter); + } + (DerivationTree::Derived(derived1), DerivationTree::Derived(derived2)) => { + match ( + self.line_ref_of(derived1.shared_id), + self.line_ref_of(derived2.shared_id), + ) { + (Some(ref1), Some(ref2)) => self.lines.push(formatter.explain_both_ref( + ref1, + derived1, + ref2, + derived2, + ¤t.terms, + )), + (Some(ref1), None) => { + self.build_recursive(derived2, formatter); + self.lines + .push(formatter.and_explain_ref(ref1, derived1, ¤t.terms)); + } + (None, Some(ref2)) => { + self.build_recursive(derived1, formatter); + self.lines + .push(formatter.and_explain_ref(ref2, derived2, ¤t.terms)); + } + (None, None) => { + self.build_recursive(derived1, formatter); + if derived1.shared_id.is_some() { + self.lines.push(String::new()); + self.build_recursive(current, formatter); + } else { + self.add_line_ref(); + let ref1 = self.ref_count; + self.lines.push(String::new()); + self.build_recursive(derived2, formatter); + self.lines.push(formatter.and_explain_ref( + ref1, + derived1, + ¤t.terms, + )); + } + } + } + } + } + } + + fn report_one_each( + &mut self, + derived: &Derived, UnavailableReason>, + external: &External, UnavailableReason>, + current_terms: &Map>>, + formatter: &PubGrubReportFormatter<'_>, + ) { + if let Some(ref_id) = self.line_ref_of(derived.shared_id) { + self.lines.push(formatter.explain_ref_and_external( + ref_id, + derived, + external, + current_terms, + )); + } else { + self.report_recurse_one_each(derived, external, current_terms, formatter); + } + } + + fn report_recurse_one_each( + &mut self, + derived: &Derived, UnavailableReason>, + external: &External, UnavailableReason>, + current_terms: &Map>>, + formatter: &PubGrubReportFormatter<'_>, + ) { + match (derived.cause1.as_ref(), derived.cause2.as_ref()) { + (DerivationTree::Derived(prior_derived), DerivationTree::External(prior_external)) + | (DerivationTree::External(prior_external), DerivationTree::Derived(prior_derived)) => { + self.build_recursive(prior_derived, formatter); + self.lines.push(formatter.and_explain_prior_and_external( + prior_external, + external, + current_terms, + )); + } + _ => { + self.build_recursive(derived, formatter); + self.lines + .push(formatter.and_explain_external(external, current_terms)); + } + } + } + + fn add_line_ref(&mut self) { + self.ref_count += 1; + if let Some(line) = self.lines.last_mut() { + *line = format!("{line} ({})", self.ref_count); + } + } + + fn line_ref_of(&self, shared_id: Option) -> Option { + shared_id.and_then(|id| self.shared_with_ref.get(&id).copied()) + } +} + impl ReportFormatter, UnavailableReason> for PubGrubReportFormatter<'_> { @@ -770,44 +934,48 @@ impl PubGrubReportFormatter<'_> { .or_insert_with(|| range.clone()); } - self.generate_hints( - &derived.cause1, - index, - selector, - index_locations, - index_capabilities, - available_indexes, - unavailable_packages, - incomplete_packages, - fork_urls, - fork_indexes, - env, - current_environment, - tags, - workspace_members, - options, - &cause1_inherited_exclude_newer_ranges, - output_hints, - ); - self.generate_hints( - &derived.cause2, - index, - selector, - index_locations, - index_capabilities, - available_indexes, - unavailable_packages, - incomplete_packages, - fork_urls, - fork_indexes, - env, - current_environment, - tags, - workspace_members, - options, - &cause2_inherited_exclude_newer_ranges, - output_hints, - ); + with_growing_stack(|| { + self.generate_hints( + &derived.cause1, + index, + selector, + index_locations, + index_capabilities, + available_indexes, + unavailable_packages, + incomplete_packages, + fork_urls, + fork_indexes, + env, + current_environment, + tags, + workspace_members, + options, + &cause1_inherited_exclude_newer_ranges, + output_hints, + ); + }); + with_growing_stack(|| { + self.generate_hints( + &derived.cause2, + index, + selector, + index_locations, + index_capabilities, + available_indexes, + unavailable_packages, + incomplete_packages, + fork_urls, + fork_indexes, + env, + current_environment, + tags, + workspace_members, + options, + &cause2_inherited_exclude_newer_ranges, + output_hints, + ); + }); } } } @@ -840,8 +1008,8 @@ impl PubGrubReportFormatter<'_> { } DerivationTree::External(_) => {} DerivationTree::Derived(derived) => { - collect(&derived.cause1, exclude_newer_ranges); - collect(&derived.cause2, exclude_newer_ranges); + with_growing_stack(|| collect(&derived.cause1, exclude_newer_ranges)); + with_growing_stack(|| collect(&derived.cause2, exclude_newer_ranges)); } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 365b7ec4202d8..032f8d5326eae 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -43,7 +43,7 @@ use uv_warnings::warn_user_once; use crate::candidate_selector::{Candidate, CandidateDist, CandidateSelector}; use crate::dependency_provider::UvDependencyProvider; -use crate::error::{NoSolutionError, ResolveError}; +use crate::error::{NoSolutionError, ResolveError, derivation_tree_packages}; use crate::fork_indexes::ForkIndexes; use crate::fork_strategy::ForkStrategy; use crate::fork_urls::ForkUrls; @@ -2699,7 +2699,7 @@ impl ResolverState ResolverState ResolverState Result<()> { @@ -390,6 +397,77 @@ fn compile_constraints_txt() -> Result<()> { Ok(()) } +/// +#[test] +fn compile_constraints_many_versions() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let mut package_versions = BTreeMap::new(); + for patch in 0..1_000 { + let version = Version::from_str(&format!("1.0.{patch}"))?; + package_versions.insert( + version.clone(), + PackageMetadata { + requires: vec![Requirement::from_str(&format!("dependency=={version}"))?], + sdist: false, + wheel: true, + ..PackageMetadata::default() + }, + ); + } + + let mut scenario = Scenario::empty(); + scenario.packages.insert( + PackageName::from_str("package")?, + Package { + versions: package_versions, + }, + ); + scenario.packages.insert( + PackageName::from_str("dependency")?, + Package { + versions: BTreeMap::from([( + Version::from_str("2.0.0")?, + PackageMetadata { + sdist: false, + wheel: true, + ..PackageMetadata::default() + }, + )]), + }, + ); + let server = PackseServer::from_scenario(&scenario); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("package<2")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("dependency>=2")?; + + let mut filters = context.filters(); + filters.push((r"WARN Range requests not supported.*\n", "")); + filters.push(( + r"(?s) × No solution found when resolving dependencies:.*requirements are unsatisfiable\.", + " × No solution found when resolving dependencies: [LONG DERIVATION]", + )); + + uv_snapshot!(filters, context.pip_compile() + .arg("requirements.in") + .arg("--constraint") + .arg("constraints.txt") + .arg("--index-url") + .arg(server.index_url()) + .env(EnvVars::UV_STACK_SIZE, (4 * 1024 * 1024).to_string()), @" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: [LONG DERIVATION] + "); + + Ok(()) +} + /// Resolve a package from a `requirements.in` file, with an inline constraint. #[test] fn compile_constraints_inline() -> Result<()> { From 67cc7580e3e54b3e70a0ebdf26b2be259d85e493 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 13:55:01 -0400 Subject: [PATCH 2/8] Drop transformed resolver error trees iteratively --- crates/uv-resolver/src/error.rs | 67 ++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 032d10d16ff83..57282a7e005b2 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet, Bound}; use std::fmt::Formatter; +use std::ops::{Deref, DerefMut}; use std::sync::{Arc, OnceLock}; use indexmap::IndexSet; @@ -220,6 +221,40 @@ fn drop_derivation_tree(derivation_tree: ErrorTree) { } } +struct StackSafeErrorTree(Option); + +impl StackSafeErrorTree { + fn new(derivation_tree: ErrorTree) -> Self { + Self(Some(derivation_tree)) + } +} + +impl Deref for StackSafeErrorTree { + type Target = ErrorTree; + + fn deref(&self) -> &Self::Target { + self.0 + .as_ref() + .expect("derivation tree is only taken during drop") + } +} + +impl DerefMut for StackSafeErrorTree { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0 + .as_mut() + .expect("derivation tree is only taken during drop") + } +} + +impl Drop for StackSafeErrorTree { + fn drop(&mut self) { + if let Some(derivation_tree) = self.0.take() { + drop_derivation_tree(derivation_tree); + } + } +} + /// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report. pub struct NoSolutionError { error: Option>, @@ -489,7 +524,7 @@ impl NoSolutionError { }; // Transform the error tree for reporting - let mut tree = self.error().clone(); + let mut tree = StackSafeErrorTree::new(self.error().clone()); simplify_derivation_tree_markers(&self.python_requirement, &mut tree); let should_display_tree = std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE) .is_some() @@ -1642,3 +1677,33 @@ fn simplify_range( true }))) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn drops_transformed_derivation_tree_without_recursion() -> std::io::Result<()> { + let thread = std::thread::Builder::new() + .stack_size(256 * 1024) + .spawn(|| { + let package = PubGrubPackage::from(PubGrubPackageInner::Root(None)); + let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64]))); + let mut tree = leaf.clone(); + + for _ in 0..100_000 { + tree = ErrorTree::Derived(Derived { + terms: pubgrub::Map::default(), + shared_id: None, + cause1: Arc::new(tree), + cause2: Arc::new(leaf.clone()), + }); + } + + let _tree = StackSafeErrorTree::new(tree); + })?; + + assert!(thread.join().is_ok()); + Ok(()) + } +} From 20ac75ac816cb4037bab8f619184812e961380f4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 14:06:00 -0400 Subject: [PATCH 3/8] Drop resolver transform inputs iteratively --- crates/uv-resolver/src/error.rs | 113 ++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 36 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 57282a7e005b2..3ffe043c9cb74 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -348,7 +348,7 @@ impl NoSolutionError { /// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities /// wrap an [`PubGrubPackageInner::Extra`] package. pub(crate) fn collapse_proxies(derivation_tree: ErrorTree) -> ErrorTree { - fn collapse(derivation_tree: ErrorTree) -> Option { + fn collapse(derivation_tree: &ErrorTree) -> Option { with_growing_stack(|| match derivation_tree { DerivationTree::Derived(derived) => { match (&*derived.cause1, &*derived.cause2) { @@ -359,20 +359,21 @@ impl NoSolutionError { ( DerivationTree::External(External::FromDependencyOf(package, ..)), cause, - ) if package.is_proxy() => collapse(cause.clone()), + ) if package.is_proxy() => collapse(cause), ( cause, DerivationTree::External(External::FromDependencyOf(package, ..)), - ) if package.is_proxy() => collapse(cause.clone()), + ) if package.is_proxy() => collapse(cause), (cause1, cause2) => { - let cause1 = collapse(cause1.clone()); - let cause2 = collapse(cause2.clone()); + let cause1 = collapse(cause1); + let cause2 = collapse(cause2); match (cause1, cause2) { (Some(cause1), Some(cause2)) => { Some(DerivationTree::Derived(Derived { cause1: Arc::new(cause1), cause2: Arc::new(cause2), - ..derived + terms: derived.terms.clone(), + shared_id: derived.shared_id, })) } (Some(cause), None) | (None, Some(cause)) => Some(cause), @@ -381,11 +382,12 @@ impl NoSolutionError { } } } - DerivationTree::External(_) => Some(derivation_tree), + DerivationTree::External(_) => Some(derivation_tree.clone()), }) } - collapse(derivation_tree) + let derivation_tree = StackSafeErrorTree::new(derivation_tree); + collapse(&derivation_tree) .expect("derivation tree should contain at least one external term") } @@ -395,17 +397,18 @@ impl NoSolutionError { /// implement PEP 440 semantics for local version equality. For example, `1.0.0+foo` needs to /// satisfy `==1.0.0`. pub(crate) fn collapse_local_version_segments(derivation_tree: ErrorTree) -> ErrorTree { - fn strip(derivation_tree: ErrorTree) -> Option { + fn strip(derivation_tree: &ErrorTree) -> Option { with_growing_stack(|| match derivation_tree { - DerivationTree::External(External::NotRoot(_, _)) => Some(derivation_tree), + DerivationTree::External(External::NotRoot(_, _)) => Some(derivation_tree.clone()), DerivationTree::External(External::NoVersions(package, versions)) => { - if SentinelRange::from(&versions).is_complement() { + if SentinelRange::from(versions).is_complement() { return None; } - let versions = SentinelRange::from(&versions).strip(); + let versions = SentinelRange::from(versions).strip(); Some(DerivationTree::External(External::NoVersions( - package, versions, + package.clone(), + versions, ))) } DerivationTree::External(External::FromDependencyOf( @@ -414,26 +417,33 @@ impl NoSolutionError { package2, versions2, )) => { - let versions1 = SentinelRange::from(&versions1).strip(); - let versions2 = SentinelRange::from(&versions2).strip(); + let versions1 = SentinelRange::from(versions1).strip(); + let versions2 = SentinelRange::from(versions2).strip(); Some(DerivationTree::External(External::FromDependencyOf( - package1, versions1, package2, versions2, + package1.clone(), + versions1, + package2.clone(), + versions2, ))) } DerivationTree::External(External::Custom(package, versions, reason)) => { - let versions = SentinelRange::from(&versions).strip(); + let versions = SentinelRange::from(versions).strip(); Some(DerivationTree::External(External::Custom( - package, versions, reason, + package.clone(), + versions, + reason.clone(), ))) } - DerivationTree::Derived(mut derived) => { - let cause1 = strip((*derived.cause1).clone()); - let cause2 = strip((*derived.cause2).clone()); + DerivationTree::Derived(derived) => { + let cause1 = strip(&derived.cause1); + let cause2 = strip(&derived.cause2); match (cause1, cause2) { (Some(cause1), Some(cause2)) => Some(DerivationTree::Derived(Derived { cause1: Arc::new(cause1), cause2: Arc::new(cause2), - terms: std::mem::take(&mut derived.terms) + terms: derived + .terms + .clone() .into_iter() .map(|(pkg, term)| { let term = match term { @@ -456,7 +466,8 @@ impl NoSolutionError { }) } - strip(derivation_tree).expect("derivation tree should contain at least one term") + let derivation_tree = StackSafeErrorTree::new(derivation_tree); + strip(&derivation_tree).expect("derivation tree should contain at least one term") } /// Given a [`DerivationTree`], identify the largest required Python version that is missing. @@ -1682,25 +1693,55 @@ fn simplify_range( mod tests { use super::*; + fn deep_derivation_tree() -> ErrorTree { + let package = PubGrubPackage::from(PubGrubPackageInner::Root(None)); + let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64]))); + let mut tree = leaf.clone(); + + for _ in 0..100_000 { + tree = ErrorTree::Derived(Derived { + terms: pubgrub::Map::default(), + shared_id: None, + cause1: Arc::new(tree), + cause2: Arc::new(leaf.clone()), + }); + } + + tree + } + #[test] fn drops_transformed_derivation_tree_without_recursion() -> std::io::Result<()> { let thread = std::thread::Builder::new() .stack_size(256 * 1024) .spawn(|| { - let package = PubGrubPackage::from(PubGrubPackageInner::Root(None)); - let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64]))); - let mut tree = leaf.clone(); - - for _ in 0..100_000 { - tree = ErrorTree::Derived(Derived { - terms: pubgrub::Map::default(), - shared_id: None, - cause1: Arc::new(tree), - cause2: Arc::new(leaf.clone()), - }); - } + let _tree = StackSafeErrorTree::new(deep_derivation_tree()); + })?; - let _tree = StackSafeErrorTree::new(tree); + assert!(thread.join().is_ok()); + Ok(()) + } + + #[test] + fn collapse_proxies_drops_source_tree_without_recursion() -> std::io::Result<()> { + let thread = std::thread::Builder::new() + .stack_size(256 * 1024) + .spawn(|| { + let tree = NoSolutionError::collapse_proxies(deep_derivation_tree()); + drop_derivation_tree(tree); + })?; + + assert!(thread.join().is_ok()); + Ok(()) + } + + #[test] + fn collapse_local_versions_drops_source_tree_without_recursion() -> std::io::Result<()> { + let thread = std::thread::Builder::new() + .stack_size(256 * 1024) + .spawn(|| { + let tree = NoSolutionError::collapse_local_version_segments(deep_derivation_tree()); + drop_derivation_tree(tree); })?; assert!(thread.join().is_ok()); From b6faee46a02eb9f11a262692fcc97ca0bbc38d9d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 14:07:12 -0400 Subject: [PATCH 4/8] Format resolver errors with a growing stack --- crates/uv-resolver/src/error.rs | 75 +++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 3ffe043c9cb74..50dcf35092f21 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -1,5 +1,5 @@ use std::collections::{BTreeMap, BTreeSet, Bound}; -use std::fmt::Formatter; +use std::fmt::{Debug, Formatter}; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, OnceLock}; @@ -255,6 +255,41 @@ impl Drop for StackSafeErrorTree { } } +struct StackSafeDebugErrorTree<'a>(&'a ErrorTree); + +impl Debug for StackSafeDebugErrorTree<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + with_growing_stack(|| match self.0 { + DerivationTree::External(external) => { + f.debug_tuple("External").field(external).finish() + } + DerivationTree::Derived(derived) => f + .debug_tuple("Derived") + .field(&StackSafeDebugDerived(derived)) + .finish(), + }) + } +} + +struct StackSafeDebugDerived<'a>(&'a Derived, UnavailableReason>); + +impl Debug for StackSafeDebugDerived<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let Derived { + terms, + shared_id, + cause1, + cause2, + } = self.0; + f.debug_struct("Derived") + .field("terms", terms) + .field("shared_id", shared_id) + .field("cause1", &StackSafeDebugErrorTree(cause1)) + .field("cause2", &StackSafeDebugErrorTree(cause2)) + .finish() + } +} + /// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report. pub struct NoSolutionError { error: Option>, @@ -628,9 +663,11 @@ impl std::fmt::Debug for NoSolutionError { f.debug_struct("NoSolutionError") .field( "error", - error - .as_ref() - .expect("no-solution error is only taken during drop"), + &StackSafeDebugErrorTree( + error + .as_ref() + .expect("no-solution error is only taken during drop"), + ), ) .field("included_versions", included_versions) .field("available_versions", available_versions) @@ -1747,4 +1784,34 @@ mod tests { assert!(thread.join().is_ok()); Ok(()) } + + #[test] + fn formats_derivation_tree_without_recursion() -> std::io::Result<()> { + let thread = std::thread::Builder::new() + .stack_size(256 * 1024) + .spawn(|| { + let tree = StackSafeErrorTree::new(deep_derivation_tree()); + let _formatted = format!("{:?}", StackSafeDebugErrorTree(&tree)); + })?; + + assert!(thread.join().is_ok()); + Ok(()) + } + + #[test] + fn stack_safe_debug_matches_pubgrub_debug() { + let package = PubGrubPackage::from(PubGrubPackageInner::Root(None)); + let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64]))); + let tree = ErrorTree::Derived(Derived { + terms: pubgrub::Map::default(), + shared_id: Some(1), + cause1: Arc::new(leaf.clone()), + cause2: Arc::new(leaf), + }); + + assert_eq!( + format!("{tree:?}"), + format!("{:?}", StackSafeDebugErrorTree(&tree)) + ); + } } From 53adf5bfa0bfded00ab8be3b1d5a1bef8aba1cfe Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 14:13:49 -0400 Subject: [PATCH 5/8] Deduplicate packages in resolver error trees --- crates/uv-resolver/src/error.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 50dcf35092f21..916e7ebf00ef5 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -7,7 +7,7 @@ use indexmap::IndexSet; use itertools::Itertools; use owo_colors::OwoColorize; use pubgrub::{DerivationTree, Derived, External, Range, Ranges, Term}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use tracing::trace; use uv_configuration::min_stack_size; @@ -181,19 +181,21 @@ pub(crate) fn with_growing_stack(callback: impl FnOnce() -> R) -> R { pub(crate) fn derivation_tree_packages( derivation_tree: &ErrorTree, ) -> impl Iterator { - let mut packages = Vec::new(); + let mut packages = FxHashSet::default(); let mut trees = vec![derivation_tree]; while let Some(tree) = trees.pop() { match tree { DerivationTree::External(external) => match external { External::FromDependencyOf(package, _, dependency, _) => { - packages.push(package); - packages.push(dependency); + packages.insert(package); + packages.insert(dependency); } External::NoVersions(package, _) | External::NotRoot(package, _) - | External::Custom(package, _, _) => packages.push(package), + | External::Custom(package, _, _) => { + packages.insert(package); + } }, DerivationTree::Derived(derived) => { packages.extend(derived.terms.keys()); @@ -1759,6 +1761,12 @@ mod tests { Ok(()) } + #[test] + fn derivation_tree_packages_are_unique() { + let tree = StackSafeErrorTree::new(deep_derivation_tree()); + assert_eq!(derivation_tree_packages(&tree).count(), 1); + } + #[test] fn collapse_proxies_drops_source_tree_without_recursion() -> std::io::Result<()> { let thread = std::thread::Builder::new() From 1cfd27dd33ddac2f108d2bca70796df0abbf4e7a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 14:15:40 -0400 Subject: [PATCH 6/8] Test resolver reporter parity for shared nodes --- crates/uv-resolver/src/pubgrub/report.rs | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 4abbeba084939..27c3c4e3ffcee 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -2605,3 +2605,80 @@ fn padded<'a, T: std::fmt::Display + ?Sized>( Ok(()) }) } + +#[cfg(test)] +mod tests { + use pubgrub::{DefaultStringReporter, Reporter}; + use uv_distribution_types::RequiresPython; + use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder}; + + use super::*; + + fn derived( + cause1: DerivationTree, UnavailableReason>, + cause2: DerivationTree, UnavailableReason>, + shared_id: Option, + ) -> DerivationTree, UnavailableReason> { + DerivationTree::Derived(Derived { + terms: Map::default(), + shared_id, + cause1: cause1.into(), + cause2: cause2.into(), + }) + } + + #[test] + fn stack_safe_reporter_matches_pubgrub_for_shared_nodes() { + let marker_environment = MarkerEnvironment::try_from(MarkerEnvironmentBuilder { + implementation_name: "cpython", + implementation_version: "3.12.0", + os_name: "posix", + platform_machine: "x86_64", + platform_python_implementation: "CPython", + platform_release: "", + platform_system: "Linux", + platform_version: "", + python_full_version: "3.12.0", + python_version: "3.12", + sys_platform: "linux", + }) + .expect("valid marker environment"); + let python_requirement = PythonRequirement::from_marker_environment( + &marker_environment, + RequiresPython::greater_than_equal_version(&Version::new([3_u64, 12])), + ); + let included_versions = FxHashMap::default(); + let available_versions = FxHashMap::default(); + let workspace_members = BTreeSet::default(); + let formatter = PubGrubReportFormatter { + included_versions: &included_versions, + available_versions: &available_versions, + python_requirement: &python_requirement, + workspace_members: &workspace_members, + tags: None, + }; + + let package = PubGrubPackage::from(PubGrubPackageInner::Root(None)); + let external1 = + DerivationTree::External(External::NotRoot(package.clone(), Version::new([1_u64]))); + let external2 = + DerivationTree::External(External::NotRoot(package.clone(), Version::new([2_u64]))); + let external3 = DerivationTree::External(External::NotRoot(package, Version::new([3_u64]))); + let shared = derived(external1.clone(), external2.clone(), Some(1)); + let unshared = derived(external2.clone(), external3.clone(), None); + + let trees = [ + derived(shared.clone(), external3.clone(), None), + derived(external3.clone(), shared.clone(), None), + derived(shared.clone(), unshared, None), + derived(shared.clone(), shared, None), + ]; + + for tree in trees { + assert_eq!( + report(&tree, &formatter), + DefaultStringReporter::report_with_formatter(&tree, &formatter) + ); + } + } +} From 77609dee72866e35d8eb3ecef8ccb6f7de909a74 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 14:28:48 -0400 Subject: [PATCH 7/8] Add RustDoc --- crates/uv-resolver/src/error.rs | 18 +++++++++++++++++- crates/uv-resolver/src/pubgrub/report.rs | 5 ++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 916e7ebf00ef5..d7878821981fe 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -173,11 +173,17 @@ pub type ErrorTree = DerivationTree, UnavailableR const DERIVATION_TREE_RED_ZONE: usize = 64 * 1024; /// Run one recursive derivation-tree step with enough stack to reach the next growth point. +/// +/// Recursive tree walkers should call this at every child edge so no individual stack segment +/// needs to accommodate the full depth of the derivation tree. pub(crate) fn with_growing_stack(callback: impl FnOnce() -> R) -> R { stacker::maybe_grow(DERIVATION_TREE_RED_ZONE, min_stack_size(), callback) } -/// Visit the packages in a derivation tree without recursive calls. +/// Visit each distinct package in a derivation tree without recursive calls. +/// +/// The iteration order is unspecified, matching the set semantics of +/// [`DerivationTree::packages`]. pub(crate) fn derivation_tree_packages( derivation_tree: &ErrorTree, ) -> impl Iterator { @@ -208,6 +214,10 @@ pub(crate) fn derivation_tree_packages( packages.into_iter() } +/// Drop an exclusively owned derivation tree without recursing through its children. +/// +/// Shared [`Arc`] children are left for their remaining owners; once the last owner is processed, +/// [`Arc::try_unwrap`] exposes the child for iterative destruction. fn drop_derivation_tree(derivation_tree: ErrorTree) { let mut trees = vec![derivation_tree]; @@ -223,6 +233,10 @@ fn drop_derivation_tree(derivation_tree: ErrorTree) { } } +/// Own a derivation tree whose destruction must not recurse through the process stack. +/// +/// The `Option` allows [`Drop`] to take ownership of the tree and applies the same iterative +/// teardown during normal returns and unwinding. struct StackSafeErrorTree(Option); impl StackSafeErrorTree { @@ -257,6 +271,7 @@ impl Drop for StackSafeErrorTree { } } +/// Preserve PubGrub's [`Debug`] representation while growing the stack at each tree edge. struct StackSafeDebugErrorTree<'a>(&'a ErrorTree); impl Debug for StackSafeDebugErrorTree<'_> { @@ -273,6 +288,7 @@ impl Debug for StackSafeDebugErrorTree<'_> { } } +/// Format a derived incompatibility with stack-safe wrappers around its recursive causes. struct StackSafeDebugDerived<'a>(&'a Derived, UnavailableReason>); impl Debug for StackSafeDebugDerived<'_> { diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 27c3c4e3ffcee..64e32f3e8d1dc 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -53,7 +53,8 @@ pub(crate) struct PubGrubReportFormatter<'a> { /// Render a PubGrub report while growing the stack between recursive steps. /// -/// This mirrors [`pubgrub::DefaultStringReporter`], whose recursive entry point is private. +/// This preserves the output and shared-node reference behavior of +/// [`pubgrub::DefaultStringReporter`], whose recursive entry point is private. pub(crate) fn report( derivation_tree: &ErrorTree, formatter: &PubGrubReportFormatter<'_>, @@ -68,6 +69,7 @@ pub(crate) fn report( } } +/// Accumulates the report state used by [`report`] while bounding recursion depth per stack. #[derive(Default)] struct StackSafeReporter { ref_count: usize, @@ -76,6 +78,7 @@ struct StackSafeReporter { } impl StackSafeReporter { + /// Render one derived incompatibility, growing the stack before descending further. fn build_recursive( &mut self, derived: &Derived, UnavailableReason>, From 46bb6037fb30eeab535be152f1778ebccfd75103 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 14:36:30 -0400 Subject: [PATCH 8/8] Simplify stack-safe resolver tree handling --- crates/uv-resolver/src/error.rs | 291 +++++++++++++++++--------------- 1 file changed, 155 insertions(+), 136 deletions(-) diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index d7878821981fe..0c1de9f7feb2a 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -172,10 +172,10 @@ pub type ErrorTree = DerivationTree, UnavailableR const DERIVATION_TREE_RED_ZONE: usize = 64 * 1024; -/// Run one recursive derivation-tree step with enough stack to reach the next growth point. +/// Run a recursive derivation-tree function with enough stack to reach the next growth point. /// -/// Recursive tree walkers should call this at every child edge so no individual stack segment -/// needs to accommodate the full depth of the derivation tree. +/// Recursive tree walkers should call this at every recursive descent so no individual stack +/// segment needs to accommodate the full depth of the derivation tree. pub(crate) fn with_growing_stack(callback: impl FnOnce() -> R) -> R { stacker::maybe_grow(DERIVATION_TREE_RED_ZONE, min_stack_size(), callback) } @@ -237,6 +237,7 @@ fn drop_derivation_tree(derivation_tree: ErrorTree) { /// /// The `Option` allows [`Drop`] to take ownership of the tree and applies the same iterative /// teardown during normal returns and unwinding. +#[derive(Clone)] struct StackSafeErrorTree(Option); impl StackSafeErrorTree { @@ -271,6 +272,12 @@ impl Drop for StackSafeErrorTree { } } +impl Debug for StackSafeErrorTree { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + StackSafeDebugErrorTree(self).fmt(f) + } +} + /// Preserve PubGrub's [`Debug`] representation while growing the stack at each tree edge. struct StackSafeDebugErrorTree<'a>(&'a ErrorTree); @@ -310,7 +317,7 @@ impl Debug for StackSafeDebugDerived<'_> { /// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report. pub struct NoSolutionError { - error: Option>, + error: StackSafeErrorTree, index: InMemoryIndex, /// The versions that were available for each package after `exclude-newer` filtering. /// @@ -365,7 +372,7 @@ impl NoSolutionError { options: Options, ) -> Self { Self { - error: Some(error), + error: StackSafeErrorTree::new(error), index, included_versions, available_versions, @@ -387,12 +394,6 @@ impl NoSolutionError { } } - fn error(&self) -> &ErrorTree { - self.error - .as_ref() - .expect("no-solution error is only taken during drop") - } - /// Get the cached report and hints, computing them on first access. fn cached(&self) -> &(String, IndexSet) { self.cached.get_or_init(|| self.compute_report_and_hints()) @@ -526,10 +527,14 @@ impl NoSolutionError { /// Given a [`DerivationTree`], identify the largest required Python version that is missing. pub fn find_requires_python(&self) -> LowerBound { fn find(derivation_tree: &ErrorTree, minimum: &mut LowerBound) { + with_growing_stack(|| find_inner(derivation_tree, minimum)); + } + + fn find_inner(derivation_tree: &ErrorTree, minimum: &mut LowerBound) { match derivation_tree { DerivationTree::Derived(derived) => { - with_growing_stack(|| find(derived.cause1.as_ref(), minimum)); - with_growing_stack(|| find(derived.cause2.as_ref(), minimum)); + find(derived.cause1.as_ref(), minimum); + find(derived.cause2.as_ref(), minimum); } DerivationTree::External(External::FromDependencyOf(.., package, version)) => { if let PubGrubPackageInner::Python(_) = &**package { @@ -546,7 +551,7 @@ impl NoSolutionError { } let mut minimum = LowerBound::default(); - find(self.error(), &mut minimum); + find(&self.error, &mut minimum); minimum } @@ -557,7 +562,7 @@ impl NoSolutionError { /// Get the packages that are involved in this error. pub fn packages(&self) -> impl Iterator { - derivation_tree_packages(self.error()) + derivation_tree_packages(&self.error) .filter_map(|p| p.name()) .unique() } @@ -588,7 +593,7 @@ impl NoSolutionError { }; // Transform the error tree for reporting - let mut tree = StackSafeErrorTree::new(self.error().clone()); + let mut tree = self.error.clone(); simplify_derivation_tree_markers(&self.python_requirement, &mut tree); let should_display_tree = std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE) .is_some() @@ -679,14 +684,7 @@ impl std::fmt::Debug for NoSolutionError { cached: _, } = self; f.debug_struct("NoSolutionError") - .field( - "error", - &StackSafeDebugErrorTree( - error - .as_ref() - .expect("no-solution error is only taken during drop"), - ), - ) + .field("error", error) .field("included_versions", included_versions) .field("available_versions", available_versions) .field("available_indexes", available_indexes) @@ -707,14 +705,6 @@ impl std::fmt::Debug for NoSolutionError { } } -impl Drop for NoSolutionError { - fn drop(&mut self) { - if let Some(error) = self.error.take() { - drop_derivation_tree(error); - } - } -} - impl std::error::Error for NoSolutionError {} impl uv_errors::Hint for NoSolutionError { @@ -760,12 +750,20 @@ fn display_tree_inner( error: &DerivationTree, UnavailableReason>, lines: &mut Vec, depth: usize, +) { + with_growing_stack(|| display_tree_inner_impl(error, lines, depth)); +} + +fn display_tree_inner_impl( + error: &DerivationTree, UnavailableReason>, + lines: &mut Vec, + depth: usize, ) { let prefix = " ".repeat(depth); match error { DerivationTree::Derived(derived) => { - with_growing_stack(|| display_tree_inner(&derived.cause1, lines, depth + 1)); - with_growing_stack(|| display_tree_inner(&derived.cause2, lines, depth + 1)); + display_tree_inner(&derived.cause1, lines, depth + 1); + display_tree_inner(&derived.cause2, lines, depth + 1); for (package, term) in &derived.terms { match term { Term::Positive(versions) => { @@ -803,6 +801,12 @@ fn display_tree_inner( fn collapse_redundant_no_versions( tree: &mut DerivationTree, UnavailableReason>, +) { + with_growing_stack(|| collapse_redundant_no_versions_inner(tree)); +} + +fn collapse_redundant_no_versions_inner( + tree: &mut DerivationTree, UnavailableReason>, ) { match tree { DerivationTree::External(_) => {} @@ -821,7 +825,7 @@ fn collapse_redundant_no_versions( DerivationTree::External(External::NoVersions(package, versions)), ) => { // First, always recursively visit the other side of the tree - with_growing_stack(|| collapse_redundant_no_versions(other)); + collapse_redundant_no_versions(other); // Retrieve the nearest terms, either alongside this node or from the parent. let package_terms = if let DerivationTree::Derived(derived) = other { @@ -853,12 +857,8 @@ fn collapse_redundant_no_versions( } // If not, just recurse _ => { - with_growing_stack(|| { - collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause1)); - }); - with_growing_stack(|| { - collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause2)); - }); + collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause1)); + collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause2)); } } } @@ -901,6 +901,12 @@ fn collapse_redundant_no_versions( /// incompatibilities. fn collapse_redundant_no_versions_tree( tree: &mut DerivationTree, UnavailableReason>, +) -> bool { + with_growing_stack(|| collapse_redundant_no_versions_tree_inner(tree)) +} + +fn collapse_redundant_no_versions_tree_inner( + tree: &mut DerivationTree, UnavailableReason>, ) -> bool { match tree { DerivationTree::External(_) => false, @@ -932,11 +938,8 @@ fn collapse_redundant_no_versions_tree( } // If not, just recurse _ => { - with_growing_stack(|| { - collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause1)) - }) || with_growing_stack(|| { - collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause2)) - }) + collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause1)) + || collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause2)) } } } @@ -948,6 +951,13 @@ fn collapse_redundant_no_versions_tree( fn collapse_no_versions_of_workspace_members( tree: &mut DerivationTree, UnavailableReason>, workspace_members: &BTreeSet, +) { + with_growing_stack(|| collapse_no_versions_of_workspace_members_inner(tree, workspace_members)); +} + +fn collapse_no_versions_of_workspace_members_inner( + tree: &mut DerivationTree, UnavailableReason>, + workspace_members: &BTreeSet, ) { match tree { DerivationTree::External(_) => {} @@ -960,9 +970,7 @@ fn collapse_no_versions_of_workspace_members( (DerivationTree::External(External::NoVersions(package, _)), ref mut other) | (ref mut other, DerivationTree::External(External::NoVersions(package, _))) => { // First, always recursively visit the other side of the tree - with_growing_stack(|| { - collapse_no_versions_of_workspace_members(other, workspace_members); - }); + collapse_no_versions_of_workspace_members(other, workspace_members); // Then, if the package is a workspace member... let (PubGrubPackageInner::Package { name, .. } @@ -980,18 +988,14 @@ fn collapse_no_versions_of_workspace_members( } // If not, just recurse _ => { - with_growing_stack(|| { - collapse_no_versions_of_workspace_members( - Arc::make_mut(&mut derived.cause1), - workspace_members, - ); - }); - with_growing_stack(|| { - collapse_no_versions_of_workspace_members( - Arc::make_mut(&mut derived.cause2), - workspace_members, - ); - }); + collapse_no_versions_of_workspace_members( + Arc::make_mut(&mut derived.cause1), + workspace_members, + ); + collapse_no_versions_of_workspace_members( + Arc::make_mut(&mut derived.cause2), + workspace_members, + ); } } } @@ -1021,6 +1025,12 @@ fn collapse_no_versions_of_workspace_members( /// hole in the range — `A>=1,<3` is not a subset of `A>=2,<3`. fn collapse_redundant_depends_on_no_versions( tree: &mut DerivationTree, UnavailableReason>, +) { + with_growing_stack(|| collapse_redundant_depends_on_no_versions_impl(tree)); +} + +fn collapse_redundant_depends_on_no_versions_impl( + tree: &mut DerivationTree, UnavailableReason>, ) { match tree { DerivationTree::External(_) => {} @@ -1043,16 +1053,8 @@ fn collapse_redundant_depends_on_no_versions( } // If not, just recurse _ => { - with_growing_stack(|| { - collapse_redundant_depends_on_no_versions(Arc::make_mut( - &mut derived.cause1, - )); - }); - with_growing_stack(|| { - collapse_redundant_depends_on_no_versions(Arc::make_mut( - &mut derived.cause2, - )); - }); + collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1)); + collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2)); } } } @@ -1064,6 +1066,16 @@ fn collapse_redundant_depends_on_no_versions_inner( tree: &mut DerivationTree, UnavailableReason>, package: &PubGrubPackage, versions: &Range, +) { + with_growing_stack(|| { + collapse_redundant_depends_on_no_versions_inner_impl(tree, package, versions); + }); +} + +fn collapse_redundant_depends_on_no_versions_inner_impl( + tree: &mut DerivationTree, UnavailableReason>, + package: &PubGrubPackage, + versions: &Range, ) { match tree { DerivationTree::External(_) => {} @@ -1103,16 +1115,12 @@ fn collapse_redundant_depends_on_no_versions_inner( } // If not, just recurse _ => { - with_growing_stack(|| { - collapse_redundant_depends_on_no_versions(Arc::make_mut( - &mut derived.cause1, - )); - }); - with_growing_stack(|| { - collapse_redundant_depends_on_no_versions(Arc::make_mut( - &mut derived.cause2, - )); - }); + collapse_redundant_depends_on_no_versions(Arc::make_mut( + &mut derived.cause1, + )); + collapse_redundant_depends_on_no_versions(Arc::make_mut( + &mut derived.cause2, + )); } } } @@ -1130,6 +1138,13 @@ fn collapse_redundant_depends_on_no_versions_inner( fn simplify_derivation_tree_markers( python_requirement: &PythonRequirement, tree: &mut DerivationTree, UnavailableReason>, +) { + with_growing_stack(|| simplify_derivation_tree_markers_inner(python_requirement, tree)); +} + +fn simplify_derivation_tree_markers_inner( + python_requirement: &PythonRequirement, + tree: &mut DerivationTree, UnavailableReason>, ) { match tree { DerivationTree::External(External::NotRoot(pkg, _)) => { @@ -1153,18 +1168,14 @@ fn simplify_derivation_tree_markers( (pkg, term) }) .collect(); - with_growing_stack(|| { - simplify_derivation_tree_markers( - python_requirement, - Arc::make_mut(&mut derived.cause1), - ); - }); - with_growing_stack(|| { - simplify_derivation_tree_markers( - python_requirement, - Arc::make_mut(&mut derived.cause2), - ); - }); + simplify_derivation_tree_markers( + python_requirement, + Arc::make_mut(&mut derived.cause1), + ); + simplify_derivation_tree_markers( + python_requirement, + Arc::make_mut(&mut derived.cause2), + ); } } } @@ -1174,6 +1185,12 @@ fn simplify_derivation_tree_markers( /// version. fn collapse_unavailable_versions( tree: &mut DerivationTree, UnavailableReason>, +) { + with_growing_stack(|| collapse_unavailable_versions_inner(tree)); +} + +fn collapse_unavailable_versions_inner( + tree: &mut DerivationTree, UnavailableReason>, ) { match tree { DerivationTree::External(_) => {} @@ -1192,7 +1209,7 @@ fn collapse_unavailable_versions( DerivationTree::External(External::Custom(package, versions, reason)), ) => { // First, recursively collapse the other side of the tree - with_growing_stack(|| collapse_unavailable_versions(other)); + collapse_unavailable_versions(other); // If it's not a derived tree, nothing to do. let DerivationTree::Derived(Derived { @@ -1264,12 +1281,8 @@ fn collapse_unavailable_versions( } // If not, just recurse _ => { - with_growing_stack(|| { - collapse_unavailable_versions(Arc::make_mut(&mut derived.cause1)); - }); - with_growing_stack(|| { - collapse_unavailable_versions(Arc::make_mut(&mut derived.cause2)); - }); + collapse_unavailable_versions(Arc::make_mut(&mut derived.cause1)); + collapse_unavailable_versions(Arc::make_mut(&mut derived.cause2)); } } } @@ -1286,6 +1299,13 @@ fn collapse_unavailable_versions( fn drop_root_dependency_on_project( tree: &mut DerivationTree, UnavailableReason>, project: &PackageName, +) { + with_growing_stack(|| drop_root_dependency_on_project_inner(tree, project)); +} + +fn drop_root_dependency_on_project_inner( + tree: &mut DerivationTree, UnavailableReason>, + project: &PackageName, ) { match tree { DerivationTree::External(_) => {} @@ -1317,25 +1337,15 @@ fn drop_root_dependency_on_project( } // Recursively collapse the other side of the tree - with_growing_stack(|| drop_root_dependency_on_project(other, project)); + drop_root_dependency_on_project(other, project); // Then, replace this node with the other tree *tree = other.clone(); } // If not, just recurse _ => { - with_growing_stack(|| { - drop_root_dependency_on_project( - Arc::make_mut(&mut derived.cause1), - project, - ); - }); - with_growing_stack(|| { - drop_root_dependency_on_project( - Arc::make_mut(&mut derived.cause2), - project, - ); - }); + drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause1), project); + drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause2), project); } } } @@ -1582,6 +1592,22 @@ fn simplify_derivation_tree_ranges( included_versions: &FxHashMap>, candidate_selector: &CandidateSelector, resolver_environment: &ResolverEnvironment, +) { + with_growing_stack(|| { + simplify_derivation_tree_ranges_inner( + tree, + included_versions, + candidate_selector, + resolver_environment, + ); + }); +} + +fn simplify_derivation_tree_ranges_inner( + tree: &mut DerivationTree, UnavailableReason>, + included_versions: &FxHashMap>, + candidate_selector: &CandidateSelector, + resolver_environment: &ResolverEnvironment, ) { match tree { DerivationTree::External(external) => match external { @@ -1631,22 +1657,18 @@ fn simplify_derivation_tree_ranges( }, DerivationTree::Derived(derived) => { // Recursively simplify both sides of the tree - with_growing_stack(|| { - simplify_derivation_tree_ranges( - Arc::make_mut(&mut derived.cause1), - included_versions, - candidate_selector, - resolver_environment, - ); - }); - with_growing_stack(|| { - simplify_derivation_tree_ranges( - Arc::make_mut(&mut derived.cause2), - included_versions, - candidate_selector, - resolver_environment, - ); - }); + simplify_derivation_tree_ranges( + Arc::make_mut(&mut derived.cause1), + included_versions, + candidate_selector, + resolver_environment, + ); + simplify_derivation_tree_ranges( + Arc::make_mut(&mut derived.cause2), + included_versions, + candidate_selector, + resolver_environment, + ); // Simplify the terms derived.terms = std::mem::take(&mut derived.terms) @@ -1815,7 +1837,7 @@ mod tests { .stack_size(256 * 1024) .spawn(|| { let tree = StackSafeErrorTree::new(deep_derivation_tree()); - let _formatted = format!("{:?}", StackSafeDebugErrorTree(&tree)); + let _formatted = format!("{tree:?}"); })?; assert!(thread.join().is_ok()); @@ -1826,16 +1848,13 @@ mod tests { fn stack_safe_debug_matches_pubgrub_debug() { let package = PubGrubPackage::from(PubGrubPackageInner::Root(None)); let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64]))); - let tree = ErrorTree::Derived(Derived { + let tree = StackSafeErrorTree::new(ErrorTree::Derived(Derived { terms: pubgrub::Map::default(), shared_id: Some(1), cause1: Arc::new(leaf.clone()), cause2: Arc::new(leaf), - }); + })); - assert_eq!( - format!("{tree:?}"), - format!("{:?}", StackSafeDebugErrorTree(&tree)) - ); + assert_eq!(format!("{:?}", &*tree), format!("{tree:?}")); } }