Skip to content

Rust: expand attribute macros #19334

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

Merged
merged 13 commits into from
May 14, 2025
Merged

Rust: expand attribute macros #19334

merged 13 commits into from
May 14, 2025

Conversation

redsun82
Copy link
Contributor

No description provided.

@github-actions github-actions bot added the Rust Pull requests that update Rust code label Apr 17, 2025
@redsun82 redsun82 force-pushed the redsun82/rust-expand-attr-macros branch from 7c8fc79 to 49cf173 Compare April 25, 2025 14:17
@redsun82 redsun82 changed the title Rust: extract getExpended on Items Rust: expand attribute macros Apr 25, 2025
@redsun82 redsun82 marked this pull request as ready for review April 25, 2025 15:43
@redsun82 redsun82 requested a review from a team as a code owner April 25, 2025 15:43
Copy link
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial comments.

@@ -0,0 +1,2 @@
| attr_macro_expansion.rs:1:1:2:11 | fn foo | attr_macro_expansion.rs:2:4:2:6 | Static |
| attr_macro_expansion.rs:1:1:2:11 | fn foo | attr_macro_expansion.rs:2:4:2:10 | fn foo |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it not the attribute that is expanded? It seems weird that the function can expand to two things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's what the attribute macro does: it takes in an AST (in this case, the definition of a function) and replaces it with possibly multiple items like in this case. So it's not really the attribute that is expanded, it's really the AST node it is attached to that gets entirely replaced.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for clarifying.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the expanded items then be ordered, i.e. getExpanded(int i) instead of just getExpanded()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked a bit more into this. I went with reusing MacroCall's getExpanded, with the same signature. This is defined as returning an AstNode, but it really returns one of MacroStmts | MacroItems | Pat | Type | Expr. In case of attribute macros, this will always be MacroItems, which is just a wrapper around an ordered list of items.

I'm thinking about how we could be more precise here. We would need to have separate names for the property, as MacroCalls are Items, therefore macro call expansion could not have the same name as attribute or derive macro expansions. Maybe we could rename getExpanded to getMacroCallExpansion for MacroCall, and use getAttributeMacroExpansion (returning MacroItems) for this, getDeriveMacroExpansion (same) for the derive macros, and then reunite them all in a getMacroExpansion that would work for all cases (returning a more generic AstNode).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up separating macro call and attribute macro expansions in the DB. We can think whether we need a single predicate to do whatever macro expansion, but we can do that in QL if needed.

@hvitved
Copy link
Contributor

hvitved commented Apr 28, 2025

I have started a DCA run.

{
generated::PathSegment::emit_trait_type_repr(label, t, &mut self.trap.writer)
}
}
}

pub(crate) fn emit_item_expansion(&mut self, node: &ast::Item, label: Label<generated::Item>) {
(|| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a closure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code felt much more natural to write using ? to exit early in case of None results. After all

let x = foo()?;

reads (and writes) much nicer than

let Some(x) = foo() else { return }

But in order to use ?, one needs to be in a function returning Option or Result. So I did that in the context of a closure returning Option<()> which is immediately called

let ExpandResult {
value: expanded, ..
} = semantics.expand_attr_macro(node)?;
// TODO emit err?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to register any failure reason as a diagnostic, so we can keep track of how many macro expansions were successful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after looking a bit at the rust analyzer code, I have the feeling the err in the ExpandResult gets recovered again in parse_macro_expansion_error which is called by our emit_macro_expansion_parse_errors function called just below. So I think we should ignore the err here, as otherwise we would be emitting a diagnostic twice about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that makes sense. I guess that leaves the ? after the cast as a potential error that may be worth reporting. I suppose the result is always a MacroItem, but perhaps it could be just an Item if the macro expands to a single item, or an Error if something went wrong (although in that case I'd expect a parse error to be reported).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, maybe reporting an error there makes sense, just to be defensive. I think attribute and derive macros will always expand to MacroItems, as that's the only context in which they appear.

@@ -0,0 +1,2 @@
#[ctor::ctor]
fn foo() {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some more extensive tests? Covering items with more than one macro attribute, and also some macro attributes that expand to "nothing", like #[cfg(all(unix, not(unix)))] or perhaps even a macro attribute that adds another macro attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in order to do that we would need to define our own attribute macro. That requires a separate crate, so it will probably require some additional work on the qltest side to pull off. Trying something that expand to nothing is easy though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a custom attribute macro in the test requires tweaking the qltest code, I would very much prefer to do that in a follow-up PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, I'm not even sure cfg is really implemented as an attribute macro, even if it behaves like one. There's definitely no instance of regular attribute macro definitions in the standard library, this I checked (I would have used those instead of relying on a third party crate).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I can confirm cfg does not work through attribute macro expansion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an additional integration test (to be able to use an additional crate for the macros) covering the cases you mentioned (using a repeat attribute macro).

@@ -0,0 +1,6 @@
import rust
import TestUtils
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps some "printAst"-like output would be helpful to show the expanded AST?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that, but I find printAst-heavy tests to tend to need a lot of updating. But we can have it in if you prefer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a PrintAst test

@redsun82 redsun82 requested a review from aibaars May 14, 2025 08:12
Comment on lines +4 to +5
| src/lib.rs:6:1:8:11 | fn bar | 0 | src/lib.rs:7:1:8:10 | fn bar_0 |
| src/lib.rs:6:1:8:11 | fn bar | 1 | src/lib.rs:7:1:8:10 | fn bar_1 |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had not expected fn bar_0 and fn bar_1 here. Shouldn't the expansion be as follows?

#[repeat(2)]
#[repeat(3)]
fn bar() {}

-->

#[repeat(3)]
fn bar_0() {}
#[repeat(3)]
fn bar_1() {}

-->

fn bar_0_1() {}
fn bar_0_2() {}
fn bar_0_3() {}
fn bar_1_1() {}
fn bar_1_2() {}
fn bar_1_3() {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, that is the expansion, but all steps of the expansion are in the database. So you get the immediate expansion bar_0 and bar_1 with their repeat attribute attached to them, and then you get bar_i_j as getAttributeMacroExpansion() under each bar_i

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is similar to what we already do with macro call expansions: we don't get the full expansion right away, and each macro call gets in turn expanded within the expansion of the top-level macro call

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect , that makes sense. I was just wondering.

@redsun82 redsun82 merged commit e4b7b91 into main May 14, 2025
21 checks passed
@redsun82 redsun82 deleted the redsun82/rust-expand-attr-macros branch May 14, 2025 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Rust Pull requests that update Rust code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants