[KongchangAI]
· 3 min read· 1,792 words

A Rust Unicode Bug: Hands-On Comparison of Three Coding Models

A Rust Unicode Bug: Hands-On Comparison of Three Coding Models

Comparing three coding models on one Rust Unicode slicing bug with a fixed prompt and a reproducible eval method.

The article uses a byte-slicing Rust function that panics on valid UTF-8 input like `café` as a test. It compares first responses from DeepSeek V4 Pro, Claude Fable 5.1, and Grok Build 0.1 using an identical prompt, temperature 0, and regression tests. Evaluation relies on `cargo test`, compiler output, token usage, latency, and model version rather than explanation confidence. A correct fix uses `char_indices` to find the byte boundary after the nth scalar value while keeping the `&str` return type. The article stresses that one bug cannot rank models—the best coding LLM depends on the task.

A Rust Unicode Bug: Hands-On Comparison of Three Coding Models

Rust Unicode Bug coding model comparison

Coding model leaderboards rarely tell you how well a model handles real bugs. A Rust function that works fine on ASCII can panic on perfectly valid Unicode input because it slices at the wrong UTF-8 byte boundary. This article uses one reproducible bug, one fixed prompt, and the same regression tests to compare the patch quality from three models—DeepSeekAd V4 Pro, Claude Fable 5.1, and Grok Build 0.1. We look at the code, not at how confident the explanation sounds.

Why this bug is worth testing models on

The function under test is supposed to return the first n characters of a string:

pub fn prefix(input: &str, n: usize) -> &str {
    if n >= input.len() {
        return input;
    }
    &input[..n]
}

The problem is that it counts bytes, not characters. In UTF-8 the trailing é in café takes two bytes, so when n=4 the slice cuts right through the middle of that character and triggers a panic. The reference test cases are as follows:

#[test]
fn ascii() { assert_eq!(prefix("rustacean", 4), "rust"); }

#[test]
fn unicode() { assert_eq!(prefix("café", 4), "café"); }

#[test]
fn beyond_length() { assert_eq!(prefix("é", 10), "é"); }

Save the code as src/lib.rs in a new Cargo library project and run cargo test to reproduce the failure on the Unicode case. The beauty of this bug is its clean boundaries: a fix must keep the &str return type, respect character boundaries, and pass the regression tests. Any shortcut will show up in the compiler or the tests.

UTF-8 is a variable-length encoding: ASCII characters take 1 byte, Latin extended characters (like é) take 2 bytes, CJK characters usually take 3 bytes, and emoji often take 4 bytes. Rust string slicing &input[..n] indexes by byte, so if n lands in the middle of a multi-byte character it produces an invalid UTF-8 sequence. Rust detects this at runtime and panics rather than returning garbage—which is exactly why this bug is so conspicuous. Understanding it requires distinguishing three levels: bytes, Unicode scalar values (a single char), and grapheme clusters (one character as perceived by the user). For example, é can be a single scalar value U+00E9, or it can consist of e plus the combining accent U+0301 as two scalar values, yet at the grapheme-cluster level both display as one character. This task asks for counting by scalar value, so using char_indices rather than splitting by grapheme cluster is also correct.

Three models, one problem

The three candidates in this comparison are DeepSeek V4 Pro, Claude Fable 5.1, and Grok Build 0.1. The original article stresses that model availability, exact API identifiers, and supported parameters must be verified against the online catalog before actually running anything—never filled in from memory.

ModelAPI identifierTemperatureInputVersion
DeepSeek V4 ProConfirm from catalog0Same Rust file and promptRecord API version
Claude Fable 5.1Confirm from catalog0Same Rust file and promptRecord API version
Grok Build 0.1Confirm from catalog0Same Rust file and promptRecord API version

All models use the same request body, system instructions, max output quota, and test environment, and the model ID actually returned by the API is recorded. Setting temperature to 0 reduces randomness, but the original article also notes that it does not guarantee identical answers across repeated calls. This point is critical to the rigor of a "reproducible comparison."

The prompt: not a single word changed

Every model receives exactly the same prompt:

Fix the Rust function below. It must return the first n Unicode scalar values without panicking. Keep the return type &str. Explain the bug briefly, return a complete src/lib.rs, and add a regression test for an emoji and a combining character. Do not add external dependencies.

The full original src/lib.rs is attached after the prompt. The rules are equally clear: don't tell one model about another model's failure, and don't quietly fix an answer before testing it. This guarantees a fair comparison—each model gets the same problem, not one that has been "fed" the answer.

What to look for in each model's patch

DeepSeek V4 Pro

Capture the raw response, the reported input/output token counts, the request latency, and the output of the first cargo test. A correct patch should walk char boundaries rather than slice at arbitrary byte indices. If the returned code doesn't compile, keep the error message and count the next prompt as another attempt.

Claude Fable 5.1

Use exactly the same source and prompt. Pay particular attention to whether it preserves the borrowed &str return type and whether it handles the case where the requested length exceeds the character count. Watch the combining-character case especially: a Unicode scalar value is not a grapheme cluster, so requesting one scalar value from e plus a combining accent may return just e—which, by this task's definition, is correct.

