or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Do you want to remove this version name and description?
Syncing
xxxxxxxxxx
Structural Equality
What is this
A write-up concerning the design space around structural equality.
Motivation
Pattern matching and const generics require a stricter notion of equality than
PartialEq + Eq
can promise.Sometimes we need to know that the
PartialEq
impl is equivalent to running thePartialEq
impl of each field and using&&
to combine the results. This is true for automatically derivedPartialEq
impls.When using constants in patterns we can use this knowledge to "destructure" the constant and consider it during exhaustiveness checking. In case this property does not apply to the field of whatever we are matching on we can always fall back to treating the constant as unknown wrt exhaustiveness and call
PartialEq::eq
as if the user would have used a pattern guard instead of a pattern. An example of such fallback (more details later) would beFor const generics it must be a recursive property, as we must be able to process the entire constant to all of its leaves.
Possible changes from current behaviour
Before the next section digs into the details of how everything behaves, this section will give a summary of the possible and suggested (by @oli-obk) changes.
Pattern matching on constants
PartialEq + Eq
is derived (recursively), deconstruct the constant.Note that this is checked for a given value, not the type itself.
Enums can have some variants that are recursively
PartialEq + Eq
while other variants are not.
PartialEq
orEq
is manually implemented anywhere (for fields' types or fields' fields' types…) fall back to invokingPartialEq
at the top level.PartialEq
is not recursive), but not a breaking change[unsafe] impl StructuralEq
or similar.PartialEq + Eq
impl in patterns@oli-obk would prefer option 3, but that is a breaking change. Thus prefers option 2, because it allows completely getting rid of deconstructing
mir::ConstValue
(allocation based) and allows working withty::ValTree
. We will need to keep maintaining two systems for deconstructing constants.RalfJ wants option 2 (but mediated via an
unsafe trait StructuralEq
).@lcnr wants option 2
Floats
Things we could do:
Eq
f32::NAN
matches exactlyf32::NAN
[Ralfj: what about different NAN bit patterns? It is not clear what the semantics of comparison would even be here.]PartialEq
implPartialEq
@oli-obk would like floats to be
StructuralEq
(option 2)RalfJ thinks we can make all floats except for NaN be
StructuralEq
(so the type wouldn't beStructuralEq
but most of its values would). They don't think we can havef32: StructuralEq
while still having meaningful guarantees associated withStructuralEq
… if we don't want to go that route, RalfJ prefers 3.Const generics
StructuralEq
recursive, unsafe + stabilize,fn foo<T: StructuralEq, const C: T>
,unsafe impl StructuralEq for TheirType {}
, which allows divergence ofPartialEq
from structural equality, as long as it still matches some rules for symbol generationunsafe
is irrelevant, as there are no soundness concerns, just weird behaviour and with post monomorphization errors being preventable asStructuralEq
impls can be verified by the compiler (Similar toCopy
impls).StructuralEq
recursive and autogenerateT: StructuralEq
bounds for allT
that are used in const generic parameters' types likefn foo<T, const C: T>
. (@lcnr: autogeneratingT: StructuralEq
bounds seems difficult to get right)@oli-obk proposes to go for option 3, as the effects of allowing users to write
StructuralEq
impls are similar to option 1, but explicitly opted in to.@lcnr strongly favors 2 here, with
StructuralEq
being safe to implement[RalfJ: The difference between 2 and 3 is just whether we allow the user to
unsafe impl StructuralEq
or keep that as a compiler priviledge, right? So we can always start with 3 and remain future compatible with 2?] [oli-obk: yes]Requirements and considerations
The term structural equality is used in two very different situations in the Rust language:
T<C1>
andT<C2>
equal?While the actual details are very similar, the motivation for requiring structural equality is not the same.
Pattern matching
Constants used in patterns participate in exhaustiveness checking. So
complains about an unreachable arm in the second arm.
For aggregated constants, rustc deconstructs the constant into individual patterns, so
could also participate in exhaustiveness checking.
The problem here is that it is unclear whether
Foo
has aPartialEq
impl, and whether it behaves exactly the same way as a derivedPartialEq
impl. While we can obtain this information during const->pat deconstruction, the question is what we do with it.PartialEq
is derived, deconstructing the constant does not change behaviour when compared to just lowering the constant in the pattern to a PartialEq callPartialEq
is manually implemented, deconstructing the constant may change the behaviour, and be very suprisingPartialEq
is not implemented, adding an implementation later may thus change the behaviour of matchesPartialEq
implSo what we do right now is
PartialEq
is derived, deconstruct the constantPartialEq
is manually implemented, emit a compile-time errorPartialEq
PartialEq
is not implemented, emit a compile-time errorFloats
Floats are not structural-eq (they aren't
Eq
after all) [RalfJ: aren't fn ptrs structural-eq even though some of them are not evenPartialEq
?]. Among other things, this is because aNaN
is not equal to itself.Even ignoring different representation of
NaN
, the first match arm will never match anything, because matching is done viaPartialEq
. Amusingly, we do report an "unreachable pattern" lint on the second pattern, because the bits of the two constants are equal.Const generics
From ralfs github comment
The problem is that we can't use
PartialEq
, as that is inherently a runtime operation, and const generics needs to compare constants at compile time, or in the case of symbols for functions with const generic args, we need to make sure that two equal constants generate the exact same symbol.If we just encoded the
Allocation
of a constant into the mangled name of a const generic function, depending on how the constant was created, two equal constants could end up with different mangled names (e.g. because padding bytes could differ).This is avoided in the current implementation by carefully deconstructing the
Allocation
into its components. The confidence in this code is not very high, as it is not very maintainable or extendable. It is also a fallible operation. If we encounter something that cannot be represented, we have no option but to bail out with an error. If we didn't have lots of restrictions on what types are allowed in const generics, we would get post-monomorphization errors whenever a constant has an unrepresentable value.On the implementation side, the proposal is to move to valtrees. For the implementation these have the advantage that they can just be serialized, deserialized and compared directly and require no type-based special handling at any level.
[RalfJ: I view valtrees as also being a nice intermediate representation for const → pattern conversion. So I wouldn't burry them in the const generic section.]
Generic const parameter types
In order to know what further types we can allow for the types of generic constants, we need two things:
When/if we get generic const parameter types (think
fn foo<T, const X: T>()
), we will need a trait bound onT
that makes sure only types are used that satisfy the two rules above.Unifying generic constants
In the future we want to be able to check for equality between generic constants without evaluating them, i.e.
N + 1
andN + 1
should be equal. For this to be sound, structurally equivalent values have to be considered equal by the compiler. While this means that we need some notion of structural equivalence here, this should not influence its design.[RalfJ: What we need for this is a notion of functions respecting structural equality. I can't quite make sense of this subsection.]
Links to other documents
Background concepts
Representing data structures via
Allocation
An
Allocation
is an untyped, opaque "blob of bytes" representation (with explicit undef markings and pointer/relocation markings) for a constant. This is what miri/CTFE uses. Allocations can represent a large range of values, including unions and other types that don't have a structured representation.Representing data structures via
ValTree
A
ValTree
is an "up and coming" alternative representation for a constant value that is more structured. You can think of it as a tree with "scalar values" of various kinds at the leaves:This set of leaf types is carefully chosen to represent only values where a deep notion of equality is possible.
i32
,(i32, i32)
,struct Foo(i32)
or even references give rise to values that can always be represented with a val-tree.Option<*const i32>
, have some values which can be represented with a valtree (None
) and some which cannot (Some(p)
).Valtrees could be used to formalize the exact requirements of
StructuralEq
, if we end up having it as an unsafe trait.Questions
Mark: Is there a description of exactly what the stricter requirements are?
Mark's hypothesis:
match
[T; N + 1] == [T; N + 1]
need 'reliable' comp-time PartialEqRalf: When it comes to exhaustivness checking, the question is really about "decomposition" of a constant into patterns. If we are able to decompose constants into patterns, the impl doesn't matter. So here the concerns are:
Post-monomorphization errors: we have constants sometimes (e.g., with associated constants) whose value we don't know
Weird "non-linearities":
Ralf: This has to do with a part of the compiler that is converting a constant into a Pattern, where a Pattern has
some of the proposal for how we conert constants into Patterns have included "fallback", and that is prone to non-linearities
Mark: What does it mean for a value to be unrepresentable?
Ralf: Rust values like raw pointers can't be represented in some well-defined set for pattern matching, or things like uninitialized memory.
Does it make sense that
StructuralEq
inherit fromEq
?If floats were
StructuralEq
, that would imply that it's notunsafe trait StructuralEq : Eq {}
. Would that be weird?Ralf: I don't think so; StructuralEq is all about the behavior of PartialEq. I don't think Eq has to have much to do with it. But it might be more intuitive if it does.
Ralf: note that we already allow matching on floats and have to deal with that
Oli: not clear that there is a connection between partial-eq and structural-eq exactly, right?
Ralf: two kinds of reasons you might want structural eq
pnkfelix: my memory of why we even have this dispatch into partial eq was more about saving on code size for match generation
ralf: no method, it's a marker trait
pnkfelix: …still, if we choose to divorce partial-eq and structural-eq, there will be some code attached to structural-eq, because you need the code to do the traversal of the constant, right?
ralf: I am not talking about code size, just observable behavior
pnkfelix: even talking about observable semantics, are you ever intending someone to override the behavior to use a structural eq that doesn't correspond like structural traversal?
nikomatsakis: I'm a bit confused what we're talking about, do we mean only constants whose types implement structural eq? e.g. floats
pnkfelix: my interpretation, in terms of matching, if we add structural eq, and we start talk about semantics of match in terms of structural eq…
ralf: semantics of matches is not defined in terms in terms of structural eq, it's defined in terms of the language of patterns
pnkfelix: why does that have to be the case?
pnkfelix: my memory was that the cases where we currently allow structural equality are pretty subtle
ralf: rough structure is that we start with a pattern in surface syntax which may contain constants
pnkfelix: edition boundary might … permit us to change rules for opaque patterns …
ralf: to what?
pnkfelix: to structural eq!
ralf: but then it doesn't have code… that's a corner I hadn't explored, not sure what that code would do
pnkfelix: to me the float situation "boils my bottom"
ralf: floats are primitive patterns, right?
pnkfelix: I don't see the motivation for saying PartialEq is the thing
A possible resolution here would be that this is more like
IntoValTree
, but I think maybe be should move off this specific question, as the other questions might go to the more-interesting parts more helpfully.Q
What does
StructuralEq
"only for some of the values" mean? Does that make it not a normal trait?Copy
only for some values":None::<Vec<i32>>
is, for all intents and purposes,Copy
. This is similar, only the property is more complicated.Q
Ralf: Do we agree that the rules around pattern matching should ensure that adding a
PartialEq
impl, and (if that exists) adding aStructuralEq
(potentially unsafe, then we assume it follows the contract) does not change the behavior of existing matches?Q
Mark: The document states that we can't rely on PartialEq as a "inherently a runtime operation", but we are moving towards const impls. Is
const PartialEq
not a compile-time property?StructuralEq
. We could try to do something like "deconstruct, then runconst PartialEq
on all elements and&&
the results and see if that matches theconst PartialEq
of the parent", but that seems awful.Q
lcnr: What exactly are the safety requirements of an
unsafe StructuralEq
impl and where would we be able to rely on them.StructuralEq
Q
Josh: Do any of your recommendations change if you have an edition boundary to work with? Or, even more strongly, if there were some way to make a difficult transition and not worry about the older semantics? (I'd like to know what we would do if not for compatibility, separate from what we would do as an evolutionary step.)
Q
PartiaEq::eq
in match patterns? My attempt to translate the example to the playground hit: "error: to use a constant of typeNotStructEq
in a pattern,NotStructEq
must be annotated with#[derive(PartialEq, Eq)]
"Q
nikomatsakis: The doc talks a lot about dead patterns, but I think there are soundness concerns involving exhaustiveness, right? For example:
This code, I think, compiles today yes? If we had a
PartialEq
impl that were buggy or what have you, then it might wind up with "no path" to execute and hence an unsound result, correct? (This also applies to running user-defined code mid-match, as that could potentially alter shared data…?)PartialEq
impl. This seems both unlikely to happen as a bug and undesirable to be implemented. So I don't think we would ever do this even ifStructuralEq
was anunsafe
trait. [Ralf: agreed.]UnsafeCell
not permitting matching on its field.Q
nikomatsakis: I have to admit I had a hard time understanding exactly what scheme was being proposed here. Maybe it's because of the range of options. I'm trying to wrap my head around what this "feels like as a user" and not quite there. It seems like:
Q
Example 1:
Example 2:
Example 3:
Ralf's compiler from syntactic patterns to semantic patterns:
match x { C => true, _ => false }
andx == C
to be equivalentOli's example of non-StructuralEq-match
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=dc3b8eb7eb67cfaa0c0a38c0526a9ed9
desugars to
and will call
<Wrap<Foo> as PartialEq>::eq
to pattern-match.because we unroll to the constant: https://github.com/rust-lang/rust/blob/master/compiler/rustc_mir_build/src/thir/pattern/const_to_pat.rs#L337-L340 and catch that unroll here: https://github.com/rust-lang/rust/blob/29d61427ac47dc16c83e1c66b929b1198a3ccc35/compiler/rustc_mir_build/src/thir/pattern/const_to_pat.rs#L508
Q
Josh: What are the goals of this effort? What problems are we trying to solve?
Josh: I felt quite a bit confused about the context. Feels like a conversation-in-progress came to a meeting that includes several people who weren't in that conversation, and continued from where it left off without any context.
Ralf: Current structural-eq semantics might not be what we want (see Oli's example, which AFAIK works by accident). The extact contract of the StructuralEq trait is unclear. Now we want const generics so we need to extend our structural eq story, for which we first need to clean up that story around matches (unless we want a totally independent story for match and const generics, which I doubt).