Description
Not sure if this should be considered a bug or a diagnostic issue.
Having a const left_val
or const right_val
declared breaks assert_eq!
. This has to do with its expansion and Rust's rules for macro hygiene: https://sabrinajewson.org/blog/truly-hygienic-let
Consider this code
fn main() {
let x: u8 = 0;
assert_eq!(x, 0);
}
according to cargo expand
it expands to
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
let x: u8 = 0;
match (&x, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
}
Since assert_eq!
wants to use the value of the provided expressions twice (once for comparison, once for printing the result on failure), but it only wants to evaluate each expression once, it does a match
to bind them to a pattern (left_val, right_val)
. However, having a const
named left_val
or right_val
in scope changes the meaning of the pattern.
fn main() {
let x: u8 = 0;
const left_val: i8 = -123;
assert_eq!(x, 0);
}
error[E0308]: mismatched types
--> src/main.rs:4:5
|
3 | const left_val: i8 = -123;
| ------------------ constant defined here
4 | assert_eq!(x, 0);
| ^^^^^^^^^^^^^^^^
| |
| expected `&u8`, found `i8`
| this expression has type `(&u8, &{integer})`
| `left_val` is interpreted as a constant, not a new binding
| help: introduce a new binding instead: `other_left_val`
|
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0614]: type `i8` cannot be dereferenced
--> src/main.rs:4:5
|
4 | assert_eq!(x, 0);
| ^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
The error message, admittedly, is not very helpful.
Thankfully, you can't use this to make assert_eq
pass/fail when it shouldn't. The worst you can achieve is a cryptic error message from the compiler. I think. So this "bug" is not really exploitable, plus chances of accidentally breaking this are probably pretty low (const
s are usually named in UPPER_CASE
in Rust), but the diagnostic is admittedly not very helpful.
The article I've linked above (https://sabrinajewson.org/blog/truly-hygienic-let) offers a potential solution for this. TL;DR: due to shadowing shenanigans having a function named left_val
will prevent left_val
from being interpreted as a const in patterns.
@rustbot label A-macros A-diagnostics C-bug D-confusing D-terse
Activity
assert_eq
#131443GrigorenkoPV commentedon Oct 9, 2024
Unfortunately if we do this
then this starts to compile
so still not 100% hygienic.
GrigorenkoPV commentedon Oct 9, 2024
I see 3 ways of dealing with this:
oriongonza commentedon Oct 9, 2024
If this hasn't been a problem for the past 9 years we could change the name of
left_val
andright_val
to__left_val
and__right_val
(like c++ does) to make it even less of a problemzachs18 commentedon Oct 9, 2024
We could have
assert_eq!
(and other macros) useleft_val@_
(etc) to give an explicit "match bindings cannot shadow constants" error if someone has aconst left_val
in scope (the left hand side of a@
pattern is (currently) always an identifer pattern; it is not superseded by path/const patterns like "bare" identifier patterns are).GrigorenkoPV commentedon Oct 9, 2024
Well, technically C++ also forbids user-created identifiers to start with
_
IIRC, but yeah.This sounds like a good option. Not ideal, but given it is an obscure issue in the first place, probably good enough.
danielhenrymantilla commentedon Oct 11, 2024
😰
GrigorenkoPV commentedon Oct 19, 2024
feed
would need to bepub
.I wonder how it will play with the stability system.
rpitasky commentedon Oct 21, 2024
What if the language had a way to create temporary variables in macros perfectly hygienically? Ex. Suppose we permit creating variables in macro_rules! that start with
$
, just like the macro arguments (i.e.let $left_val = 10;
). These variables would have to have some clever scoping rules to prevent problems across macro expansions, but this would wholly eliminate this hygiene problem (at least, between macros and regular code, because$
is not a valid way to start an identifier in regular code).I suppose this is a fourth way for your consideration, @GrigorenkoPV.
GrigorenkoPV commentedon Oct 21, 2024
Or just a more concrete version of the 3rd. Technically, from what I can tell, in
let a = b
,a
is treated like a pattern, so it can beconst
, in which case it gets used as a name for the new bindingconst
, in which case it turns into aconst
pattern1
,'a'
, or"foo"
Typename(inner)
orTypename{field: inner}
none of the above require any external information to disambiguate between, except the first two, which is our case. This, by itself, is not an issue 99.999% of the times, because you are the one who writes the code and controls what identifiers exist, but it turns out to not be the case inside macros.
So without any major changes to scoping rules, this can be fixed by any syntax that would allow to disambiguate the two without relying on the external info. For example, an attribute like
#[rustc_this_is_not_const]
or some symbol like~
or$
, which would say "never treat this as a const", would do.As for the details of which exact spelling to use, $ would probably be conflicting with the existing macro syntax a bit.
GrigorenkoPV commentedon Oct 21, 2024
Anyways, cc #39412 as their declarative macros
still suffer from this problem, but at least they are still unstable, so it is possible to do breaking changes to the scoping rules and whatnot(sorry, misinformation).zachs18 commentedon Oct 21, 2024
Actually, it looks like macros 2.0 handles this correctly (or at least, how I would expect)
This one I would expect to fail, since the macro definition also sees the
const
.This one works, since the macro definition does not see the
const
.GrigorenkoPV commentedon Oct 23, 2024
Oh, yes, you are right. I have indeed misplaced it. Thank you for the correction.
That's good news that macros 2.0 solve this problem. So I guess given that this particular issue is probably of a rather low priority, we can afford to just wait however many years it takes for macros 2.0 to get stabilized. Or maybe we can rush ahead and change
assert_eq
to be a new shiny kind of macro now, but that can probably cause more breakage than it potentially fixes.traviscross commentedon Nov 15, 2024
cc @rust-lang/wg-macros
jhpratt commentedon Nov 15, 2024
My ideal solution to this (and other similar situations) is to give macro authors a way to generate a unique identifier out of thin air, presumably with some sort of seed so that it is deterministic.
akarahdev commentedon Dec 3, 2024
I'm not the most versed on how
rustc
works, but I imagine it could work something like this:The
${ident(...)}
metavariable expression would allow you to generate a new identifier out of thin air. It could generate a new identifier that's guaranteed to be unique by hashing the identifier through some hashing method to a short string, then prepending various underscores until it's now a unique identifier.Or, a whole new category of identifiers could be created specifically for this purpose. Since identifiers are a struct
rustc_span::symbol::Ident
, a new flag could be added to differentiate whether the identifier is coming from a macro or not.The
originating_macro
in theMacroGenerated
variant would be the name of the macro (or maybe the Id that refers to it in Macros 2.0) to minimize conflict by tracking origin. But what if the same macro nests itself?originating_id
would do that. When a macro is expanding, each time it expands, a value somewhere is increased by1
, let's call itMACRO_TOTAL_DEPTH
. After expanding,MACRO_TOTAL_DEPTH
would not be reset to give each macro a unique expansion.Again, I'm not entirely sure if this would work with how
rustc
works, or maybe I have a logic error. This is just an idea I have.Veykril commentedon Dec 3, 2024
I don't quite see why we'd need to generate unique identifiers for this. The issue here lies with
macro_rules
macros having mixed site/semi-transparent hygiene which doesn't play well with patterns as is shown that they can refer to local variables (where they'd have def site/opaque hygiene) or consts/items (where they would have call site/transparent hygiene).If one replaces the macro definition with the unstable
macro
which uses def site/opaque hygiene the error disappears https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=3eba965a7069578d09a4ba82c4c6016bThat is we already have a concept to fix this, its just not finished design wise. So there isn't really a need for any new idea here in my opinion. (Except for possibly a metavar expression to change hygiene of something, as
macro
as designed today is far too rigid wrt to hygiene to be useful)maxdexh commentedon Jun 12, 2025
This also affects
r#try!
anddbg!
.