Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

typl — Type Specification DSL

typl is the MarkSpec Type Specification DSL. It declares what $Name identifiers mean inside an entry’s body — their kind (signal, event, command, …) and their shape (float range, record, enum, …). Every $Name token that appears in an entry body may be given a typl binding; the compiler collects all bindings into a per-entry types object and a corpus-level typeRegistry for downstream tooling such as codegen and LSP hover.


Statement forms

typl has exactly two statement forms.

Binding

$Name : [kind] shape

Declares what $Name represents. The kind keyword is optional and defaults to value when omitted.

$Speed   : signal float[0..300]
$Enabled : bool
$Idle    : state

Typedef

type Name = shape

Declares a named shape that bindings in the same entry can reference by name.

type Track = { id: int, range_m: float[0..300], velocity_ms: float }
$CurrentTrack : signal Track

Kind vocabulary

The kind vocabulary is closed. Ten keywords are defined:

KindMeaning
valueA plain data value with no lifecycle semantics. Default when omitted.
eventA named occurrence, optionally carrying a payload.
signalA continuously observable quantity with a measurable shape.
commandA request to perform an action, optionally with parameters.
stateA named mode or state-machine state.
constA named constant whose value does not change at runtime.
configA configuration parameter supplied before system start.
documentA structured artefact (log record, report, notification).
streamA sequence of values produced over time.
namespaceDeclares a base path for relative references in the published tier. Scaffolding, not a symbol — it carries no shape. See Published declarations.

Shape grammar

A shape describes the type of the data carried by a binding. Ten shape variants are available.

primitive

One of the five built-in primitive types. No constraints.

$Enabled  : bool
$Payload  : bytes
$Label    : string
$Counter  : int
$Ratio    : float

Primitive names: int, float, bool, string, bytes.

range

A numeric primitive with min/max bounds or an exact value.

$Speed    : signal float[0..300]
$Throttle : signal float[0..1.0]
$Status   : int[0..255]
$Magic    : const int[42]

Syntax: int[min..max], float[min..max], int[exact], float[exact]. Either bound may be omitted (int[0..], int[..255]).

length

A string or bytes with a constrained length.

$Code    : string[3..6]
$Hash    : bytes[32]
$Prefix  : string[..8]

Syntax: string[min..max], string[exact], bytes[min..max], bytes[exact].

pattern

A string constrained to a regular expression.

$CountryCode : pattern /^[A-Z]{3}$/
$Ident       : pattern /^[a-z_][a-z0-9_]*$/i

Syntax: /regex/ or /regex/flags. Supported flags: i (case-insensitive).

array

An element shape repeated zero or more times, with optional length bounds.

$Readings  : int[]
$Track     : signal float[](..8)
$Waypoints : float[](1..64)
$Matrix3x3 : float[4](9)

Syntax: element[], element[](min..max), element[](exact). The element itself may be any shape, including a ranged type (float[0..300][]).

enum

A closed set of string or numeric literals.

$Priority : 'low' | 'mid' | 'high'
$State    : 0 | 1 | 2
$Toggle   : true | false

Syntax: literal | literal | …. String literals use single quotes.

record

A structured object with named fields.

type Track = { id: int, range_m: float[0..300], velocity_ms: float }
type Log   = { ts: int, level: 'info' | 'warn' | 'error', msg: string }

Syntax: { field: shape, field: shape, … }. Fields are comma-separated. Field values may be any shape, including nested records and refs.

literal

A single fixed value.

$MaxRetries : const int[3]
$Version    : const '1.0'
$Debug      : const true

Literals are the exact form of range, length, or bare true/false. In practice, int[3] and 'fixed' are the most common literal forms.

ref

A bare PascalCase name referencing a typedef in the same entry.

type ErrorCode = int[0..127]
$LastError : signal ErrorCode

Syntax: a PascalCase identifier with no punctuation. The name must resolve to a typedef declared in the same entry (entry-local scope — see §Scope).

optional

A shape that may be absent.

$Timestamp : int?
$Metadata  : string?
$Extra     : bytes[32]?

Syntax: any shape followed by ?. Optional wraps the innermost shape: float[0..1.0]? is valid.


Markdown surfaces

typl declarations may appear in four Markdown surfaces. All four parse to the same Shape AST.

Fence block

