<?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: LubuSeb</title>
    <description>The latest articles on DEV Community by LubuSeb (@lubuseb).</description>
    <link>https://dev.to/lubuseb</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%2F4059178%2F1b19713e-e615-48ab-904d-6397a0e4a844.jpg</url>
      <title>DEV Community: LubuSeb</title>
      <link>https://dev.to/lubuseb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lubuseb"/>
    <language>en</language>
    <item>
      <title>I ported Picomatch to Rust. It passed 1,977 tests and lost the benchmark by 18x</title>
      <dc:creator>LubuSeb</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:09:38 +0000</pubDate>
      <link>https://dev.to/lubuseb/i-ported-picomatch-to-rust-it-passed-1977-tests-and-lost-the-benchmark-by-18x-19fa</link>
      <guid>https://dev.to/lubuseb/i-ported-picomatch-to-rust-it-passed-1977-tests-and-lost-the-benchmark-by-18x-19fa</guid>
      <description>&lt;p&gt;&lt;em&gt;A Port Mortem 2026 write-up about a JavaScript to Rust port, one strange Unicode character, and what green tests do not tell you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first benchmark result was not flattering.&lt;/p&gt;

&lt;p&gt;Picomatch, running under Node, handled about 7.18 million matches per second in my test. The direct Rust implementation managed about 395,000. That made the Rust version roughly 18.2 times slower.&lt;/p&gt;

&lt;p&gt;I kept the result because it makes the point of the project clearer. The hard part was verification: how much evidence would it take to trust an AI-assisted rewrite beyond the obvious cases?&lt;/p&gt;

&lt;h2&gt;
  
  
  The small library that was not a small target
&lt;/h2&gt;

&lt;p&gt;Picomatch is a dependency-free glob matcher used across the JavaScript ecosystem. Its committed runtime is only 2,444 lines of JavaScript, which made it look like a sensible target for a 72-hour port.&lt;/p&gt;

&lt;p&gt;The scope was reasonable for 72 hours, but I underestimated the compatibility surface.&lt;/p&gt;

&lt;p&gt;Matching &lt;code&gt;*.js&lt;/code&gt; is easy. Matching Picomatch means dealing with globstars at path boundaries, nested extglobs, braces, negation, captures, callbacks, Bash and minimatch options, Windows separators, JavaScript regex flags, UTF-16 indexes, and inputs designed to make a parser or regex engine suffer.&lt;/p&gt;

&lt;p&gt;I chose the official Picomatch snapshot at commit &lt;code&gt;4f41a8e&lt;/code&gt; and wrote a standalone Rust scanner, compiler, and matcher. The command-line program runs without Node. A small JavaScript adapter exists for the unchanged upstream test suite because those tests expect JavaScript callbacks, &lt;code&gt;RegExp&lt;/code&gt; objects, and synchronous API behavior. It does not contain a fallback glob matcher. Every search that is not an API shortcut runs in Rust.&lt;/p&gt;

&lt;p&gt;Getting that boundary right mattered. Otherwise I could have produced a convincing test report while quietly letting JavaScript do the difficult work.&lt;/p&gt;

&lt;h2&gt;
  
  
  All 1,977 tests were green. The port was still wrong
&lt;/h2&gt;

&lt;p&gt;The inherited Picomatch suite eventually passed in full. All 1,977 tests were green, and the repository also had 28 native Rust tests.&lt;/p&gt;

&lt;p&gt;Then differential testing found more problems.&lt;/p&gt;

&lt;p&gt;One of the clearest mismatches involved &lt;code&gt;ſ&lt;/code&gt;, the Latin long s. It looks a little like an &lt;code&gt;f&lt;/code&gt; without the full crossbar. JavaScript treats it differently depending on whether a regular expression uses legacy case-insensitive matching or Unicode-aware case-insensitive matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ſ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;iu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ſ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// true&lt;/span&gt;

&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;K&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;iu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;K&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My early implementation treated case-insensitive matching as one Unicode problem. JavaScript actually has two relevant behaviors here. Legacy &lt;code&gt;/i&lt;/code&gt; uses its own Canonicalize rules. Adding &lt;code&gt;u&lt;/code&gt; changes the result for characters such as long s and the Kelvin sign.&lt;/p&gt;

&lt;p&gt;An engine can call itself ECMAScript-compatible and still miss exactly this sort of historical corner.&lt;/p&gt;

&lt;p&gt;Instead of adding only those two characters as regressions, I pinned Node 24.18.0, derived the complete legacy Canonicalize table for the Basic Multilingual Plane, and added it to the native engine. The proof walks all 65,536 BMP code units, finds every nonidentity mapping, and checks both literal and character-class matching against Node and the pinned Picomatch version.&lt;/p&gt;

&lt;p&gt;That produced 1,169 nonidentity mappings, 2,392 ordered equivalences, and 4,784 literal and class checks. The odd character that exposed the bug became a test for the whole defined scope.&lt;/p&gt;

&lt;p&gt;The upstream suite was useful, but I did not want the final claim to depend on one set of examples or on hashes created inside my own repository.&lt;/p&gt;

&lt;p&gt;The proof grew in layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Upstream provenance&lt;/td&gt;
&lt;td&gt;38 test and fixture files pinned to Picomatch commit &lt;code&gt;4f41a8e&lt;/code&gt;, fetched and byte-compared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original suite&lt;/td&gt;
&lt;td&gt;1,977 unchanged tests passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native regressions&lt;/td&gt;
&lt;td&gt;28 Rust tests passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated differential cases&lt;/td&gt;
&lt;td&gt;100,000 comparisons across five fixed seeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directed differential cases&lt;/td&gt;
&lt;td&gt;535 executions, replaying 107 known edge cases under every seed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy case folding&lt;/td&gt;
&lt;td&gt;4,784 literal and class checks over the derived BMP mappings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The provenance check deserves a little explanation. A local checksum only proves that files did not change after I hashed them. It does not prove that I started with the upstream files. The stronger check fetches the exact upstream commit, confirms that the manifest covers the complete test tree, normalizes line endings, and byte-compares every file.&lt;/p&gt;

&lt;p&gt;The differential harness runs the pinned JavaScript implementation and the Rust port on the same generated patterns, paths, platform modes, and options. It includes extglobs, braces, classes, negation, Unicode, separators, and captures. Five fixed seeds keep it reproducible. Known failures are kept as directed cases instead of being left to chance.&lt;/p&gt;

&lt;p&gt;The final run reported zero mismatches across 100,535 differential executions.&lt;/p&gt;

&lt;p&gt;That is not a formal proof over every possible string. It is bounded, reproducible evidence. I think saying where the evidence stops is part of making it useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extglobs needed a compiler, not substitutions
&lt;/h2&gt;

&lt;p&gt;Another early mistake was thinking too locally about extglobs. Operators such as &lt;code&gt;?()&lt;/code&gt;, &lt;code&gt;*()&lt;/code&gt;, &lt;code&gt;+()&lt;/code&gt;, &lt;code&gt;@()&lt;/code&gt;, and &lt;code&gt;!()&lt;/code&gt; look like five pieces of syntax that can be translated one at a time.&lt;/p&gt;

&lt;p&gt;They cannot. Nesting changes the meaning. So do suffixes, alternation, path separators, captures, and repetition. A shallow implementation handled ordinary examples, then stalled or under-matched cases with nested negative extglobs.&lt;/p&gt;

&lt;p&gt;The correction was structural. The compiler now reduces pure chains of nested negation by parity while preserving the surrounding suffix and alternative context. It rewrites only repeated languages that it can show are safe, and it keeps the ambiguous cases literal when that is what Picomatch requires.&lt;/p&gt;

&lt;p&gt;Earlier adversarial and differential runs were more useful than the final zero-mismatch total because they supplied the cases that became permanent regressions. Those cases include nested negation, legacy case folding, capture numbering, regex flags, typed transport fields, Windows separators, globstar boundaries, and eager ignore behavior.&lt;/p&gt;

&lt;p&gt;A second compiler problem came from Picomatch's public regex source. APIs such as &lt;code&gt;parse&lt;/code&gt; and &lt;code&gt;makeRe&lt;/code&gt; expose the generated regular expression, and some upstream tests inspect it. At the same time, the Rust matcher needs an execution form that makes path-separator rules explicit.&lt;/p&gt;

&lt;p&gt;I initially tried to make one generated regex serve both purposes. That forced a bad choice. I could satisfy observable source assertions, or I could enforce native path behavior, but not always both with the same representation.&lt;/p&gt;

&lt;p&gt;The final compiler emits two related forms. One is the public source used by the compatibility API. The other is a private, slash-safe execution source used by Rust. Both come from the same compiler, but they have different jobs.&lt;/p&gt;

&lt;p&gt;Captures made the same lesson harder to ignore. With &lt;code&gt;capture: true&lt;/code&gt;, compiler-generated groups affect backreference numbering. Wildcards, braces, brackets, extglobs, named groups, and unmatched groups all need to be planned while the regex is emitted. Rust now returns the full match and every capture as UTF-16 spans. The adapter uses those spans to reconstruct the JavaScript result, including named groups and &lt;code&gt;d&lt;/code&gt;-flag indices. JavaScript owns state such as &lt;code&gt;g&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; &lt;code&gt;lastIndex&lt;/code&gt;, but Rust performs each search from the requested position.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safety limit must not look like a non-match
&lt;/h2&gt;

&lt;p&gt;Glob patterns eventually become regular expressions, and regular expressions can backtrack badly. Limiting the parser was not enough. A valid-looking pattern could still consume unreasonable execution work.&lt;/p&gt;

&lt;p&gt;The compiler now bounds pattern length, nesting depth, branch count, unmatched bracket markers, and total compile work. The vendored regex engine also has deterministic execution fuel. Dispatch, scans, backtracking, and backreference comparisons all spend from the same budget.&lt;/p&gt;

&lt;p&gt;When that budget runs out, the matcher returns a recoverable &lt;code&gt;safe work limit&lt;/code&gt; error. It does not return &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters. A non-match says the engine finished and found no match. Exhaustion says the engine could not safely finish. Treating those as the same result could select or skip the wrong files in a build pipeline.&lt;/p&gt;

&lt;p&gt;The safety tests include both sides. A linear match against a one-million-character input succeeds. The hostile pattern &lt;code&gt;+(a*)b&lt;/code&gt; against a short near-miss reaches the work limit and returns an explicit error. The next ordinary request still succeeds through the same persistent process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the benchmark measured
&lt;/h2&gt;

&lt;p&gt;I benchmarked three paths using the same four precompiled pattern and input pairs. Each path returned the same 75 percent match rate. The figures below are medians from three timed runs after warm-up.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Median throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Picomatch on Node&lt;/td&gt;
&lt;td&gt;7,184,496 ops/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct native Rust API&lt;/td&gt;
&lt;td&gt;395,048 ops/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust through the synchronous proof adapter&lt;/td&gt;
&lt;td&gt;11,602 ops/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;V8's optimized regular-expression engine is extremely fast. The Rust implementation uses a safe ECMAScript interpreter with explicit fuel accounting. The proof adapter adds worker wakeups, framing, IPC, cache lookup, and JavaScript object reconstruction on top of that.&lt;/p&gt;

&lt;p&gt;If throughput were the only goal, this port would lose. The direct native matcher is still practical at roughly 0.40 million matches per second for this workload, but I am not presenting it as a speed improvement.&lt;/p&gt;

&lt;p&gt;So the port's value is not higher throughput. The Rust API and CLI run independently of Node, expensive work fails explicitly, and the repository provides a reproducible compatibility check. Publishing only the flattering measurements would weaken that claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;If I started again, I would build a small JavaScript regex conformance probe before committing to the first execution-engine design.&lt;/p&gt;

&lt;p&gt;It would cover legacy &lt;code&gt;/i&lt;/code&gt; versus &lt;code&gt;/iu&lt;/code&gt;, UTF-16 offsets, named and unmatched captures, &lt;code&gt;d&lt;/code&gt; indices, &lt;code&gt;g&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; state, lookarounds, and backreferences. I would also separate the public regex source from the private execution form on day one.&lt;/p&gt;

&lt;p&gt;Instead, those facts arrived as failures during integration. The failures led to a better design, but they cost time late in the build when every change touched the compiler, matcher, adapter, and proof harness.&lt;/p&gt;

&lt;p&gt;I would also add direct upstream byte comparison earlier. Repository-owned hashes felt reassuring until an independent review pointed out that the repository was effectively certifying itself.&lt;/p&gt;

&lt;p&gt;That review process was deliberate. I used separate coding agents to challenge the Rust design, provenance, fuzz grammar, captures, flags, safety, benchmarks, and presentation. I only accepted a concern when I could reproduce it as a failing case and run the fix through the full suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would reuse on the next port
&lt;/h2&gt;

&lt;p&gt;The process that held up was fairly simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pin the exact source revision and fetch it during verification.&lt;/li&gt;
&lt;li&gt;Keep the inherited tests unchanged.&lt;/li&gt;
&lt;li&gt;Run the source and target implementations on the same generated inputs.&lt;/li&gt;
&lt;li&gt;Turn every mismatch into a permanent directed regression.&lt;/li&gt;
&lt;li&gt;Exhaustively test small domains that are defined by a specification.&lt;/li&gt;
&lt;li&gt;Benchmark the unflattering paths too.&lt;/li&gt;
&lt;li&gt;State the implementation boundary and the limits of the proof.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Picomatch Mortis now passes all 1,977 frozen upstream tests and 28 native tests. Its current bounded differential corpus reports zero mismatches across 100,535 executions. Rust owns scanning, compilation, and every non-short-circuited match, capture, and ignore search. JavaScript remains at the API boundary where JavaScript objects and callbacks require it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receipts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/LubuSeb/picomatch-mortis" rel="noopener noreferrer"&gt;Source and reproduction instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/LubuSeb/picomatch-mortis/blob/main/DECISIONS.md" rel="noopener noreferrer"&gt;Decision log&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/LubuSeb/picomatch-mortis/blob/main/BENCHMARK.md" rel="noopener noreferrer"&gt;Benchmark report&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/LubuSeb/picomatch-mortis/releases/tag/port-mortem-demo-v1" rel="noopener noreferrer"&gt;Judge demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for &lt;a href="https://www.raptors.dev/" rel="noopener noreferrer"&gt;Hackathon Raptors&lt;/a&gt;, &lt;a href="https://x.com/raptors_hack" rel="noopener noreferrer"&gt;@raptors_hack&lt;/a&gt;, and the Port Mortem 2026 JavaScript to Rust track.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>javascript</category>
      <category>testing</category>
      <category>hackathon</category>
    </item>
  </channel>
</rss>