A Unicode scalar value is any code point except the surrogate range U+D800–U+DFFF, and Rust's char type corresponds exactly to one scalar value. A grapheme cluster, on the other hand, is the smallest unit of writing as humans perceive it and may be composed of multiple scalar values—for example, e plus a combining acute accent, or a family emoji built from multiple code points joined by zero-width joiners. The task explicitly asks for the first n scalar values, so for input like e plus a combining accent, returning e when 1 scalar value is requested is compliant; if the model returns the whole grapheme cluster, it has actually deviated from the requirement. This distinction is the key to judging whether a model truly understands the problem rather than vaguely "handling Unicode."

Grok Build 0.1

Repeat the same process without modifying the prompt. Review whether the model changed the API, introduced dependencies, or returned String instead of &str. Even if the visible output looks correct, these changes matter just as much. Record first attempts and subsequent attempts separately.

When evaluating patches, cargo test only verifies behavior, while cargo fmt --check and cargo clippy check formatting consistency and common defects respectively. clippy is the official Rust static analysis tool; it catches patterns like unnecessary allocations, suspicious type conversions, and inefficient iteration. For coding models, it often exposes code that "runs but isn't idiomatic." cargo fmt --check doesn't modify files—it only reports formatting differences—making it suitable as a gate in automated pipelines. Using all three together lets you distinguish quality tiers such as "compiles and passes tests," "style doesn't meet the bar," and "has lint warnings," avoiding the mistake of overrating a model's output just because one cargo test run came back green.

What a correct fix looks like

One effective approach is to use char_indices to find the byte boundary after the nth scalar value:

pub fn prefix(input: &str, n: usize) -> &str {
    let end = input
        .char_indices()
        .nth(n)
        .map(|(index, _)| index)
        .unwrap_or(input.len());
    &input[..end]
}

Note that this reference implementation is the "answer key" for judging correctness—it is not the output of any model, and it must be kept out of the model prompts. Test cases should cover ASCII, multi-byte characters, the empty string, n equal to 0, n greater than the character count, emoji, and decomposed accents.

The scorecard: fill in the table before talking rankings

ModelFirst-passAttemptsLatencyInput tokensOutput tokensNotes
DeepSeek V4 ProFrom cargo testFrom logsFrom logsFrom APIFrom APICompile and behavior notes
Claude Fable 5.1From cargo testFrom logsFrom logsFrom APIFrom APICompile and behavior notes
Grok Build 0.1From cargo testFrom logsFrom logsFrom APIFrom APICompile and behavior notes

The original article is blunt about this: the scorecard only means something once every row is filled in from first responses, Cargo output, and API logs. Until then, it's just a reproducible evaluation worksheet, not a model ranking.

How to choose a model for different coding tasks

For short patches, first-pass compilation and correct edge cases matter more than pretty explanations. For large refactors, observe how often the model touches unrelated code. For code review scenarios, test whether it can spot the "bytes vs. characters" distinction without being prompted. For terminal automation, compare how consistently each model follows the required output format.

The original article's conclusion: the best coding LLM depends on the task. One Rust bug cannot determine which model is strongest across an entire repo, language, or toolchain, but it can reveal whether a model handles a specific failure mode under controlled conditions.

How to reproduce this comparison

Create a Cargo library with cargo new unicode-prefix --lib, replace src/lib.rs with the buggy version, and run cargo test. Send the same prompt and file to each confirmed model identifier, saving the raw JSON response, request timestamps, token usage fields, and model version. For each attempt, extract the candidate code into a clean worktree, run cargo fmt --check, cargo test, and cargo clippy, and record failures without modifying the first answer.

A suggested repository layout:

baseline/src/lib.rs
oracle/src/lib.rs
prompts/task.txt
runs/<model>/response.json
runs/<model>/cargo-test.txt

The reproducible artifacts are the code and logs, not a ranking chart. A shareable comparison should include the repository, unedited first responses, and Cargo logs, so readers can both inspect the candidate fixes and see the failures.

One-line Summary

Using one Rust Unicode slicing bug and a fixed prompt to compare patch quality and a reproducible evaluation method across three coding models.

Paragraph Summary

The article uses a byte-slicing Rust function as its test: it panics on valid UTF-8 input like café by cutting through the middle of a character. The author compares the first responses of DeepSeek V4 Pro, Claude Fable 5.1, and Grok Build 0.1 using an identical prompt, temperature 0, and regression tests. Evaluation ignores explanation confidence and instead looks at cargo test, compiler output, token usage, latency, and model version, with API identifiers verified against the online catalog. A correct fix uses char_indices to find the byte boundary after the nth scalar value while keeping the &str return type. The article stresses that a single bug cannot rank models—the best coding LLM depends on the specific task.

#Rust Unicode bug#coding model comparison#UTF-8 slicing#DeepSeek V4 Pro#Claude Fable 5.1#Grok Build 0.1#char_indices
Share:

Related articles