Use a fenced code block tagged typl when an entry carries several bindings or typedefs. The fence collects related declarations visually.

- [SYS_RADAR_0012] Radar track output format

  The sensor fusion module shall publish one `$Track` record per tracked object
  at every 100 ms cycle.

  ```typl
  type Track = { id: int, range_m: float[0..300], velocity_ms: float }
  $Track   : signal Track
  $CycleHz : const int[10]
  ```

      Id: 01JZEXAMPLEULID000000000001
      Satisfies: STK_RADAR_0001

Bullet glossary

Use a bullet item with the form - $Name : … inside an existing bulleted list to annotate sparse identifiers. The bullet surface reads naturally as a glossary note.

- [SRS_BRAKE_0030] Brake pedal debounce

  The brake controller shall debounce raw pedal readings over a configurable
  `$Window` millisecond sliding window before emitting `$Stable`.

  - $Window : config int[1..50]
  - $Stable : signal bool

    Id: 01JZEXAMPLEULID000000000002

Inline span

Use a backtick span of the form `$Name : …` to annotate a single identifier in-prose without interrupting the surrounding text.

- [SRS_CTRL_0005] Gain scheduling

  The controller shall select a gain `$Gain : signal float[0.5..2.0]` based on
  the current operating mode.

      Id: 01JZEXAMPLEULID000000000003

Table

Use a GFM table to declare many identifiers that share the same columns. Each data row holds a $Name in its first cell and the binding’s kind shape in its second; a third description column, and any further columns, carry documentation and are ignored. Rows whose first cell is not a $Name are skipped, so declaration rows may be mixed with plain rows. Only bindings are recognised in a table — typedefs use the other three surfaces. A shape that contains | (a union or enum) must escape each pipe as \| so it is not read as a column separator; the cell un-escapes before typl parses it.

- [SYS_HMI_0007] Cluster display signals

  | Signal        | Kind shape           | Description      |
  | ------------- | -------------------- | ---------------- |
  | $VehicleSpeed | signal float[0..300] | km/h, road speed |
  | $EngineRpm    | signal int[0..8000]  | crankshaft speed |

      Id: 01JZEXAMPLEULID000000000004

A Table: caption adjacent to the table may carry a published base — an absolute name, dotted ($a.b) or single-segment ($vehicle) — that scopes the table’s relative rows ($.x) through the same entry-local base resolver the bullet glossary’s nested namespaces use. A table row resolves against its caption base, then the entry root; unlike a bullet, a table nested inside a namespace does not inherit that namespace’s base.


Scope

typl has two declaration tiers.

Entry-local — a plain $Name binding (no dots) and every type Name typedef are scoped to their declaring entry. A typedef declared in one entry cannot be referenced by name from another. Two entries that each declare $Speed declare two independent symbols; there is no cross-entry consistency rule for plain names.

Published — a dotted $a.b.c binding is a corpus-wide symbol, declared exactly once and citable from any entry. See Published declarations.

Within a single entry, all surfaces (fence, bullet, inline, table) share one namespace. A $Name binding or type Name declared in one surface is visible to the others in the same entry.


Published declarations

A published symbol is a $Name whose path has two or more dot-separated segments ($powertrain.brake.pedal_position). Unlike an entry-local plain $Name, a published symbol is declared exactly once across the whole corpus and may be cited from any entry. A published symbol cannot be a bare name — publication forces namespace ownership.

Namespace declarations

A namespace-kind declaration establishes a base path that relative references resolve against. It is scaffolding, not a symbol: it carries no shape and is not itself subject to declared-once.

`$powertrain.brake : namespace`

Relative references

A reference of the form $.name is relative: it resolves against the base of the nearest enclosing namespace declaration (innermost wins), falling back to the entry’s root namespace. An absolute reference has an identifier character immediately after the sigil ($powertrain.brake.pedal_position); a relative reference has a dot ($.pedal_position). There is no form in between, and the $ sigil stays on the relative form.

- `$powertrain.brake : namespace` — brake subsystem signals
  - `$.pedal_position : signal float[0..100]` — resolves to
    `$powertrain.brake.pedal_position`
  - `$.line_pressure : signal float[0..250]`

  Latency budgets apply to `$.pedal_position`.

