# drawlang cheat sheet
Precision diagrams as code, built for AI-agent authors. You write `.drawl`,
the renderer verifies your work: every render can emit machine-readable
geometry + lints. Iterate like you iterate on tests:
edit → check → render --report → fix lints → repeat.
## CLI loop
drawlang check file.drawl # all errors at once, with fixes
drawlang render file.drawl -o out.svg --report # SVG + geometry/lints JSON
drawlang render file.drawl -o out.png --scale 2 --theme dark
drawlang query file.drawl 'gpus[2]' # one element's bbox + ports
drawlang fmt file.drawl # canonical formatting
drawlang freeze file.drawl # lock layout into file.drawl.lock
# (delete the .lock to re-layout)
drawlang explain E0214 # long-form help for any code
Exit codes: 0 ok, 2 errors. `--json` (global) switches diagnostics to JSON.
Trust the report, not your imagination: bbox = [x, y, w, h] px, y grows
downward. The report's top-level keys: `canvas`, `elements[]` (path, kind,
bbox, ports), `edges[]` (from, to, points, label_bbox), `lints[]`
(code, target, message, fix), `stats` (crossings, overlaps). Every lint's
`fix` field is a concrete next step — apply it and re-render until clean.
## File skeleton
drawl 0.1 // required version header, first line
// comments: `//` to end of line
canvas { // optional, top-level only
theme: paper // paper | dark
flow: right // right | down | left | up (layout direction)
padding: 32 // px around the drawing
title: "My diagram" // bold heading above the canvas
}
## Nodes & properties
cpu // bare node (label defaults to its name)
cpu { label: "EPYC 9754" } // props separated by `;` or newline
big {
label: "Line 1\nLine 2" // \n makes multi-line labels
width: 200 // explicit size (default: auto-fit the text)
shape: pill // rect (default) | pill | ellipse
}
gpu {
label: "GPU 0"
hbm { label: "80 GB HBM3e" } // nested child renders stacked inside
}
l3 { width: fill } // stretch to the parent's content width
## Containers (rigid placement, invisible)
gpus: row { ... } // left→right; address children as gpus[0]…
stack: column { ... } // top→bottom
cores: grid 2x4 { ... } // columns x rows, filled row-major
## Groups (labeled boxes, auto-layout inside)
group host "Host" { // name, then optional display label
cpu { label: "EPYC 9754" }
}
Inside a group, edges drive the layout (a DAG laid out along `flow`). With no
edges among the children, an explicit `flow: down` / `right` stacks them along
that axis; otherwise reach for a `row` / `column` container for rigid order.
## Components & loops
def gpu(i) { // top-level only; params become variables
label: "GPU {i}" // {expr} interpolation in any string
port pcie { side: bottom }
}
gpu(0) // anonymous instance (addressed by index)
g0: gpu(0) // named instance
g1: gpu(1) { width: 220 } // override block applies after the def body
for i in 0..4 { gpu(i) } // half-open: 0,1,2,3 — works anywhere
Expressions: `+ - * / %` with parens; `%` is Euclidean so `(i + 1) % 4`
ring-wraps; `+` also concatenates strings.
String escapes: \" \\ \n \t \{ \}.
## Ports
port nvlink { side: top } // side: top | right | bottom | left
port rx { side: left; order: 0 } // order positions ports sharing a side
## Edges
a -> b // directed
a <-> b // bidirectional (arrows both ends)
a <- b // reverse (stored as b -> a)
a -> b : "PCIe 5.0 x16" // label
a -> b : "lane" { class: bus } // edge props / class
host.cpu -> gpus[i].pcie // endpoints: paths, indices, ports
## Constraints & pins
constrain {
align(a, b, top) // top|bottom|left|right|centerx|centery
align(gpus[*], top) // [*] = all children (constraints only)
gap(a, b, 48) // exact px between facing borders
below(a, b) // also: above, left-of, right-of
same-width(a, b) // also: same-height, same-size
}
pin legend at (24, 600) // absolute px, (0,0) = top-left of content;
// top-level elements only
Constraints relate siblings only. Inside row/column/grid, only align() and
gap() apply — ordering is declaration order.
## Classes & style keys
class bus { stroke: 2; color: @accent; routing: orthogonal }
n { class: bus } // apply (repeat the line to stack classes)
node/group: shape · width (px or `fill`) · height · fill · color
text.color · stroke · dashed · corner · padding · gap · flow
wrap (px) · bold · text.size · label · class
port: side · order · label
canvas: theme · flow · padding · title
Colors: `#rgb` / `#rrggbb` / `#rrggbbaa`, or theme tokens that adapt to both
themes: @accent @accent2 @hot @ok @warn @muted @faint @ink @bg @surface.
Prefer tokens. Use `routing: orthogonal` for buses/lanes — it avoids boxes;
splines go straight through.
## Paths & scoping (the rules that bite)
- Lookup is lexical: a statement sees its own scope, then enclosing scopes.
Top-level statements must qualify into groups: `host.cpu`, not `cpu`.
- Container children go by index (`gpus[2]`) or name; a trailing segment can
be a port (`gpus[2].pcie`).
- Edges connect siblings or anything except their own container.
- Ranges are half-open; sibling names must be unique.
## Reading lints (full detail: `drawlang explain <CODE>`)
W0301 label overflows its shape → widen, or `wrap: N`
W0401 two elements overlap → constraints or pins collided
W0402 element outside the canvas → check pin coordinates
W0410 edge cuts through a box → set `routing: orthogonal`
W0411 edge label collides → shorten label or add gap()
W0412 edge crosses 3+ others → reorder declarations
W0501 unused def/class → delete or wire it up
W0502 constraint had no effect → wildcard matched < 2 elements
W0503 constraint ignored in container → reorder children instead
Errors: E01xx syntax · E02xx names/paths · E03xx properties/values ·
E04xx constraints.
## Quality bar before you call it done
1. `drawlang check` exits 0 with zero warnings.
2. `render --report` shows zero lints and a crossings count you can justify.
3. Verify intent from the report (e.g. "is host left of gpus?" → compare
bbox x ranges). Don't guess from the source.