dms+kdl — DMS dialect for KDL
Brand: dms+kdl
File extension: .dms.kdl
Spec version: 0.1 (draft)
DMS tier required: 1 (_dms_tier: 1 in front matter)
Parent spec: TIER1.md
Tier-0 base: SPEC.md
KDL spec: kdl.dev (KDL Document Language v2)
Pre-1.0: breaking changes are still possible. No version-bump rules apply yet.
What this is
dms+kdl is a tier-1 dialect that lets you write KDL-shaped
documents in DMS. Each KDL node becomes a |<node_name>(...)
decoration; positional args become a positional param group;
named props become a named param group; child blocks become
decorated list-item children.
Why a DMS shape for KDL? See comparison.md §KDL — the short version is that KDL's "node has name + args + props + children" model puts the data across four channels per line, and DMS tier 1 places those channels in distinct visual slots (sigil-prefixed call vs positional group vs named group vs child list) so a reader scanning for values lands on a stable column.
What dms+kdl is not. It is not a KDL evaluator. It is not a
KDL ↔ JiK / XiK conversion tool. It is the DMS-shaped
representation of KDL nodes. A separate runtime / render layer
converts a decoded Document_t1 to and from native KDL when
that's needed.
Quick comparison
package "my-app" version="1.0.0" {
author "Ada Lovelace" email="ada@example.com"
dependencies {
log "0.4" features="std"
serde "1.0"
}
}
In dms+kdl:
+++
_dms_tier: 1
_dms_imports:
+ dialect: "kdl"
version: "1.0.0"
+++
+ |package("my-app")(version: "1.0.0")
+ |author("Ada Lovelace")(email: "ada@example.com")
+ |dependencies
+ |log("0.4")(features: "std")
+ |serde("1.0")
Each KDL line is a node, and each dms+kdl line is a
|<name>(args)(props) call with optional child block. The
positional args group (...) carries KDL's positional values;
the named props group (...) carries key=value pairs.
A node with only positional args and no props collapses to a
single group: |serde("1.0"). A node with only props collapses
similarly: |terraform(version: "1.0") (rare in KDL, common in
HCL).
Realistic example
A KDL-based project manifest — the kind of file you'd use for a Rust-style project tool or a task runner. Covers package metadata, dependencies with feature flags, build scripts, and workspace members.
Native KDL
// Project manifest for "my-app"
package "my-app" version="0.3.1" edition="2024" {
description "A CLI tool for processing DMS documents"
license "MIT OR Apache-2.0"
repository "https://github.com/example/my-app"
authors {
author "Ada Lovelace" email="ada@example.com"
author "Grace Hopper" email="grace@example.com"
}
}
// Runtime dependencies
dependencies {
// Serialization — note: slashdash comments out the async feature for now
serde version="^1.0" features="derive"
serde_json version="^1.0"
/-tokio version="^1.0" features="full" // disabled pending async rewrite
clap version="^4.0" features="derive" features="env"
tracing version="^0.1"
tracing-subscriber version="^0.3" features="env-filter"
anyhow version="^1.0"
thiserror version="^1.0"
}
dev-dependencies {
assert_cmd version="^2.0"
predicates version="^3.0"
tempfile version="^3.0"
insta version="^1.30" features="yaml"
}
// Build scripts
scripts {
test "cargo test --all-features"
lint "cargo clippy -- -D warnings"
fmt "cargo fmt --all -- --check"
build "cargo build --release"
ci {
pre "cargo fmt --all -- --check"
run "cargo test --all-features"
post "cargo clippy -- -D warnings"
}
}
// Workspace sub-crates
workspace {
members "crates/my-app-core" "crates/my-app-cli" "crates/my-app-test-utils"
exclude "scratch"
}
dms+kdl equivalent
+++
_dms_tier: 1
_dms_imports:
+ dialect: "kdl"
version: "1.0.0"
+++
// Project manifest for "my-app"
+ |package("my-app")(version: "0.3.1", edition: "2024")
+ |description "A CLI tool for processing DMS documents"
+ |license "MIT OR Apache-2.0"
+ |repository "https://github.com/example/my-app"
+ |authors
+ |author("Ada Lovelace")(email: "ada@example.com")
+ |author("Grace Hopper")(email: "grace@example.com")
// Runtime dependencies
+ |dependencies
// Serialization — comment on the dep declaration, survives round-trip
+ |serde(version: "^1.0", features: "derive")
+ |serde_json(version: "^1.0")
/* |tokio(version: "^1.0", features: "full") */ // disabled: DMS block comment
// clap: two features props — variadic positional collects multiple values
+ |clap("^4.0")(features: "derive", env: true)
+ |tracing(version: "^0.1")
+ |tracing-subscriber(version: "^0.3")(features: "env-filter")
+ |anyhow(version: "^1.0")
+ |thiserror(version: "^1.0")
+ |dev-dependencies
+ |assert_cmd(version: "^2.0")
+ |predicates(version: "^3.0")
+ |tempfile(version: "^3.0")
+ |insta(version: "^1.30")(features: "yaml")
// Build scripts — each script node: name as positional arg, command as second
+ |scripts
+ |test "cargo test --all-features"
+ |lint "cargo clippy -- -D warnings"
+ |fmt "cargo fmt --all -- --check"
+ |build "cargo build --release"
+ |ci
+ |pre "cargo fmt --all -- --check"
+ |run "cargo test --all-features"
+ |post "cargo clippy -- -D warnings"
// Workspace — variadic positional args collect all member paths into one slot
+ |workspace
+ |members("crates/my-app-core", "crates/my-app-cli", "crates/my-app-test-utils")
+ |exclude("scratch")
What's visible in DMS that's lost or obscured in KDL
Variadic positional args are first-class. |members("crates/my-app-core",
"crates/my-app-cli", "crates/my-app-test-utils") collects all three paths
into the variadic args slot as a typed list. In KDL, members
"path1" "path2" "path3" is valid but the arity is unconstrained and the
collected-list shape is implicit. dms+kdl makes the collection explicit in
the positional group and the dialect spec documents the variadic slot.
Comments on dependency declarations round-trip. The // Serialization
comment attached above |serde(...) and the inline comment on the disabled
tokio entry are carried as comment AST nodes. KDL's slashdash /- maps
to a DMS block comment (/* ... */) — this is a documented lossy
translation (slashdash position not preserved, comment content is), but
the intent ("this dep is disabled") survives in the DMS source and
re-emits on round-trip.
Stable grep targets. grep '|serde' returns every serde-family
dependency. grep '|dependencies' returns the top-level dependencies node.
In KDL, grep 'serde' would also match strings inside description
fields, repository URLs, or comments. DMS's sigil-prefixed node names
create a grep-stable namespace separate from value text.
Dialect canonical spec
+++
_dms_tier: 0
+++
name: "kdl"
version: "1.0.0"
version_strategy: "caret" # default; npm-style ^
families:
# ── KDL node: name + positional args + named props + children ──
+ name: "node"
default_sigils: ["|"]
empty_default: [] # no child block → empty children list
# No content_slot. KDL has no flow-form children syntax;
# child blocks are always indent-block list-items in dms+kdl.
params:
mode: "positional"
positional:
# Variadic — KDL nodes accept arbitrary positional args.
# The slot's type describes each *element* (KDL allows
# heterogeneous values), so `type: "any"` is correct
# rather than `list_of any`.
- { name: "args", type: "any", variadic: true }
typed:
# Properties are wildcard — any name accepted, any value
# type. KDL doesn't constrain prop key shapes beyond the
# bareword/string rule.
# (No typed entries; everything passes through wildcard.)
# ── KDL type annotation: (u8)10, (date)"...", etc. ─────────
+ name: "tagged"
default_sigils: ["@"]
empty_default: ""
content_slot: "value"
params:
mode: "wildcard_with_typed"
typed:
value: { type: "any", required: true }
Two sigils. | for nodes (the universal KDL shape) and @
for type-tagged values (KDL's (type)value form). Two sigils
avoid the multi-family-per-sigil resolution conflict that
would otherwise force every |tagged to be qualified.
Variadic positional. The node family declares a single
positional slot args with variadic: true. Every value in
the positional group accumulates into that slot as a list
element. See TIER1.md §"Variadic positional slot" for the rules
(only the last slot may be variadic; element-level typing;
zero-or-more semantics; no AST shape change).
Content semantics
KDL nodes → |<name>(args)(props) + child list
KDL nodes have four channels:
- Name — the node's identifier
- Positional args — zero or more values
- Named properties — zero or more
key=valuepairs - Child block — optional
{ ... }
dms+kdl maps these as:
- Name → decoration fn name.
|package→ fn namepackage. - Positional args → positional param group.
("my-app")is a one-element positional group;("a", "b", 3)is three-element. - Named properties → named param group.
(version: "1.0.0", author: "Ada")is the named group, separate from the positional one. - Child block → decorated list-item children. Each KDL
child node becomes a
+ |child_name(...)list item under the parent.
Encoder canonical form for the multi-group call:
| KDL shape | dms+kdl form |
|---|---|
node |
|node |
node arg1 |
|node("arg1") |
node arg1 arg2 |
|node("arg1", "arg2") |
node prop=val |
|node(prop: "val") |
node arg1 prop=val |
|node("arg1")(prop: "val") |
node arg1 arg2 prop1=v1 prop2=v2 { child } |
|node("arg1", "arg2")(prop1: "v1", prop2: "v2") + child block |
node {} |
|node (empty child block elides) |
The "two-group" pattern ((positional)(named)) is the same
multi-group form dms+hcl uses for HCL block headers. dms+kdl
applies it more often because most KDL nodes have at least one
positional arg and one named prop.
KDL type annotations → @<type> value
KDL allows type annotations in parens before any value:
node (u8)10 (date)"2024-01-01" version=(semver)"1.0.0"
In dms+kdl, type annotations become decorated values inside positional and named groups:
|node(@u8 10, @date "2024-01-01")(version: @semver "1.0.0")
The @<type> value form uses the tagged family's
content_slot hoisting — @u8 10 is equivalent to
@u8(value: 10).
The tagged family is wildcard fn name — any type tag the
KDL spec defines (u8, i32, f64, date, time,
date-time, regex, base64, decimal, currency, email,
hostname, irl, irl-reference, uuid, ...) plus
user-defined tags pass through. The dialect doesn't validate
the tag name — that's a render-layer / consumer concern.
KDL strings, raw strings, identifiers
KDL has three string-ish forms:
- Quoted string
"..."→ DMS"..."(basic string) - Raw string
#"..."#→ DMS'...'(literal string) when no special chars, else DMS heredoc with'''for content containing the closing delimiter - Bareword identifier (used as node name or unquoted string-ish value) → DMS bare key when valid, else quoted
The dialect doesn't add a new string form; it reuses tier-0's.
KDL slashdash /- → DMS block comment
KDL's /- slashdash comments out the next node / value /
property in place:
node arg1 /-arg2 arg3 // arg2 commented out
node /-prop=val // prop commented out
node /-{ // entire child block commented out
inner-stuff
}
In dms+kdl, slashdash maps to DMS block comments:
|node("arg1", /* "arg2", */ "arg3")
|node /* (prop: "val") */
|node /* + |inner_stuff */
This is a lossy translation — the slashdash form is lost
on round-trip; the decoder reconstructs DMS block comments.
Round-tripping dms+kdl → KDL → dms+kdl may not preserve the
exact slashdash placement. Documented limitation.
KDL #null, #true, #false
KDL v2 wraps boolean and null literals in # to disambiguate
from bareword strings. DMS has no null; #null translates
to key absence in property positions and is a parse error
in positional positions (an explicit-null marker would be a
future @null family-level decoration — not in scope for
v0.1).
#true / #false map to DMS booleans true / false.
Comments
KDL has # line comments, // line comments, /* */ block
comments, and /- slashdash. DMS has the same minus
slashdash. Round-trip behavior:
#and//line comments → DMS line comments (#or//as appropriate)/* */block comments → DMS/* */block comments/-slashdash → see above (lossy)
Versioning
dms+kdl follows semver. The first published version is 1.0.0.
The default match strategy is caret.
What counts as a version bump:
- Breaking (major). Renaming a family; changing sigil
bindings; switching modes; removing the
taggedfamily. - Additive (minor). Adding new content_slot hoisting rules;
adding a new family (e.g., a future
kdl_v1family for KDL 1.0 compatibility — see "Open questions"). - Bug fix (patch). Documentation, typo fixes, dialect-spec text consistency.
KDL itself made breaking changes from v1 → v2 (raw strings
moved from r"..." to #"..."#; null/true/false got #
prefixes). dms+kdl targets KDL v2; KDL v1 representation would
be a separate dialect (dms+kdl-v1) if a consumer needs it.
File extension
dms+kdl documents use the .dms.kdl extension.
config/cargo.dms.kdl
config/manifest.dms.kdl
What's not in scope (this dialect)
- KDL evaluation / interpretation. dms+kdl preserves the source structure; semantic interpretation is a runtime concern.
- Source-to-source migration tooling (
.kdl ↔ .dms.kdl). The dialect spec defines the target shape; conversion utilities are separate projects. - JiK / XiK profiles (KDL's official JSON-in-KDL and
XML-in-KDL microsyntaxes). Each is its own opinionated
encoding pattern; if someone wants
dms+jikordms+xik, they'd be separate dialects. - KDL Schema Language. Validation against an external
.kdl-schemafile is a render-layer concern. - KDL v1 compatibility. This dialect targets KDL v2.
Open questions for v0.2+
-
Type-tag validation. Currently the
taggedfamily is wildcard. A future revision could publish KDL's standard tag list (u8,i32,date, ...) as typed entries with proper type-checking on the wrapped value. Defer until a consumer needs the tighter validation. -
Slashdash preservation. Currently slashdash → block comment is lossy on round-trip. A future revision could add an
original_formsidecar entry (parallel to tier-0'soriginal_formsfor value literals) to preserve slashdash precisely. Defer until a consumer cares about exact KDL source preservation. -
Bareword strings (KDL allows unquoted bareword strings as values in some positions). dms+kdl currently emits all string values quoted. A future revision could let users opt into bareword emission via an encoder option, or via a per-value
original_formmarker. Defer.