A cross-entry citation must be absolute — a relative reference never leaves its declaring entry. An entry may declare at most one root (top-level) namespace; a second is an error (TYPL-012). Zero is fine — that is every entry without published symbols.


Diagnostic catalogue

CodeSeverityDescription
TYPL-001errorDuplicate $Name binding within the same entry — first occurrence wins.
TYPL-002errorDeprecated — retired by the published tier; never emitted. Kind mismatch across entries.
TYPL-003errorDeprecated — retired by the published tier; never emitted. Shape mismatch across entries.
TYPL-004errorTypedef name redefined within the same entry.
TYPL-005errorReference to an undefined typedef name.
TYPL-006errorMalformed schema — unparseable shape expression.
TYPL-007errorUnknown kind keyword; expected one of the ten closed-vocabulary kinds.
TYPL-008errorLiteral value violates the declared constraint.
TYPL-009errorPublished symbol declared more than once — declared-once violation.
TYPL-010errorRelative reference has no namespace base in scope.
TYPL-011errorCitation of an undeclared published symbol.
TYPL-012errorMore than one root namespace declaration in a single entry.

uxil — UX Interaction Language

uxil is a declaration DSL for typed UI/HMI surfaces and interactions: ux: URI references, one root surface per contract entry, element and child-surface bullets, and a corpus-wide surface registry. It is the sibling of the typl DSL on the shared declaration-surface machinery.


Reference grammar

Lexical tokens

A ux: reference or declaration is tokenized on a single line; whitespace (space, tab) between tokens is insignificant.

TokenMatches
IDENTone or more of [A-Za-z0-9_]
DOT.
AT@
SLASH/
COLON:
BANG!
COMMA,
ARROW->
LBRACE / RBRACE{ / }

? and # are reserved and never valid anywhere in a reference or declaration; either fires UXIL-002 and stops structural parsing of that span (the lexer drops the character, so no downstream token is trustworthy). Unrecognised characters outside this reserved pair are skipped silently by the lexer — the parser, not the lexer, is responsible for turning the resulting gap into a diagnostic.

Reference (citation / nav-target) form

ux-ref  ::= [ "ux:" ] surface [ "@" state ] [ "/" element [ ":" key ] [ "!" verb ] ]
surface ::= IDENT ( "." IDENT )*
key     ::= IDENT | "{" IDENT "}"

state, element, and verb are each a single IDENT. The ux: scheme is optional: a bare wire form parses identically to its scheme-prefixed counterpart, differing only in the hasScheme flag captured on the parsed UxRef.

ux:media.home/play
media.home/play

Both references resolve to the same surface (media.home) and element (play) — only hasScheme differs (true for the first, false for the second).

  • surface — one or more dot-separated path segments (media.home).
  • @state — an optional single state name. A reference cites at most one state; a declaration (below) allows a comma-separated state set.
  • /element — an optional element name on that surface.
  • :key — an optional key, valid only after an element: a concrete value (:track42) or a {name} template placeholder (:{track_id}).
  • !verb — an optional verb, valid only after an element (and after any key).
ux:media.home@loading
ux:media.list/item:{id}
ux:media.home/play!activate

Declaration forms (grammar summary)

The three declaration forms below are documented with worked examples in Declaration forms; their grammar, for reference:

root-decl    ::= [ "ux:" ] surface ":" kind [ "@" state ( "," state )* ]
element-decl ::= "/" element ":" verb ( "," verb )* [ ":" key-template ]
                 [ "@" state ( "," state )* ] [ "->" ux-ref ]
child-decl   ::= "." surface [ "@" state ( "," state )* ]
key-template ::= "{" IDENT "}"
kind         ::= IDENT   (* one of the three closed kinds — see Closed vocabularies *)
verb         ::= IDENT   (* one of the eleven closed verbs — see Closed vocabularies *)

A root-decl’s : introduces its kind, not a reference key — the two : productions never collide because a reference’s key only appears after a /element, while a root declaration’s surface is never followed by /. An element-decl’s key clause is a key-template only (a concrete key is a citation-only form); its -> ux-ref nav target is itself a full reference, scheme-optional, most often relative (-> media.settings).


Declaration forms

uxil declarations are written as inline code spans (the root form) or as the leading code span of a bullet paragraph (element and child-surface forms) inside a declaring entry’s body.

Root

ux:surface : kind @state, state, …

Exactly one root declaration is required per declaring entry — none is UXIL-011, more than one is UXIL-012 for every declaration past the first. The scheme is optional here too: media.home : screen @ ready classifies as a root declaration exactly like `ux:media.home : screen @ ready`.

- [UXI_0012] Media home screen contract

  The media home screen (`ux:media.home : screen @ loading, ready`) offers
  playback control.

      Id: 01JZEXAMPLEULID000000000010

Element bullet

/element : verb[, verb…] [: {key}] [@state, …] [-> nav-ref]

The leading code span declares the element’s verb set (one or more verbs), an optional key template for a repeated element, an optional state set, and an optional navigation target. A navigation target is itself a ux: reference and may likewise omit the scheme, e.g. `/queue : navigate -> media.queue`. The prose that follows the code span — with a leading em dash separator stripped — is the element’s event dictionary; it is mandatory (UXIL-006 when absent).

- [UXI_0012] Media home screen contract

  The media home screen (`ux:media.home : screen @ loading, ready`) offers
  playback control.

  - `/play : activate` — starts playback of the currently highlighted track.
  - `/favorite_toggle : toggle : {track_id}` — marks a track favourite.

    Id: 01JZEXAMPLEULID000000000010

Child-surface bullet

.path @state, state, …

A child-surface bullet declares a nested surface. Its own nested bullets are its elements; kind is never re-declared on a child surface — every descendant surface inherits the root’s kind (see Base resolution for how .path resolves).

- [UXI_0012] Media home screen contract

  The media home screen (`ux:media.home : screen @ loading, ready`) offers
  playback control.

  - `/play : activate` — starts playback of the currently highlighted track.
  - `.confirm_dialog @ default` — delete confirmation dialog.
    - `/confirm : activate` — confirms the deletion.

      Id: 01JZEXAMPLEULID000000000010

Closed vocabularies

Two vocabularies are closed and core-owned — extension is a markspec release decision, not a profile concern (ADR-009).

Kinds

KindNavigableStatefulVisual
screenyesyesyes
panelnonoyes
agentnoyesno
  • Navigable — a valid navigate -> target. Only screen.
  • Stateful — may declare @ states (UXIL-013 otherwise). screen and agent.
  • Visual — subject to visibility assertions and observe anchors (UXIL-025 otherwise). screen and panel.

Verbs

VerbRequires nav targetExclusive
activatenono
togglenono
selectnono
adjustnono
inputnono
scrollnono
dragnono
navigateyesno
dismissnono
asknono
observenoyes
  • Requires nav target — the element must carry a -> target clause (UXIL-026 otherwise). Only navigate.
  • Exclusive — cannot combine with any other verb on the same element (UXIL-014 otherwise). Only observe.

Base resolution

A child-surface bullet’s path is always a relative reference. It resolves against the base of the nearest enclosing ancestor surface — innermost wins — through the same DSL-agnostic engine typl’s published tier uses: resolveRef in core/decl/resolve.ts. uxil supplies its own reference operations (UX_REF_OPS in assemble.ts): joining a base with a relative path is dot-concatenation, and there is no absolute internal path form. Unlike typl, which parses a genuinely absolute $a.b.c form, every uxil internal join is relative — UX_REF_OPS’s isAbsolute is kept only for API symmetry with the shared engine and never fires on a real child-surface path.

The nearest ancestor is the closest enclosing child-surface bullet; an element bullet never establishes a base. With no child-surface ancestor in scope, the base is the entry’s root surface. A child-surface bullet nested under a broken (diagnostic-producing) ancestor is never silently reparented to a grandparent or the root — its descendants are dropped rather than resolved against the wrong base.


Corpus registry

UxRegistry (built by buildUxRegistry in registry.ts) is the corpus-wide index of every declared surface, keyed by absolute surface path. A path’s declarations are not collapsed: every declaration found across the corpus is kept, in source order. A surface declared more than once is not an error at the registry layer — the validator surfaces the collision as UXIL-015.

Each declaration is a SurfaceRecord:

FieldTypeDescription
pathstringAbsolute, dot-joined surface path (media.home.confirm_dialog).
kindstringThe surface kind — one of the three closed kinds.
statesreadonly string[]The declared state set, in declaration order.
owningEntryDisplayIdstringDisplay ID of the entry that declared this surface.
owningEntryFilestringFile path of the owning entry.
elementsreadonly UxElement[]The surface’s declared interaction elements.
locationSourceLocationSource position of the declaration.

Machine projection

projectUxRegistry (in projection.ts) turns a UxRegistry into a UxProjection: a deterministic, JSON-serialisable manifest for downstream codegen and other tooling. projectUxRegistry(registry) is byte-identical across repeated calls on the same registry:

  • surfaces are sorted by id (the surface path);
  • elements within a surface are sorted by name;
  • states are sorted;
  • verbs are kept in declaration order — order is load-bearing for compound controls, so it is not sorted.

A surface declared more than once corpus-wide (UXIL-015) projects using only its first-declared record; the duplicate itself stays a validator concern.

A ProjectedSurface carries id, kind, states, parent (the dot-truncated parent path, when it is itself a registered surface — else null), and elements. A ProjectedElement carries name, verbs, keyTemplate (the template name, or null), nav (the resolved navigation target path, or null), and states.

The media.home screen declared in Declaration forms projects to:

{
  "surfaces": [
    {
      "id": "media.home",
      "kind": "screen",
      "states": ["loading", "ready"],
      "parent": null,
      "elements": [
        {
          "name": "favorite_toggle",
          "verbs": ["toggle"],
          "keyTemplate": "track_id",
          "nav": null,
          "states": []
        },
        {
          "name": "play",
          "verbs": ["activate"],
          "keyTemplate": null,
          "nav": null,
          "states": []
        }
      ]
    }
  ]
}

Activation — the declaring entry type

uxil validation is profile-gated. A profile designates the contract entry type by setting declares: ux-surface on a type:

profile:
  types:
    ux-contract:
      extends: Contract
      display-id-pattern: "UXI_{n:4d}"
      declares: ux-surface

With no designation anywhere in the active profile chain, uxil content is inert: uxil-looking code spans stay opaque and draw no diagnostics.

With a designation:

  • entries of a declaring type are compiled and validated in full;
  • ux: citations are validated from every entry, whatever its type;
  • a root declaration (`ux:… : kind` span) in a non-declaring entry is UXIL-023. Element (/-led) and child-surface (.-led) bullets outside a declaring entry stay opaque — they are ambiguous with ordinary prose code spans.
  • cross-entry resolution codes (UXIL-016, UXIL-017, UXIL-018) are reported on project scope (bare check, the editor); a file-local check of an explicit subset suppresses them — a subset registry cannot tell a dangling reference from an unchecked file.

Diagnostic catalogue

CodeSeverityDescription
UXIL-001errorMalformed uxil reference.
UXIL-002errorReserved character in a uxil reference.
UXIL-003errorux://authority form is reserved; use a scheme-relative reference.
UXIL-004errorRoot declaration missing its kind.
UXIL-005errorElement declaration with an empty verb set.
UXIL-006errorElement declaration missing its trailing event dictionary.
UXIL-007errorMalformed key template.
UXIL-008errorMalformed surface.
UXIL-009errorUnknown surface kind (expected screen, panel, or agent).
UXIL-010errorUnknown interaction verb.
UXIL-011errorContract entry declares no root surface.
UXIL-012errorMore than one root surface declared in a contract entry.
UXIL-013error@ states declared on a stateless kind.
UXIL-014errorobserve combined with other verbs (it is exclusive).
UXIL-015errorSurface declared more than once corpus-wide.
UXIL-016errorDangling namespace parent — a nested surface whose dotted ancestor is declared nowhere.
UXIL-017errornavigate -> target does not resolve to a navigable (screen) surface.
UXIL-018errorCitation of an undeclared surface.
UXIL-019errorCitation of an undeclared element.
UXIL-020errorCited verb not in the element’s declared verb set.
UXIL-021errorCited state not declared on the surface.
UXIL-022errorConcrete key cited where the element declares a key template.
UXIL-023erroruxil declaration outside the declaring entry type.
UXIL-024errorRelative reference with no base in scope. Reserved — reachable once the uxil table surface lands (#717 §A).
UXIL-025errorobserve declared on a surface whose kind is not visual.
UXIL-026errornavigate declared without a -> target clause.

Editor integrations receive each code’s documentation link as an LSP codeDescription targeting https://markspec.dev/extensions/uxil#uxil-0xx anchors in this chapter.