Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / derivation_tree.rs
15 kB 411 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2025 The Gleam contributors 3 4use crate::error::wrap; 5use ecow::EcoString; 6use hexpm::version::Version; 7use im::HashSet; 8use itertools::Itertools; 9use petgraph::Direction; 10use petgraph::algo::all_simple_paths; 11use petgraph::graph::NodeIndex; 12use petgraph::prelude::StableGraph; 13use pubgrub::External; 14use pubgrub::{DerivationTree, Derived, Ranges}; 15use std::collections::HashMap; 16use std::hash::RandomState; 17use std::ops::Bound::{Excluded, Included, Unbounded}; 18use std::sync::Arc; 19 20macro_rules! wrap_format { 21 ($($tts:tt)*) => { 22 wrap(&format!($($tts)*)) 23 } 24} 25 26/// Makes a best effort at turning a derivation tree into a nice readable error 27/// message. 28/// 29pub struct DerivationTreePrinter { 30 derivation_tree: DerivationTree<String, Ranges<Version>, String>, 31 32 /// The name of the root package for which we're trying to add new 33 /// dependencies. This is the starting point we use to find and report 34 /// dependency conflicts! 35 root_package_name: EcoString, 36 37 /// The graph of dependencies built from the derivation tree. The nodes are 38 /// packages and the arcs connecting them represent a dependency: 39 /// 40 /// ```txt 41 /// wibble ---- (range1, range2) ---> wobble 42 /// ``` 43 /// 44 /// Means "package wibble with version `range1` requires package wobble 45 /// with version `range2`". 46 /// 47 dependencies: StableGraph<String, (Ranges<Version>, Ranges<Version>)>, 48 49 /// A map going from package name to its index in the dependencies graph. 50 /// 51 nodes: HashMap<String, NodeIndex>, 52} 53 54impl DerivationTreePrinter { 55 pub fn new( 56 root_package_name: EcoString, 57 mut derivation_tree: DerivationTree<String, Ranges<Version>, String>, 58 ) -> Self { 59 // We start by trying to simplify the derivation tree as much as 60 // possible. 61 derivation_tree.collapse_no_versions(); 62 simplify_derivation_tree(&mut derivation_tree); 63 64 let mut dependencies = StableGraph::new(); 65 let mut nodes = HashMap::new(); 66 build_dependencies_graph(&derivation_tree, &mut dependencies, &mut nodes); 67 68 DerivationTreePrinter { 69 root_package_name, 70 derivation_tree, 71 dependencies, 72 nodes, 73 } 74 } 75 76 pub fn print(&self) -> String { 77 self.explanation_for_missing_version() 78 .or_else(|| self.explanation_for_complex_failure()) 79 .unwrap_or_else(|| self.fallback_explanation()) 80 } 81 82 /// This catches the case in which we try adding a dependency's version that 83 /// doesn't exist. That produces a very simple decision tree with just two 84 /// nodes and can have an ad hoc explanation, telling the user the version 85 /// they tried adding doesn't exist! 86 /// 87 fn explanation_for_missing_version(&self) -> Option<String> { 88 if let DerivationTree::External(External::FromDependencyOf( 89 base, 90 _, 91 package, 92 package_ranges, 93 )) = &self.derivation_tree 94 && *base == self.root_package_name 95 { 96 let pretty_range = pretty_range(package_ranges); 97 let message = if is_single_version(package_ranges) { 98 format!("The package `{package}` doesn't have a version {pretty_range}.") 99 } else { 100 format!("The package `{package}` has no versions in the range {pretty_range}.",) 101 }; 102 Some(message) 103 } else { 104 None 105 } 106 } 107 108 /// Tries and print a pretty explanation for the given resolution tree. 109 /// If for some reason our heuristic to produce a nice error message fails 110 /// we return `None` so we can still produce a good enough error message! 111 /// 112 fn explanation_for_complex_failure(&self) -> Option<String> { 113 let root_package_index = self.nodes.get(self.root_package_name.as_str())?; 114 let unresolvable_nodes = self.find_unresolvable_nodes(); 115 if unresolvable_nodes.is_empty() { 116 return None; 117 } 118 119 let mut unresolvable = vec![]; 120 for unresolvable_node in unresolvable_nodes { 121 let paths = all_simple_paths::<Vec<_>, _, RandomState>( 122 &self.dependencies, 123 *root_package_index, 124 unresolvable_node, 125 0, 126 None, 127 ); 128 129 let package = self 130 .dependencies 131 .node_weight(unresolvable_node) 132 .expect("package is in the graph"); 133 134 let heading = format!("There's no compatible version of `{package}`:"); 135 let explanation = paths.sorted().map(|path| self.pretty_path(path)).join("\n"); 136 unresolvable.push(format!("{heading}\n{explanation}")); 137 } 138 Some(unresolvable.join("\n\n")) 139 } 140 141 fn pretty_path(&self, path: Vec<NodeIndex>) -> String { 142 let (you, dependee, rest) = match path.as_slice() { 143 [you, dependee, rest @ ..] => (you, dependee, rest), 144 _ => panic!("path with less than two nodes"), 145 }; 146 147 let dependee_name = self 148 .dependencies 149 .node_weight(*dependee) 150 .expect("path node is in the graph"); 151 let (_, dependee_range) = self 152 .ranges_between(you, dependee) 153 .expect("path edge is in the graph"); 154 155 let mut message = format!( 156 " - You require {dependee_name} {}", 157 pretty_range(dependee_range) 158 ); 159 160 let mut previous = dependee; 161 for next in rest { 162 let previous_name = self 163 .dependencies 164 .node_weight(*previous) 165 .expect("path node is in the graph"); 166 let next_name = self 167 .dependencies 168 .node_weight(*next) 169 .expect("path node is in the graph"); 170 let (_, next_range) = self 171 .ranges_between(previous, next) 172 .expect("path edge is in the graph"); 173 174 message.push_str("\n - "); 175 message.push_str(previous_name); 176 message.push_str(" requires "); 177 message.push_str(next_name); 178 message.push(' '); 179 message.push_str(&pretty_range(next_range)); 180 181 previous = next; 182 } 183 message 184 } 185 186 fn find_unresolvable_nodes(&self) -> Vec<NodeIndex> { 187 self.dependencies 188 .node_indices() 189 .filter(|node_index| { 190 self.dependencies 191 .neighbors_directed(*node_index, Direction::Incoming) 192 .count() 193 > 1 194 }) 195 .sorted() 196 .collect_vec() 197 } 198 199 fn ranges_between( 200 &self, 201 one: &NodeIndex, 202 other: &NodeIndex, 203 ) -> Option<(&Ranges<Version>, &Ranges<Version>)> { 204 let edge = self.dependencies.find_edge(*one, *other)?; 205 self.dependencies 206 .edge_weight(edge) 207 .map(|(one, other)| (one, other)) 208 } 209 210 /// A good enough explanation in case we're not able to produce anything 211 /// nicer. 212 fn fallback_explanation(&self) -> String { 213 let mut conflicting_packages = HashSet::new(); 214 collect_conflicting_packages(&self.derivation_tree, &mut conflicting_packages); 215 216 wrap_format!( 217 "Unable to find compatible versions for \ 218the version constraints in your gleam.toml. \ 219The conflicting packages are: 220 221{} 222", 223 conflicting_packages 224 .into_iter() 225 .map(|s| format!("- {s}")) 226 .join("\n") 227 ) 228 } 229} 230 231fn build_dependencies_graph( 232 derivation_tree: &DerivationTree<String, Ranges<Version>, String>, 233 graph: &mut StableGraph<String, (Ranges<Version>, Ranges<Version>)>, 234 nodes: &mut HashMap<String, NodeIndex<u32>>, 235) { 236 match derivation_tree { 237 DerivationTree::External(External::FromDependencyOf( 238 one, 239 range_one, 240 other, 241 range_other, 242 )) => { 243 let one_index = match nodes.get(one) { 244 Some(index) => *index, 245 None => { 246 let index = graph.add_node(one.clone()); 247 let _ = nodes.insert(one.clone(), index); 248 index 249 } 250 }; 251 252 let other_index = match nodes.get(other) { 253 Some(index) => *index, 254 None => { 255 let index = graph.add_node(other.clone()); 256 let _ = nodes.insert(other.clone(), index); 257 index 258 } 259 }; 260 261 let edges = graph.edges_connecting(one_index, other_index); 262 let edge_weight = match edges.peekable().peek() { 263 Some(edge) => { 264 let (old_range_one, old_range_other) = edge.weight(); 265 ( 266 range_one.union(old_range_one), 267 range_other.union(old_range_other), 268 ) 269 } 270 None => (range_one.clone(), range_other.clone()), 271 }; 272 273 let _ = graph.update_edge(one_index, other_index, edge_weight); 274 } 275 DerivationTree::External(_) => (), 276 DerivationTree::Derived(Derived { cause1, cause2, .. }) => { 277 build_dependencies_graph(cause1, graph, nodes); 278 build_dependencies_graph(cause2, graph, nodes); 279 } 280 } 281} 282 283/// This function collapses adjacent levels of a derivation tree that are all 284/// relative to the same dependency. 285/// 286/// By default a derivation tree might have many nodes for a specific package, 287/// each node referring to a specific version range. For example: 288/// 289/// - package_wibble `>= 1.0.0 and < 1.1.0` requires package_wobble `>= 1.1.0` 290/// - package_wibble `>= 1.1.0 and < 1.2.0` requires package_wobble `>= 1.2.0` 291/// - package_wibble `1.1.0` requires package_wobble `>= 1.1.0` 292/// 293/// This level of fine-grained detail would be quite overwhelming in the vast 294/// majority of cases so we're fine with collapsing all these details into a 295/// single node taking the union of all the ranges that are there: 296/// 297/// - package_wibble `>= 1.0.0 and < 1.2.0` requires package_wobble `>= 1.1.0` 298/// 299/// This way we can print an error message that is way more concise and still 300/// informative about what went wrong, at the cost of 301/// 302fn simplify_derivation_tree(derivation_tree: &mut DerivationTree<String, Ranges<Version>, String>) { 303 match derivation_tree { 304 DerivationTree::External(_) => {} 305 DerivationTree::Derived(derived) => { 306 simplify_derivation_tree(Arc::make_mut(&mut derived.cause1)); 307 simplify_derivation_tree(Arc::make_mut(&mut derived.cause2)); 308 simplify_derivation_tree_outer(derivation_tree); 309 } 310 } 311} 312 313fn simplify_derivation_tree_outer( 314 derivation_tree: &mut DerivationTree<String, Ranges<Version>, String>, 315) { 316 match derivation_tree { 317 DerivationTree::External(_) => {} 318 DerivationTree::Derived(derived) => { 319 match ( 320 Arc::make_mut(&mut derived.cause1), 321 Arc::make_mut(&mut derived.cause2), 322 ) { 323 ( 324 DerivationTree::External(External::FromDependencyOf( 325 package, 326 package_range, 327 required_package, 328 required_package_range, 329 )), 330 DerivationTree::External(External::FromDependencyOf( 331 maybe_package, 332 other_package_range, 333 maybe_required_package, 334 other_required_package_range, 335 )), 336 ) if package == maybe_package && required_package == maybe_required_package => { 337 *derivation_tree = DerivationTree::External(External::FromDependencyOf( 338 package.clone(), 339 package_range.union(other_package_range), 340 required_package.clone(), 341 required_package_range.union(other_required_package_range), 342 )); 343 } 344 345 _ => {} 346 } 347 } 348 } 349} 350 351fn collect_conflicting_packages<'dt>( 352 derivation_tree: &'dt DerivationTree<String, Ranges<Version>, String>, 353 conflicting_packages: &mut HashSet<&'dt String>, 354) { 355 match derivation_tree { 356 DerivationTree::External(external) => match external { 357 External::NotRoot(package, _) 358 | External::NoVersions(package, _) 359 | External::Custom(package, _, _) => { 360 let _ = conflicting_packages.insert(package); 361 } 362 External::FromDependencyOf(package, _, dep_package, _) => { 363 let _ = conflicting_packages.insert(package); 364 let _ = conflicting_packages.insert(dep_package); 365 } 366 }, 367 DerivationTree::Derived(derived) => { 368 collect_conflicting_packages(&derived.cause1, conflicting_packages); 369 collect_conflicting_packages(&derived.cause2, conflicting_packages); 370 } 371 } 372} 373 374fn pretty_range(range: &Ranges<Version>) -> String { 375 range 376 .iter() 377 .map(|(lower, upper)| match (lower, upper) { 378 (Included(lower), Included(upper)) if lower == upper => format!("{lower}"), 379 (Included(lower), Included(upper)) => format!(">= {lower} and <= {upper}"), 380 (Included(lower), Excluded(upper)) => format!(">= {lower} and < {upper}"), 381 (Excluded(lower), Included(upper)) => format!("> {lower} and <= {upper}"), 382 (Excluded(lower), Excluded(upper)) => format!("> {lower} and < {upper}"), 383 384 (Included(version), Unbounded) => format!(">= {version}"), 385 (Excluded(version), Unbounded) => format!("> {version}"), 386 (Unbounded, Included(version)) => format!("<= {version}"), 387 (Unbounded, Excluded(version)) => format!("< {version}"), 388 389 (Unbounded, Unbounded) => "".into(), 390 }) 391 .join(" or ") 392} 393 394fn is_single_version(range: &Ranges<Version>) -> bool { 395 // Note: at the time of writing this, `Ranges` has a method called 396 // `as_singleton` which, according to its doc, should do the same thing. 397 // However, it strangely seems to consider as a single version ranges like 398 // this one `> 11.0.0 and <= 12.0.0`. To me this doesn't read as a single 399 // version! 400 401 // The range needs to have exactly one segment that includes exactly one 402 // version number. 403 let mut segments = range.iter(); 404 if let Some((Included(lower), Included(upper))) = segments.next() 405 && segments.next().is_none() 406 { 407 lower == upper 408 } else { 409 false 410 } 411}