[Pre-RFC] Well-known import paths

Do you think this could be related to the on_unimplemented hinting for tooling and diagnostics that's been recently discussed on Zulip?

While reading it I was wondering if a good first step would be to have a kind of a #[hint::*] attribute namespace below which everything isn't a set of validated arguments, but rather a structured message to tooling (including rustc). Then the Rust project could simply document the hint message structures it itself understands (via rustc, rust-analyzer, clippy, etc).

It's not unreasonable to expect tooling attributes applied to every function and struct (and maybe even statements and expressions). This is quite likely with static analysis tooling. For example, the contracts crate encourages annotating each function with multiple #[requires] and #[ensures] attributes (that specific crate performs dynamic assertions, but the same approach may be expected for static analysis).

Using the proc macro workaround for custom attributes would blow up compile times, and may have very negative impact on IDE experience (since it would need to expand every macro).

How expensive is a noöp proc-macro-attribute? My assumption with how the bridge works is it should be close to free.

Proc macro receives and returns TokenStream, but the compiler works with a syntax tree (which is also quite different from the public API of e.g. syn). At the very least you incur the cost of serializing and parsing back the annotated fragment of code. This is not a lot, but it adds up if all your functions have multiple attributes.

The proc-macro bridge means the proc-macro actually only receives handles to the compilers internal data structures. As long as you don't attempt to inspect the tokens the actual data doesn't get transferred across the bridge. So simply returning the incoming TokenStream should involve no serialization.

As a quick test I inserted 10 copies of a noop proc-macro-attribute onto 10 functions in a deeply nested macro that has 768 output arms. So a total of 76,800 invocations of the proc-macro-attribute. That gave

> time sh -c 'for i in {0..10}; do cargo clean -p stylish-core && cargo check -p stylish-core --all-features; done'
sh -c   24.42s user 1.17s system 98% cpu 25.904 total

I then removed the attributes but left the 7,680 empty functions behind, that gave

sh -c   12.24s user 1.09s system 97% cpu 13.664 total

So, overall ~16μs per proc-macro-attribute invocation.

Actually, I guess having it on tiny functions doesn't really show the point. I also tried putting 100 copies each on 35 real items (functions and struct definitions), so a total of 3500 invocations with realistic amounts of syntax. That gave:

sh -c   10.07s user 0.78s system 97% cpu 11.130 total
# vs without attributes
sh -c   7.41s user 0.77s system 96% cpu 8.445 total

So ~767μs per invocation. Some overhead, but still <1s compile overhead per thousand proc-macros. Which seems good enough to me for tooling to start experimenting with and better show the need for custom tool attributes.

1 Like

Yeah, my benchmarks show about 10% increase of compile time for a single function, per each attribute (non-cumulatively). Which is, quite significant, but an affordable cost.

No-op proc macro attributes are very much not free for rust-analyzer though. It has to keep both the original syntax tree and the expansion in memory, do syntax fixup if the syntax is incomplete, map tokens down and up, and do various other things to make IDE features work through the proc macro.

I thought Rust added this ages ago, but apparently I was just remembering an RFC that got accepted but sadly stalled out:

I think the topic got a bit carried away with the topic of IDE attributes. While allowing external tools to pioneer the addition of attributes before stabilization is a great idea, in the end it would be a better idea to merge most of such attributes into the language. We don't want boilerplate like the CSS --moz/--webkit everywhere in Rust code.

Nevertheless, for this topic alone, conventional import paths are relevant to multiple components (clippy, IDEs, rustdoc, etc), and it is probably appropriate to implement this in the Rust language first to avoid repetition.

Another unresolved issue is the effect on re-exports. Should conventional imports be allowed on re-exports, and if they are, how do we decide which one to use?

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.