d2-rs 0.1.5

The d2-rs command-line tool, a Rust port of D2's d2cli package.
Documentation

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:

curl -fsSLO https://github.com/nelsonjchen/d2-rs/releases/download/v0.1.5/d2-rs-v0.1.5-linux-amd64.tar.gz
tar -xzf d2-rs-v0.1.5-linux-amd64.tar.gz
make -C d2-rs-v0.1.5 install   # to /usr/local, or ~/.local if that is not writable

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:

cargo install d2-rs
d2-rs input.d2 output.svg
d2-rs --layout=tala input.d2 output.png

The installed command is d2-rs, and it takes the same subcommands and flags as Go's d2:

  • fmt, validate, --theme, --sketch, --pad and --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):

npm install @d2-rs/wasm

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-language to parse, compile and format D2 source;
  • d2-rs-layout to lay out a compiled graph with Dagre, ELK or TALA, including grids, sequence diagrams and near placement;
  • d2-rs-render for SVG and sketch rendering;
  • d2-rs-export for 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

cargo build --release --locked -p d2-rs
target/release/d2-rs input.d2 output.svg
cd crates/d2-rs-wasm && pnpm build   # needs wasm-bindgen 0.2.128 and pnpm

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.py covers upstream's e2e goldens;
  • other scripts cover raster, CLI and language suites;
  • scripts/perf-bench.py covers 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: strconv float parsing and formatting, sort, math (including its per-architecture routines) and math/rand, compress/flate, compress/zlib, compress/lzw, hash/adler32, image/png, image/gif and JPEG decoding, archive/zip, encoding/xml name rules, encoding/json string quoting, net/url, html and unicode/utf8.
  • Go libraries D2 uses: goldmark (Markdown), chroma (code highlighting), mathjax-go (LaTeX), go-text/typesetting (text shaping, through a fork of Rustybuzz in vendor/), rough-go (sketch mode), dagro (Dagre), elk-go (ELK), Go-PDF/FPDF (PDF), go-quantize (GIF palettes), csscolorparser, golang.org/x/net/html name 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+z into 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.

cargo test --workspace --locked
uv run python scripts/references.py verify
# needs the pinned e2e case dump (reference-manifest.json `e2e_cases`);
# see docs/SCOREBOARD-WORKFLOW.md
cargo build --locked --profile diagnostic -p d2-rs
uv run python scripts/e2e-scoreboard.py --output e2e.json

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.