Skip to content

Conversation

@JayanAXHF
Copy link
Contributor

@JayanAXHF JayanAXHF commented Jan 9, 2026

This PR adds a feature that enables rustc --explain <error> to have syntax highlighted code blocks. Due to performance, size and complexity constraints, the highlighter is very heuristc, relying on conventions for capitalizations and such to infer what an identifier represents. The details for the implementation are specified below.

Changes

  1. Change term::entrypoint to term::entrypoint_with_formatter, which takes an optional third argument, which is a function pointer to a formatter. (compiler/rustc_errors/src/markdown/mod.rs)
  2. Change MdStream::write_anstream_buf to be a wrapper around a new function, MdStream::write_anstream_buf_with_formatter, which takes a function pointer to a formatter. (compiler/rustc_errors/src/markdown/mod.rs)
  3. Change compiler/rustc_driver_impl/src/lib.rs to call MdStream::write_anstream_buf_with_formatter instead of MdStream::write_anstream_buf.
  4. Add a compiler/rustc_driver_impl/src/highlighter.rs file, which contains the actual syntax highlighter.

Implementation Details

  1. The highlighter starts from the highlight function defined in compiler/rustc_driver_impl/src/highlighter.rs. It creates a new instance of the Highlighter struct, and calls its highlight_rustc_lexer function to start highlighting.
  2. The highlight_rustc_lexer function uses rustc_lexer to lex the code into Tokens. rustc_lexer was chosen since it preserves the newlines after scanning.
  3. Based on the kind of token (TokenKind), we color the corresponding lexeme.

Highlighter Implementation

Identifiers

  1. All identifiers that match a (non-exhaustive and minimal) list of keywords are coloured magenta.
  2. An identifier that begins with a capital letter is assumed as a type. There is no distinction between a Trait and a type, since that would involve name resolution, and the parts of rustc that perform name resolution on code do not preserve the original formatting. (An attempt to use rustc_parse's lexer and TokenStream was made, which was then printed with the pretty printer, but failed to preserve the formatting and was generally more complex to work with)
  3. An identifier that is immediately followed by a parenthesis is recognized as a function identifier, and coloured blue.

Literals

  1. A String literal (or its corresponding Raw, C and Byte versions) is colored green.
  2. All other literals are colored bright red (orange-esque)

Everything Else

Everything else is colored bright white and dimmed, to create a grayish colour.


Demo

image Command: rustc --explain E0520

This description was not generated by an LLM (:p)

cc: @bjorn3

Changed `highlight_rustc_lexer` to have only one place where the
original lexeme is extracted. This reduces dereferences and slices. Very
minor perf change.
@rustbot
Copy link
Collaborator

rustbot commented Jan 9, 2026

These commits modify the Cargo.lock file. Unintentional changes to Cargo.lock can be introduced when switching branches and rebasing PRs.

If this was unintentional then you should revert the changes before this PR is merged.
Otherwise, you can ignore this comment.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 9, 2026
@rustbot
Copy link
Collaborator

rustbot commented Jan 9, 2026

r? @fee1-dead

rustbot has assigned @fee1-dead.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@JayanAXHF
Copy link
Contributor Author

PS (Link to Zulip Topic) : #t-compiler/diagnostics > Colored output for `rustc --explain`

@Kivooeo
Copy link
Member

Kivooeo commented Jan 9, 2026

Is this change requires MCP?

@JayanAXHF
Copy link
Contributor Author

Is this change requires MCP?

Could you clarify what you mean by MCP? I'm very new to rustc (this is my first contribution)

@Kivooeo
Copy link
Member

Kivooeo commented Jan 9, 2026

A process where you propose a change and other members brainstorm it whether it worth adding or not

@Kivooeo
Copy link
Member

Kivooeo commented Jan 9, 2026

https://forge.rust-lang.org/compiler/proposals-and-stabilization.html

@JayanAXHF
Copy link
Contributor Author

JayanAXHF commented Jan 9, 2026

A process where you propose a change and other members brainstorm it whether it worth adding or not

Oh thanks. I don't think that this qualifies as a major change. It just adds some very simple code that basically colours tokens by matching on its kind and lexeme. IMO it falls in the light-weight proposal category, as it doesn't break anything (if it errors, the plain block is printed)

@tgross35
Copy link
Contributor

tgross35 commented Jan 9, 2026

I don't think this needs an MCP, it's a reasonably local change that could be backed out at any time.

@Kivooeo
Copy link
Member

Kivooeo commented Jan 9, 2026

r? me

@rustbot rustbot assigned Kivooeo and unassigned fee1-dead Jan 9, 2026
@tgross35
Copy link
Contributor

tgross35 commented Jan 9, 2026

Fyi this will need a test somehow, a UI test is probably fine

@JayanAXHF
Copy link
Contributor Author

Fyi this will need a test somehow, a UI test is probably fine

The test could probably me giving it a string of code and comparing its colored output to what was expected.

@rust-log-analyzer

This comment has been minimized.

@JayanAXHF
Copy link
Contributor Author

The job aarch64-gnu-llvm-20-2 failed! Check out the build log: (web) (plain enhanced) (plain)

The existing tests need to be updated

@tgross35
Copy link
Contributor

tgross35 commented Jan 9, 2026

IIRC that test can be updated with RUSTC_BLESS=1

@JayanAXHF
Copy link
Contributor Author

IIRC that test can be updated with RUSTC_BLESS=1

Could you elaborate please (I'm sorry I'm new to the codebase)

@tgross35
Copy link
Contributor

tgross35 commented Jan 9, 2026

Sorry, you don't need to set that env - just run --bless when you run the tests locally (./x t <relevant path>).

let bless = std::env::var_os("RUSTC_BLESS").is_some_and(|v| v != "0");
. Just means to auto update test that check output and can be annoying to update manually, so you can look at the diff (of course, doesn't mean it is correct!).

Not actually sure why that test failed, the code formatter isn't hooked there.

The problem was something related to the fast that we use a `Style::new`
default style when no style is matched
@JayanAXHF
Copy link
Contributor Author

I have fixed the failing test.

@JayanAXHF
Copy link
Contributor Author

Fyi this will need a test somehow, a UI test is probably fine

Where would I create the test?

@Kivooeo
Copy link
Member

Kivooeo commented Jan 10, 2026

Can we consider colouring all primitives types as well? The same way it was made for keywords

https://doc.rust-lang.org/rust-by-example/primitives.html

@Kivooeo
Copy link
Member

Kivooeo commented Jan 10, 2026

Can write_anstream_buf now be deleted? I don't see any reasons to keep it for now, since it just calls write_anstream_buf_with_formatter, why not call it directly

Same for write_stream

@JayanAXHF
Copy link
Contributor Author

Can we consider colouring all primitives types as well? The same way it was made for keywords

https://doc.rust-lang.org/rust-by-example/primitives.html

Sure!

@JayanAXHF
Copy link
Contributor Author

Same for write_stream

This would require having to change stuff in a few places (in the same file i believe)

Can write_anstream_buf now be deleted? I don't see any reasons to keep it for now, since it just calls write_anstream_buf_with_formatter, why not call it directly

This can done, since iirc write_anstream_buf is not called anywhere except the current test (which can be updated)

@Kivooeo
Copy link
Member

Kivooeo commented Jan 10, 2026

This would require having to change stuff in a few places

Feel free to do this changes

Removed unnecessary wrapper around `write_anstream_buf_with_formatter`
and renamed it to `write_anstream_buf`.
@JayanAXHF
Copy link
Contributor Author

JayanAXHF commented Jan 10, 2026

This would require having to change stuff in a few places

Feel free to do this changes

Done in #b84220 and #34f6890!

cc: @Kivooeo

Copy link
Member

@Kivooeo Kivooeo left a comment

Choose a reason for hiding this comment

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

All changes looks good to me!

There's just a small typo. Also, I need some more time to think about whether we need to create a test, and if so, how

View changes since this review

@JayanAXHF
Copy link
Contributor Author

All changes looks good to me!

Wonderful! I'll push the highlighting for the primitive types tomorrow.

There's just a small typo.

Fixed

Also, I need some more time to think about whether we need to create a test, and if so, how

Please, take your time :D

@rust-log-analyzer

This comment has been minimized.

@tgross35
Copy link
Contributor

There's just a small typo. Also, I need some more time to think about whether we need to create a test, and if so, how

Yes any kind of behavior change generally needs a test. Why the green check if you're not yet sure :)

@JayanAXHF the test can be the same as tests/ui/explain/basic.rs but adding --color=always. Also please squash the commits before merge.

@Kivooeo
Copy link
Member

Kivooeo commented Jan 11, 2026

Why the green check if you're not yet sure :)

This approval is for implementation

@Kivooeo
Copy link
Member

Kivooeo commented Jan 11, 2026

but adding --color=always

What will this look like in the .stdout file?

@tgross35
Copy link
Contributor

but adding --color=always

What will this look like in the .stdout file?

I believe it should just show the escape codes, similar to the output.stdout file that's in this PR now (though code highlighting isn't hooked there of course)

These are two commits squashed together:

feat: add support for primitive types

refactor: refactor out the list of keywords into a global const
@Kivooeo
Copy link
Member

Kivooeo commented Jan 11, 2026

I believe it should just show the escape codes, similar to the output.stdout file that's in this PR now

I've just tried this locally. Instead of showing ANSI codes in the .stdout file, it created a basic.svg file containing just a solid black rectangle, which doesn't seem correct

Here is what I did so you can try to reproduce it or point out what I might have done wrong

//@ compile-flags: --explain E0591 --error-format=human --color=always
//@ check-pass

(Without specifying --error-format, it gave an error. It seems the --color flag might be for diagnostics)

@JayanAXHF
Copy link
Contributor Author

Can we consider colouring all primitives types as well? The same way it was made for keywords

https://doc.rust-lang.org/rust-by-example/primitives.html

Done in 7d96470

@JayanAXHF
Copy link
Contributor Author

JayanAXHF commented Jan 11, 2026

but adding --color=always

What will this look like in the .stdout file?

I believe it should just show the escape codes, similar to the output.stdout file that's in this PR now (though code highlighting isn't hooked there of course)

