dms+ron — DMS dialect for RON
Brand: dms+ron
File extension: .dms.ron
Spec version: 0.1 (draft)
DMS tier required: 1 (_dms_tier: 1 in front matter)
Parent spec: TIER1.md
Tier-0 base: SPEC.md
RON spec: github.com/ron-rs/ron (Rusty Object Notation)
Pre-1.0: breaking changes are still possible. No version-bump rules apply yet.
What this is
dms+ron is a tier-1 dialect that lets you write RON-shaped
documents (algebraic data types, tagged variants, named structs,
tuple structs, type annotations) in DMS. RON is the canonical
serialization format for Rust's serde-typed data; dms+ron
gives you a DMS-shaped representation of the same content.
What dms+ron is not. It is not a Rust runtime, not a
serde-style typed deserializer, not a type-resolution engine.
RON's distinguishing feature — that values carry type identity
(variant tag, struct name, type annotation) — is preserved
through decoration; the actual interpretation against a Rust
type system is the runtime / consumer's job.
Quick comparison
GameConfig(
players: [
Player(name: "Alice", score: Some(42), team: Color::Red),
Player(name: "Bob", score: None, team: Color::Blue),
],
map: "forest",
settings: {
"difficulty": "hard",
"max_rounds": 10,
},
bounds: ((-100, -100), (100, 100)),
)
In dms+ron:
+++
_dms_tier: 1
_dms_imports:
+ dialect: "ron"
version: "1.0.0"
+++
|game_config(
players: [
|player(name: "Alice", score: |some 42, team: |red),
|player(name: "Bob", score: |none, team: |blue),
],
map: "forest",
settings: {
"difficulty": "hard",
"max_rounds": 10,
},
bounds: [[-100, -100], [100, 100]],
)
Each RON named-struct or variant becomes a |<name>(...)
decoration. Variant payloads use the inline base_value form
when single (|some 42) or the positional group form when
multi-arg (|score(95, 5)).
Realistic example
A Bevy game scene: multiple entities with ECS components (Transform, Mesh, Material, Camera), a resource block, and nested enum variants. Native RON first, then the dms+ron equivalent.
Native RON
// Bevy scene file
(
entities: {
0: (
components: [
// Main camera
Camera3d(
clear_color: ClearColorConfig::Custom(Color::Srgba(Srgba {
red: 0.1,
green: 0.1,
blue: 0.15,
alpha: 1.0,
})),
),
Transform {
translation: Vec3 { x: 0.0, y: 5.0, z: 10.0 },
rotation: Quat { x: -0.1830, y: 0.0, z: 0.0, w: 0.9831 },
scale: Vec3 { x: 1.0, y: 1.0, z: 1.0 },
},
PerspectiveProjection(
fov: 1.0471976,
near: 0.1,
far: 1000.0,
),
],
),
1: (
components: [
// Ground plane
Mesh(Handle(AssetId::Index { index: 0, generation: 1 })),
MeshMaterial3d(Handle(AssetId::Index { index: 0, generation: 1 })),
Transform {
translation: Vec3 { x: 0.0, y: -0.5, z: 0.0 },
rotation: Quat { x: 0.0, y: 0.0, z: 0.0, w: 1.0 },
scale: Vec3 { x: 50.0, y: 0.1, z: 50.0 },
},
],
),
2: (
components: [
// Point light
PointLight(PointLight {
intensity: 1500.0,
color: Color::Srgba(Srgba { red: 1.0, green: 0.9, blue: 0.8, alpha: 1.0 }),
shadows_enabled: true,
range: 40.0,
}),
Transform {
translation: Vec3 { x: 4.0, y: 8.0, z: 4.0 },
rotation: Quat { x: 0.0, y: 0.0, z: 0.0, w: 1.0 },
scale: Vec3 { x: 1.0, y: 1.0, z: 1.0 },
},
],
),
},
resources: {
AmbientLight: AmbientLight {
color: Color::Srgba(Srgba { red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0 }),
brightness: 80.0,
},
},
)
dms+ron equivalent
+++
_dms_tier: 1
_dms_imports:
+ dialect: "ron"
version: "1.0.0"
+++
// Bevy scene file
|scene(
entities: {
"0": |entity(
components: [
// Main camera — comment survives round-trip
|camera_3d(
clear_color: |custom(
|color_srgba(|srgba(red: 0.1, green: 0.1, blue: 0.15, alpha: 1.0))
),
),
// Transform as named-struct — brace vs paren form unified to paren
|transform(
translation: |vec3(x: 0.0, y: 5.0, z: 10.0),
rotation: |quat(x: -0.1830, y: 0.0, z: 0.0, w: 0.9831),
scale: |vec3(x: 1.0, y: 1.0, z: 1.0),
),
|perspective_projection(fov: 1.0471976, near: 0.1, far: 1000.0),
],
),
"1": |entity(
components: [
// Ground plane
|mesh(|handle(|asset_id_index(index: 0, generation: 1))),
|mesh_material_3d(|handle(|asset_id_index(index: 0, generation: 1))),
|transform(
translation: |vec3(x: 0.0, y: -0.5, z: 0.0),
rotation: |quat(x: 0.0, y: 0.0, z: 0.0, w: 1.0),
scale: |vec3(x: 50.0, y: 0.1, z: 50.0),
),
],
),
"2": |entity(
components: [
// Point light — tagged variant: |color_srgba is enum-arm, inner is struct
|point_light(
intensity: 1500.0,
color: |color_srgba(|srgba(red: 1.0, green: 0.9, blue: 0.8, alpha: 1.0)),
shadows_enabled: true,
range: 40.0,
),
|transform(
translation: |vec3(x: 4.0, y: 8.0, z: 4.0),
rotation: |quat(x: 0.0, y: 0.0, z: 0.0, w: 1.0),
scale: |vec3(x: 1.0, y: 1.0, z: 1.0),
),
],
),
},
resources: {
"AmbientLight": |ambient_light(
// brightness is float — not silently coerced from a string
color: |color_srgba(|srgba(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)),
brightness: 80.0,
),
},
)
What's visible in DMS that's lost or obscured in RON
Comments survive round-trip. // Main camera and // Ground plane are
comment AST nodes in dms+ron. RON parsers typically discard comments; a
Bevy .ron scene processed by a scene editor may silently drop all
annotations. dms+ron carries comments through decode → mutate → re-emit.
Named variants get typed signatures. |vec3(x: 0.0, y: 5.0, z: 10.0)
has three named params all typed any in the wildcard-pass-through family.
A runtime layer can impose tighter constraints — "vec3 requires x, y, z,
all float" — and the dialect's wildcard mode lets those constraints live
in the runtime without changing the dialect spec. By contrast, in RON, struct
field validation is purely the serde target type; the RON parser itself
has no schema.
Tuple-vs-list distinction is explicit when needed. RON tuples
((x: 0.0, y: 0.0, z: 0.0)) and lists ([0.0, 0.0, 0.0]) lower to
different DMS shapes: named-struct via |vec3(x: ..., y: ..., z: ...) vs
plain DMS list [0.0, 0.0, 0.0]. When using a |tuple(...) wrapper, the
distinction is explicit in the source and preserved through round-trip — no
ambiguity about whether the Rust type is a Vec<f32> or an (f32, f32, f32).
Float literals stay float. brightness: 80.0 is a DMS float. RON's
YAML-like numeric parsing can coerce 80 to integer under some
deserializers. DMS requires the decimal point to be present for float
values; 80 and 80.0 are different types and the dialect enforces the
distinction at the source level.
Dialect canonical spec
+++
_dms_tier: 0
+++
name: "ron"
version: "1.0.0"
version_strategy: "caret" # default; npm-style ^
families:
# ── RON value: variant, struct, tuple struct, unit ────────
+ name: "value"
default_sigils: ["|"]
empty_default: {} # empty struct shape; RON's unit / no-payload variants
# No family-level content_slot. Variants with single
# inline payload use the base_value hoist form
# (|some 42); multi-arg payloads stay in the positional
# group and don't hoist into the value tree.
params:
mode: "positional"
positional:
# Variadic — RON tuple structs and variants with
# arbitrary positional fields (Score(95, 5), Active(5, "online")).
- { name: "fields", type: "any", variadic: true }
typed:
# Named-struct fields are wildcard — RON struct field
# names are user-defined; the dialect doesn't constrain
# them. Type-checking lives in the runtime.
# ── RON type annotation: (i32)42, (Vec<u8>)[1, 2, 3] ─────
+ name: "typed"
default_sigils: ["@"]
empty_default: ""
content_slot: "value"
params:
mode: "wildcard_with_typed"
typed:
value: { type: "any", required: true }
Two sigils. | for the universal RON value shape (variants,
structs, tuple structs), @ for type annotations. Two sigils
avoid the multi-family-per-sigil resolution conflict that would
otherwise force every type tag to be qualified.
Why one value family for variants + structs + tuple
structs. All three RON shapes lower to "named call with
positional and/or named arguments." A variant Some(5) is
indistinguishable from a tuple struct Score(5) at the syntax
level — both are Name(positional_args). The dialect doesn't
try to discriminate them; the runtime resolves which is which
from the host Rust type. dms+ron stores the syntactic shape
(name + positional + named) and lets the consumer interpret.
Content semantics
Variants
Some(5) → |some 5 # single payload, inline base_value
None → |none # no payload
Active(5, "ok") → |active(5, "ok") # multi-arg payload, positional group
Color::Red → |red # bare variant (enum disambiguation lives in the runtime)
Status { code: 200, msg: "OK" } → |status(code: 200, msg: "OK") # named-payload variant
Single-payload variants prefer the inline base_value form
(|some 42) over the positional group form (|some(42)):
- Inline base_value
|some 42→ value tree position holds42 - Positional group
|some(42)→ value tree position holds the family's empty_default{}; the42lives inparams[0][0](positional group element 0)
The two are not equivalent. For value-tree-friendly access (the common case), inline base_value is canonical. The encoder emits single-payload variants in inline form when the payload is a scalar or simple flow value; falls back to positional group form when the payload is itself a complex decorated value.
No-payload variants use no parens or empty parens; both
decode to params: [{}]:
|none→ no positional group, value tree{}|none()→ empty group (back-compat to[{}]), value tree{}
Named structs
Player(name: "Alice", score: 42) → |player(name: "Alice", score: 42)
Player { name: "Alice", score: 42 } → |player(name: "Alice", score: 42)
RON has two equivalent surface forms for named structs (parens or curly braces). dms+ron unifies on the parens form — the dialect's named param group covers both. Round-trip from a RON source preserves field order; the brace-vs-paren distinction is not preserved (lossy in that one direction; the dialect spec documents this).
Tuple structs
Score(95, 5) → |score(95, 5)
Point(1.0, 2.0, 3.0) → |point(1.0, 2.0, 3.0)
Tuple structs use a positional group. The variadic fields
slot collects all positional elements.
Mixed positional + named (rare)
RON doesn't allow mixing positional and named in the same call,
so dms+ron's two-group form (|name(positional)(named)) is not
typically used. If a future RON spec extension allows it, the
multi-group syntax handles it without changes.
Tuples (anonymous)
(1, "two", 3.0) → [1, "two", 3.0]
((1, 2), (3, 4)) → [[1, 2], [3, 4]]
RON's anonymous tuples lower to plain DMS lists. The
tuple-vs-list distinction is lost on round-trip from RON;
dms+ron treats them as lists. If a consumer needs to preserve
the tuple shape, wrap in |tuple(...):
|tuple(1, "two", 3.0)
Most RON consumers don't need this distinction — Rust's type
system tells the deserializer whether a list is a Vec<T>
(list) or a (T1, T2, T3) (tuple) from the target type.
Documented limitation; lossy in the dms+ron → RON → dms+ron
direction unless the wrapper is used.
Maps
{ "key": "value", "other": 42 } → { "key": "value", "other": 42 }
{ Color::Red: 1, Color::Blue: 2 } → not directly representable (see below)
RON allows arbitrary value types as map keys (including variants and other complex values). DMS map keys are strings only. For non-string-keyed RON maps, dms+ron falls back to a list of two-tuples:
[
[|red, 1],
[|blue, 2],
]
The runtime layer knows from context that this is a map
representation. Round-trip is preserved via a |map(...) wrapper
if explicit marking is needed (defer to v0.2+).
Type annotations
(i32)42 → @i32 42
(Vec<u8>)[1, 2, 3] → @vec_u8 [1, 2, 3]
(Option<String>)Some(5) → @option_string |some 5
Type annotations become @<type_name> value decorations using
the typed family with content_slot hoisting. The type tag is
preserved as the fn name; angle brackets (Vec<u8>) are
flattened to a snake_case bareword (vec_u8) since < and >
aren't valid characters in tier-0 bare keys. The runtime layer
maps back to the Rust type from the bareword.
For type names with characters that don't fit even after
flattening, use a quoted-string fn name — but quoted fn names
aren't yet supported in tier-1 (parked). The fallback is a
@type_ref(name: "<original>") form:
@type_ref(name: "HashMap<String, Vec<u8>>") { ... }
Verbose; used only for type names that can't lower to barewords.
Char and byte strings
RON has 'a' (char) and b"..." (byte string). DMS has
neither. Conventions:
- Char: write as a single-element string with
@chardecoration:@char "a". The runtime resolves to a Rustcharbased on the type context. - Bytes: write as a base64 string with
@bytesdecoration:@bytes "QUJDRA=="(encoding"ABCD"). Or as a list:@bytes [65, 66, 67, 68]. Either form round-trips through the runtime.
These are convention, not validated by the dialect spec — runtime layer handles both.
Comments
RON has // and /* */. DMS has both. Round-trip preserves
comment attachment (per tier-0 comment AST machinery).
Trailing commas
Both RON and DMS flow forms allow trailing commas. The encoder canonical form (per SPEC.md §"Flow forms — canonical multi-line layout") emits trailing commas in multi-line flow form.
Versioning
dms+ron 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 the
valueortypedfamilies; changing sigil bindings; switching thevaluefamily frompositionalmode; changing the bareword-flattening convention for type names. - Additive (minor). Adding a new convention for a RON
feature not yet covered (
@bytesformats, char encodings, non-string map key wrappers). - Bug fix (patch). Documentation, typo fixes, dialect-spec text consistency.
File extension
dms+ron documents use the .dms.ron extension.
config/game.dms.ron
config/save_state.dms.ron
What's not in scope (this dialect)
serde-style typed deserialization. dms+ron preserves the syntactic shape; mapping to Rust types is a runtime / consumer concern.- RON ↔ dms+ron source-to-source migration. The dialect spec defines the target shape; conversion utilities are separate projects.
- RON's optional struct-type-name omission. RON allows
omitting the outer struct name when the type is inferable
(
(name: "x", score: 5)instead ofPlayer(name: "x", score: 5)). dms+ron requires the name (every value-tree position needs a decoration if it's a tagged value); unnamed-struct shape lowers to a plain DMS map. - Extension-style enum syntax (
Color::Red) preservation. The::notation isn't part of dms+ron — variants resolve by bare name plus runtime context. - Float NaN / infinity literals. RON has
inf,-inf,nan. DMS has them too (inf,-inf,nanper SPEC.md §Floats); maps directly without dialect-specific handling.
Open questions for v0.2+
- Tuple-vs-list distinction preservation. Currently RON
tuples become plain DMS lists, losing the tuple shape on
round-trip from RON. A
|tuple(...)wrapper preserves the distinction at the cost of verbosity. Future revisions could make tuple wrapping the default and require explicit|list(...)for plain lists; or pick the inverse. Defer until consumer feedback. - Non-string map key handling. Currently
variant-keyed-and-other-complex-keyed RON maps lower to lists
of two-tuples. A
|map(...)wrapper could mark these explicitly. Defer. - Quoted-string fn names. Tier-1 currently requires bare
identifier fn names. Type annotations like
HashMap<String, Vec<u8>>need either bareword flattening (hashmap_string_vec_u8— ugly) or a@type_ref(name: "...")workaround. Promoting quoted fn names from parked-or-not-yet-considered to defined would clean this up. - Brace-vs-paren struct form preservation. RON's
Player { name: "x" }andPlayer(name: "x")are equivalent; dms+ron unifies on the paren form. A future revision could preserve the distinction via anoriginal_formsidecar entry if a consumer cares about exact RON source preservation. - Multi-line tuple struct readability. Long tuple structs
(
|score(95, 5, 12, 87, ...)) get wide. Multi-line flow form (per SPEC.md §"Flow forms — canonical multi-line layout") applies, but the all-positional shape may benefit from a per-element-on-own-line emitter mode. Defer until use cases surface.