-
Notifications
You must be signed in to change notification settings - Fork 605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle universal vs. fork markers with ResolverMarkers
#5099
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The improvements here look good.
With respect to MarkerTree
and Option<MarkerTree>
, my idea here was to collapse states that weren't semantically distinct. I now see that the is_universal
check was added later as a way of determining whether the resolver was in "universal" mode or not. (And this is doubly confusing because the resolver is in universal mode when MarkerTree::is_universal
is false.) The idea was that the canonical way of determining whether the resolver was in universal mode or not is whether the resolver has a marker environment, and not whether there's a marker expression that is trivially true or not. So for example, this code:
uv/crates/uv-resolver/src/resolver/mod.rs
Lines 309 to 318 in 545bbf9
if !state.markers.is_universal() { | |
if let Some(requires_python) = state.requires_python.as_ref() { | |
debug!( | |
"Solving split {} (requires-python: {})", | |
state.markers, requires_python | |
); | |
} else { | |
debug!("Solving split {}", state.markers); | |
} | |
} |
Should I think be using self.markers
(which, confusingly, is a MarkerEnvironment
) to determine whether to emit log messages about a split.
This one can too:
uv/crates/uv-resolver/src/resolver/mod.rs
Lines 585 to 590 in 545bbf9
if markers.is_universal() { | |
debug!( | |
"Pre-fork split universal took {:.3}s", | |
start.elapsed().as_secs_f32() | |
); | |
} else { |
But this one is trickier:
uv/crates/uv-resolver/src/fork_urls.rs
Lines 42 to 47 in 545bbf9
return if fork_markers.is_universal() { | |
Err(ResolveError::ConflictingUrls( | |
package_name.clone(), | |
conflicting_url, | |
)) | |
} else { |
And I think it's that case that caused MarkerTree::is_universal
to come into existence. And I agree that is_universal
is Not Good, and that given the choice between "checking for a single trivially true marker expression" and "using an option," the latter is better. But I'm worried about Option<MarkerTree>
carrying subtly different semantics for the None
case depending on where we are in the code. But maybe we don't yet have the right abstractions to deal with this nicely.
if !possible_to_satisfy_markers(markers, requirement) { | ||
trace!("skipping {requirement} because of context resolver markers {markers}"); | ||
return false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm. So I very intentionally used MarkerTree
instead of Option<MarkerTree>
. For example, in this routine, I don't think there is any semantic difference between None
and Some(MarkerTree::And(vec![]))
. They are redundant with one another, which I find confusing, because it injects an extra state into the type system that you need to think about. Moreover, there are perhaps cases where "a marker that is always true" and a "possibly absent marker" are semantically distinct, and IMO that's when we should use Option<MarkerTree>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we ever get a fork with Some(MarkerTree::And(vec![]))
? It seems to me that we shouldn't fork in that case since we're still doing a universal resolution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe so, but that invariant isn't encoded into the type system. It's more like it's manifest in the concept of forking itself. It's not something you can easily employ local reasoning about when looking at just the types.
I think this is a fine change for now. I think this change is probably better than the status quo. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I encoded it in the type system using ResolverMarkers
. Probably needs a fresh read tomorrow morning, but the api feels a good bit better this way.
ResolverMarkers
ResolverMarkers
ResolverMarkers
* Use `Option<MarkerTree>` in the fork state. This is better than the `MarkerTree::And(Vec::new())` check * Report the timing with universal resolution instead of two spaces when there are no markers * On resolution error, show the split that we're in. I'm not sure how to word this, since we're doing a universal resolution until we fork, so the trace may contain information from requirements that are not part of this fork
947f833
to
c0caf1d
Compare
c0caf1d
to
281aaa3
Compare
#[derive(Debug, Default, Clone)] | ||
pub(crate) enum ResolverMarkers { | ||
#[default] | ||
Universal, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm. So I think the confusing thing about this name is that, if I'm understanding correctly, ResolverMarkers::Universal
is used when the resolver is not in universal mode. It has the right semantics, but the name is I think confusable.
I'm actually finding it quite difficult to come up with a good name here. I think the problem is in using the markers (and not the marker environment) to identify whether we're employing a possibly-forking strategy or not.
Maybe... ResolverMarkers::AlwaysTrue
? ¯\_(ツ)_/¯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The markers represent a trinary state - specific environment, universal resolution, resolution in a fork - so i changed the type to express that too.
// If we're in a fork in universal mode, ignore any dependency that isn't part of | ||
// this fork (but will be part of another fork). | ||
if let ResolverMarkers::Fork(markers) = markers { | ||
if !possible_to_satisfy_markers(markers, constraint) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have a newtype for this, I think I'd rather see these case analyses collapsed. Perhaps a method like ResolverMarkers::possible_to_satisfy
or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think this is better. I think we can still collapse/encapsulate case analyses, but that can be a follow-up.
ResolverMarkers
check in the fork state. This is better than theMarkerTree::And(Vec::new())
check.