Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions text/0000-doc-interp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
- Feature Name: `doc_interp`
Comment thread
clarfonthey marked this conversation as resolved.
- Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

## Summary
[summary]: #summary

Add the ability to use `#{...}` expressions in documentation comments for macro interpolation.

For example, `#{$name}` would be replaced with the result of `stringify!($name)` if used in documentation comments, and expressions like `#{my_macro!($name)}` also work.

## Motivation
[motivation]: #motivation

Right now, generating documentation via macros is incredibly overbearing. If any part of a documentation comment requires macro input, particularly doctests, then you need to replace the documentation comment with a `#[doc = ...]` macro, which usually takes the form of a `#[doc = concat!(...)]` expression, since splitting the attribute into multiple parts would result in the multiple pieces being displayed across separate lines.

An obvious example is `core/src/num/int_macros.rs` and `core/src/num/uint_macros.rs` from the standard library, and here's just one example:

```rust
/// Checked integer subtraction. Computes `self - rhs`, returning `None` if
/// overflow occurred.
///
/// # Examples
///
/// ```
#[doc = concat!("assert_eq!((", stringify!($SelfT), "::MIN + 2).checked_sub(1), Some(", stringify!($SelfT), "::MIN + 1));")]
#[doc = concat!("assert_eq!((", stringify!($SelfT), "::MIN + 2).checked_sub(3), None);")]
Comment on lines +27 to +28
Copy link
Copy Markdown
Contributor

@tgross35 tgross35 May 18, 2026

Choose a reason for hiding this comment

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

As an alternative solution to solve the same problem, things would probably be less terrible if we had an inline concat syntax. Something like:

#[doc = concat2!("assert_eq!(({$SelfT}::MIN + 2).checked_sub(1), Some({$SelfT}), "::MIN + 1));")]
#[doc = concat2!("assert_eq!(({$SelfT}::MIN + 2).checked_sub(3), None);")]

Not that we couldn't have both :)

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I actually do like this idea a lot, although I would like to still keep the doc comment syntax since I think that the misalignment between /// and #[doc = ...] really makes reading things harder.

General string interpolation for macros itself is something that would be worth exploring as worthwhile in its own right, and probably doesn't need to replicate the entire format_args! machinery to be useful.

/// ```
```

This notation is incredibly difficult to read, especially when code is being output. It would be substantially easier to read this as:

```rust
/// Checked integer subtraction. Computes `self - rhs`, returning `None` if
/// overflow occurred.
///
/// # Examples
///
/// ```
/// assert_eq!((#{$SelfT}::MIN + 2).checked_sub(1), Some(#{$SelfT}::MIN + 1));
/// assert_eq!((#{$SelfT}::MIN + 2).checked_sub(3), None);
/// ```
```

Similarly, even simple macro-generated documentation could be improved:

```rust
#[doc = concat!("Creates a ", $thing, ".")]
```

becomes:

```rust
/// Creates a #{$thing}.
```

## Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Internally, Rust converts `/// documentation comments` into `#[doc = "documentation comments"]` attributes. Multiple `#[doc = ...]` attributes are combined into a single documentation comment, where each attribute's text is put on its own line.

Taking this in mind, you *could* use this sugaring to generate documentation in macros, but the result is quite difficult to read:

```rust
#[doc = concat!("Creates a ", stringify!($thing), " without checking for validity.")]
///
/// # Safety
///
#[doc = concat!("Since this does not check if you've passed in a valid ", stringify!($thing), ",")]
/// you must check for validity yourself.
```

Instead, you can use the special `#{...}` notation to include arbitrary macro content in documentation:

```rust
/// Creates a #{$thing}.
///
/// # Safety
///
/// Since this does not check if you've passed in a valid #{$thing},
/// you must check for validity yourself.
```

Internally, the contents inside `#{...}` are replaced as if they were passed to a call to the `stringify!(...)` macro, and can accept arbitrary macro expressions. However, note that since the entire comment is a string, you don't need to use `concat!(...)` to combine multiple pieces:

```rust
/// Creates multiple #{concat!(stringify!($thing), "s")}.
```

Instead, you can do this instead:

```rust
/// Creates multiple #{$thing}#{s}.
```

Which is ultimately:

```rust
/// Creates multiple #{$thing}s.
```

Invalid `#{...}` are ignored entirely, so, the following:

```rust
/// Here's some #{(invalid rust code}.
```

Will just be output as-is and emit a warning when running `cargo doc`, rather than failing to compile or build documentation.

If for whatever reason, you want to write literal `#{...}` in your code, use `#{{...}}` instead.

## Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

`#{...}` expressions are rustdoc-only, so, they require no changes to the compiler, only rustdoc. However, they should apply to both `/// comment` syntax and `#[doc = "comment"]` syntax, since there should be no meaningful distinction between the two.
Copy link
Copy Markdown

@lebensterben lebensterben May 19, 2026

Choose a reason for hiding this comment

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

even though I understand the intention of this PR, it still makes me feel like this new expression isn't just rustdoc only, but changing the transcribing of macro by example.

also, how would repetition be dealt with?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is making me think that this could probably genuinely use a few more examples.

Repetition, to me, would be done exactly how it's done any other way:

$(
    /// ${$thing}.
    struct $thing;
)

Would work, if $thing is repeating at its current depth.

Copy link
Copy Markdown
Member

@fmease fmease May 26, 2026

Choose a reason for hiding this comment

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

#{...} expressions are rustdoc-only, so, they require no changes to the compiler, only rustdoc. However, they should apply to both /// comment syntax and #[doc = "comment"] syntax, since there should be no meaningful distinction between the two.

[…] #{...} cannot simply be replaced with equivalent #[doc = ...] attributes before documenting […]

The ///X#[doc = "X"], /**X*/#[doc="X"] desugaring is a proper user-observable part of the Rust language semantics. Consider this valid Rust program:

