-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
assert_eq!
is not 100% hygienic
#131446
Comments
Unfortunately if we do this --- a/library/core/src/macros/mod.rs
+++ b/library/core/src/macros/mod.rs
@@ -41,6 +41,11 @@ macro_rules! panic {
#[allow_internal_unstable(panic_internals)]
macro_rules! assert_eq {
($left:expr, $right:expr $(,)?) => {
+ {
+ #[expect(dead_code)]
+ fn left_val(){}
+ #[expect(dead_code)]
+ fn right_val(){}
match (&$left, &$right) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
@@ -52,6 +57,7 @@ macro_rules! assert_eq {
}
}
}
+ }
};
($left:expr, $right:expr, $($arg:tt)+) => {
match (&$left, &$right) { then this starts to compile let x: () = ();
assert_eq!(x, {left_val()}); so still not 100% hygienic. |
I see 3 ways of dealing with this:
|
If this hasn't been a problem for the past 9 years we could change the name of |
We could have |
Well, technically C++ also forbids user-created identifiers to start with
This sounds like a good option. Not ideal, but given it is an obscure issue in the first place, probably good enough. |
fn feed<Arg, R>(
arg: Arg,
f: impl FnOnce(Arg) -> R,
) -> R {
f(arg)
}
macro_rules! assert_eq {
(
$left:expr, $right:expr $(,)?
) => (
$crate::feed(
(&$left, &$right),
{
fn _left_val() {}
fn _right_val() {}
|(_left_val, _right_val)| {
if !(*_left_val == *_right_val) {
// …
}
}
}
)
);
} 😰 |
I wonder how it will play with the stability system. |
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 I suppose this is a fourth way for your consideration, @GrigorenkoPV. |
Or just a more concrete version of the 3rd. Technically, from what I can tell, in
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 As for the details of which exact spelling to use, $ would probably be conflicting with the existing macro syntax a bit. |
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. #![feature(decl_macro)]
// const ident: u8 = 2;
macro hygeine($a:expr) {
let ident = $a;
println!("{ident}");
}
fn main() {
hygeine!(1);
} |
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 #![feature(decl_macro)]
const ident: u8 = 2;
macro hygeine($a:expr) {
let ident = $a;
println!("{ident}");
}
fn main() {
hygeine!(1); // fails
} This one works, since the macro definition does not see the #![feature(decl_macro)]
macro hygeine($a:expr) {
let ident = $a;
println!("{ident}");
}
fn main() {
const ident: u8 = 2;
hygeine!(1); // works
} |
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 |
cc @rust-lang/wg-macros |
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. |
I'm not the most versed on how pub macro some_macro() {
let ${ident(x)} = 10;
println!("{}", ${ident(x)});
} The Or, a whole new category of identifiers could be created specifically for this purpose. Since identifiers are a struct pub struct Ident {
pub name: String,
pub span: Span,
pub origin: Origin
}
enum Origin {
Code,
MacroGenerated {
originating_macro: String,
originating_id: u64
}
} The Again, I'm not entirely sure if this would work with how |
I don't quite see why we'd need to generate unique identifiers for this. The issue here lies with If one replaces the macro definition with the unstable #![feature(decl_macro)]
macro assert_eq {
($left:expr, $right:expr $(,)?) => {
match (&$left, &$right) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
unimplemented!()
}
}
}
}
}
fn main() {
let x: u8 = 0;
const left_val: i8 = -123;
assert_eq!(x, 0);
} That 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 |
Not sure if this should be considered a bug or a diagnostic issue.
Having a
const left_val
orconst right_val
declared breaksassert_eq!
. This has to do with its expansion and Rust's rules for macro hygiene: https://sabrinajewson.org/blog/truly-hygienic-letConsider this code
according to
cargo expand
it expands toSince
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 amatch
to bind them to a pattern(left_val, right_val)
. However, having aconst
namedleft_val
orright_val
in scope changes the meaning of the pattern.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 inUPPER_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 preventleft_val
from being interpreted as a const in patterns.@rustbot label A-macros A-diagnostics C-bug D-confusing D-terse
The text was updated successfully, but these errors were encountered: