RFA-628 · Case file with fixtures · Case 600 of 694 · Compiler evidence
Raw String Delimiters Must Close with the Same Hash Count
Raw strings avoid escape noise by choosing an exact delimiter. Match the hash counts, use only enough hashes for the payload, and test generated source at the token boundary.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- A raw literal fence was edited or generated asymmetrically, so later Rust tokens are consumed as part of an unterminated string.
- First discriminating check
- Count the opening hashes, close with exactly the same count, and use the smallest fence sequence absent from the payload.
Raw strings are useful when JSON, regular expressions, HTML, SQL, or Windows paths would require many backslash escapes. Their boundary is exact: an r, some number of hashes, an opening quote, then a closing quote followed by the same number of hashes. The failing fixture opens with two and closes with one, producing E0748.
This failure happens during tokenisation
Before Rust can type-check an expression, the lexer must determine where its tokens end. With an unmatched raw delimiter, later quotes, semicolons, or braces may be consumed as apparent string content. This is why one small mismatch can create confusing errors far below the real line.
The official E0748 explanation says the trailing hash count must match the leading count. The Reference specifies raw string literal tokens, including their delimiter form and content rules.
I inspect the opening and closing boundary first. Reformatting the content or adding escapes inside a raw string cannot repair an unmatched token.
Use the smallest sufficient delimiter
The repaired fixture closes its two-hash string with two hashes. It could also use a one-hash delimiter because the payload does not contain the sequence "#. In real text I choose the smallest count that cannot occur as a closing sequence inside the payload.
A plain raw string r"..." works when the content has quotes only if none of them form its immediate closing boundary; adding hashes lets quotes remain ordinary content. If the payload contains "#, I use two hashes, and so on.
I avoid excessive hashes added “for safety.” Long fences are harder to count in review and invite exactly this typo. For very large embedded files, include_str! may keep source clearer and let editors understand the original format.
Raw does not mean uninterpreted by the consumer
Rust does not process backslash escapes inside a raw string, but the parser receiving that string still applies its own grammar. A raw regex can contain invalid regex syntax. Raw JSON can contain malformed JSON. Raw SQL can still be unsafe if user data is interpolated elsewhere.
I test the consumer parse, not only Rust compilation. For schemas or queries, focused assertions validate meaningful fields and failure cases. Snapshotting the entire string can help detect whitespace changes, but semantic parsing gives stronger evidence.
Raw strings also preserve newlines and indentation. Source formatting may become runtime payload. I make this explicit for golden files, command templates, and signatures where one space can matter.
Generated Rust needs token-aware escaping
Code generators cannot choose one fixed hash count safely for arbitrary input. They should inspect the payload, select a delimiter sequence absent from it, and emit matching boundaries. Better still, procedural macros can construct literal tokens through supported libraries instead of concatenating Rust source text.
The Reference’s procedural macro chapter explains token streams and literal tokens. When generated output fails with E0748, I inspect the expansion or saved generated file because the input template may look correct.
Template nesting deserves special tests: Rust source containing JSON that contains regex text has three grammars with different escape rules. I name each layer and verify the value after each parse instead of adding backslashes until it compiles.
Security depends on the destination grammar
Raw literal syntax prevents Rust escape processing; it does not parameterise a database query, quote a shell argument, or encode HTML. Static trusted query structure can be raw, while dynamic values go through the destination API’s parameter mechanism.
Secrets and certificates are often pasted as multiline raw strings during experiments. I keep production secrets outside source and avoid compiler diagnostics or snapshots that expose them. include_str! also embeds contents into the binary, so it is not a secret store.
Linting and formatter output can make delimiter mistakes easier to spot, but compile tests are the final lexer evidence. Generated-source tests should include payloads containing quotes and increasing hash sequences.
My E0748 checklist
- How many hashes follow
rbefore the opening quote? - Does the closing quote have exactly the same number after it?
- Could a smaller sufficient delimiter be easier to review?
- Does the payload itself contain the candidate closing sequence?
- Are later diagnostics only consequences of the unterminated token?
- Does the destination parser accept the resulting runtime string?
- Is generated code selecting delimiters from the actual payload?
- Are dynamic values handled safely by the destination API rather than string assembly?
The core principle is that a raw string replaces escaping with a chosen lexical fence. E0748 says the two ends disagree. I repair the fence first, keep it minimal, and then validate the separate grammar that consumes the text.