Would it be fine if the test just focused on the highlighter?

@JayanAXHF
Copy link
Contributor Author

@Kivooeo (cc: @tgross35) I have figured out (i think) why --color always doesn't always print out in color. Apparently, within rustc_driver_impl,

        if io::stdout().is_terminal() {
            show_md_content_with_pager(&text, color);
        } else {
            safe_print!("{text}");
        }

prevents printing the output with color if stdout isn't a terminal. This is why --bless didn't output a .stdout file with ANSI escapes

@JayanAXHF
Copy link
Contributor Author

JayanAXHF commented Jan 13, 2026

Following my previous message, I have added the tests for the highlighter. However, I did have to change rustc_driver_impl's handle_explain function to be indifferent to stdout when --color always is passed. I hope that is alright

@rust-log-analyzer

This comment has been minimized.

@JayanAXHF JayanAXHF force-pushed the rustc_colored_explain branch from dddc3ea to f53159e Compare January 13, 2026 16:20
Comment on lines +490 to +492
if color == ColorConfig::Always {
show_md_content_with_pager(&text, color);
}
Copy link
Member

Choose a reason for hiding this comment

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

My original thought was to create not a UI test, but something that just tests the highlight function, maybe? I don't really like how this ANSI test looks; this change feels unrelated to this PR, and creating this SVG file is also something that I'd like to try to avoid

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 agree with the test looking weird. Should I convert this to a highlighter test? IMO the change, however unrelated, makes the function behave as it should.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we could do this for different PR if you really thinks so

Copy link
Member

Choose a reason for hiding this comment

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

But I don't really think that this change will address any real problem or even be any kind of improvement (maybe I don't see something about this change, so you could elaborate on this a little more)

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 fact that --color always didn't always output in color made its name and description a misnomer. Usually, when you set a program to output color always, it does so, regardless of the output being a terminal or not. This is just my opinion, I'd love to hear what you think about this.

Copy link
Member

@Kivooeo Kivooeo Jan 13, 2026

Choose a reason for hiding this comment

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

I think it's like #[inline(always)] which is also not always doing inline

Yeah, I think it's better to separate this problems, in this PR we are working on making rustc --explain colourful, and I'd maybe suggest to open a zulip discussion on that matter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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 think it's like #[inline(always)] which is also not always doing inline

Yeah, I think it's better to separate this problems, in this PR we are working on making rustc --explain colourful, and I'd maybe suggest to open a zulip discussion on that matter

So should I refactor the test to be of the highlighter instead?

Copy link
Member

@Kivooeo Kivooeo Jan 13, 2026

Choose a reason for hiding this comment

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

So should I refactor the test to be of the highlighter instead?

Yeah would be nice, maybe even revert last commit

I see there is some tests in compiler/rustc_errors/src/markdown/tests/, maybe it would be possible to add such test case somewhere here? Could to take a look on possibility to add a test here? The simplest test would also be fine

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants