<?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: Pranav Gupta</title>
    <description>The latest articles on DEV Community by Pranav Gupta (@pranav-dev).</description>
    <link>https://dev.to/pranav-dev</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%2F4046984%2F943eba14-9db5-48c5-87eb-8aeeed5e801e.jpg</url>
      <title>DEV Community: Pranav Gupta</title>
      <link>https://dev.to/pranav-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pranav-dev"/>
    <language>en</language>
    <item>
      <title>I ported `python-semanticversion` to Rust in 72 hours</title>
      <dc:creator>Pranav Gupta</dc:creator>
      <pubDate>Wed, 05 Aug 2026 21:29:53 +0000</pubDate>
      <link>https://dev.to/pranav-dev/i-ported-python-semanticversion-to-rust-in-72-hours-5e83</link>
      <guid>https://dev.to/pranav-dev/i-ported-python-semanticversion-to-rust-in-72-hours-5e83</guid>
      <description>&lt;p&gt;For &lt;strong&gt;Port Mortem 2026&lt;/strong&gt; (a 72-hour "resurrect dead code" hackathon, Track D: Python → Rust), I rewrote &lt;a href="https://github.com/rbarrois/python-semanticversion" rel="noopener noreferrer"&gt;&lt;code&gt;python-semanticversion&lt;/code&gt;&lt;/a&gt; — SemVer 2.0 parsing/comparison plus npm-style &lt;code&gt;SimpleSpec&lt;/code&gt; / &lt;code&gt;NpmSpec&lt;/code&gt; / &lt;code&gt;LegacySpec&lt;/code&gt; range matching — as a from-scratch Rust port. Solo.&lt;/p&gt;

&lt;p&gt;The result: the &lt;strong&gt;original, unmodified pytest suite passes against the Rust build&lt;/strong&gt; — &lt;code&gt;54 passed, 16 skipped, 586 subtests&lt;/code&gt;, zero test edits — with &lt;strong&gt;zero &lt;code&gt;unsafe&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;0 fuzz divergences over 24,500 differential pairs&lt;/strong&gt;, and &lt;strong&gt;0 panics over 2.5M crash-fuzz runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the story of how, and the mistakes that nearly sank it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one constraint that shaped everything
&lt;/h2&gt;

&lt;p&gt;The rules said the original test suite must pass &lt;strong&gt;unmodified&lt;/strong&gt;, hashed at kickoff. That's a brutal constraint, and it's the best thing about the event — you can't fudge your way to green.&lt;/p&gt;

&lt;p&gt;It forced the central design decision: I built a &lt;strong&gt;PyO3/maturin extension named &lt;code&gt;semantic_version&lt;/code&gt;&lt;/strong&gt;. In the test venv, &lt;code&gt;import semantic_version&lt;/code&gt; resolves to my Rust code, so the original tests run byte-for-byte as written against the port. No shims, no test edits, no "adapted" suite. &lt;code&gt;make&lt;/code&gt; does the whole thing in one command:&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;make
&lt;span class="go"&gt;VIRTUAL_ENV=... maturin develop
pytest tests/original/ -q
54 passed, 16 skipped, 586 subtests passed in 0.37s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The 16 skips are the Django tests, which skip identically in the &lt;em&gt;original&lt;/em&gt; baseline — "Django not installed". Parity, not exclusion.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Method: ground truth first, port second
&lt;/h2&gt;

&lt;p&gt;The fastest way to fail a port is to port your &lt;em&gt;assumptions&lt;/em&gt;. Python's &lt;code&gt;semantic_version&lt;/code&gt; is full of deliberate quirks, so before writing a line of Rust I &lt;strong&gt;probed the original and captured its exact behavior&lt;/strong&gt; — AST shapes, match results, error strings — and treated that as the spec.&lt;/p&gt;

&lt;p&gt;A few things the probes revealed that I would have gotten wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;__eq__&lt;/code&gt; includes build metadata; ordering does not.&lt;/strong&gt; &lt;code&gt;Version("1.0.0+a") == Version("1.0.0+b")&lt;/code&gt; is &lt;code&gt;False&lt;/code&gt;, but neither is &lt;code&gt;&amp;lt;&lt;/code&gt; nor &lt;code&gt;&amp;gt;&lt;/code&gt; the other. That violates Rust's &lt;code&gt;Ord&lt;/code&gt; contract (&lt;code&gt;a == b ⟺ cmp(a,b) == Equal&lt;/code&gt;), so I removed &lt;code&gt;Ord&lt;/code&gt; from &lt;code&gt;Version&lt;/code&gt; and exposed explicit &lt;code&gt;precedence_lt/le/gt/ge&lt;/code&gt; helpers instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;__ne__&lt;/code&gt; compares raw tuples&lt;/strong&gt;, not &lt;code&gt;!eq&lt;/code&gt; — a Python-2-era relic where a partial and non-partial version can be &lt;code&gt;eq&lt;/code&gt; yet &lt;code&gt;ne&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error messages are single-quoted&lt;/strong&gt; (&lt;code&gt;Invalid version string: 'garbage'&lt;/code&gt;), matching Python's &lt;code&gt;%r&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Spec&lt;/code&gt; is just &lt;code&gt;LegacySpec&lt;/code&gt;.&lt;/strong&gt; One class, two names. The binding exposes one pyclass + an alias.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The hardest 40 lines: npm's prerelease OR-expansion
&lt;/h2&gt;

