Open GrigorenkoPV opened 1 week ago
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.
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 left_val
and right_val
to __left_val
and __right_val
(like c++ does) to make it even less of a problem
We could have assert_eq!
(and other macros) use left_val@_
(etc) to give an explicit "match bindings cannot shadow constants" error if someone has a const 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).
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 problem
Well, technically C++ also forbids user-created identifiers to start with _
IIRC, but yeah.
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).
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) {
// …
}
}
}
)
);
}
😰
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