<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Claude on Nelson Chen's Blog</title><link>https://mindflakes.com/tags/claude/</link><description>Recent content in Claude on Nelson Chen's Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>Copyright Nelson Chen</copyright><lastBuildDate>Sat, 07 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://mindflakes.com/tags/claude/index.xml" rel="self" type="application/rss+xml"/><item><title>Introducing d2-rs: a Rust port of D2 for the CLI and WebAssembly</title><link>https://mindflakes.com/posts/2026/11/07/d2-rs-behavior-parity/</link><pubDate>Sat, 07 Nov 2026 00:00:00 +0000</pubDate><guid>https://mindflakes.com/posts/2026/11/07/d2-rs-behavior-parity/</guid><description>&lt;p&gt;I&amp;rsquo;ve released &lt;a href="https://github.com/nelsonjchen/d2-rs"&gt;d2-rs&lt;/a&gt;, a Rust port of the &lt;a href="https://www.d2lang.com/tour/intro/"&gt;D2&lt;/a&gt; diagramming toolchain. It includes the compiler, Dagre, ELK and TALA layout engines, SVG rendering, PNG/PDF/PPTX/GIF exports, and a command-line app. It also builds as a WebAssembly package for Node and browsers, which was one of my original reasons for wanting the port.&lt;/p&gt;
&lt;p&gt;I directed the project over about two months, from September to early November. Codex did most of the investigation and implementation for the first six weeks. Then Claude supervised Codex runs, reviewed changes and chose the next work, as well as contributing implementation. The port follows D2&amp;rsquo;s source and uses its test suites to check the results.&lt;/p&gt;
&lt;p&gt;The current release targets Go D2 master at &lt;code&gt;6a7b48378&lt;/code&gt;, from October 11. All 944 boards in upstream&amp;rsquo;s end-to-end golden suite match: 933 SVGs and 11 expected errors, across all three layout engines. That comparison normalizes the XML declaration, version stamp and trailing newline; the rest of the SVG must match byte for byte.&lt;/p&gt;
&lt;p&gt;You can install the command-line app from &lt;a href="https://crates.io/crates/d2-rs"&gt;crates.io&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cargo install d2-rs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;d2-rs input.d2 output.svg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or install the &lt;a href="https://www.npmjs.com/package/@d2-rs/wasm"&gt;WebAssembly package&lt;/a&gt;, which follows D2.js&amp;rsquo;s &lt;code&gt;compile&lt;/code&gt; / &lt;code&gt;render&lt;/code&gt; API shape:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm install @d2-rs/wasm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s an independent, unofficial project, licensed under MPL-2.0 like D2. All three layout engines run in process.&lt;/p&gt;
&lt;figure&gt;&lt;a href="https://mindflakes.com/images/d2-rs-workflow.png"&gt;&lt;img src="https://mindflakes.com/images/d2-rs-workflow.png"
alt="D2 source goes through Go D2 and d2-rs, producing diagrams whose output, time and memory are compared. The d2-rs output is labelled This diagram." width="520"&gt;&lt;/a&gt;&lt;figcaption&gt;
&lt;p&gt;d2-rs drawing its own comparison workflow, rendered with ELK. A little meta.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The &lt;a href="https://mindflakes.com/images/d2-rs-workflow.d2"&gt;D2 source for this diagram&lt;/a&gt; is available too.&lt;/p&gt;
&lt;h2 id="we-ended-up-porting-a-lot-of-go"&gt;We ended up porting a lot of Go&lt;/h2&gt;
&lt;p&gt;This part still amazes me: getting D2&amp;rsquo;s output to match meant reimplementing a substantial amount of the Go code underneath it in Rust. A perfectly good Rust library can produce a different compressed stream, format a float differently, or choose a different sort order. Those differences matter when the target is the same bytes.&lt;/p&gt;
&lt;p&gt;So the agents ported the Go behavior D2 depends on: float parsing and formatting, sorting, random-number generation, compression, image encoding, ZIP output, and more. Even the arithmetic needed attention. Go can fuse a multiply and add on arm64 into a single rounding step, while the x86-64 implementation uses separate operations. The Rust port had to follow the corresponding architecture to get the same result.&lt;/p&gt;
&lt;p&gt;That reached into D2&amp;rsquo;s dependencies too: Markdown parsing, code highlighting, LaTeX, text shaping, sketch rendering and PDF generation. The &lt;a href="https://github.com/nelsonjchen/d2-rs#go-behavior-ported"&gt;project&amp;rsquo;s inventory&lt;/a&gt; gives a fuller picture. Go&amp;rsquo;s deflate and zlib implementation is already available as the standalone &lt;code&gt;d2-rs-goflate&lt;/code&gt; crate; many of the other ports are modules within the crates that use them.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s pretty hardcore. I set out wanting D2 in Rust and WebAssembly, and the work reached all the way down to compression decisions and floating-point rounding. The shipped implementation runs in Rust; Go is used as the reference for testing it.&lt;/p&gt;
&lt;h2 id="correct-but-stupidly-slow"&gt;Correct, but stupidly slow&lt;/h2&gt;
&lt;p&gt;Getting the output to match was a lot of work, but I was dismayed that GPT considered the resulting performance acceptable. Codex had produced a version that followed Go&amp;rsquo;s behavior, but it was stupidly slow. I wanted a performant Rust version that improved on the original. Getting the right answer was necessary, but performance was a big part of why I wanted this at all. I had to push explicitly for that part of the goal.&lt;/p&gt;
&lt;p&gt;An October 26 benchmark on a clean Linux arm64 host took 377 ms to lay out &lt;code&gt;grid_large_checkered&lt;/code&gt; with Dagre, against Go&amp;rsquo;s 44 ms: about 8.6 times as long. Across the twelve TALA cases in that run, the total runtime was about 3.9 times Go&amp;rsquo;s. Some tiny diagrams were already faster, which made a single headline median misleading. The larger diagrams were where the port was falling behind.&lt;/p&gt;
&lt;p&gt;We were going to run a lot of tests against this thing. A slowdown like that can turn an hour-long test run into three hours, and that cost comes back every time we check a change. It would make the whole project slower to work on.&lt;/p&gt;
&lt;p&gt;The trouble was that GPT tried to emulate too much of Go&amp;rsquo;s internal machinery in Rust. Go&amp;rsquo;s shared, mutable graph objects became layers of reference counting and locking. Elsewhere, the port copied collections to satisfy Rust&amp;rsquo;s ownership rules where Go shared a slice or pointer. Inside a layout engine that tries thousands of candidate placements, those costs kept adding up: extra allocations, copies and bookkeeping on every attempt.&lt;/p&gt;
&lt;p&gt;Matching Go&amp;rsquo;s output mattered, but that didn&amp;rsquo;t require carrying all that overhead into the Rust implementation. A lot of the later speed work was about preserving the behavior while changing how the data was stored, shared and reused.&lt;/p&gt;
&lt;h2 id="claude-directing-codex"&gt;Claude directing Codex&lt;/h2&gt;
&lt;p&gt;Claude had a much better sense of what mattered here. It recognized that a port this slow would be painful to use and test, even if the output matched. The later work focused on measuring where the time and memory went, fixing the implementation, and keeping the output matching Go throughout.&lt;/p&gt;
&lt;p&gt;I also wanted to take advantage of the numerous Codex usage resets during this period. My idea was to have the model with better judgment direct models that were smart and capable at coding, but hadn&amp;rsquo;t shown the same sense of priorities on this project. Claude could choose the next work and review the results while Codex did much of the implementation.&lt;/p&gt;
&lt;p&gt;That work reached into graph storage, routing queues, repeated compiler work and memory allocation. Preserving Go&amp;rsquo;s behavior still mattered, but the way the Rust code achieved it needed substantial changes.&lt;/p&gt;
&lt;p&gt;As the port got faster, the tests got faster too. More changes could be tried and checked in the same amount of time, so the project started improving much more rapidly. The performance work made the rest of the work easier to do.&lt;/p&gt;
&lt;h2 id="where-it-ended-up"&gt;Where it ended up&lt;/h2&gt;
&lt;p&gt;The initial benchmark covered twelve diagrams through three engines. That left too much unmeasured, so the work expanded to the whole upstream corpus and PNG, PDF, PPTX and GIF exports as well as SVG.&lt;/p&gt;
&lt;p&gt;On the recorded Linux arm64 corpus sweep, median SVG runtime was about 21% of Go&amp;rsquo;s for Dagre and ELK, and 47% for TALA. Median peak memory was lower too, and no measured row was slower than Go. Every timed run also had to produce identical output. The other export formats were faster on the measured corpus as well.&lt;/p&gt;
&lt;p&gt;The WebAssembly result was especially satisfying given why I wanted this in the first place. In Node, the Rust build produced identical SVGs on all 36 benchmark rows and took about 27–33% of Go&amp;rsquo;s total runtime, depending on the engine. The raw module is about 31 MB instead of 60 MB, starting in about a tenth of the time. The native executable is larger than Go&amp;rsquo;s, though: about 49 MB against 41 MB, stripped on Linux arm64. The &lt;a href="https://github.com/nelsonjchen/d2-rs/blob/main/docs/BENCHMARKS.md"&gt;benchmark notes&lt;/a&gt; cover the methods and limits.&lt;/p&gt;
&lt;p&gt;Both arm64 and Linux x86-64 have been checked against Go built for the corresponding architecture. Windows passes the default test tier and all 944 golden boards natively with MSVC and under Wine. All 67 applicable upstream CLI assertions pass, along with the retained CLI goldens that the pinned Go still reproduces. Differential testing covered 5,024 original jobs; five mutated inputs crash the pinned Go, while d2-rs reports an error instead.&lt;/p&gt;
&lt;h2 id="matching-the-awkward-details"&gt;Matching the awkward details&lt;/h2&gt;
&lt;p&gt;Two layout engines can draw the same diagram and still get there differently. Sometimes that difference is harmless. Sometimes it is the thing that makes the next diagram go wrong.&lt;/p&gt;
&lt;p&gt;One early example came from September, when the port still targeted Go D2 v0.9.0. Codex did the investigation and implementation described here.&lt;/p&gt;
&lt;h3 id="the-same-children-in-a-different-order"&gt;The same children in a different order&lt;/h3&gt;
&lt;p&gt;One investigation involved a diagram with nested containers, grids, and arrows running in several directions. The Rust and Go gap-connectivity traces disagreed. That trace records how the layout engine discovers connected nodes while deciding which spaces it can adjust.&lt;/p&gt;
&lt;p&gt;The objects were there. The edges were there. The problem was which view of the children the traversal used.&lt;/p&gt;
&lt;p&gt;Go&amp;rsquo;s connected-node walk could still see a container&amp;rsquo;s retained child slice after a nested placement graph had projected those children into a different order. The Rust code had the active children available, but walking that list did not always reproduce what the Go objects still exposed.&lt;/p&gt;
&lt;p&gt;That changes the sequence of decisions the algorithm makes. Translating the obvious graph structure wasn&amp;rsquo;t sufficient; the retained view was part of the behavior too.&lt;/p&gt;
&lt;p&gt;The fix had to preserve that order when the retained entries actually covered the active children, while falling back to the active slice for stale partial lists. Extracted children still needed to be traversable. An earlier version of that handling exposed another two-node order mismatch, so always preferring the old list wasn&amp;rsquo;t a useful shortcut either.&lt;/p&gt;
&lt;p&gt;After the correction, the nested-direction case matched the complete normalized gap trace at six seeds. Each run contained roughly 31,000 events. That gave us a way to check the sequence of decisions behind the matching pictures.&lt;/p&gt;
&lt;h3 id="when-go-disagrees-with-itself"&gt;When Go disagrees with itself&lt;/h3&gt;
&lt;p&gt;There was another wrinkle in the same corpus. A sequence-diagram case produced different gap traces across fresh Go processes, even with the seed held fixed. Rust&amp;rsquo;s trace stayed stable, and the public graph output stayed stable in both implementations.&lt;/p&gt;
&lt;p&gt;The source walks a Go map in that path. Repeating the reference executable was necessary to distinguish its varying traversal order from a stable mistake in the translation.&lt;/p&gt;
&lt;p&gt;The comparison kept those differences visible. Sorting every event would have removed the ordering information that had just helped find the real bug. It could identify a demonstrated map-order variation without claiming that the raw execution streams matched.&lt;/p&gt;
&lt;p&gt;What I liked about this investigation was how concrete the failure became. “The diagram is different” leaves a lot to search through. “This connected-node walk is reading a different child list” gave Codex something specific to fix.&lt;/p&gt;
&lt;h2 id="what-the-tests-missed"&gt;What the tests missed&lt;/h2&gt;
&lt;p&gt;Going beyond the upstream corpus was necessary. Generated cases and mutations exposed gaps in code highlighting, extreme sizes, small shapes, text rendering and error handling. Some large TALA cases still use more memory or take longer than Go; those are tracked in the &lt;a href="https://github.com/nelsonjchen/d2-rs#known-issues"&gt;known issues&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;On November 7, I asked Claude to render an isometric sample from upstream. The SVG and PNG both matched the pinned Go byte for byte, but the SVG took about 7.63 seconds against Go&amp;rsquo;s 0.77 seconds. PNG was faster, about 0.70 seconds against 0.82 seconds. The performance suite hadn&amp;rsquo;t exercised the isometric option. Even after expanding the benchmarks, a normal request to render a picture found another missing case.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m excited to have this out! I wanted D2 in Rust and WebAssembly, and now there&amp;rsquo;s a version people can actually install and use. After two months of work, seeing it render the same diagrams and run faster on the measured corpus is really satisfying. I hope it&amp;rsquo;s useful to other people too.&lt;/p&gt;
&lt;p&gt;Thanks to the D2 team for the language and the test suites that made this work possible. Questions and feedback are welcome on the &lt;a href="https://github.com/d2lang/d2/discussions/2958"&gt;community announcement&lt;/a&gt;.&lt;/p&gt;</description></item></channel></rss>