Skip to content

ICE: "slice index starts at N but ends at N-1" in the annotate-snippets diagnostic renderer when a diagnostic points at a source line containing non-ASCII (multibyte) characters, under --error-format=json #157148

@OpenBST

Description

@OpenBST

Summary

rustc panics (ICE) inside the diagnostic renderer (annotate_snippets / StyledBuffer::replace) with slice index starts at N but ends at N-1 whenever it has to render a diagnostic whose underlined source line contains multibyte UTF-8 characters (e.g. CJK), and the JSON error format is used (--error-format=json --json=diagnostic-rendered-ansi, which is what Cargo/Clippy/rustdoc/cargo-nextest/rust-analyzer pass by default).

The panic is in the rendering of the rendered field of the JSON diagnostic, i.e. it happens after the warning/error/lint has already been produced. As a result, an ordinary, fixable diagnostic (e.g. unused_mut) turns into an opaque the compiler unexpectedly panicked and the actual diagnostic is never shown.

Version

rustc 1.95.0 (59807616e 2026-04-14)
host: x86_64-unknown-linux-gnu

--error-format=human / --error-format=short are not affected (those paths do not go through the annotate-snippets snippet renderer).

Panic message + backtrace (anonymized)

thread 'rustc' panicked at library/core/src/slice/index.rs:1031:55:
slice index starts at 17 but ends at 16
stack backtrace:
   ...
  11: core::slice::index::slice_index_fail
  12: <annotate_snippets::renderer::styled_buffer::StyledBuffer>::replace
  13: annotate_snippets::renderer::render::render
  14: <rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter>::emit_messages_default
  15: <rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter as rustc_errors::emitter::Emitter>::emit_diagnostic
  16: <rustc_errors::json::Diagnostic>::from_errors_diagnostic
  17: <rustc_errors::json::JsonEmitter as rustc_errors::emitter::Emitter>::emit_diagnostic
  18: <rustc_errors::DiagCtxtInner>::emit_diagnostic::{closure#3}
   ...
  23: rustc_middle::lint::diag_lint_level::diag_lint_level_impl
   ...
  31: rustc_interface::passes::analysis
   ...

error: the compiler unexpectedly panicked. This is a bug.

note: rustc 1.95.0 (59807616e 2026-04-14) running on x86_64-unknown-linux-gnu
note: compiler flags: -C embed-bitcode=no -C debuginfo=2

query stack during panic:
#0 [mir_borrowck] borrow-checking `some_function`
#1 [analysis] running analysis passes on crate `mycrate`
end of query stack

I observed the identical crash (same StyledBuffer::replace slice panic, with values such as 13 but ends at 12, 17 but ends at 16) from several different diagnostic producers — a plain rustc lint (unused_mut), rustdoc's invalid_html_tags, and a Clippy lint. The producing lint is irrelevant; the bug is in the shared snippet renderer, so the byte-offset values vary with the column layout of the offending line.

Reproduction

// repro.rs
fn main() {
    // The line below triggers `unused_mut`; the trailing comment puts multibyte
    // (non-ASCII) characters on the same line that the diagnostic underlines.
    let mut x = 0;            // 这是一段中文注释,用于在被下划线标注的行中加入多字节字符
    let _ = x;
}
$ rustc --edition 2021 --error-format=json --json=diagnostic-rendered-ansi repro.rs

Expected: a normal unused_mut warning, emitted as JSON with a rendered field.

Actual: error: the compiler unexpectedly panicked with the backtrace above.

Notes:

  • Whether a given line triggers the panic depends on the exact byte offset of the diagnostic span relative to the multibyte characters on that line (the renderer mixes byte indices and display columns). The example is representative; if it does not crash as-is on your machine, adding/removing a few characters before the span (so the underline start crosses a multibyte boundary) reliably surfaces it.
  • In a real codebase, essentially any warning/error/lint emitted on a line that contains CJK (or other multibyte) text reproduces it under the JSON error format.

Workaround

$ rustc --error-format=short repro.rs           # or: cargo <cmd> --message-format short

--error-format=short (and cargo … --message-format short) bypass the snippet renderer, so the real diagnostic is printed without the ICE.

Impact

  • Any toolchain front-end that drives rustc with the default --error-format=json --json=diagnostic-rendered-ansi (Cargo, Clippy, rustdoc, cargo-nextest, rust-analyzer, etc.) ICEs instead of reporting the diagnostic, as soon as one warning/error/lint lands on a source line containing multibyte characters.
  • The crash happens in the renderer after the lint is produced, so #[allow(...)] / --cap-lints do not prevent it (the diagnostic still has to be rendered).
  • The first lint to hit such a line aborts the whole crate's diagnostic emission, masking every subsequent (and possibly the original) diagnostic — making the real problem very hard to locate without switching to --message-format short.

This looks like an off-by-one / byte-vs-column indexing bug in annotate_snippets's StyledBuffer::replace (or its caller in renderer::render) when a rendered line contains characters whose UTF-8 byte length differs from their display width.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-diagnosticsArea: Messages for errors, warnings, and lintsC-bugCategory: This is a bug.D-diagnostic-infraDiagnostics: Issues that affect all diagnostics, or relate to the diagnostic machinery itself.I-ICEIssue: The compiler panicked, giving an Internal Compilation Error (ICE) ❄️T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions