<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel</title>
    <description>The latest articles on DEV Community by Daniel (@dannyamah).</description>
    <link>https://dev.to/dannyamah</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4070262%2Fccd05ec3-044d-474f-81ad-cc386a04cfea.png</url>
      <title>DEV Community: Daniel</title>
      <link>https://dev.to/dannyamah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dannyamah"/>
    <language>en</language>
    <item>
      <title>140 Bugs Were Hiding in One Function, and My Tests Couldn't See Any of Them</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Mon, 10 Aug 2026 09:57:25 +0000</pubDate>
      <link>https://dev.to/dannyamah/140-bugs-were-hiding-in-one-function-and-my-tests-couldnt-see-any-of-them-a0p</link>
      <guid>https://dev.to/dannyamah/140-bugs-were-hiding-in-one-function-and-my-tests-couldnt-see-any-of-them-a0p</guid>
      <description>&lt;p&gt;Anyone can port a library. Point a translator at the source, clean up the output, get it to compile, and you have something that looks like a port. The actual engineering problem is different and much harder: proving that the new code means the same thing as the old code, across thirty algorithms, hundreds of edge cases, and a test suite written by people who were not thinking about you.&lt;/p&gt;

&lt;p&gt;This is the story of porting &lt;a href="https://github.com/life4/textdistance" rel="noopener noreferrer"&gt;textdistance&lt;/a&gt;, a Python library for measuring string similarity, to Rust. The result is &lt;a href="https://github.com/Len3hq/textdistance-rs" rel="noopener noreferrer"&gt;textdistance-rs&lt;/a&gt;. The porting took a fraction of the time. Everything else: the differential fuzzing, the 140 divergences, the 35 year old threshold I violated, the floating-point drift at the 15th decimal place, is what this writeup is actually about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thirty algorithms and one architectural bet
&lt;/h2&gt;

&lt;p&gt;The original &lt;code&gt;textdistance&lt;/code&gt; covers a lot of ground: edit-based distances (Levenshtein, Damerau-Levenshtein, Hamming, Jaro-Winkler), token-based measures (Jaccard, Sørensen-Dice, cosine, Tversky), sequence-based methods (LCS, Ratcliff-Obershelp), phonetic algorithms (MRA, Editex), and compression-based distances built on normalized compression distance. Over thirty algorithms in total, all reimplemented in Rust.&lt;/p&gt;

&lt;p&gt;But before writing a single algorithm, I had to make the decision that shaped everything downstream: how does the existing Python test suite (397 tests I did not write) talk to the Rust code?&lt;/p&gt;

&lt;p&gt;The obvious answer is PyO3: wrap every algorithm in a &lt;code&gt;#[pyclass]&lt;/code&gt;, build a native extension, and the Python tests import Rust directly. The answer I chose instead was a subprocess CLI. The Rust core is a standalone binary that speaks JSON over stdin/stdout, and a thin Python adapter shells out to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The entire cross-language surface is one struct.&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;qval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.algorithm&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"hamming"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;hamming&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"levenshtein"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;levenshtein&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"jaro_winkler"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;jaro&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;winkler_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// …thirty more arms&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unknown algorithm"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reasoning: PyO3 binds you to Python's ABI, complicates the build for anyone who just wants the Rust crate, and turns every signature mismatch into a compile-and-link problem. A JSON pipe is crude, but it is debuggable with your eyes. You can replay any failing case by piping a JSON string into the binary from your shell. When the goal is verifying behavioral equivalence against a foreign test suite, that inspectability is worth a lot.&lt;/p&gt;

&lt;p&gt;It was a speed decision, and I knew it. What I did not know was that this bet had quietly closed a door I would walk straight into later on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that was older than the port
&lt;/h2&gt;

&lt;p&gt;The single worst debugging session of the project traced back to Jaro-Winkler, and specifically to a paper published in 1990.&lt;/p&gt;

&lt;p&gt;Jaro similarity comes from Matthew Jaro's 1989 work on record linkage for census data. William Winkler's 1990 refinement adds a bonus for strings that share a common prefix, on the empirical observation that clerical typos rarely occur in the first few characters of a name. My first implementation applied that prefix boost the way the formula usually gets quoted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What I wrote first: boost applied unconditionally.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jaro_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks right. It passes the examples people paste into blog posts. And it is wrong, because Winkler's actual method only applies the boost when the base Jaro score exceeds a threshold of 0.7 in his published work. Below that, the strings are too dissimilar for a shared prefix to mean anything, and boosting them inflates scores for garbage matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What Winkler actually specified in 1990.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jaro_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BOOST_THRESHOLD&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 0.7&lt;/span&gt;
    &lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;jaro&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with a second, smaller mistake: the original library treats two empty strings as perfectly similar (1.0), and my port returned 0.0. This one function accounted for 140 divergences in fuzz testing. One commit fixed all of them.&lt;/p&gt;

