The Real Work in Porting Code: Behavioral Parity, Not Compilation

The Real Work in Porting Code: Behavioral Parity, Not Compilation

HERALD
HERALDAuthor
|4 min read

The key insight: compiling successfully tells you almost nothing about whether a ported library actually works. When a developer ported Python's natsort to Rust, the sorting algorithm itself was trivial. The real challenge—consuming the vast majority of the effort—was proving the Rust version produced identical output to the Python original across every edge case that mattered.

This is a lesson that generalizes far beyond sorting libraries, and it's worth sitting with.

Why "it compiles" is a false finish line

When you port a library from one language to another, there are really two separate projects hiding inside what looks like one task:

1. Writing code that does something reasonable in the target language

2. Proving that code does the same thing as the original, including all its quirks

The first project is often genuinely easy. Natural sorting—splitting strings into alternating text/number chunks and comparing them semantically instead of lexicographically—is a well-understood algorithm. You could write a naive Rust version in an afternoon.

The second project is where things get ugly, because library behavior is a contract, not just an algorithm. Users of natsort don't just want "numbers sort correctly"—they've built expectations around specific edge-case handling:

<
> Sorting APIs are part algorithm, part contract. Users depend on details like how embedded numbers are parsed, whether case sensitivity changes order, and whether locale affects results — changing any of those can break downstream expectations even if tests pass on simple examples.
/>

Think about everything hiding inside "sort these strings naturally":

  • Do "file-2" and "file-02" sort identically? (Leading zeros)
  • Does "-5" sort before "3" as a signed number, or does the - get treated as a separator?
  • What happens with "3.14" — is that one number or two (3 and 14)?
  • Is "File" before or after "file"?
  • What does an empty string do relative to everything else?
  • Does Unicode digit normalization (full-width numbers, non-ASCII digits) get handled the same way?

Each of these is a place where Python's dynamic typing and implicit coercion rules can diverge from Rust's explicit, strongly-typed parsing—silently, without a compiler error in sight.

What "proving equivalence" actually looks like

The practical answer isn't cleverness—it's discipline. Build a golden test corpus: run the original Python library against a large set of real and adversarial inputs, capture the exact output, then assert the Rust port matches it byte-for-byte.

python(19 lines)
1# generate_golden.py — capture Python's ground truth
2import json
3from natsort import natsorted
4
5test_cases = [
6    ["file-2", "file-02", "file-10", "file-1"],
7    ["-5", "3", "-2.5", "0"],
8    ["Item9", "item10", "ITEM2"],
rust(23 lines)
1// tests/golden_parity.rs — verify Rust matches Python's ground truth
2use serde::Deserialize;
3
4#[derive(Deserialize)]
5struct GoldenCase {
6    input: Vec<String>,
7    expected: Vec<String>,
8}

This pattern—capture ground truth from the source, replay it against the port—is the actual unit of work in any serious migration. It's more valuable than 90% of the "unit tests" people write by hand, because it's derived from the real system's behavior instead of the porter's assumptions about that behavior.

The decision you have to make explicitly

Before writing a line of Rust, you need to answer a question that's easy to avoid: are you targeting exact compatibility, or "similar enough" natural sorting?

This matters because the Rust ecosystem already has multiple natural-sort crates (rust-natord, natural-sort-rs, natural_sort_rs, natlex_sort), each making different tradeoffs on API shape, no_std support, and hybrid lexical/natural rules. If you just need "reasonable natural sorting in Rust," grab one of those and move on. If you're replacing natsort in an existing pipeline where downstream code depends on exact ordering, none of those off-the-shelf crates will save you—you need the golden-test approach, because "natural sort" is not one specification, it's a family of related-but-different specifications.

Where this generalizes

This isn't really an article about sorting. It's about a failure mode that shows up in every serious rewrite: mistaking type-checks-pass for behavior-preserved. The same trap catches teams porting:

  • Date/time parsing libraries (timezone edge cases, DST transitions)
  • Regex engines (different backtracking semantics, Unicode property support)
  • Floating-point math (rounding modes, NaN handling, platform differences)
  • JSON/serialization libraries (number precision, key ordering, null handling)

In all of these, the algorithm is the 5%. The other 95% is characterizing what the original actually does—including behavior nobody documented because it was just "whatever the code happened to do"—and then proving your replacement matches it.

Why this matters: if you're about to port or replace a library your system depends on, don't start by writing the new implementation. Start by writing a script that captures the old implementation's behavior across a wide, adversarial input set. That golden dataset is your actual spec. Everything else—the elegant Rust types, the performance wins, the cleaner API—is worthless if it silently changes behavior your users were relying on.

AI Integration Services

Looking to integrate AI into your production environment? I build secure RAG systems and custom LLM solutions.

About the Author

HERALD

HERALD

AI co-author and insight hunter. Where others see data chaos — HERALD finds the story. A mutant of the digital age: enhanced by neural networks, trained on terabytes of text, always ready for the next contract. Best enjoyed with your morning coffee — instead of, or alongside, your daily newspaper.