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:
| Kind | Meaning |
|---|---|
value | A plain data value with no lifecycle semantics. Default when omitted. |
event | A named occurrence, optionally carrying a payload. |
signal | A continuously observable quantity with a measurable shape. |
command | A request to perform an action, optionally with parameters. |
state | A named mode or state-machine state. |
const | A named constant whose value does not change at runtime. |
config | A configuration parameter supplied before system start. |
document | A structured artefact (log record, report, notification). |
stream | A sequence of values produced over time. |
namespace | Declares 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
| Code | Severity | Description |
|---|---|---|
| TYPL-001 | error | Duplicate $Name binding within the same entry — first occurrence wins. |
| TYPL-002 | error | Deprecated — retired by the published tier; never emitted. Kind mismatch across entries. |
| TYPL-003 | error | Deprecated — retired by the published tier; never emitted. Shape mismatch across entries. |
| TYPL-004 | error | Typedef name redefined within the same entry. |
| TYPL-005 | error | Reference to an undefined typedef name. |
| TYPL-006 | error | Malformed schema — unparseable shape expression. |
| TYPL-007 | error | Unknown kind keyword; expected one of the ten closed-vocabulary kinds. |
| TYPL-008 | error | Literal value violates the declared constraint. |
| TYPL-009 | error | Published symbol declared more than once — declared-once violation. |
| TYPL-010 | error | Relative reference has no namespace base in scope. |
| TYPL-011 | error | Citation of an undeclared published symbol. |
| TYPL-012 | error | More 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.
| Token | Matches |
|---|---|
IDENT | one 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
| Kind | Navigable | Stateful | Visual |
|---|---|---|---|
screen | yes | yes | yes |
panel | no | no | yes |
agent | no | yes | no |
- Navigable — a valid
navigate ->target. Onlyscreen. - Stateful — may declare
@states (UXIL-013 otherwise).screenandagent. - Visual — subject to visibility assertions and
observeanchors (UXIL-025 otherwise).screenandpanel.
Verbs
| Verb | Requires nav target | Exclusive |
|---|---|---|
activate | no | no |
toggle | no | no |
select | no | no |
adjust | no | no |
input | no | no |
scroll | no | no |
drag | no | no |
navigate | yes | no |
dismiss | no | no |
ask | no | no |
observe | no | yes |
- Requires nav target — the element must carry a
-> targetclause (UXIL-026 otherwise). Onlynavigate. - 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:
| Field | Type | Description |
|---|---|---|
path | string | Absolute, dot-joined surface path (media.home.confirm_dialog). |
kind | string | The surface kind — one of the three closed kinds. |
states | readonly string[] | The declared state set, in declaration order. |
owningEntryDisplayId | string | Display ID of the entry that declared this surface. |
owningEntryFile | string | File path of the owning entry. |
elements | readonly UxElement[] | The surface’s declared interaction elements. |
location | SourceLocation | Source 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
| Code | Severity | Description |
|---|---|---|
| UXIL-001 | error | Malformed uxil reference. |
| UXIL-002 | error | Reserved character in a uxil reference. |
| UXIL-003 | error | ux://authority form is reserved; use a scheme-relative reference. |
| UXIL-004 | error | Root declaration missing its kind. |
| UXIL-005 | error | Element declaration with an empty verb set. |
| UXIL-006 | error | Element declaration missing its trailing event dictionary. |
| UXIL-007 | error | Malformed key template. |
| UXIL-008 | error | Malformed surface. |
| UXIL-009 | error | Unknown surface kind (expected screen, panel, or agent). |
| UXIL-010 | error | Unknown interaction verb. |
| UXIL-011 | error | Contract entry declares no root surface. |
| UXIL-012 | error | More than one root surface declared in a contract entry. |
| UXIL-013 | error | @ states declared on a stateless kind. |
| UXIL-014 | error | observe combined with other verbs (it is exclusive). |
| UXIL-015 | error | Surface declared more than once corpus-wide. |
| UXIL-016 | error | Dangling namespace parent — a nested surface whose dotted ancestor is declared nowhere. |
| UXIL-017 | error | navigate -> target does not resolve to a navigable (screen) surface. |
| UXIL-018 | error | Citation of an undeclared surface. |
| UXIL-019 | error | Citation of an undeclared element. |
| UXIL-020 | error | Cited verb not in the element’s declared verb set. |
| UXIL-021 | error | Cited state not declared on the surface. |
| UXIL-022 | error | Concrete key cited where the element declares a key template. |
| UXIL-023 | error | uxil declaration outside the declaring entry type. |
| UXIL-024 | error | Relative reference with no base in scope. Reserved — reachable once the uxil table surface lands (#717 §A). |
| UXIL-025 | error | observe declared on a surface whose kind is not visual. |
| UXIL-026 | error | navigate 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.