&lt;p&gt;The lesson that stuck with me: I was not diverging from the Python library. I was diverging from a 35-year-old paper that the Python library had implemented faithfully and I had implemented from memory. When you port a library, you inherit its citations, not just its code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof by ten thousand disagreements
&lt;/h2&gt;

&lt;p&gt;That raises the obvious question: how did I even know there were 140 divergences? The unit tests for Jaro-Winkler passed. Example-based tests check the inputs somebody thought of, and nobody thinks of &lt;code&gt;"ab"&lt;/code&gt; vs &lt;code&gt;"aaab"&lt;/code&gt; with a coincidental prefix and a sub-threshold Jaro score.&lt;/p&gt;

&lt;p&gt;The answer was differential fuzzing — running both implementations on the same randomized inputs and diffing the outputs. The technique goes back to McKeeman's work on differential testing for compilers: when you have two implementations that claim to compute the same function, disagreement between them is a test oracle you get for free. You don't need to know the correct answer; you only need to know they should match.&lt;/p&gt;

&lt;p&gt;The harness is embarrassingly simple, which is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hypothesis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategies&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;textdistance&lt;/span&gt;          &lt;span class="c1"&gt;# the Python original
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;textdistance_rs&lt;/span&gt;       &lt;span class="c1"&gt;# the port, via the JSON pipe
&lt;/span&gt;
&lt;span class="nd"&gt;@given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_jaro_winkler_parity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textdistance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jaro_winkler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textdistance_rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jaro_winkler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hypothesis, Python's property-based testing library, descended from Claessen and Hughes' QuickCheck, generates the adversarial inputs: empty strings, single characters, Unicode combining marks, strings that are prefixes of each other. Every property-based failure gets shrunk to a minimal reproducing case, which is how a six-hour bug hunt eventually collapsed into a two-character counterexample I could reason about on paper.&lt;/p&gt;

&lt;p&gt;After the Jaro-Winkler fix and a handful of smaller ones, the fuzzer reached zero divergences across every algorithm and input class it could generate. That number, not the passing unit tests, is the claim I actually stand behind. But notice the caveat hiding in that sentence: zero divergences on everything that could cross the pipe. Two things could not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The door I closed on myself
&lt;/h2&gt;

&lt;p&gt;Several of &lt;code&gt;textdistance&lt;/code&gt;'s alignment algorithms — Needleman-Wunsch, Smith-Waterman, Gotoh — accept a &lt;code&gt;sim_func&lt;/code&gt; parameter: an arbitrary Python callable that scores the similarity of two characters. The test suite exercises it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# From the original test suite. This cannot be serialized.
&lt;/span&gt;&lt;span class="n"&gt;alg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NeedlemanWunsch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sim_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lambda cannot be serialized into JSON and sent down a pipe to a Rust subprocess. It is a closure over a live Python interpreter. There is no workaround that isn't a horror, no eval-strings-in-Rust, no RPC-callback-per-character scheme that would be slower than just running the Python.&lt;/p&gt;

&lt;p&gt;So those tests fail. 22 of them, permanently, by architecture rather than by bug. This is the direct, foreseeable cost of the subprocess decision from the beginning of this article. PyO3 would have handled it natively: a &lt;code&gt;#[pyfunction]&lt;/code&gt; can hold a reference to a Python callable and invoke it from Rust mid-algorithm. I traded that capability for build simplicity and debuggability, and for 28 algorithms the trade was clearly right. For these 4, I built a wall and then walked into it.&lt;/p&gt;

&lt;p&gt;If I ran this project again, I would keep the subprocess architecture for development and verification, it earned it's keep during fuzzing, and add a PyO3 layer at the end specifically for callable-taking algorithms. That hybrid was always possible. I just didn't see the need until the tests told me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15th decimal place
&lt;/h2&gt;

