<?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: Shivharakh</title>
    <description>The latest articles on DEV Community by Shivharakh (@shivharakhyadav).</description>
    <link>https://dev.to/shivharakhyadav</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%2F4067661%2Fabd0b63d-ff88-4f88-9c56-a398dfc26191.png</url>
      <title>DEV Community: Shivharakh</title>
      <link>https://dev.to/shivharakhyadav</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shivharakhyadav"/>
    <language>en</language>
    <item>
      <title>I Ported a Python Library to Rust. All 1,059 Original Tests Passed — and It Was Still Broken.</title>
      <dc:creator>Shivharakh</dc:creator>
      <pubDate>Sun, 09 Aug 2026 14:39:52 +0000</pubDate>
      <link>https://dev.to/shivharakhyadav/i-ported-a-python-library-to-rust-all-1059-original-tests-passed-and-it-was-still-broken-3mge</link>
      <guid>https://dev.to/shivharakhyadav/i-ported-a-python-library-to-rust-all-1059-original-tests-passed-and-it-was-still-broken-3mge</guid>
      <description>&lt;p&gt;The rule for this hackathon is blunt: take the original project's test suite, don't touch it, make it pass against your rewrite. One edited test file is an automatic zero.&lt;/p&gt;

&lt;p&gt;Mine passed. 1,059 tests, green, on a file I never opened. The port was still wrong, and the thing that found it wasn't a test — it was 500,000 randomly generated price strings run through both implementations side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1,059 passed, 134 xfailed&lt;/strong&gt; from upstream's own suite, byte-for-byte unmodified — SHA-256 verified
on every CI run, and &lt;strong&gt;one commit&lt;/strong&gt; in the entire repository history for that directory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500,000 differential fuzz cases in 100.4 seconds, zero divergences&lt;/strong&gt;, seed &lt;code&gt;20260803&lt;/code&gt;, replayable
(&lt;code&gt;fuzz/log.txt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero &lt;code&gt;unsafe&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;forbid(unsafe_code)&lt;/code&gt; makes it a compile error, not a promise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Around 4× throughput&lt;/strong&gt;, and around 13× startup for a native Rust caller (about 3× from Python) —
deliberately imprecise, because the measurement is noisier than a clean multiple would suggest&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/scrapinghub/price-parser" rel="noopener noreferrer"&gt;price-parser&lt;/a&gt; is Scrapinghub's Python library for pulling a price and a currency out of scraped text — &lt;code&gt;"US$ 1,234.56"&lt;/code&gt; in, &lt;code&gt;1234.56&lt;/code&gt; and &lt;code&gt;"US$"&lt;/code&gt; out. Small library, unglamorous job, used in a lot of scrapers.&lt;/p&gt;

&lt;p&gt;I picked it deliberately. Almost all of its behaviour lives in two places where Python and Rust quietly disagree: &lt;strong&gt;regular expressions&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;Decimal&lt;/code&gt;&lt;/strong&gt;. That makes it a bad afternoon project and a good test of the actual question, which isn't "can I write Rust" — it's "can I reproduce someone else's exact behaviour, including the parts they probably never intended?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track D (Python → Rust), Port Mortem / Code Resurrection 2026.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The suite is frozen, and it passed
&lt;/h2&gt;

&lt;p&gt;"&lt;strong&gt;Don't touch it&lt;/strong&gt;" is enforced, not trusted. The original test file is hashed at kickoff, and the hash is checked on every push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python tools/verify_hashes.py
&lt;span class="go"&gt;OK: 1 original test file(s) verified unmodified
  92c72582cca6b9d0201782ddc2665538d9c30602619a4bc06c5afc7fd5f17966  test_price_parsing.py
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The history says the same thing from a different direction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; tests/original/
&lt;span class="go"&gt;0118b95 test: vendor original suite with SHA-256 manifest
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One commit. Added once, never edited — not "I only changed the imports."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest tests/original &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;span class="go"&gt;1059 passed, 134 xfailed in 23.87s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 134 xfails are upstream's own, marked inside that same hashed file — there is no &lt;code&gt;conftest.py&lt;/code&gt; in this repository to add them, and they're &lt;code&gt;strict=True&lt;/code&gt;, so if one of them unexpectedly &lt;em&gt;passed&lt;/em&gt; here, pytest would report it as a failure. They aren't a place to hide anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug 1,059 tests could not find
&lt;/h2&gt;

&lt;p&gt;That green line is where a port is easy to declare finished. Mine sat there for about a day. Then the differential fuzzer ran — generate a price string, feed the same bytes to upstream Python and to the Rust, compare every field — and found it on the first real run.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Decimal("٥")&lt;/code&gt; is 5. Python's &lt;code&gt;Decimal&lt;/code&gt; accepts &lt;strong&gt;any&lt;/strong&gt; Unicode decimal digit. &lt;code&gt;rust_decimal&lt;/code&gt; accepts ASCII and nothing else.&lt;/p&gt;

&lt;p&gt;The nasty part is where the divergence sat. Both regex engines match &lt;code&gt;\p{Nd}&lt;/code&gt; for &lt;code&gt;\d&lt;/code&gt;, so extraction agreed perfectly — the port found the digits, correctly, every time. The failure was entirely in the conversion afterwards. No exception, no error, no warning. A price written in Arabic-Indic, Devanagari or Bengali numerals just came back with no amount at all.&lt;/p&gt;

&lt;p&gt;Upstream's test corpus is scraped Western storefronts. It is effectively all ASCII. &lt;strong&gt;There is no test among those 1,059 that could have caught this, and there never would have been&lt;/strong&gt; — not because the suite is bad, but because a test suite can only cover inputs somebody thought of.&lt;/p&gt;

&lt;p&gt;The fix leans on a fact about Unicode: every character in general category &lt;code&gt;Nd&lt;/code&gt; sits in a contiguous run of ten starting at its own script's zero. So you don't need a table of digits, only the 68 run starts — generated from the Unicode data, not typed by hand — and the value falls out by subtraction.&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="cd"&gt;/// Code point of the zero character for each decimal-digit run, ascending.&lt;/span&gt;
&lt;span class="cd"&gt;/// Unicode 15.0.0, 68 runs. Generated by tools/gen_unicode_digits.py.&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DECIMAL_RUN_STARTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mi"&gt;0x00030&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// DIGIT ZERO&lt;/span&gt;
    &lt;span class="mi"&gt;0x00660&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ARABIC-INDIC DIGIT ZERO&lt;/span&gt;
    &lt;span class="mi"&gt;0x006f0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// EXTENDED ARABIC-INDIC DIGIT ZERO&lt;/span&gt;
    &lt;span class="mi"&gt;0x00966&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// DEVANAGARI DIGIT ZERO&lt;/span&gt;
    &lt;span class="mi"&gt;0x009e6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// BENGALI DIGIT ZERO&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 63 more&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;decimal_digit_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;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;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ASCII is overwhelmingly the common case; skip the search for it.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="nf"&gt;.is_ascii_digit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;b'0'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DECIMAL_RUN_STARTS&lt;/span&gt;&lt;span class="nf"&gt;.partition_point&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;start&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DECIMAL_RUN_STARTS&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="nf"&gt;.checked_sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.then_some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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;&lt;code&gt;amount_text&lt;/code&gt; still returns the original digits untouched, because that is what upstream does. Only the numeric conversion folds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways Python and Rust quietly disagree
&lt;/h2&gt;

&lt;p&gt;These are the dangerous ones. Each compiles, survives a careful reading, and is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;len()&lt;/code&gt; counts characters. &lt;code&gt;str::len()&lt;/code&gt; counts bytes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Upstream sorts currency symbols by length, longest first, so &lt;code&gt;US$&lt;/code&gt; is tried before &lt;code&gt;$&lt;/code&gt;. Port that with Rust's &lt;code&gt;.len()&lt;/code&gt; and &lt;code&gt;€&lt;/code&gt; — one character, three bytes — gets ranked alongside &lt;code&gt;US$&lt;/code&gt;, silently reordering the alternation and matching the wrong currency. The fix is &lt;code&gt;.chars().count()&lt;/code&gt;, and nothing about the code &lt;em&gt;looks&lt;/em&gt; different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Python's regex &lt;code&gt;\s&lt;/code&gt; matches &lt;code&gt;U+001C&lt;/code&gt;–&lt;code&gt;U+001F&lt;/code&gt;. Rust's doesn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rust's &lt;code&gt;\s&lt;/code&gt; is the Unicode &lt;code&gt;White_Space&lt;/code&gt; property, which excludes the file, group, record and unit separators. Upstream normalises &lt;code&gt;"1\x1c234"&lt;/code&gt; to &lt;code&gt;"1 234"&lt;/code&gt;. The character class had to be widened to &lt;code&gt;[\s\x{1c}-\x{1f}]&lt;/code&gt;. &lt;code&gt;str.strip()&lt;/code&gt; has exactly the same gap, so &lt;code&gt;parse_number&lt;/code&gt; strips with a predicate matching Python's notion of whitespace rather than Rust's.&lt;/p&gt;

&lt;p&gt;I verified this by probing CPython rather than trusting the docs, and I'd recommend that habit to anyone doing a port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Python's &lt;code&gt;$&lt;/code&gt; also matches before a single trailing newline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rust's &lt;code&gt;$&lt;/code&gt; matches only at end-of-haystack. Upstream returns &lt;code&gt;'.'&lt;/code&gt; as the decimal separator for &lt;code&gt;"12.99\n"&lt;/code&gt;; a direct translation returns &lt;code&gt;None&lt;/code&gt;. One trailing newline gets stripped before matching — exactly one, and trailing &lt;em&gt;spaces&lt;/em&gt; are left alone, because &lt;code&gt;"12.99 "&lt;/code&gt; gives &lt;code&gt;None&lt;/code&gt; on both sides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Thirty-two currency symbols carry an invisible &lt;code&gt;U+200F&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right-to-left marks on Arabic currency symbols: 16 in the national-symbol table, 16 more in the safe list. A generator that dropped them would look perfectly correct on inspection while failing to match real input. They're escaped as &lt;code&gt;\u{…}&lt;/code&gt; in the generated source, and a test pins the count at 16 so a silent loss breaks the build instead of shipping.&lt;/p&gt;

&lt;p&gt;There's a fifth that isn't a language difference but bit just as hard: &lt;strong&gt;two of upstream's three currency lists are built with &lt;code&gt;list({…})&lt;/code&gt; over a set.&lt;/strong&gt; I ran the generator three times and got three different orderings. Emitting that directly would have produced a different file on every run, so both are sorted — safe here, because order only matters between candidates of &lt;em&gt;different&lt;/em&gt; lengths, and length precedence is applied separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The regex Rust cannot express
&lt;/h2&gt;

&lt;p&gt;Upstream's euro-as-decimal-separator pattern contains this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\d(?(1)\d|\d*?)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;(?(1)yes|no)&lt;/code&gt; is a conditional group: &lt;em&gt;if group 1 participated, match this, otherwise match that.&lt;/em&gt; Rust's &lt;code&gt;regex&lt;/code&gt; crate has no conditionals — and no lookaround, and no backreferences. That's a deliberate design choice, not a gap; it's what buys the linear-time guarantee.&lt;/p&gt;

&lt;p&gt;It also doesn't need one here. A conditional has exactly two outcomes, so the pattern splits into two, tried in the order the engine itself would try them:&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="cd"&gt;/// The *yes* arm. `(\s*?)?` participates -- matching zero or more&lt;/span&gt;
&lt;span class="cd"&gt;/// whitespace -- so exactly two digits must follow.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;euro_participating_regex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LazyLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;LazyLock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;r"[\d\s.,']*?\d\s*?€\s*?\d\d(?:$|[^\d])"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"euro participating pattern must compile"&lt;/span&gt;&lt;span class="p"&gt;)&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;RE&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cd"&gt;/// The *no* arm. Reached only by backtracking, when the arm above fails.&lt;/span&gt;
&lt;span class="cd"&gt;/// Skipping the group consumes no whitespace, so a digit must follow the&lt;/span&gt;
&lt;span class="cd"&gt;/// euro sign immediately -- and the run is lazy and unbounded, not fixed at two.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;euro_skipped_regex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LazyLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;LazyLock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;r"[\d\s.,']*?\d\s*?€\d\d*?(?:$|[^\d])"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"euro skipped pattern must compile"&lt;/span&gt;&lt;span class="p"&gt;)&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;RE&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ordering rule is the part that's easy to get wrong, and I got it wrong first: &lt;strong&gt;the leftmost starting position always wins&lt;/strong&gt;, and arm order only decides ties. &lt;code&gt;$|US$&lt;/code&gt; and &lt;code&gt;US$|$&lt;/code&gt; both yield &lt;code&gt;"US$"&lt;/code&gt; on &lt;code&gt;"US$100"&lt;/code&gt;, because at index 0 only &lt;code&gt;US$&lt;/code&gt; can match at all. A failing test corrected me.&lt;br&gt;
Match that and the behaviour lines up exactly — &lt;code&gt;"12€345"&lt;/code&gt; matches the skipped arm with three digits, while &lt;code&gt;"12€ 345"&lt;/code&gt; matches neither and falls through to the ordinary rule.&lt;/p&gt;
&lt;h2&gt;
  
  
  About that PyO3 layer
&lt;/h2&gt;

&lt;p&gt;Worth being direct about, since it's the first thing anyone reading the repo will notice.&lt;br&gt;
The core crate has no PyO3 in it at all. &lt;code&gt;parse_price&lt;/code&gt; is ordinary Rust, and a Rust caller never touches Python. The CPython extension module is an &lt;strong&gt;optional feature&lt;/strong&gt; — it exists so upstream's suite can run against this code unmodified, which is the whole scoring criterion.&lt;/p&gt;

&lt;p&gt;That's also why the &lt;code&gt;unsafe&lt;/code&gt; story has an asterisk:&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;// Unsafe is forbidden outright in the pure-Rust build. With the `python`&lt;/span&gt;
&lt;span class="c1"&gt;// feature it must be permitted, since PyO3's macro expansion relies on it at&lt;/span&gt;
&lt;span class="c1"&gt;// the FFI boundary -- but no hand-written unsafe exists in this crate.&lt;/span&gt;
&lt;span class="nd"&gt;#![cfg_attr(not(feature&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"python"&lt;/span&gt;&lt;span class="nd"&gt;),&lt;/span&gt; &lt;span class="nd"&gt;forbid(unsafe_code))]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;forbid&lt;/code&gt; rather than &lt;code&gt;deny&lt;/code&gt; is the point: it can't be overridden by an inner &lt;code&gt;allow&lt;/code&gt;, so this is a compile error, not a lint someone switches off later. And the differential fuzzer doesn't link Python either — it spawns real CPython as a separate process and compares across the process boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark that lied to me twice
&lt;/h2&gt;

&lt;p&gt;Neither lie was caught by a test. Both were caught by asking whether a number could possibly be true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First lie: the build profile.&lt;/strong&gt; &lt;code&gt;maturin develop&lt;/code&gt; defaults to a debug build, which is roughly twenty times slower. The first benchmark run compared a debug extension against a release binary and concluded the port was five times &lt;em&gt;slower&lt;/em&gt; than the Python it replaces. The module now reports its own profile and the benchmark refuses to run against debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second lie: peak memory, twice, on two platforms.&lt;/strong&gt; On Windows a &lt;code&gt;ctypes&lt;/code&gt; call returned a constant ~3.4 MiB no matter the workload. I didn't take that on faith — I made a child process allocate 200 MB and watched the reported figure not move. On Linux,&lt;br&gt;
&lt;code&gt;getrusage(RUSAGE_CHILDREN).ru_maxrss&lt;/code&gt; returned an identical 393 MiB for all three implementations, because it's a high-water mark across &lt;em&gt;every child the process has ever reaped&lt;/em&gt; — it was reporting an earlier &lt;code&gt;cargo build&lt;/code&gt;, forever. It reads like a real measurement and is nothing of the kind.&lt;br&gt;
Linux now samples &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt; &lt;code&gt;VmHWM&lt;/code&gt; while the child is alive. Windows reports &lt;code&gt;null&lt;/code&gt;, because publishing nothing beats publishing a number you know is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both broken versions returned a constant.&lt;/strong&gt; That's the tell, and it's the one thing here worth stealing.&lt;/p&gt;

&lt;p&gt;The numbers that survived:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Python&lt;/th&gt;
&lt;th&gt;Port (from Python)&lt;/th&gt;
&lt;th&gt;Port (native Rust)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;per parse&lt;/td&gt;
&lt;td&gt;21.98 µs&lt;/td&gt;
&lt;td&gt;4.56 µs&lt;/td&gt;
&lt;td&gt;4.65 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;parses/sec&lt;/td&gt;
&lt;td&gt;45,498&lt;/td&gt;
&lt;td&gt;219,261&lt;/td&gt;
&lt;td&gt;215,258&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;startup&lt;/td&gt;
&lt;td&gt;324.3 ms&lt;/td&gt;
&lt;td&gt;108.1 ms&lt;/td&gt;
&lt;td&gt;25.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p50&lt;/td&gt;
&lt;td&gt;18.30 µs&lt;/td&gt;
&lt;td&gt;4.50 µs&lt;/td&gt;
&lt;td&gt;3.50 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99&lt;/td&gt;
&lt;td&gt;70.00 µs&lt;/td&gt;
&lt;td&gt;10.60 µs&lt;/td&gt;
&lt;td&gt;9.50 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99.9&lt;/td&gt;
&lt;td&gt;586.10 µs&lt;/td&gt;
&lt;td&gt;59.80 µs&lt;/td&gt;
&lt;td&gt;65.90 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max&lt;/td&gt;
&lt;td&gt;16,499 µs&lt;/td&gt;
&lt;td&gt;881 µs&lt;/td&gt;
&lt;td&gt;1,515 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the bottom two rows: native Rust &lt;em&gt;loses&lt;/em&gt; to the FFI path that wraps it. That's impossible as a real result — a wrapper can't beat the thing it wraps — which is exactly why those rows stay in. It means the two Rust paths can't be told apart on this hardware, and the honest headline is &lt;strong&gt;around 4×&lt;/strong&gt;, not 4.82×.&lt;/p&gt;
&lt;h2&gt;
  
  
  Watch it live
&lt;/h2&gt;

&lt;p&gt;The frozen suite running, the hash check, the fuzz log, and the zero &lt;code&gt;unsafe&lt;/code&gt; claim demonstrated rather than asserted:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/TeQ5G4jFta4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  All the numbers in one place
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1,059 passed, 134 xfailed&lt;/strong&gt; — upstream's suite, unmodified and hash-verified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500,000 fuzz cases, 100.4 s, 4,982 per second, zero divergences&lt;/strong&gt; — seed &lt;code&gt;20260803&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;390,723 of 500,000&lt;/strong&gt; generated cases carried a parseable amount&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,178 price strings&lt;/strong&gt; in the benchmark corpus, extracted from the frozen suite with &lt;code&gt;ast&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;68&lt;/strong&gt; Unicode &lt;code&gt;Nd&lt;/code&gt; run starts · &lt;strong&gt;32&lt;/strong&gt; symbols carrying an invisible &lt;code&gt;U+200F&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero &lt;code&gt;unsafe&lt;/code&gt;&lt;/strong&gt;, compiler-enforced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;34 documented decisions&lt;/strong&gt;, including the two where I was wrong and Python settled it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No bug claimed in the original.&lt;/strong&gt; Differential testing finds places where &lt;em&gt;I&lt;/em&gt; diverge from
upstream, not places upstream is wrong. Every divergence found was mine. I looked, and I'm not going
to invent one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Go poke at it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ShivharakhYadav/price-parser-rs
&lt;span class="nb"&gt;cd &lt;/span&gt;price-parser-rs
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; price-parser-rs &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; price-parser-rs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No toolchain of your own required. That last command verifies the test-file hashes, runs the Rust tests, runs the original Python suite, and then &lt;strong&gt;verifies the hashes a second time&lt;/strong&gt; — so a suite that had been quietly edited somewhere in the middle couldn't produce a green run either side of it.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ShivharakhYadav/price-parser-rs" rel="noopener noreferrer"&gt;github.com/ShivharakhYadav/price-parser-rs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every number above is a committed file — &lt;code&gt;fuzz/log.txt&lt;/code&gt;, &lt;code&gt;bench/results.json&lt;/code&gt;, and &lt;code&gt;DECISIONS.md&lt;/code&gt;, which is 34 entries and not a highlight reel.&lt;/p&gt;

&lt;p&gt;Thanks for reading. 🦀&lt;/p&gt;

</description>
      <category>rust</category>
      <category>python</category>
      <category>portmortem</category>
      <category>hackathonraptors</category>
    </item>
  </channel>
</rss>
