Indirect attribute macro application leads to compilation error, direct application does not

I'm facing an issue that has me stumped. tl;dr: I'm encountering a compiler error when using macro_rules_attr to apply a macro-generated attribute. Compilation works fine when I apply the attribute directly. cargo expand shows no difference between the two versions. Huh?

Consider the following setup, if you will.

# Cargo.toml
[package]
name = "wtf_macros"
version = "0.0.0"
edition = "2024"

[dependencies]
macro_rules_attr = "0.1.3"
proptest = "1.7.0"
test-strategy = "0.4"
// main.rs
macro_rules! prop_test {
    ($item:item) => {
        #[test_strategy::proptest]
        $item
    };
}

#[macro_rules_attr::apply(prop_test)]
fn foo(#[strategy(0..2)] _v: i32) {}

fn main() {}

It's a bit roundabout, but should work. At least, I think that it should work. The compiler doesn't think so:

    Checking wtf_macros v0.0.0 (/home/code/wtf_macros)
error: could not compile `wtf_macros` (bin "wtf_macros" test) due to 1 previous error
error[E0425]: cannot find value `args` in this scope
 --> src/main.rs:9:19
  |
9 | fn foo(#[strategy(0..2)] _v: i32) {}
  |                   ^ not found in this scope
  |
help: consider importing this function
  |
1 + use std::env::args;
  |

If I change the main.rs like so:

- #[macro_rules_attribute::apply(prop_test)]
+ #[test_strategy::proptest]

then everything works perfectly fine.

What puzzles me the most is that cargo expand reports no difference between the two versions. How can there be no difference, but one version compiles and the other does not? Is cargo expand the wrong tool for the job? How can I go about debugging this? I'm happy about any pointers, really.

I believe this is a hygienic issue, probably in the test_strategy::Arbitrary derive macro. the macro_rules_attribute is unlikely the root cause of the problem.

I suggest you to ask the maintainer of test_strategy for help.

here's what I found out playing around:

this compiles ok:

- #[macro_rules_attribute::apply(prop_test)]
+ #[test_strategy::proptest]

but this does NOT, it gives the same error for macro_rules_attr::apply:

proptest!(
    fn foo(#[strategy(0..2)] _v: i32) {}
);

the #[test_strategy::proptest] attribute macro generates a struct like this:

#[cfg(test)]
#[derive(test_strategy::Arbitrary, Debug)]
struct _FooArgs {
	#[strategy(0..2)]
	_v: i32,
}

in the error case, you only see the error with cargo test, NOT cargo build, so I assume the #[test_strategy::proptest] attribute macro is good, but the error originated from the further expansion of the test_strategy::Arbitrary derive macro.

so I did another experiment with a macro:

macro_rules! arbitrary {
	($item:item) => {
		#[derive(Debug, test_strategy::Arbitrary)]
		$item
	};
}

and applying #derive(test_strategy::Arbitrary)] compiles fine without error:

#[derive(Debug, test_strategy::Arbitrary)]
struct Bar {
	#[strategy(0..2)]
	x: i32,
}

but it gave same error if it was applied from a macro_rules:

/// error: can't find `args` in this scope
arbitrary!(
	struct Bar {
		#[strategy(0..2)]
		x: i32,
	}
);

and when hovering over the squiggles under (0..2), rust-analyzer hints to this snipptes in the expanded code:

impl Arbitrary for Bar {
	//...
	fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
		//...
		#[allow(unused_variables)]
		let args = std::rc::Rc::new(args);
		proptest::strategy::Strategy::boxed({
			// ---- vvvv ----
			// rust-analyzer points here when (0..2) is hoverred:
			#[allow(dead_code)]
			#[allow(non_snake_case)]
			fn _strategy_of_x<T: std::fmt::Debug, S: proptest::strategy::Strategy<Value = T>>(
				s: S,
			) -> impl proptest::strategy::Strategy<Value = T> {
				s
			}
			let strategy_0 = _strategy_of_x::<i32, _>({
				// ---- vvv ----
				// this `args` is probably what the compile error refers to:
				#[allow(unused_variables)]
				let args = std::ops::Deref::deref(&args);
				0..2
			});
			let strategy_0 = proptest::strategy::Strategy::prop_map(strategy_0, |_x| Self {
				x: _x,
			});
			strategy_0
		})
	}
}

I took a quick look at the Arbitrary derive macro, and this snippet is expanded like this:

I suspect the args identifier in the #expr fragment was generated using the wrong span, since #expr was parsed from the helper attributes, which had so-called mixed-site hygiene because it was transformed by a macro_rules.

this is just my hypothesis. I examined the code based purely on syntax analysis.

however, in order to understand the StrategyBuilder, I need to know the semantics of the test-strategy crate, which I didn't, so I'll stop here.

you should report this issue to test-strategy, I suggest you to link this thread in the report, which would hopefully help the maintainer save a little bit of time finding the root cause.

2 Likes

Thanks for your time, your detective work, and for showing me how to go about debugging similar issues in the future. :detective: In hindsight, it makes perfect sense that macro hygiene is to blame.