&lt;p&gt;The other place the port compromised was subtler, and it's the result I'm least happy reporting: the "pure Rust" story does not fully hold for the compression-based algorithms.&lt;/p&gt;

&lt;p&gt;Normalized compression distance, from Cilibrasi and Vitányi's work on clustering by compression, approximates the information distance between strings using a real compressor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NCD(x, y) = (C(xy) − min(C(x), C(y))) / max(C(x), C(y))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By construction, the output is exquisitely sensitive to the compressor's exact byte counts and the arithmetic done on them.&lt;/p&gt;

&lt;p&gt;The original library computes arithmetic-coding probabilities using Python's &lt;code&gt;Fraction&lt;/code&gt;, exact rational arithmetic, no rounding. My Rust implementation used &lt;code&gt;f64&lt;/code&gt;. The fuzzer caught divergences on the order of 1e-15: not wrong in any way a human would notice, but not equal, and for NCD "close" is doing real work in the formula, because those values get subtracted and divided in ways that can amplify the drift.&lt;/p&gt;

&lt;p&gt;I tried rational arithmetic in Rust. I tried reordering operations to match Python's evaluation order. What shipped is the honest defeat: for the arithmetic-coding NCD path, the final distance computation moved back into the Python adapter, where &lt;code&gt;Fraction&lt;/code&gt; guarantees exact parity. Rust still does the heavy lifting; Python does the last arithmetic mile. Eight algorithms carry that asterisk, and I'd rather document the asterisk than pretend it isn't there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the port actually proved
&lt;/h2&gt;

&lt;p&gt;None of these four things — the JSON pipe, the Winkler threshold, the fuzzing harness, the &lt;code&gt;sim_func&lt;/code&gt; wall were about writing Rust. They were about the much slower work of finding every place a fast, plausible looking reimplementation quietly disagrees with the thing it's replacing, and then deciding, for each one, whether to fix it, work around it, or write it down and ship anyway.&lt;/p&gt;

&lt;p&gt;That's the actual deliverable here, more than the crate itself: not thirty algorithms in Rust, but a documented account of where equivalence held, where it took a 35 year old paper to restore, and where it simply didn't. A port you can't interrogate isn't really verified, it's just translated. This one, at zero fuzz divergences and two known, named exceptions, is the former.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/life4/textdistance" rel="noopener noreferrer"&gt;&lt;code&gt;life4/textdistance&lt;/code&gt;&lt;/a&gt; — the original Python library.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Len3hq/textdistance-rs" rel="noopener noreferrer"&gt;&lt;code&gt;Len3hq/textdistance-rs&lt;/code&gt;&lt;/a&gt; — the Rust port discussed throughout.&lt;/li&gt;
&lt;li&gt;Jaro, M. A. (1989). &lt;em&gt;Advances in record-linking methodology as applied to matching the 1985 census of Tampa, Florida.&lt;/em&gt; Journal of the American Statistical Association, 84(406), 414–420.&lt;/li&gt;
&lt;li&gt;Winkler, W. E. (1990). &lt;em&gt;String Comparator Metrics and Enhanced Decision Rules in the Fellegi-Sunter Model of Record Linkage.&lt;/em&gt; Proceedings of the Section on Survey Research Methods, American Statistical Association.&lt;/li&gt;
&lt;li&gt;McKeeman, W. M. (1998). &lt;em&gt;Differential Testing for Software.&lt;/em&gt; Digital Technical Journal, 10(1), 100–107.&lt;/li&gt;
&lt;li&gt;Claessen, K., &amp;amp; Hughes, J. (2000). &lt;em&gt;QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs.&lt;/em&gt; Proceedings of the Fifth ACM SIGPLAN International Conference on Functional Programming (ICFP '00).&lt;/li&gt;
&lt;li&gt;Cilibrasi, R., &amp;amp; Vitányi, P. M. B. (2005). &lt;em&gt;Clustering by Compression.&lt;/em&gt; IEEE Transactions on Information Theory, 51(4), 1523–1545.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full port and the DECISIONS log this writeup is pulled from is up at &lt;a href="https://github.com/Len3hq/textdistance-rs" rel="noopener noreferrer"&gt;Len3hq/textdistance-rs&lt;/a&gt; if you want to dig in yourself. &lt;br&gt;
Written for &lt;a href="https://x.com/raptors_hack" rel="noopener noreferrer"&gt;Hackathon Raptors&lt;/a&gt;, worth a follow if you're into this kind of writeup.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
