Tracking issue for RFC 2011: Generic assert
This is a tracking issue for the RFC 2011 (rust-lang/rfcs#2011).
The actual steps here are mostly just "implement this" but will likely start off by moving assert! to a procedural macro defined in the compiler. After this has been done the exact format and "nicer message" improvements will likely be debated on the PRs themselves.
This is a tracking issue for rust-lang/rfcs#2011. The feature gate for the issue is
#![feature(generic_assert)].Sub tracking-issue: #96949.
About tracking issues
Tracking issues are used to record the overall progress of implementation. They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions. A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature. Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.
Steps
- [x] Implementation (see section Implementation history)
- [ ] Final comment period (FCP)^1
- [ ] Stabilization PR
Blockers
Implementation history
- #97233
- #97450
- #97665
- #98148
- #98337
- #111928
- #135139
- #135712
Oli Scherer at 2022-05-16 06:43:50
@alexcrichton The last time that you and I discussed this (in March 2013: https://github.com/rust-lang/rust/pull/5436#issuecomment-15142762) you noted that
assert_eqhas different and potentially unexpected semantics compared to straightassert, in thatassert_eqchecks for equality commutatively, whereasassertdoes not.If this RFC were to be implemented, we would either need to decide whether to make
assert!(foo == bar)also assert whenbar == foo, which is technically a breaking change. The alternative is to leave the behavior ofassert!(foo == bar)as-is, but then it no longer has the exact same semantics asassert_eq, which the RFC intends to deprecate. I think this is a pretty important unanswered question.Is the idea that nobody needs the commutative equality, so we should just deprecate
assert_eqwithout duplicating its semantics?bstrie at 2017-09-27 18:53:11
@bstrie your concern was obsoleted in 2015 :)
Alex Burka at 2017-09-27 20:18:51
Glad to hear it. :)
bstrie at 2017-09-27 20:21:27
Quick question, is it expected for this to have some sort of switch to less-nice error messages? Sometimes people care about code size, and including a bunch of debug formatters on release builds may not be acceptable.
Emilio Cobos Álvarez at 2018-02-07 12:35:37
This was one of the concerns in the RFC, and following are some points:
- The performance impact may be minimal, as the first step is just making assert having assert_eq's functionality.
- If notable regression was found, we may decide to disable some of the rules for release builds.
- For certain critical path, debug assertions or custom formatters may be used to minimize (control) the performance impact, just as before.
Tatsuyuki Ishi at 2018-02-07 12:48:02
I am trying to implement this, but have not been successful so far. The problem is that we don't know if a operand is
Copyduring macro expansion.Status quo(https://github.com/sinkuu/rust/commit/ba00a0effd4c51f7c98247b994661a6db31ea7de):
assert!(a == b && c)expands to
let mut __capture0 = None; let mut __capture1 = None; let mut __capture2 = None; // `try_copy` copies it if it implements `Copy` if !({ let __tmp = a; __capture0 = __tmp.try_copy(); __tmp } == { let __tmp = b; __capture1 = __tmp.try_copy(); __tmp } && { let __tmp = c; __capture2 = __tmp.try_copy(); __tmp }) { panic!("assertion failed: a == b && c\nwith expansion: {:?} == {:?} && {:?}", ...); }But this moves non-
Copyoperands intoassert, and causes use-after-move afterward.@ishitatsuyuki Do you have concrete implementation strategies in mind?
Shotaro Yamada at 2018-03-08 04:05:34
@sinkuu Nice work! However,
PartialEqtakes self by reference, which means you don't have to copy/clone it. In this case, we may have to call the trait methods directly instead of the operators.In the case of moving Ops like
Add, your approach is good and should just work.Another thing is that we should use our own enum instead of
Optionhere, as we need to handle the case of non-Copy type and the case of short-circuit differently.Tatsuyuki Ishi at 2018-03-08 04:34:40
You might be able to use the tricks that
assert_eq!uses to take references to the arguments as well.Jake Goulding at 2018-03-08 13:47:43
I’m probably not meant to comment now given the RFC has been merged, but spare a thought for if it’s possible to line up both sides of an == vertically above and below one another as that can make things much easier to spot the difference. Really love this rfc btw, looking forward to it.
Squirrel at 2018-03-21 15:57:31
@gilescope You're welcome to comment on how we should form the message. The RFC is just meant to be a brief proposal, and there are libraries in other languages that has much more helpful expansion outputs. (Example: https://github.com/power-assert-js/power-assert)
Tatsuyuki Ishi at 2018-03-21 16:03:13
As some of the people who attempted to implement might have noticed, the "fallback" mechanism cannot be implemented in a sound way with specialization. I regret that I added that "fallback" mechanism in the last minute of RFC discussion without thinking about its interaction with the type system. Sorry in advance.
The values should be printed with Debug, and a plain text fallback if the following conditions fail:
- the type doesn't implement Debug.
- the operator is non-comparison (those in std::ops) and the type (may also be a reference) doesn't implement Copy.
It's tempting to make a trait like
AlwaysDebugwith a fallback implementation as the default and a specialized implementation forT: Debug; however, this is unsound in the existence of impls likeimpl Debug for SomeType<'static>.With this impl, it is expected that the specialized
Debugimplementation is only called if the lifetime parameter is 'static. However, since lifetimes are erased on codegen, we cannot statically determine this. Boom.This is basically one of the rejected cases in the "always applicable impl" specialization proposal.
And even if we don't use specialization directly (one of the ideas at the time of the proposal was to "rely on compiler internals" to determine if the
Debugimpl or fallback should be used), the same problem arises. Think about the following function:fn test<'a, 'b>(a: SomeType<'a>, b: SomeType<'b>) { assert!(a == b); }Since this function monomorphic (is not compiled as a generic function), it's basically impossible to determine whether the fallback should be used or not.
There is also the "specialize conservatively" idea, but it introduces different drawbacks and is unlikely to be implemented in the compiler.
At this point, it's pretty likely that the RFC needs a revision (or it might be dropped). Two obvious choices are:
- Make this a backward-incompatible change and bundle it into the next edition.
- Make a new assert macro that people needs to migrate to.
Thoughts?
Tatsuyuki Ishi at 2021-08-23 09:23:40
A conservative approach would be to require the types to implement
Debug(if they don't, it's an error). To useassert!()with a!Debugtype the user would have to wrap the expression into a block explicitly:assert!({ a == b }). This restriction could then be relaxed later.Sergey Bugaev at 2021-08-23 09:32:44
A conservative approach would be to require the types to implement
Debug(if they don't, it's an error). To useassert!()with a!Debugtype the user would have to wrap the expression into a block explicitly:assert!({ a == b }).I don't see how this is conservative? This is a breaking change as a lot of existing code were written with types that do not implement Debug.
Tatsuyuki Ishi at 2021-08-23 10:01:39
With this impl, it is expected that the specialized
Debugimplementation is only called if the lifetime parameter is 'static.I think it would be ok for
test::<'a, 'b>to always call the non-specialized implementation, even when called with'a == 'b == 'static.Simon Sapin at 2021-08-23 10:02:12
I don't see how this is conservative?
It's conservative in the sense that you don't have to figure out how to make specialization sound now, so you'd only make it work with a (large) subset of types. Then later it could be extended to types that don't implement (or conditionally implement)
Debug.This is a breaking change as a lot of existing code were written with types that do not implement Debug.
The new
assert!()macro could be predicated on an edition change.Sergey Bugaev at 2021-08-23 10:17:38
I think it would be ok for test::<'a, 'b> to always call the non-specialized implementation, even when called with 'a == 'b == 'static.
That matches aturon's proposal but impls like
impl Debug for Box<T> where T: Debugcomplicate things. It has also received less attention than "always applicable impls" and I think it's unlikely to get implemented in the compiler.Tatsuyuki Ishi at 2021-08-23 10:40:18
I've implemented a stable version of this RFC in the anyhow crate's
ensure!macro, without needing specialization. I believe that analogous generated code would work forassert!in libcore as well. Making the behavior conditional onDebugimpls is handled by autoref-based stable specialization.use anyhow::{ensure, Result}; #[derive(Debug, PartialEq)] enum Kind { File, Directory, Symlink, } fn main() -> Result<()> { let kind = Kind::Symlink; ensure!(kind == Kind::File); Ok(()) }Condition failed: `kind == Kind::File` (Symlink vs File)and if you comment out the
Debugderive:Condition failed: `kind == Kind::File`David Tolnay at 2021-11-22 19:56:43
Just a quick mention of some prior art, in case it makes a helpful comparison: Python's
pytestframework has done something like this for a while. (Using I-don't-know-what interpreter magic to hook into the builtinassertkeyword.) Here's a simple example:X = 1 Y = 2 Z = 3 def test_foo(): assert X > Y + Z$ pytest example.py ============================= test session starts ============================== platform linux -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-0.13.1 rootdir: /tmp collected 1 item example.py F [100%] =================================== FAILURES =================================== ___________________________________ test_foo ___________________________________ def test_foo(): > assert X > Y + Z E assert 1 > (2 + 3) example.py:7: AssertionError =========================== short test summary info ============================ FAILED example.py::test_foo - assert 1 > (2 + 3) ============================== 1 failed in 0.01s ===============================Jack O'Connor at 2021-11-22 22:09:56
What is the status of this feature? Is someone at least trying implementing it?
Caio at 2022-03-31 20:43:22
A few people previously tried but I don't think anyone is still working on it. dtolnay's solution seems promising and if you are interested in implementing you should use their code as a starting point.
Tatsuyuki Ishi at 2022-04-01 10:01:39
A few people previously tried but I don't think anyone is still working on it. dtolnay's solution seems promising and if you are interested in implementing you should use their code as a starting point.
@ishitatsuyuki Thanks, I shall give it a try
Caio at 2022-04-01 10:20:20
Implementation is waiting for review -> https://github.com/rust-lang/rust/pull/96496
Caio at 2022-05-20 18:25:08
The only thing blocking progress is the lack of formatting in constant environments, i.e., things like
const _: () = { panic!("{}", 1i32); };don't work. This current limitation is something that I will try to figure out after enabling the capture of variables inmatches!declarations.Hope to come back here with good news in the next few months.
Caio at 2022-10-31 11:43:07
The only thing blocking progress is the lack of formatting in constant environments, i.e., things like
const _: () = { panic!("{}", 1i32); };don't work. This current limitation is something that I will try to figure out after enabling the capture of variables inmatches!declarations.@c410-f3r IIUC the most straightforward implementation (where you write
constand~constin a few places and rustc does the rest) requires dynamic dispatch in CTFE, which in turn requiresconst fn()function pointers. This is becauseFormatterinternally holds and writes to adyn fmt::Write.Here's a little const formatting demo i threw together a couple months ago: https://gist.github.com/Sky9x/e11cb7ba0772289c05172ebda7dcaeda the code is pretty crap but it all leads to this line:
const MEOW: &str = const_format!("meow", " :3 ", -59999, ' ', true, ' ', 'x', '3');where
MEOWis a&'static strand after const evaluation it has the content"meow :3 -59999 true x3".I couldn't be bothered to implement template parsing so instead it just formats and concatenates everything into one big buffer, then returns it as a
&'static str. The macro will also panic if called at runtime.However, that API is not suitable for use in
stdbecause it makesFormatterandDisplay::fmtgeneric over the writer type, which is a breaking change.Sky at 2025-02-23 19:50:05
The intention is to "constify" all methods and related traits of https://github.com/rust-lang/rust/blob/master/library/core/src/asserting.rs, which is on the library side.
If additional support is necessary on the compiler side beyond the ongoing
#[const_trait]effort, then it is probably wise to just skipgeneric_assertin constant environments.Caio at 2025-02-24 10:38:12