&lt;p&gt;npm ranges with a prerelease bound don't expand to a simple interval. &lt;code&gt;&amp;gt;=1.0.0-rc.1 &amp;lt;2.0.0&lt;/code&gt; becomes a two-branch &lt;code&gt;AnyOf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AnyOf(
  AllOf(&amp;lt;1.0.1 [always],  &amp;gt;=1.0.0-rc.1 [same-patch]),  # prerelease branch
  AllOf(&amp;gt;=1.0.0 [same-patch], &amp;lt;2.0.0 [same-patch])     # release branch
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first pass flattened this and silently diverged on &lt;code&gt;||&lt;/code&gt;-joined specs with a single-block group. The differential fuzzer is what caught it — more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof layer
&lt;/h2&gt;

&lt;p&gt;Passing the suite is necessary but not sufficient; the suite only covers what the original authors thought to test. So I built a proof layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Differential fuzz:&lt;/strong&gt; 49 seeds × 500 pairs = &lt;strong&gt;24,500&lt;/strong&gt; random (version, spec) inputs run through the original Python and the Rust binding, compared on parse/match/compare/str/repr/hash. &lt;strong&gt;0 hard divergences.&lt;/strong&gt; (1,619 soft diffs are error-&lt;em&gt;wording&lt;/em&gt; only, both sides raising &lt;code&gt;ValueError&lt;/code&gt; — documented, not hidden.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crash fuzz:&lt;/strong&gt; &lt;strong&gt;2,554,822&lt;/strong&gt; libFuzzer runs over arbitrary bytes → &lt;strong&gt;0 panics&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero &lt;code&gt;unsafe&lt;/code&gt;.&lt;/strong&gt; The whole port is safe Rust; &lt;code&gt;grep -rn unsafe src/&lt;/code&gt; is empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 20-entry decision log&lt;/strong&gt; (&lt;code&gt;DECISIONS.md&lt;/code&gt;, D00–D19), every non-trivial divergence with Python behavior → Rust choice → rationale → tradeoff → test impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the fuzzer caught (honesty section)
&lt;/h2&gt;

&lt;p&gt;The differential fuzzer found &lt;strong&gt;8 latent bugs — all in &lt;em&gt;my&lt;/em&gt; port, none in the original.&lt;/strong&gt; Including 18 &lt;code&gt;u64&lt;/code&gt; overflow-panic sites (Python has bignums; Rust doesn't) that I hardened with &lt;code&gt;saturating_add&lt;/code&gt;, an empty-prerelease acceptance, &lt;code&gt;~*&lt;/code&gt;/&lt;code&gt;^*&lt;/code&gt; wildcard gates, and the &lt;code&gt;||&lt;/code&gt; empty-group case above.&lt;/p&gt;

&lt;p&gt;I'm &lt;em&gt;not&lt;/em&gt; claiming a "bug catcher" bonus: the original library was correct, and my job was to converge to it. But the fuzzer turning my own blind spots into a fix-list is exactly why differential testing is the only oracle that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks, with the boring parts included
&lt;/h2&gt;

&lt;p&gt;On a hackathon cloud VM (16GB RAM 2 physical / 4 logical cores, not bare metal):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~9×&lt;/strong&gt; aggregate speedup, &lt;strong&gt;60×&lt;/strong&gt; on npm spec matching, &lt;strong&gt;~11×&lt;/strong&gt; on parsing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21% lower&lt;/strong&gt; peak RSS (12.5 MB vs 15.9 MB)&lt;/li&gt;
&lt;li&gt;And the honest caveat: the PyO3 &lt;code&gt;precedence_key&lt;/code&gt; path drags one aggregate number down (Python tuple overhead); native precedence runs at ~386 ns p50.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Throughput-only benchmarks are marketing. Distributions + confounders are engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself at hour 0
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Probe before you port.&lt;/strong&gt; The original is the spec; your memory of it is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let a fuzzer argue with you.&lt;/strong&gt; It will find the cases your tests never imagined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honesty is a feature.&lt;/strong&gt; Judges trust "94% and here's why" over "100%" that won't reproduce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An AI agent is a force multiplier only if a human gates every commit.&lt;/strong&gt; Multi-model, single-writer, review-everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&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/rahulgupta0-dev/semanticversion-rs
&lt;span class="nb"&gt;cd &lt;/span&gt;semanticversion-rs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make   &lt;span class="c"&gt;# builds + runs the ORIGINAL suite against Rust&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3,683 lines of safe Rust, 20 decisions, one command to believe it. Whether or not it places, it's the most rigorously verified thing I've ever shipped in 72 hours.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>python</category>
      <category>hackathon</category>
      <category>npm</category>
    </item>
  </channel>
</rss>
