Repository navigation
Description
RFC 1445 introduced the notion of a type supporting "structural match", which enables constants of that type to be used in patterns. The goal was to only let primitive types and types with derive(PartialEq, Eq) participate in patterns. Since then the #[structural_match] attribute was replaced by two traits: StructuralPartialEq and StructuralEq. More recently, a value-based analysis was introduced as an alternative to the type-based traits.
In the past, if my understanding is correct, this was only used as a kind of "lint" to avoid strange behavior on match. (Though there are some question marks here and some matches on constants can affect exhaustiveness checking, which is crucial for soundness.) But my understanding is that recently, it also started playing a role around const generics -- and thus the entire story could become critical for soundness, and I start to become interested. ;) So far, the traits mentioned above are safe, so in principle nothing can depend on them for soundness. They are also unstable though, so any such soundness issue would affect just nightly Rust.
There is a lot of good documentation on when and how these traits are implemented, but what I am missing is good documentation on what the trait means. I have the feeling that there is a very concrete guarantee associated with these traits, a guarantee that const generics and match both would like to rely on. I'd like to "tease out" that guarantee from the existing trait and analysis with the benefit of hindsight, and then hopefully that enables us to simplify this story, or at least document it better.
So, here is what I think the guarantee is, based on what I saw so far:
- A value
vhas structural equality if (a) it can be converted to a value tree, and (b)PartialEq::eq(v, v2)returnstrueif and only if the value tree representation ofv2equals that ofv(in particular, ifv2cannot be represented in a value tree, the return value must befalse). - A type has structural equality if all of its values do.
Now I wonder, does this make sense? Of course this is slightly hypothetical as value trees are not implemented yet, but in most cases I think it is intuitively clear what the tree for a value looks like -- and when it is not clear, it likely has no tree. (In particular, unions, raw pointers [except when cast from an integer] and function pointers cannot be represented in a value tree.) I think this is exactly the guarantee that const generics need, but I only have a birds-eye view of const generics so I am not certain of this. And this also seems closely related to the idea of "which const values are reasonable to use as a pattern". I'd be interested in any counterexamples that you might know of.
If this definition works, then I think we could replace both of the Structural* traits by a single marker trait that just represents this guarantee. But to evaluate if that definition makes sense, we first have to figure out what we want from "structural equality".
EDIT (2021-05): Also see this more recent analysis.
EDIT (2023-09): Here's an update of the current status. My view changed quite a bit due to how the compiler developed over the last 2 years.
Open problems
There is one problem that I know of so far: the StructuralEq trait docs have this example
#[derive(PartialEq, Eq)]
struct Wrap<X>(X);
fn higher_order(_: &()) { }
const CFN: Wrap<fn(&())> = Wrap(higher_order);
fn main() {
match CFN {
CFN => {}
_ => {}
}
}However, function pointers do not have a well-behaved notion of equality (Rust functions are "unnamed" in LLVM terms), and as such they are not suited for a value tree representation. Indeed function pointers should not be used for const generics; I assume there is some safeguard in place that rejects even some "structural equality" values/types for const generics. Moreover higher-order function pointers do not implement PartialEq.
Curiously, floats are allowed in patterns but show a future-compat warning. If floats are problematic even though they have deterministic equality, then for sure we could deprecate matching on function pointers whose equality is non-deterministic and can change when codegen units are partitioned differently!
But leaving that aside, I could imagine handling this by changing (b) to say that if T implements PartialEq, then this property has to hold (so values of types not implementing PartialEq are fine as long as we can put them into a valtree). For (a) I could imagine an "extended valtree" that permits function pointers (represented as a ty::Instance<'tcx>) in its leaves; we would only permit such leaves for "structural equality checking for matches" but strictly disallow them for constant generics.
Activity
oli-obk commentedon Jul 22, 2020
So, one thing I was surpised about but now think is important is that structural equality is not a recursive property. If we used a marker trait, that would mean that
[T; 3]is only has structural equality ifThas it. But we can destructure such an array trivially, we just can't continue destructuring theTif that doesn't have structural equality. We have to fall back toT::eqto check for equality.So if we accept that we can match on
!StructuralEqtypes, but keep falling back to runtime equality (so no exhaustiveness or duplication checking, meaning a secondCFNpattern in your example would stop linting about unreachable arms), then we keep backwards compatibility to all existing code, but just allow arbitrary matching.Note that you can work around any of our checks by using a reference in your constant: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=dc3b8eb7eb67cfaa0c0a38c0526a9ed9
Constants of reference type already have this PartialEq behaviour, we can just extend it to everything. This is in conflict with an existing RFC #70743 (comment) but I don't know how we can get the behaviour of the RFC without breaking existing code.
RalfJung commentedon Jul 24, 2020
Indeed. Condition (a) that I listed above is recursive (except for unions), but condition (b) is not as
PartialEqcould be overwritten to not match value tree equality any more.I am confused by this and the rest of your post, what do you mean here? It all seems to pertain only to matching and not do const generics?
What exactly is this working around?
oli-obk commentedon Jul 24, 2020
Well, either we find rules that apply to both (which my post was assuming), and thus we can't leave out patterns, or we need to find different rules for patterns and const generics. And as far as I can tell, without doing breaking changes we can't just convert constants in patterns to valtrees
If you change the constants to use
Wrapdirectly instead of&Wrap, you get a hard errorRalfJung commentedon Jul 24, 2020
Oh I see. So somehow the "structural equality" test for matching assumes that all references have structural equality? That seems like a rather gaping hole in the match story.^^
oli-obk commentedon Jul 24, 2020
You're very welcome to help fix this hole by reviewing #70743 ^^
RalfJung commentedon Jul 24, 2020
That seems to be about what actually happens during pattern building though, not about the "structural eq" analysis? What I would have expected is a test there that shows that a constant of type
&NonStructuralEqTypeis no longer accepted as a pattern.oli-obk commentedon Jul 24, 2020
well, that's what's happening now, because we're essentially doing the const to value tree conversion, but the value tree is the pattern data structure. So if we move to actual value trees, we'll have a unified system and the value tree -> pattern conversion will become trivial and infallible. Though there's also the open question of whether it's ok to have
NonStructuralEqTypes in patterns and just use the runtimePartialEq::eqfunction for it, which is something that can't be done for const generics. So we may just need different systems here.RalfJung commentedon Jul 24, 2020
I guess it depends on what you mean by "okay". I think it is "okay" for soundness as long as we ignore those arms for exhaustiveness checking. But we have an RFC with the clear intent of not allowing such matches. OTOH, the implementation of that RFC seems to have gaping holes.
oli-obk commentedon Jul 24, 2020
Yea, I don't know if we can resolve this and maybe I should write a counter RFC. The breaking changes necessary to do the existing RFC don't feel like breaking changes we can do with our stability guarantees.
oli-obk commentedon Jul 24, 2020
We don't really need to resolve this for const generics though, we can just specify the const generics rules separately
RalfJung commentedon Jul 24, 2020
It would be reasonable IMO to at least put the soundness-relevant parts of pattern matching, and const generics, on common footing (like the notion of "structural equality" I proposed above). Pattern matching can have an "escape hatch", with nodes in the value/pattern tree that just hold opaque consts and that are ignored by exhaustiveness checking. The main issue I see here is that we want value trees without such opaque consts for const generics, but we also want to construct the pattern match tree from the value tree -- and those constraints are mutually exclusive, as far as I can see.
Another thing: one could argue that for const generics, it hardly matters what
PartialEq::eqdoes. But it seems like that notion of equality differing from the one used by the type system could be rather confusing.petrochenkov commentedon Jul 24, 2020
We can always do a crater run, estimate the breakage and then emit deprecation warnings until the next edition if necessary.
RalfJung commentedon Jul 24, 2020
Basically, that would be an extension of the existing future compat lint for matching on floats. It would also cover matching on function pointers, raw pointers, and
&TwhereTdoes not allow matching.2 remaining items
RalfJung commentedon Jul 28, 2020
If we decide that we cannot or do not want to entirely forbid patterns that cannot be represented as a value tree, we need a form of "fallback" leaf for the value tree that points to CTFE memory (i.e., something like the current
ConstValue::ByRef).In that case, we have to ensure that value trees participating in const generics do not contain such a node. I could imagine two schemes:
ty::Const<FallbackNodeType>and usety::Const<!>for const generics. This may induce some overhead due to having to convert between the different kind of value trees.assert_no_fallbackmethod onty::Constthat we call on every const used in a type. We better make sure we do not forget to do this call...Either way, we'll end up with some perf and complexity hit, so IMO it is worth the experiment to see how widely such patterns are even used. After all, at least for floats this was considered worthwhile; they are future-incompat since forever; maybe we can get away with the same for raw and function pointers.
Cc @rpjohnst who mentioned in rust-lang/const-eval#11 that they use raw pointers in patterns.
oli-obk commentedon Jul 28, 2020
I've come around to trying to do the breaking change that would allow us to move to value trees. I've done some experiments yesterday and it's not so easy, because the reason we are in today's mess is that I avoided some ICEs by doing the problematic destructurings. I've never been able to figure out these ICEs because they are in the middle of our exhaustiveness checks, whose paper I've never read and whose implementation I've never understood.
rpjohnst commentedon Jul 28, 2020
All the raw pointers I (plan to) use in patterns are
staticaddresses. I would expect those to be supportable asty::Constleaf nodes, even in types, at some point in the future- does that seem plausible? They should be able to function as opaque/abstract names, much like types themselves, since address stability is the raison d'etre ofstatic.RalfJung commentedon Jul 28, 2020
Hm... that argument only works for non-ZST statics, and I feel like actually supporting this is a long way off.
But indeed, if I interpret this code correctly then it is indeed a pointer to a (non-ZST) static (and not a pointer to some memory that was created for the initial value of a static).
RalfJung commentedon May 15, 2021
I wrote down a summary and my latest thoughts on this here on Zulip:
My personal thinking (which is some variant of the links I posted above) is roughly as follows:
In general, the value of a constant (
const/staticbody, but also more general anyConstantMIR operand, which includes all literals) inside rustc is just an unstructured blob of bytes (andAllocation, in technical terms). This is sufficient for when aconstis just used in code. (There's a slight inaccuracy here in that, for efficiency reasons, we do not actually create anAllocationfor each integer literal, so we have more efficient representations for some very simple constants. This is not relevant for theStructuralEqdiscussion.)However, there are 2 things the compiler does where an "unstructured blob" view of a constant is insufficient:
const, if we want exhaustiveness checking to properly take theconstinto account (i.e., "as-if" theconstwould have been written inline as a normal pattern). Note that if we don't do exhaustiveness checking then we can still treat theconstas a blob; this is sound if exhaustiveness checking considers such a match arm to not match anything, thus enforcing the addition of a fallback_arm. In this case thematchcan be implemented by callingPartialEq.PartialEqthanmemcmp, i.e., comparing references by comparing their pointees and skipping padding bytes.Not all consts of all types can be treated in this more structured way, so this is where the need of a
StructuralEqconstraint arises.To support these usecases, the compiler will use "valtrees", a high-level, more structural representation of a constant. A valtree is a tree where the leaves carry integer values and both pointer indirection and "is a field of" are represented as edges between nodes.
So e.g.
2would be a valtree with just a single node carrying a 2;(2, 2)would be a valtree where the root points to two leaf nodes that carry a 2.&2would be a valtree where the root points to a leaf node that carries a 2.(2,)would be the same valtree as&2; that's okay -- valtrees are meant to capture everything about a value that matters at a high level; comparing valtrees of different types is not very meaningful.Pattern matching exhaustiveness checking can use valtrees to basically convert a constant into a regular pattern and then treat it as-if that pattern had been written literally. Const generics can compare two valtrees (where inner nodes are compared by recursively comparing the children) to determine equality of const values.
Now, which values are safe to be converted into valtees? I think this is the class of values that we want to say has "structural equality", and then we can say that
T: StructuralEqif all values ofThave "structural equality". (Note that e.g. the valueNone::<T>has structural equality for allT, that's why it makes sense to distinguish value-level and type-level notions here. I am not suggesting we actually do anything like this in rustc, but I think it is good to develop the theory in this style. And if we want to allow non-NAN floats in const generics, we do have to take this approach.)There are some values which we simply cannot turn into valtrees, so they are out, most notably all values of union type (because a union is just a bag of bytes, there's no high-level representation we can turn that into). But we also quickly realize that there is a deep link with equality here: the behavior of pattern matching against a constant should ideally be equivalent to testing equality with
PartialEq. And likewise for const generics, when two constants are equal according toPartialEq, we better treat them as "equal" from the perspective of the type system. This guides us to the following key constraint that I think must be satisifed to ensure things make sense:STRUCT1: Given two values
v1,v2of typeT: PartialEq, if both can be converted to a valtree then the valtrees are equal if and only ifv1 == v2. Moreover, ifv1can be converted to a valtree andv2cannot, then!(v1 == v2).(Remember that "can be converted to a valtree" is equivalent to "has structural equality".)
This is the basic minimum. I feel like we might want to strengthen this to make it easier to reason about; making definitions conditional on whether a type implements a trait is tricky business (if we aren't careful, adding a
PartialEqimpl can now break this property! We saw withPinhow easily this goes wrong). AlsoPartialEqallows some "whacky" non-reflexive equality relations which don't make these things easier to think about. So I think we want a property more like:STRUCT2: When a value
v1of typeTcan be converted to a valtree, thenT: Eqand for anyv2: T, their valtrees are equal if and only ifv1 == v2(in particular ifv2cannot be converted into a valtree, then!(v1 == v2)).As above, a type
TisStrucuralEqif all its values can be converted to a valtree. This lends itself toward saying that the trait should be declared asclarfonthey commentedon May 26, 2022
So, not really sure where is best to mention this, but figured I'd mention this here. I have two mostly disjoint things, so, I'll separate them out.
First, it would be useful to support more forms of structural equality than the existing kind, specifically the kind that ignores certain fields. Equivalence classes like this are completely valid conceptually, and one is even used on the documentation for Eq:
I don't think there'd be any specific issues with allowing constants of type
Bookto be used in match statements, and it would simply allow all books with the same ISBN, regardless of their format. I don't think it'd be that difficult to implement, either, but I also haven't touched any of the code for structural equality.This would presumably be part of some derive attribute (like the current
#[default]that recently got stabilised), where something like#[compare(ignore)]could be marked on individual fields of a type that derivesEqorPartialEq. I use#[compare]here because it would presumably also apply toPartialOrd,Ord, andHashimplementations, but those aren't necessary for structural equality, at least for now.The second part actually applies to what I just mentioned… could there be the potential for structural ordering as well? It would be truly fantastic if code like this could eventually work:
This bit is obviously a lot harder and I don't expect it any time soon, but it would be nice if the current way structural equality were defined would be compatible with an extension like this in the future.
RalfJung commentedon May 28, 2022
The first point, I think, pretty much means the compiler has to make no assumptions about how comparing values of that type actually works, has to always require a fallback case since exhaustiveness checking is impossible, and compiles the
matchto anif elsechain of equality tests. That is certainly possible; this is what we can / have to apply when we do do not have structural equality.The second point requires expanding how ranges are handled in patterns; I think it is mostly out-of-scope for this issue but an interesting extension to consider when we got the basics sorted out.
RalfJung commentedon Sep 15, 2023
I think clarifying the rules here is blocked on us first even having coherent rules -- currently it's a fallback logic that is hard to reason about. So this is kind of blocked on #62411 I think; also see my recent question there.
RalfJung commentedon Sep 15, 2023
After reading through #62411 and seeing changes like #67343, I wonder if I've been thinking about this all wrong. Maybe "structural equality" really is just a lint. But then we should make it one. Maybe the rules should just be:
==, recursively all the way down). This is, I think, exactly what the custom_eq MIR qualif finds.Then the entire StructuralEq trait becomes merely a lint and it's completely okay for it to be safe.
There is a separate question of how exactly exhaustiveness checking works. That I am much less sure about. Right now, how exactly do we decide whether we want to "look into" a constant for exhaustiveness checking? At that point we better be sure that its PartialEq is structural, this becomes a soundness question! (At least if we want to say that match always behaves like PartialEq, which I think we do.)
And then there's const generics, which is what triggered this entire thread to begin with. But for that we now have
ConstParamTy, and that's a recursive property. It is a safe trait, which IMO makes little sense, but it's now decoupled from StructuralEq. (Well they still interact, it requires StructuralEq as well.)RalfJung commentedon Sep 15, 2023
I went through the code, and I think the answer is: if the constant
then we turn the constant into a detailed
Patthat is used for exhaustiveness checking.So the presence of the Structural(Partial)Eq trait does impact whether we look inot a constant for exhaustiveness checking or not. That can't be unsound per se since
impl StructuralPartialEq for Twill then also mean we actually don't call thePartialEqfor matching, we deconstruct the value directly. But that feels like exposing an implementation detail to me; I'd rather be able to say that we always use PartialEq semantics -- but then the trait needs to be unsafe.RalfJung commentedon Sep 16, 2023
After digging a bit more through our pattern matching code, I think here's the end state I'd like us to achieve for pattern matching on consts:
PartialEq. The semantics we guarantee is that matching behaves like==. (This is currently actually not the case, see lint towards rejecting consts in patterns that do not implement PartialEq #115893.)PartialEqtypes, and if valtree construction succeeds then valtree equality matches==.unsafe trait StructuralPartialEq : PartialEq {}, a trait that guarantees thatPartialEqon this type behaves the same as comparing all fields structurally. (This is a shallow property, it relates==on the fields with==on the type.)derive(PartialEq)automatically implement this trait; we could in principle eventually allow users to implement it as well. When valtree construction hits an ADT type, it checks whether that the type implementsStructuralPartialEq, and fails if that is not the case.==.)==to execute the match test at runtime). For some of these cases we might have warn-by-default lints informing the user that their==is a bit odd and doesn't match what one might expect formatchsemantics.We don't absolutely need the custom_eq MIR qualif for this; we could alternatively just call
search_for_structural_match_violationto determine whether we treat the constant transparently or opaquely. That would mean we honor constants for exhaustiveness checking only if their type is recursivelyStructuralPartialEq. That's the semantics I would have expected, but given that we have this MIR qualif implemented now, I guess we can make use of it to get better exhaustiveness checking? Not sure if it's worth it, I'm fine either way.Compared to today, this is a breaking change for some obscure corner cases that #115893 is intended to lint against. We can also get rid of the
StructuralEqtrait. We will accept a lot more constants in pattern-matching -- everything that implementsPartialEq. The subtle rules around structural-match only affect whether the value of a constant can be used for exhaustiveness checking, not whether the constant can be used in a match at all.Const generics are mostly orthogonal, except that they share the ValTree infrastructure, so
ConstParamTyhas to ensure that the type is valtree-constructible with the invariants given above. I think that's a property we want anyway, that const generics only work for types whose==matches ValTree==(and in particular, only for types that have==).RalfJung commentedon Sep 17, 2023
Concretely, here is the alternative that avoids the custom_eq MIR qualif:
StructuralPartialEq(stillunsafe, still hasPartialEqas supertrait) to be "all values of this type can be turned into a valtree, and its==agrees with valtree==. (Actually then we can haveEqas supertrait I think.) This is a deep recursive property. Valtree construction just checks this trait once before traversing the value.This has the downside of being a breaking change for code like this.
MyEnumis not recursivelyStructuralPartialEq, so it would be treated opaquely, so the compiler would considerV1(0)as not being covered by a match arm.This has the upside of likely getting us back the perf that was lost here.
RalfJung commentedon Apr 15, 2024
With rust-lang/rfcs#3535 being accepted, this is largely resolved. The remaining work is tracked at #120362.