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)):

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: [{}]:

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:

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:

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)

Open questions for v0.2+