Language
dg reads HCL files: the project file, packs (a spec and a style), the model, and views. This page covers the project file, kinds and the model. Styles and views have their own pages.
The project file
dg.hcl at the project’s root holds at most one project block. The file and every field are
optional; these are the defaults, except font, which has none:
project "my-diagrams" {
packs = []
models = ["*.hcl"]
read_only = []
views = "views.hcl"
layout_dir = "views"
grid = 10
font = "fonts/Brand.woff2"
}
- Paths are slash-separated, relative to the project root, and stay inside it. Symbolic links are followed only while they stay inside the project.
layout_dirholds the views’ layout files, andgridis the step of every coordinate and size in them.packslists directories, each with an optionalspec.hclandstyle.hcl. Listing"generic"is allowed and changes nothing.modelstakes glob patterns without**. By default every*.hclfile in the root is a model file, exceptdg.hcl, the views file, and thespec.hclandstyle.hclof the project and its packs, which never are. A pattern that matches no file is a warning.read_onlyfiles, such as a model generated from a running system, are never written:dg fmtskips them, operations that would change them are refused naming the file, and the editor shows their nodes with a lock (they can still be moved, since positions are in layout files).fontreplaces the bundled Inter font for all text. Exported SVG names it by the file’s base name (Brand, sans-serif).
Kinds and styles load in order: the generic pack, the listed packs in order, then the project’s
own spec.hcl and style.hcl. A later kind, edge_kind, shape or overlay_style of the same
name replaces the earlier one; a later style or edge_style with the same label overrides only
the properties it sets. dg check reports each override as information.
Kinds
A spec file declares node kinds and edge kinds:
kind "gpu-node" {
extends = "node" # the default
ports = ["nic0", "nic1"] # every node of the kind has them
default_port_side = "auto" # for ports declared without one
property "gpu_count" {
type = number # string, number or bool
default = 8
}
}
kind "rack" {
container = true # nodes may set parent to it
}
kind "table" {
rows = true # nodes may declare rows
}
edge_kind "rdma" {} # extends "default"
Every kind extends node, directly or through extends, and every edge kind extends default.
A kind inherits container, rows, ports, default_port_side and properties, and may add
ports and properties, but not declare one it inherits again. ports may call
port_range.
Styles select kinds including their sub-kinds, and so do the selectors of views: kind.service
matches a kind that extends service. The generic pack defines the
kinds most diagrams need.
Nodes
node "worker-1" {
kind = "gpu-node" # default "node"
label = "Worker 1" # default: the ID
parent = node.pool # a node whose kind is a container
tags = ["critical"]
props = { gpu_count = 8 }
port "debug" { # an instance port
side = "bottom" # top, right, bottom, left or auto
}
}
labelmay hold\nto break lines; long lines wrap at the style’smax_w.parentmakes the node a child of a container. Containment is a tree.tagsare simple IDs. Styles and selectors use them:style "tag.critical",include = [tag.critical].propssets the properties the kind declares, with values of exactly the declared type. Properties left out take the kind’sdefault. Properties are data for people and tools; they do not change the drawing.
Ports
A port is a named point on a node’s border where edges attach. A node’s ports are its kind’s
ports plus its own, declared with port blocks or all at once with
ports = port_range("p", 1, 48). A port’s side is top, right, bottom, left or auto
(the side facing the other end of its edge); ports on one side are spread evenly in declaration
order. A custom shape can place ports at its anchors, and the editor can pin a port anywhere along
a side (stored in the layout file).
Rows and compartments
A node whose kind has rows = true, such as table and class, may declare rows, either all
directly or all grouped in compartments:
node "orders" {
kind = "table"
row "id" {
type = "uuid"
key = "PK"
}
row "customer_id" {
type = "uuid"
key = "FK"
}
}
node "user" {
kind = "class"
compartment "fields" {
row "name" {}
}
compartment "methods" {
row "save()" {}
}
}
type and key are optional strings. A row is drawn as a line of the box, its key in bold, its
name and its type at the right, and compartments are separated by lines. A row is also a port:
an edge to node.orders.row.customer_id attaches at the end of that row facing the other end.
Edges
edge "w1-tor" {
from = node.worker-1.port.nic0
to = node.tor-1.port.p1
kind = "rdma" # default "default"
label = "400G" # the middle label
tags = ["hot"]
}
edge "places" {
from = node.Customer
to = node.Order
labels = { start = "1", mid = "places", end = "0..*" }
}
fromandtoarenode.<id>,node.<id>.port.<port>ornode.<id>.row.<row>.labelis short forlabels = { mid = "…" };startandendlabels sit beside the ends. An edge has one or the other.- Several edges may join the same two nodes, an edge may join a node to itself, and an edge may end at a container.
- Arrowheads, dashes and colors come from the edge kind’s style and the edge’s tags.
The generic pack’s junction kind is a small dot for buses and fan-out: edges end at it like at
any node.
IDs and references
The label of a node, edge, view, kind, port, row or other block is its ID.
- A simple ID matches
[A-Za-z_][A-Za-z0-9_-]*and is referenced asnode.apiornode.worker-1. - A quoted ID is any other text of up to 200 characters without control characters, as a
generator might write (
node "kube-system/coredns"). Reference it with an index:node["kube-system/coredns"]. Ports and rows may be quoted the same way:node.t.row["save()"]. - Kinds, edge kinds, tags, views, overlays and decorations take simple IDs only.
- These words cannot be simple IDs:
all,node,kind,tag,children,descendants,edge_kind,port,row,lift. An edge ID may not start withlift:. - Node IDs and edge IDs are separate: a node and an edge may share an ID. Port and row names are unique within their node.
An ID never changes by itself. Renaming one is a refactoring: the editor’s “Rename ID…”, the
language server’s rename, or dg op rename_id change the block
and every reference to it in model files, views.hcl and layout files at once.
When the editor or an operation creates a node without an ID, the ID is made from the label
(lower case, a-z, 0-9 and -, such as orders-db from “Orders DB”), else from the kind, with
-2, -3… added when it is taken; a node created without a label gets its ID from the first
label it is given in the same session. An edge’s ID is made from its ends, such as api-db.
Functions
Every HCL file can call:
port_range(prefix, from, to): the listprefix<from>…prefix<to>, such asport_range("p", 1, 48)forp1top48; at most 10,000 names.file(path): the contents of a UTF-8 text file, relative to the file that calls it and inside the project. Styles use it for pictures:icon = file("icons/lock.svg").
Validation
dg check, the editor and the language server report the same diagnostics. Errors include
duplicate or invalid IDs; references to nodes, ports, rows, kinds, edge kinds or selectors that do
not exist; extends and containment cycles; a parent that is not a container; rows on a kind
without rows = true; undeclared or mistyped properties; unknown style properties or bad values;
of or collapse naming a node that is not a container; and layout files of views that do not
exist. Warnings include layout entries naming IDs that no longer exist, style labels naming no
kind, selectors that match nothing, and positions stored relative to a container the node is no
longer in.