Skip to content

[ICE]: rustdoc panics with "not a char boundary" on an item name that joins a Prepend-class character to _ #160231

Description

@lazureykis

Code

pub struct abcൎ_defgh;

The identifier is abc, U+0D4E MALAYALAM LETTER DOT REPH, _defgh. U+0D4E is XID_Continue, so rustc accepts this item and emits only the mixed_script_confusables and non_camel_case_types warnings. rustdoc then aborts, so the crate cannot be documented at all — cargo doc fails on it.

rustdoc --edition 2021 --crate-type lib ident.rs -o doc

Meta

rustdoc --version --verbose:

rustdoc 1.97.0 (2d8144b78 2026-07-07)
binary: rustdoc
commit-hash: 2d8144b7880597b6e6d3dfd63a9a9efae3f533d3
commit-date: 2026-07-07
host: x86_64-unknown-linux-gnu
release: 1.97.0
LLVM version: 22.1.6

Also reproduces on current master (be3d26db984c6f96335faca1f254dc04873cb1c1), verified against a local build.

Error output

warning: the usage of Script Group `Malayalam` in this crate consists solely of mixed script confusables
 --> ident.rs:1:12
  |
1 | pub struct abcൎ_defgh;
  |            ^^^^^^^^^
  |
  = note: the usage includes 'ൎ' (U+0D4E)

warning: type `abcൎ_defgh` should have an upper camel case name
 --> ident.rs:1:12

thread 'rustc' panicked at src/librustdoc/html/escape.rs:82:37:
end byte index 4 is not a char boundary; it is inside 'ൎ' (bytes 3..6 of string)

error: the compiler unexpectedly panicked. This is a bug

note: rustc 1.97.0 (2d8144b78 2026-07-07) running on x86_64-unknown-linux-gnu
note: compiler flags: --crate-type lib

query stack during panic:
end of query stack

Cause

EscapeBodyTextWithWbr's Display impl (src/librustdoc/html/escape.rs:44-91) iterates text.grapheme_indices(true), so i is the start offset of a grapheme cluster, but the _/: word-break arm slices at i + 1:

} else if (s.contains(':') && !next_is_colon())
    || (s.contains('_') && !next_is_underscore())
{
    EscapeBodyText(&text[last..i + 1]).fmt(fmt)?;
    fmt.write_str("<wbr>")?;
    last = i + 1;
}

That assumes the cluster is exactly one byte whenever it contains _ or :. UAX#29 GB9b makes a Prepend-class character (U+0600U+0605, U+06DD, U+070F, U+0D4E, U+111C2, …) combine with the character that follows it into a single cluster, so a cluster can begin with a 2–4 byte character and still contain _. Here the cluster is ൎ_ starting at byte 3, and i + 1 == 4 falls inside the 3-byte U+0D4E.

The sibling CamelCase arm above it slices at i, which is always a cluster boundary, so only this arm is affected. The text.len() < 8 early return is what keeps shorter names from tripping it.

The i + 1 came in with 3bf8bcfbe0e ("rustdoc: properly handle path wrapping") for the : case, and ac303df4e21 ("rustdoc: move the wbr after the underscore, instead of before") later folded the _ case into the same arm — both in #126247.

Existing tests don't catch it because they only cover Extend-class clusters, which join backwards onto an ASCII base character and therefore keep i + 1 on a boundary: E("ṼẽçÑñéå") and E("V\u{0300}e\u{0300}…") both start their clusters on a 1-byte char. Prepend is the one class that joins forwards. The property test escape_body_text_with_wbr_makes_sense can't reach it either — its alphabet is const C: [u8; 3] = [b'a', b'A', b'_'], pure ASCII.

I have a fix (i + s.len() in both places) plus regression tests and will open a PR.

@rustbot label +I-ICE +T-rustdoc +A-Unicode

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-UnicodeArea: UnicodeI-ICEIssue: The compiler panicked, giving an Internal Compilation Error (ICE) ❄️T-rustdocRelevant to the rustdoc team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions