d2-rs
A Rust port of the D2 toolchain: the
compiler, the Dagre, ELK and TALA layout engines, the renderers and exporters,
and the command line, for the D2 diagram scripting language. The command is
installed as d2-rs and takes the same flags as Go's d2. Everything runs
in process, and it builds as a native CLI and as a WebAssembly package.
d2-rs targets byte-identical output with upstream Go D2 master at the commit
pinned in reference-manifest.json (6a7b48378,
2026-10-11). Upstream's own test suites are the oracle.
Announced in D2's Show and tell discussions; questions and feedback are welcome there.
Status
Parity: all 944 of upstream's e2e golden boards match byte for byte (933 SVGs and 11 expected errors) across Dagre, ELK and TALA, apart from the XML declaration, the version stamp and the trailing newline, which the goldens differ in by how they were generated.
Speed and memory: across every e2e case and output format, d2-rs is faster and smaller than Go D2 on the median, with output identical to Go's. Rust/Go ratios on Linux arm64, lower is better:
| Output | Median wall time | Median peak RSS | Rows slower than Go |
|---|---|---|---|
| SVG, Dagre / ELK | 0.21× / 0.21× | 0.60× / 0.60× | 0 of ~295 each |
| SVG, TALA | 0.47× | 0.68× | 0 of 296 |
| PNG / PDF / PPTX / GIF | 0.46× / 0.45× / 0.44× / 0.15× | 0.55–0.82× | 0 per format |
WebAssembly vs d2js, per engine (36 rows) |
0.29–0.40× | 0.32–0.61× | 0 of 36 |
The WebAssembly row gives the range of the three engines' median ratios for
compile and render in Node; over all 36 rows, total wall time is 0.27–0.33×
Go's. The WebAssembly module is about half the size of d2js (31.3 MB against
60.0 MB uncompressed, 10.1 against 15.3 MB gzipped) and starts in about a
tenth of the time. The native CLI is the other way round: stripped on Linux
arm64, it is 48.9 MB against Go's 40.8 MB (1.20×). See
benchmarks for x86-64, the generated scale sweep, the
method and caveats.
CLI: all 67 applicable assertions in upstream's e2etests-cli suite
pass, and every retained golden that the pinned Go still reproduces (51 of
66) matches byte for byte.
Platforms: parity is qualified on arm64 (Apple Silicon, Linux arm64) and x86-64 (Linux), each against Go built for that architecture, since Go's float output differs slightly between them (details). Benchmarks were measured on arm64 and in Node. Windows passes the default test tier and all 944 e2e boards natively (MSVC) and under Wine; CI builds Windows but does not run its tests.
Differential runs: the pinned Go CLI and d2-rs render the same upstream inputs and mutations of them under the same options; all 5,024 original jobs match. The only exceptions are 5 mutated inputs that crash the pinned Go itself, where d2-rs reports an error instead.
Known issues are tracked in GitHub issues.
Install
Prebuilt d2-rs binaries for Linux, macOS and Windows on x86-64 (amd64) and
arm64 are attached to each
GitHub release, packaged
like Go D2's release archives as d2-rs-<version>-<os>-<arch>.tar.gz, with a
SHA256SUMS file. Each archive holds bin/d2-rs (bin/d2-rs.exe on
Windows), the license and third-party notices, and a Makefile that installs
the binary:
The Linux binaries need glibc 2.35 or newer. On macOS, a binary downloaded with
a browser is quarantined; clear that with
xattr -d com.apple.quarantine bin/d2-rs (curl downloads are not affected).
Or the command line, from crates.io. It needs Rust 1.98.1 or newer:
The installed command is d2-rs, and it takes the same subcommands and flags
as Go's d2:
fmt,validate,--theme,--sketch,--padand--scale;- multiboard output;
- PNG, PDF, PPTX, GIF and ASCII export.
For reproducible native raster and paged exports, D2RS_SYSTEM_FONT_DIR can
restrict system font fallback to one directory, searched recursively. Bundled
fonts still take precedence. Without it, d2-rs uses the host's system fonts.
The WebAssembly package, from npm,
for Node and browsers. Its D2 class has the same compile / render shape
as D2.js (usage):
As a Rust library, there is no single high-level crate yet: each stage of the pipeline is its own crate, mirroring D2's Go packages, and the crates keep Go-shaped APIs that may change before 1.0. Depend on the stages you need:
d2-rs-languageto parse, compile and format D2 source;d2-rs-layoutto lay out a compiled graph with Dagre, ELK or TALA, including grids, sequence diagrams andnearplacement;d2-rs-renderfor SVG and sketch rendering;d2-rs-exportfor PNG, PDF, PPTX, GIF and ASCII export.
d2-rs-lib is only the serialized graph
JSON boundary between the compiler and the layout engines (D2's d2graph
serialization), not a general entry point. The d2-rs command line's source
shows how the stages fit together, and the
repository map lists
every crate. Releases are listed in the
changelog.
From source
&&
How it was built
The port started with static comparison against the Go source. A divergence was fixed at its earliest point, never by matching a fixture's output. Scoreboards choose the work:
scripts/e2e-scoreboard.pycovers upstream's e2e goldens;- other scripts cover raster, CLI and language suites;
scripts/perf-bench.pycovers speed and memory.
Every change must keep every scoreboard unchanged. Go D2 is a test oracle only: no Go code is linked into d2-rs.
Most of the code was written by AI coding agents, in two stages:
- From 2026-09-09: OpenAI's Codex, running GPT-6.1 Sol and GPT-6 Astra, started the port and worked on it alone.
- From 2026-10-22: Anthropic's Claude Opus 5.5 took the lead on the same codebase. It chose the work by scoreboard (docs/SCOREBOARD-WORKFLOW.md), wrote code itself, and supervised parallel Codex lanes running GPT-6.1 Sol, GPT-6.1 Terra and GPT-6 Astra, reviewing and verifying their patches before each commit.
Go behavior, ported
Byte-identical output rules out most Rust equivalents. A different deflate encoder, float formatter, sort order or random generator changes the bytes, even when the result looks the same. So beyond D2 itself, d2-rs ports the Go code that D2's output depends on, matching each to its pinned Go source:
- Go standard library:
strconvfloat parsing and formatting,sort,math(including its per-architecture routines) andmath/rand,compress/flate,compress/zlib,compress/lzw,hash/adler32,image/png,image/gifand JPEG decoding,archive/zip,encoding/xmlname rules,encoding/jsonstring quoting,net/url,htmlandunicode/utf8. - Go libraries D2 uses:
goldmark(Markdown),chroma(code highlighting),mathjax-go(LaTeX),go-text/typesetting(text shaping, through a fork of Rustybuzz invendor/),rough-go(sketch mode),dagro(Dagre),elk-go(ELK), Go-PDF/FPDF (PDF),go-quantize(GIF palettes),csscolorparser,golang.org/x/net/htmlname folding, the Brotli reader's errors, a TrueType font subsetter and Workiva's Fibonacci heap. - The Go compiler's arithmetic: on arm64, Go fuses
x*y+zinto one rounding step, so d2-rs does too there, and plain arithmetic on x86-64.
Each port keeps its upstream license notice in THIRD_PARTY_NOTICES.md.
These ports are useful beyond D2: any Go-to-Rust port that has to reproduce
Go's output exactly needs the same pieces. crates/d2-rs-goflate (Go's
compress/flate and compress/zlib) is already a standalone crate. Most
others are still modules inside the crates that needed them, for example
d2-rs-core/src/go_float.rs (strconv), go_sort.rs (sort),
d2-rs-export/src/go_png.rs and go_zip.rs, d2-rs-elk/src/go_math.rs and
go_trig.rs, and d2-rs-tala/src/internal/runtime/go_rng.rs
(math/rand).
Repository map
The workspace mirrors upstream D2's package layout. Each crate ports the Go packages named beside it; docs/PORT-MAP.md maps every upstream Go package to its Rust location.
Language
| Crate | Ports |
|---|---|
d2-rs-language |
d2parser, d2ir, d2compiler, d2format: parsing, compilation and formatting |
d2-rs-editor |
d2oracle, d2lsp: source edits and completions |
d2-rs-core |
d2ast, plus the shared graph, IR, geometry and layout result types |
d2-rs-lib |
the serialized graph JSON boundary between the compiler and the layout engines |
Layout
| Crate | Ports |
|---|---|
d2-rs-layout |
d2layouts: engine selection, grids, sequence diagrams, near placement |
d2-rs-tala |
TALA, the layout engine bundled in D2 |
d2-rs-dagre |
the Dagre adapter and dagro |
d2-rs-elk |
the ELK adapter and elk-go |
Text and math
| Crate | Ports |
|---|---|
d2-rs-text |
text measurement, Markdown (goldmark) and code highlighting (chroma) |
d2-rs-fontface |
D2's internal fontface package: parsing, shaping, glyph outlines, COLRv1 |
d2-rs-math |
d2latex and mathjax-go: LaTeX layout and SVG |
Rendering
| Crate | Ports |
|---|---|
d2-rs-target |
d2target: the diagram model and theme resolution |
d2-rs-scene |
d2renderers/d2scene: the renderer-neutral scene model |
d2-rs-scenebuild |
d2renderers/d2scenebuild: diagram to scene |
d2-rs-render |
d2renderers/d2svg and d2renderers/d2sketch: the SVG and sketch renderers |
d2-rs-sketch |
rough-go: the rough drawing primitives behind sketch mode |
d2-rs-isometric |
d2renderers/d2isometric: the isometric renderer |
d2-rs-raster |
d2raster: scene to pixels |
Export and front ends
| Crate | Ports |
|---|---|
d2-rs-export |
PNG, PDF, PPTX, GIF and ASCII export (lib/png, lib/pdf, lib/pptx, ...) |
d2-rs-goflate |
Go's compress/flate and compress/zlib, byte for byte |
d2-rs-cli |
d2cli: the d2-rs binary |
d2-rs-wasm |
d2js: the WebAssembly package |
d2-rs-testdata |
test-only access to the regenerated oracle fixtures |
Elsewhere
| Path | What |
|---|---|
vendor/ |
rustybuzz-go-text: Rustybuzz 0.20.1 modified so text shaping matches Go's go-text/typesetting (GPOS pair adjustment, coverage digests, Unicode 17 property tables); see its SOURCE.md |
scripts/ |
scoreboards, differential harnesses, benchmarks, fixture runner |
tools/ |
fixture recipes and hooks, upstream patches, oracle programs |
tests/ |
hand-written D2 inputs |
docs/ |
status, fixtures, benchmarks, references, provenance |
Testing
Plan for about 20 GB free. A debug test build of the workspace takes about
12 GB in target/ and grows with rebuilds; the oracle tier adds the pinned
upstream checkout, Go's build cache and the regenerated fixtures, about 5 GB
together.
# needs the pinned e2e case dump (reference-manifest.json `e2e_cases`);
# see docs/SCOREBOARD-WORKFLOW.md
Normal builds compile diagnostic tracing out. It is enabled with
--features diagnostic-traces.
License
MPL-2.0, the same license as D2. This is a derivative work of D2, Copyright
2022 Terrastruct Inc. Third-party material keeps its notices: see
THIRD_PARTY_NOTICES.md. Cargo dependency notices
for distributions are generated with node scripts/sync-licenses.mjs; see
SOURCE.md.