macro_rules! extract {
    (#[doc = $text:literal]) => { $text };
}

const _: &str = extract!(/**X*/);

That means the #{…} syntax you propose can be observed in its unexpanded form if they have the semantics you describe (assuming I've interpreted them correctly). That feels quite iffy to me.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So, to clarify: what I mean is that literally, under most circumstances, this syntax is completely ignored outside of rustdoc. That means that you'd be able to observe them as #[doc] attributes or text in doc comments and the compiler does absolutely nothing with it.

However, it does not mean, for example, replacing #[doc = "before #{...} after"] with #[doc = concat!("before ", stringify!(...), " after")] since the expansion of ... (here representing arbitrary syntax) could result in syntax errors that meaningfully affect compilation. The point is to make sure that it is 100% ignored outside rustdoc.

Copy link
Copy Markdown
Member

@fmease fmease May 26, 2026

Choose a reason for hiding this comment

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

#{...} expressions are rustdoc-only, so, they require no changes to the compiler, only rustdoc. However, they should apply to both /// comment syntax and #[doc = "comment"] syntax, since there should be no meaningful distinction between the two.

[…] #{...} cannot simply be replaced with equivalent #[doc = ...] attributes before documenting, since this could cause entire files to fail to document due to invalid syntax.

Unless I misunderstood something, the semantics these two statements are trying to describe are impossible to implement:

As far as I can tell, the second quote is saying that /// #{...} does not expand to #[doc = concat!(" ", stringify!(...))] since that would lead to lexical/syntax errors during macro expansion if placeholder ... was lexically malformed; instead it would (continue to) expand to #[doc = " #{...}"] and rustdoc would then magically expand the #{...} when analyzing the contents of the expanded doc attributes later on (rendering it verbatim and issuing lint warnings if it's ill-formed) "similar to how nightly rustdoc can perform macro expansion on source code pages"1.

However, if that were the case, rustdoc would need to be able to expand metavariables after expansion! That's an impossible ask! When encountering the string $x in #{$x}, no macro variable named $x will exist even if the doc comment / string literal comes from an expansion of "a macro that defines a macro metavariable called $x". MBE transcription happens "eagerly", it's a mere find&replace on macro transcribers!

Yes, rustdoc can probably find the corresponding macro from the source span but then what? It would need to "partially rerun" the macro somehow, taking into account macro repetitions (for e.g., $( ignore!($xs) /** #{$xs} */ )+).


Now, for the sake of argument, let's assume you say "okay, scrap this idea then; let's perform the #{...} expansion during macro expansion and accept lexical errors in #{...} being propagated". Implementation-wise that would mean modifying the compiler to provide hooks into macro expansion for rustdoc to use // implement the desired semantics if tcx.sess.opts.actually_rustdoc. Specification-wise it would mean that we end up with two distinct versions of Rust, the "real one" and the one as understood by rustdoc. That's not good. E.g., rustdoc would accept $( /**#{$xs}*/ )+ wheras rustc wouldn't (it can't "see" the $xs and thus the macro repetition is ill-defined); rustdoc would reject /**#{(}*/ wheras rustc wouldn't. Moreover, it would mean rustdoc has to inspect all string literals to look for #{...} (at that point in time, #[doc = "#{...}"] would just be a sequence of tokens, not an attribute, and we certainly don't want to speculatively try to parse attributes like that during expansion / look behind smh.), possible but undesirable(?).

Finally, we could say: Let's make #{...} part of the language then; that'd obviously require T-lang input and come with a lot more follow-up questions. It would also still mean that the #[doc = "#{...}"] idea can't be part of this as is (for the reason described in the paragraph above).

View changes since the review

Footnotes

  1. Likely what "has its own tools for macro expansions" is referring to in "There are lots of methods to implement this, but since rustdoc has access to all the compiler internals and already has its own tools for macro expansion, this should be relatively easy."

Copy link
Copy Markdown
Contributor Author

@clarfonthey clarfonthey May 28, 2026

Choose a reason for hiding this comment

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

So, I admit that my initial search was very poorly done, and that I was mostly relying upon the fact that rustdoc has full access to the internals of the compiler and has to do full macro expansion itself (via the compiler, to get the items to document in the first place) and thus can simply expand relevant macros in the same context the syntax exists.

It appears very clear that in order to validate this from a technical perspective I would more or less have to implement a POC to make this work, or at least re-evaluate it.

It does make a lot of sense that this wouldn't be possible as a rustdoc-only feature, although it might be possible as a lang feature where the syntax errors are effectively ignored by the compiler in normal operation if it can't be expanded. Will have to look way more into this.


There are lots of methods to implement this, but since rustdoc has access to all the compiler internals *and* already has its own tools for macro expansion, this should be relatively easy. If any part of macro expansion fails for a given expression, it should just emit a lint and display the original syntax in the generated documentation.
Copy link
Copy Markdown
Contributor

@notriddle notriddle May 27, 2026

Choose a reason for hiding this comment

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

We have our own tools for macro expansion? Other than running Boolean algebra on cfg() expressions, where are we doing that?

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wait, you’re talking about macro evaluation on the right-hand side of this equal sign?

#[doc=include_str!("foo")]

Sure, that might exist for rustdoc, but it’s in the compiler. If it fails to evaluate, your crate won’t build.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=14cafa595127cf01fa4673dead37c6ce

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

They might also be referring to the macro expansion on source code pages that's gated behind an unstable flag, not sure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think it makes sense to consolidate this into the thread here: #3962 (comment)

But I'm fine leaving this open if they're not mostly the same thing. Just kind of relying on @fmease's explanation being a little more accurate into trying to understand the state of this.


Note that falling back to the original source is *required* for backwards-compatibility, since we want to ensure that even documentation for old crates is able to be generated. For this reason, the `#{...}` cannot simply be replaced with equivalent `#[doc = ...]` attributes before documenting, since this could cause entire files to fail to document due to invalid syntax.

It's unclear to what extent this syntax would disrupt existing crates' documentation, although considering how weird the `#{...}` syntax is, this is expected to be minor. A simple grep of the code that would normally be run by crater should suffice, although there could also be lints added to rustdoc to be extra certain in case this ends up being more work than necessary.

Depending on the desire/need, a `#[doc(no_macro_interpolation)]` attribute could be added to opt out of this behaviour, or a `#[doc(macro_interpolation)]` attribute could be added to explicitly opt in, with the default being changed in a future edition.
Copy link
Copy Markdown
Contributor

@tmccombs tmccombs May 21, 2026

Choose a reason for hiding this comment

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

Another option, which has better backwards compatibility is that we start with an attribute to opt in, then in a future edition make that the default (possibly with an attribute to opt out.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, this is kind of what I meant here, but I should reword to be more clear. What I meant by having the option to opt out is for forward compatibility, if you want to upgrade editions.


Additionally, there are a few extra lints that could be emitted in addition to an "invalid syntax" lint:

* `#{var}` is almost certainly meant to be `#{$var}` and should be autofixable. (We don't have `cargo doc --fix`, do we?) Otherwise, this is just the literal string `var`.
* (I can't think of any more, but they might exist.)

## Drawbacks
[drawbacks]: #drawbacks

Obviously, this is a nontrivial syntax change, and that comes with its own downsides. However, the main downside of this approach is that it has the potential to disrupt the documentation of crates created before this change was added. While aesthetic changes to documentation are not part of Rust's stability guarantees, potential disruptions to doc tests should not be taken lightly, and we should verify that the change doesn't break anything before stabilising.

## Rationale and alternatives
Copy link
Copy Markdown
Contributor

@kpreid kpreid May 21, 2026

Choose a reason for hiding this comment

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

An alternative I don't see considered here is to concatenate without also including the effect of stringify!. This would allow this interpolation syntax to also be used:

  • to interpolate calls to macros that expand to strings, or
  • to interpolate strings of documentation text that were passed into a macro by matching $(#[doc = $doc:literal] )* (these must not be stringify!()ed because that would quote and escape them)

which are other use cases where one currently must write #[doc = concat!(...)]). This is of course only a convenience syntax, and so it may not be worth supporting that use case, but I would like it to be considered explicitly. Searching my own code, I have a roughly equal number of cases where stringify! is needed and cases where it must be avoided.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So, I think I understand what you're trying to say here, but it would be helpful to have an example just to make sure. Because interpolating calls to macros definitely feels like it would require more than just what you're mentioning.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

interpolating calls to macros definitely feels like it would require more than just what you're mentioning

You can, today, write:

macro_rules! string_producing_macro {
    () => { "world" };
}

#[doc = concat!("Hello ", string_producing_macro!(), ".")]
pub struct SomeItem;

#[test]
fn test_documentation_string() {
    assert_eq!(string_producing_macro!(), "world");
}

This can be used to assert that certain documentation fragments are consistent with the behavior of the code, or to repeat a single block of text across multiple items’ documentation. (include_str!() can also be used in a similar way, but I wanted to emphasize that this is not just for already-prepared text but for any macro.)

What I’m asking for is to consider making the interpolation syntax not automatically stringify (or make a variant of it that does not stringify), so that one can write

/// Hello #{string_producing_macro!()}.

to the same effect as the concat!() version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah! I understand what you mean. Yes, I think that it would be reasonable to special-case string literals to just output their contents instead of always calling stringify.

[rationale-and-alternatives]: #rationale-and-alternatives

`${...}` would conflict with the [LaTeX syntax](https://github.com/rust-lang/rfcs/pull/3958) proposed by another RFC, so, `#{...}` is chosen as an arbitrary replacement. There are a million ways to paint this shed, but we just need something that's unlikely to conflict and is memorable.

The `#{...}` syntax, although, clunky, exists for the same reason that shell scripts have a `${variable}` syntax in addition to `$variable`; there are cases where this can become ambiguous. For example, `$X_$Y` is interpreted as `${X_}${Y}`, whereas `${X}_${Y}` will properly put an underscore between these two variables.
Comment on lines +139 to +141
Copy link
Copy Markdown
Contributor

@tgross35 tgross35 May 18, 2026

Choose a reason for hiding this comment

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

Perhaps a prefixed foo( doc#{$bar} ) syntax would be a good option to (1) further reduce conflict chances, and (2) give a cue that it's documentation-related rather than something in the code.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Honestly, changing the syntax to #{...}, the syntax does kind of write itself…

Will have to ponder.


In the future, a simple `$variable` syntax could be adopted for cases where `#{$variable}` is more than necessary, although this is explicitly left out of this RFC to keep things simple. The proposal for a lint to automatically fix `#{variable}` to `#{$variable}` should also help. We may also want to change the compilation of `$variable` to *not* emit a lint if used outside of a macro, since this likely represents code in another language and not improperly written Rust code.

## Prior art
[prior-art]: #prior-art

As mentioned, shell scripts use `#{variable}` syntax to ensure that `$variable` isn't ambiguous when word characters are immediately adjacent to variable expansion. Many other languages offer a similar syntax for interpolation in string literals as well.

## Unresolved questions
[unresolved-questions]: #unresolved-questions

- Is `#{{...}}` sufficient for escaping the syntax?
Copy link
Copy Markdown
Contributor

@kpreid kpreid May 21, 2026

Choose a reason for hiding this comment

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

Markdown consistently uses \ to remove the special meaning of characters, including in odd-looking cases such as 1\. to make a non-list-item. Therefore, I think the escaping syntax should be either of \#{ or #\{, instead of #{{, for consistency.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a fair point, although another thing worth considering is that this will also be interpolated in Rust code blocks where such syntax would appear a bit weird.

- To what extent would this slow down documenting libstd if it were implemented everywhere? Would it be substantially slower since all the parsing/macro expansion code is already loaded?
- Are there cases where `#[doc = ...]` comments are used instead of the normal syntax for macro generation that wouldn't be solved by this feature?

## Future possibilities
[future-possibilities]: #future-possibilities

- Should `$variable` be allowed by itself? Is this likely to cause issues?
- Technically, since rustdoc has access to constant evaluation too, we could permit things that are especially weird like allowing `#{CONSTANT}` to expand to the literal value of a constant in documentation.
Copy link
Copy Markdown
Contributor

@notriddle notriddle May 27, 2026

Choose a reason for hiding this comment

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

Isn’t that feature, on its own, enough? If you need access to the result of evaluating a macro, you can assign it to a constant, then refer to that from the doc:

const FOO: &str = include_str!("foo.md");
/// #{FOO}
pub struct Foo;

Yes, it’s verbose, but it’s a lot better than doc=concat!(…), and I think we could implement it in the same pass as intra-doc link resolution (assuming we only let you point at a constant item, not write arbitrary inline expressions). And since it lets you transclude the results of running a const fn, it actually gives you new power, not just syntactic sugar.

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And since it lets you transclude the results of running a const fn, it actually gives you new power, not just syntactic sugar.

Tangent: It would be useful to be able to embed the result of evaluating a const expression in documentation, for similar reasons to my previous comment about not stringify!()ing everything but going further: it would allow documentation to, for example, run a function in in order to discuss example outputs of that function, or (in some cases, and perhaps with added support for allocation in const) to generate tables/diagrams/figures from data that is in the code, without involving a proc-macro.

Furthermore, this could be a compatible extension to existing doc syntax without being tied to interpolation or having breaking syntax changes at all; simply make #[doc = some_constant_expr()] valid, which it currently is not. Generalize the current “must expand to a string literal” to “must evaluate to a string”, or even “must evaluate to a Display implementor” given const trait support.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, this is an entirely new feature, but the idea is it's a related one that could still be valuable.