Built. typl has a working toolchain in this repository: the compiler accepts every construct below, and
ridl fmt, the LSP server and the code generators are built on it. See what is built.
typl Language Reference
Type Language — the vocabulary layer of the RIDL family: types, ranges,
units, constants, and composites, with package namespacing. typl is the root of
the family lattice — every other profile (ridl, rmdl, rsdl) builds on the
vocabulary typl declares.
Version: 0.1.0
Provenance. This reference re-frames Language Reference §1–§11 of the RIDL Language Reference as the named foundation layer (per the RIDL family concept note, §7.1 Stage 1), incorporates the module system of ADR-0002, and mines the preliminary markspec typl DSL as prior work (see Appendix F). Where this document and the RIDL Language Reference v0.1 overlap, this document is authoritative for the vocabulary layer; the RIDL Language Reference retains authority for interactions.
Table of Contents
- Scope and Position in the Family
- Lexical Conventions
- Packages, Imports, and Visibility
- Primitives
- Type Definitions
- Constants
- Structs
- Enums
- Enum Sets
- Unions
- Tuples
- Collections
- Comments
- Doc Comments
- Conventions
- Diagnostics
- Open Questions
- Appendix A — Standard Library
- Appendix B — Full Example
- Appendix C — Standards References
- Appendix D — Codegen Targets
- Appendix E — Formal Grammar (EBNF)
- Appendix F — Prior Work: markspec typl
- Appendix G — Coverage Analysis: JSON Schema and Other Schema Languages
- Appendix H — Glossary
1. Scope and Position in the Family
1.1 What typl is
typl is a units-aware, range-constrained schema language. It answers one question: what is the shared vocabulary? — the types, physical units, ranges, constants, and composite shapes that every other concern in a component-based reactive system describes things with.
typl is the only truly standalone member of the RIDL family. A pure typl package is useful by itself: it generates data types, validators, and documentation across every backend without any interface, behaviour, or deployment content. Every other profile requires typl; typl requires nothing above it.
The name is deliberately -L, not -DL: typl does not describe a system
aspect — it is the vocabulary the describing languages are written in.
1.2 The .typl profile
There is one grammar for the whole RIDL family; each language is a profile —
a restriction of that grammar selected by file extension. A .typl file accepts
type declarations only:
Accepted in .typl | Rejected in .typl (belongs to) |
|---|---|
package, import, as, internal | interface, signal, event, command, query, fixed (ridl) |
type, const | interaction declarations (ridl, and the rxdl spellings) |
struct, enum, enumset, union | model, node, behaviour operators (rmdl) |
tuples, collections, ? optionality | instances, wiring, deployment (rsdl) |
doc comments, @labels | timing annotations @Xms, @[min..max] (time core) |
streams <T> (ridl — see §1.3) |
typl declarations are also legal inside every other profile's files and inside
.rxdl (the total profile) — a .ridl file may declare the types its interface
uses. Profile purity is a policy decision declared per package in
ridl.toml, not a grammar limitation (concept note §4).
1.3 Profile boundary decisions
Three boundary calls are recorded here because they were genuinely undecided:
Streams belong to ridl, not typl. A stream <T> is not a data shape — it
has no bound, its framing is transport-decided, and it is only valid in
interaction position (command/query parameters and returns). That makes it
an interaction container, not a vocabulary item. typl owns the element
type (type FwBlock : bytes [1..4096]); ridl owns the <T> container. A
<T> appearing in a .typl file is a profile error (TYPL-301).
Timing is not typl. Duration literals (10ms, @[20ms..1s]) belong to the
family's time core, consumed by ridl/rmdl. The family lexer recognises
duration tokens everywhere (one lexer, one registry), but no typl construct
accepts them. The stdlib Duration type (a UCUM ms unit type) is unaffected
— it is an ordinary unit type.
Interaction kinds are not typl kinds. The markspec-typl precursor attached
kinds (signal, event, command, state, stream, config, document) to
type bindings. In the family these are split by layer:
signal/event/command/query/fixed are ridl's interaction keywords over
the interact core; state and stream processing are rmdl territory; config
maps to ridl fixed (provisioned constants); document is just a struct.
typl keeps only the pure vocabulary kinds: type, const, and the composites.
See Appendix F.
1.4 Keyword discipline
The family maintains one reserved-word registry across all profiles (concept
note §4.1). Every family keyword is reserved in every profile, including
keywords a profile does not accept — signal is not a valid identifier in a
.typl file even though .typl rejects signal declarations. This keeps .rxdl
unambiguous and keeps identifiers portable across layers.
Keywords used by the typl profile:
package import as internal
type const struct enum enumset union
boolean integer float string bytes
true false step match reserved error
Init values use bare = value (§5.8), not a keyword —
type Speed : km/h [0.0..250.0 step 0.5] = 0.0, the universal default-value
idiom. There is no default keyword (retired) and no init keyword in
typl (init is rmdl's, where two equations per flow must be disambiguated). The
wire clause and explicit width names (uint8…float64) are deferred to
v0.2 (§17.11): range-first inference plus ridl-diff breaking-change
detection cover the common case, and forward-compat width-pinning is a niche
refinement not worth ten keywords in v0.1.
Keywords reserved family-wide but rejected by .typl (the registry as
built, crates/ridl-syntax/src/keywords.rs): ridl's interface, service,
signal, event, command, query and fixed (family-neutral, ADR-0012
decision 4); the expr-core words require, ensure; the ridl clause words
states and during (general form §4.8); rmdl's model, function, let,
init, last, case, if, then, else, when, emit (it also surfaces
signal/event in signatures — same concepts, one registry entry each; init
is rmdl's alone — the memory-seed keyword, needed because a flow has two
equations; typl/ridl express init as bare = value; its ambient time values
now/dt are contextual identifiers, not keywords; pre,
->-as-followed-by, node, returns, realizes, and a surface step
considered and rejected — models are contract-blind, so no realizes; step
remains typl's quantization keyword alone), plus its reserved set merge,
current, state, transition, automaton; and rsdl's system, component,
distribution, deployment, machine, offers, requires, for (rsdl
reference §2 — the rsdl v0.1 words provides, instance, assurance,
target, place, on, transport, bundle, time, base, redundant,
supervise and degraded left the registry with rsdl v0.2, because no other
profile uses them, and each is an ordinary identifier in every profile). (node
and returns were considered and rejected by rmdl — never reserved.) The
per-profile keyword sections of each language reference enumerate their own
additions; the union of those sections is the registry until the platform
spec extracts it as a standalone normative list.
The domain spellings are not reserved words — the member-start rule
(ADR-0012 decision 4, as amended on 2026-10-11). present, notify, measure,
detect, actuate, trigger, the intent operation shapes activate,
toggle, select, adjust, dismiss, and the open candidates supply and
read are keywords only as the first token of an interface member, in a profile
that implements the spelling; no profile does in this release. Everywhere else
each is an identifier: the ROS 2 corpus names a field present
(evals/corpus/ros2/sensor_msgs/messages.typl) and a query trigger
(evals/corpus/ros2/std_srvs/interactions.ridl). An interface member line never
starts with an identifier, so a profile that gains a spelling invalidates no
source that compiled before. The uxdl words ADR-0012 retired — view,
display, input, action, fetch, navigate, scroll, drag, observe,
surface, agent — are ordinary identifiers in every profile.
Registry admission test — language, never runtime (family doctrine, audit
passed). Every registry entry names a describable property — a shape, an
interaction, a requirement, an availability, an occurrence relation — never an
execution mechanism. Steps, scheduling, acks, retries, async/await,
quarantine, and clock domains are runtime vocabulary: they appear in semantics
prose and runtime specs, never as surface syntax. Borderline entries carry their
justification where declared (now/dt: logical time is contract-constrainable
— rmdl §6.3). A future wire clause (deferred, §17.11) would clear the same bar
— wire ABI is contract, not execution. Rejected for leaking: throws, a surface
step, library time().
2. Lexical Conventions
The family has one lexer; these conventions bind family-wide. They are restated here because typl is the foundation document.
2.1 Encoding
Source files are encoded in UTF-8 (Unicode 15.0, RFC 3629). Non-ASCII characters are permitted only inside string literals and doc comments.
2.2 Whitespace, Line Endings, Separators
Whitespace (space, tab, CR, LF) is insignificant except as a token separator. A
line break is an LF, a CRLF pair, or a lone CR (a CR that is not
followed by an LF); a CRLF pair is one line break. A line comment and a doc
comment line end at a line break of any of these three forms, a string literal
and a regex literal cannot contain one, and diagnostic line numbers count each
of them. Newline and comma are interchangeable separators in all block
constructs; trailing comma is permitted. There are no semicolons — the
ADR-0002 example package veh.common.types; is errata; the grammar has none
(concept note §4.1 errata).
2.3 Identifiers
Three ASCII identifier conventions:
| Form | Pattern | Used for |
|---|---|---|
CamelCase | starts uppercase, no separators | types, structs, enums, enumsets, unions |
camelCase | starts lowercase, no separators | fields, tuple fields |
SCREAMING_SNAKE | uppercase, underscore separators | enum values, enumset bits, constants |
Identifiers start with a letter; digits permitted after the first character;
underscore only in SCREAMING_SNAKE. Reserved words (§1.4) may not be used as
identifiers.
2.4 Integer Literals
Follow ISO/IEC 9899:2018 (C17)
§6.4.4.1: decimal, no leading zeros, unary minus for negatives (0, 42,
-40).
2.5 Floating-Point Literals
IEEE 754-2019 double precision.
Must include a decimal point (0.0, 3.14, -40.0). Scientific notation is
not supported in v0.1. NaN and Inf are not permitted in range constraints.
2.6 String Literals
UTF-8 in double quotes. Escapes follow
RFC 8259 §7: \", \\, \n, \r,
\t, \uXXXX.
2.7 Regex Literals
Enclosed in forward slashes; syntax follows
ECMA-262,
restricted to the patterns that the Rust regex
crate also compiles. Forward slashes inside the pattern are escaped \/.
/^[A-HJ-NPR-Z0-9]{17}$/
A pattern that is not ECMA-262 syntax is TYPL-106. A pattern that is ECMA-262
syntax but that the regex crate cannot compile is TYPL-220. The Rust backend
emits a match constraint on a string-backed type as a regex::Regex::new
call in the generated code, under the generated crate's validate-pattern
feature (on by default), and a pattern that call refuses would fail at run time
instead of at build time. A regex constant is emitted as text and is not
compiled, but a match that names the constant is, so the constant is checked
at its declaration (§6.2).
The ECMA-262 constructs that the regex crate refuses, and that a typl pattern
therefore cannot use, include:
- lookaround:
(?=…),(?!…),(?<=…),(?<!…) - a backreference, numbered (
\1) or named (\k<name>) - the control escape
\cX, the NUL escape\0, and an identity escape of a letter theregexcrate gives no meaning, such as\e,\Qor\k. An identity escape of a letter the crate does give a meaning, such as\a,\A,\zor\pL, compiles in both engines, with the different meanings the table below records - the empty class
[], the negated empty class[^], and an unescaped[inside a class, as in[[] - the backspace escape inside a class,
[\b] - a class escape as a range bound, as in
[\d-z]or[\w-.] - a quantifier with no lower bound, such as
{,3}, and a{that opens no complete quantifier, as in{alone ora{ - a pattern whose compiled form exceeds the
regexcrate's size limit.\wand\p{..}are large Unicode classes in that crate, so an ordinary pattern such as^\w{1,256}$exceeds the limit, while^[A-Za-z0-9_]{1,256}$does not. An artificial pattern such as(a{1000}){1000}exceeds it too.
This list is not complete. The rule is that the compiler compiles the pattern
with both engines: a construct not listed here that the regex crate refuses is
TYPL-220 too.
TYPL-220 guarantees only that the Rust output can compile the pattern. It does not guarantee that the checker, the Rust output and the output of another backend match the same strings. The checker tests a declared init value against a pattern (TYPL-109) with ECMA-262 semantics, and some patterns that both engines compile have a different meaning in each (driftsys/ridl#597):
| In a pattern | ECMA-262, no u flag (the checker) | The Rust regex crate |
|---|---|---|
\d, \w | ASCII digits, ASCII word characters | Unicode digits, Unicode word characters |
\p{L} | the text p{L} | a Unicode letter |
\A, \z | the letters A and z | the start and the end of the text |
\a | the letter a | the bell character, U+0007 |
\< | the character < | a start-of-word boundary |
[a&&b] | one of a, &, b | the intersection of a and b, empty |
[[a]] | one of [, a, followed by ] | the nested class [a] |
2.8 Tokens recognised but not used by typl
Duration literals (500us, 10ms, 1s, 5min, 1h — the five suffixes ridl
§2.1 tabulates) and the @ timing sigil are lexed family-wide but rejected by
the .typl profile (TYPL-302).
3. Packages, Imports, and Visibility
The module system is ADR-0002's ns core, restated normatively for the typl
profile. Four keywords: package, import, as, internal. Distribution
(manifest, lockfile, cache) is tooling, not language — see ADR-0002.
3.1 Package Declaration
Every file begins with exactly one package declaration — dot-separated
lowercase identifiers:
package veh.common
A package is a directory. All files in the directory declare the same package name, and the name mirrors the directory path relative to the manifest root. Mismatch is a hard error (TYPL-002). The package — not the file — is the unit of visibility, cycle checking, and codegen output.
A package cannot take a name the compiler provides (TYPL-010). Today that is
ridl.std alone. Such a package can never be used: every package already
imports all of ridl.std implicitly (§3.2) rather than through an import, so
the declarations are unreachable under their own name. Where the generated
artifact is named for the package, it is overwritten by the standard package's
own as well. The reservation is the set of packages the compiler provides, not
the ridl. prefix — ridl.stdlib is an ordinary package name.
3.2 Imports
Imports are qualified and named:
import veh.common.Speed
import marine.nav.Speed as MarineSpeed // alias — collision release valve
Per ADR-0002 there are no wildcards, no relative imports, no re-exports. All three are hard errors.
Errata note. RIDL Language Reference v0.1 §2.2 showed
import veh.common.*as a linter warning. ADR-0002, accepted later, bans wildcards outright. This document follows ADR-0002: wildcard import is a compile error (TYPL-003). The RIDL Language Reference should be amended to match.
Implicit imports — always available: all definitions in the same package,
and all definitions in ridl.std (Appendix A).
Qualified references — any public definition may be referenced by fully qualified name without an import:
limit : veh.regulatory.SpeedLimit
A type reference that resolves through none of these — not the same package, not
ridl.std, not an import, not a qualified name — or that names another
package's internal declaration (§3.3) is a compile error (TYPL-011), reported
on the written path in every position that takes a type reference.
3.3 Visibility
Two levels. Public is the default; internal is package-private:
struct WheelTick { ... } // public — visible to importers
internal struct RawWheelFrame { ... } // package-private
internal may prefix any typl definition (type, const, struct, enum,
enumset, union). A public declaration must not expose an internal type in
its fields, arms, bounds constants, or backing (TYPL-005) — the contract surface
must be fully importable.
3.4 Cycles
Cycles within a package are permitted (mutually referencing declarations are normal). Cycles across packages are a hard error (TYPL-004). Note that recursive composite shapes are separately restricted — see §7.3.
4. Primitives
Five primitive types. Primitives are lowercase keywords — visually distinct from
CamelCase named types. They serve as backing types in type definitions;
direct use as field types is restricted (§15.3).
| Primitive | Meaning | Constraint |
|---|---|---|
boolean | logical true/false | none |
integer | whole number — width inferred from range | recommended — profile may require |
float | real number — width inferred from range and step | recommended — profile may require |
string | sequence of Unicode scalar values — default [0..256] if unspecified | recommended — profile may require |
bytes | opaque binary buffer — default [0..256] if unspecified | recommended — profile may require |
4.1 Boolean
No constraint syntax. Serialized as uint8 on transport — 0 = false, 1 =
true.
4.2 Integer
Range constraint [min..max] strongly recommended; active profiles may require
it. Width is inferred once by the compiler from the declared range and
passed to all backends:
| Range | Wire type |
|---|---|
[0..255] | uint8 |
[-128..127] | int8 |
[0..65535] | uint16 |
[-32768..32767] | int16 |
[0..4294967295] | uint32 |
[-2147483648..2147483647] | int32 |
larger unsigned — up to [0..9223372036854775807] | uint64 |
| larger signed or no range | int64 |
Integer domain. Every integer range must fit within [-2⁶³ .. 2⁶³−1] — the
int64 domain. Bounds outside it are a compile error (TYPL-111). This closes
the u64 hole: the language layer is always int64 (Kotlin Long, C++
int64_t), which cannot hold the top half of full-range uint64. A value that
genuinely needs all 64 unsigned bits is modelled as bytes [8].
Language layer — always int64. Transport layer — narrowest wire type
that safely contains the range. The resolved width is part of the contract:
widening a range across a width boundary changes the wire type, which is a
breaking change classified by ridl-diff. (An explicit width floor to
pre-empt such flips — a wire clause — is deferred to v0.2, §17.11.)
4.3 Float
Range and step strongly recommended; active profiles may require them. step
declares the value's quantization: valid values are origin + n·step, where n
is an integer and origin is the declared lower bound, or zero when no lower
bound is declared. This is a validity constraint, not a request to round an
input. Integer values must lie exactly on the grid. Source literals and resolved
constants use exact decimal grid membership. Floating-point constructors and
payload validators use the representation rule below and preserve the supplied
value rather than normalize it.
Floating-point grid validation uses the type's inferred wire precision. A constructor checks the value projected to that precision and preserves the supplied backing value. A payload validator checks the received representation at the same precision. This makes a constructed value's wire representation valid and keeps a decoded value valid under the same constructor check.
The runtime rule uses binary64 arithmetic over the projected value v. Prepare
the phase p = origin modulo step exactly in decimal arithmetic, in
[0, step), so shifting a grid by a large origin does not lose its phase.
Represent phase and step as a binary64 high part and a residual low part;
compensated arithmetic retains those residuals when reconstructing candidate
grid points. Subnormal coefficients are scaled by an exact power of two before
splitting and computation, then reconstructed values are scaled back.
The adjacent values at the wire precision define v's rounding cell. If the
step is smaller than that cell's width, it contains a grid point and v is
valid. Otherwise estimate q = (v - p) / step, with q = v / step - p / step
as the subtraction-overflow fallback. Check the nearest integer index and its
two neighbors, using compensated reconstruction and projecting each candidate to
the wire precision. A candidate r admits v when
abs(v - r) <= min(4 * epsilon * max(abs(v), abs(p), abs(step)), step / 4),
where epsilon is machine epsilon at the wire precision. round selects the
nearest integer, with ties away from zero. The quarter-step cap bounds the
rounding allowance where the grid is resolvable.
A positive decimal step that underflows binary64 is dense in every finite rounding cell. For a step that overflows binary64, prepare its finitely representable grid points exactly before rounding them and use the same relative rounding allowance. NaN and infinity are never grid values. This is a bounded representation rule, rather than exact decimal arithmetic in the runtime or input normalization; a contract requiring exact decimal grid identity should use the scaled-integer form below.
Width inference is count-based, not step-based. Let
N = (max − min) / step + 1 — the number of distinct representable values:
| Condition | Wire type |
|---|---|
range + step declared, N ≤ 2²⁴, both bounds exactly representable in binary32 | float32 |
otherwise — no step, no range, N > 2²⁴, or bounds not representable | float64 |
Errata note. RIDL Language Reference v0.1 §3.3 inferred float width from step alone (
step >= 0.001→float32). That rule is unsound:float [0.0..1000000.0 step 0.001]satisfies it but requires 10⁹ distinct values — far beyond binary32's 24-bit significand. The count-based rule replaces it; the RIDL Language Reference should be amended.
Quantized wire form — scaled integers. Because step anchors values at
min + n·step, a quantized float is losslessly a scaled integer:
raw = (value − min) / step, an unsigned integer whose width follows from N
via the §4.2 table. Transports with a scaled-integer convention — CAN/DBC
always, SOME/IP optionally per deployment — encode quantized floats this way
(factor = step, offset = min). This is exactly AUTOSAR's LINEAR CompuMethod, so
the ARXML mapping is direct, and it sidesteps binary-float step fuzziness (0.001
has no exact binary32 representation) on the transports that care most. proto3
and FlatBuffers keep native float/double. See Appendix D.
Language layer — always float64. Transport layer — as inferred above.
4.4 String
A string is a sequence of Unicode scalar values. string [N] bounds the
count of scalar values — not graphemes, which are unbounded, and not bytes,
which bytes counts. Default [0..256] when unspecified (warning).
The largest advised bound is 4,032 characters. A string of 4,032 characters is at most 16,128 bytes of UTF-8, which with the transport header and the encoding overhead fits in one transfer of 16,384 bytes (ADR-0030 decision 6). A bound above it draws TYPL-116 (warning), reported on the bound. The payload ceiling is 16,256 bytes, and the codecs do not yet enforce it (driftsys/ridl#996).
Every encoding carries a string as UTF-8. The in-memory representation is
the target language's own — UTF-8 in Rust, UTF-16 in TypeScript, Kotlin and C# —
and the codec converts at the buffer, as every proto and FlatBuffers runtime on
those platforms already does. The byte capacity of string [N] is therefore
4·N, which is UTF-16's worst case as well, so UTF-8 costs nothing at the
maximum.
Why not UTF-16 on the wire. proto3
stringmust be UTF-8 and FlatBuffersstringis UTF-8, so a UTF-16 wire form means emittingbytesand[ushort]in the schemas. A consumer generated from those schemas would then see an opaque byte field where the contract says text, which breaks ADR-0018 decision 4's rule that interoperability is at the bytes, mediated by the emitted schema.
One consequence is not a projection rule and belongs to a runtime rather than to
an encoding: a JavaScript string may hold a lone surrogate, which is not a
scalar value and which TextEncoder silently replaces with U+FFFD, so a codec
compiled to wasm cannot see the violation. The TypeScript runtime package checks
well-formedness at its boundary, before handing the string to the codec, and
rejects.
Two rules are an encoding's own rather than this reference's: how a string is
stored in an in-place layout, and whether a terminator is guaranteed for a C
reader. Both are settled by the podbuf projection record
(ADR-0031 decision 5): the
tail holds the bytes present followed by one NUL, and the fixed-capacity form is
a rejected alternative of that record.
4.5 Bytes
Bound is in bytes; constraint syntax mirrors string but has no match.
Default [0..256] when unspecified (warning).
The largest advised bound is 16,128 bytes, the byte size of a string at its advised bound (§4.4). A bound above it draws TYPL-116 (warning), reported on the bound.
4.6 Byte Order
Byte order is not a type property. A typl declaration never states one, and none of the ten inferred widths (§4.2, §4.3) carries an endianness.
Each encoding fixes it: proto3 and FlatBuffers each define their own, and the
podbuf projection declares one. A transport that carries a per-signal choice —
CAN and DBC's Intel/Motorola selection is the case that raises the question —
takes that choice from the deployment, which is rsdl's, and never from the type.
The rsdl surface for stating it lands with the rsdl rewrite at v0.2 (§17.9).
5. Type Definitions
The type keyword defines a named scalar — a constrained primitive or a
physical unit type. Named types carry domain meaning and are the primary
building blocks for all fields.
5.1 Physical Unit Types
A unit type associates a scalar with a unit from the Unified Code for Units of Measure (UCUM) — the unit standard used by OpenTelemetry, HL7 FHIR, and ISO 11240:
type Speed : km/h [0.0..250.0 step 0.5]
type Temperature : Cel [-40.0..125.0 step 0.1]
type Torque : N.m [0.0..500.0 step 0.1]
type RPM : /min [0.0..8000.0 step 10.0]
type Pressure : bar [0.0..10.0 step 0.01]
type Voltage : V [0.0..48.0 step 0.1]
type Ratio : % [0.0..100.0 step 0.1]
In v0.1 the underlying primitive is float, whatever the literal type of the
range (§17.12 records the v0.2 rule, under which integer literals give an
integer backing). UCUM expressions are case-sensitive. Because a unit type
carries unit, range, and step, downstream layers inherit dimensional checking
and range-driven saturation — rmdl computes with Speed, not bare real
(concept note §6).
Common automotive UCUM units: km/h, Cel, N.m, /min, m/s2, bar, V,
A, W, %.
5.2 Constrained Primitive Types
type Counter : integer [0..65535]
type Gain : float [0.0..1.0 step 0.01]
type Vin : string [17 match VIN_PATTERN]
type Frame : bytes [8]
5.3 String Constraint Syntax
| Syntax | Meaning |
|---|---|
string [N] | exactly N characters |
string [min..max] | min to max characters |
string [N match PATTERN] | exactly N, validated |
string [min..max match PATTERN] | range with validation |
match references a named regex constant or an inline regex literal.
5.4 Bytes Constraint Syntax
| Syntax | Meaning |
|---|---|
bytes [N] | exactly N bytes |
bytes [min..max] | min to max bytes |
No match — bytes are opaque.
5.5 Range Bounds
Ranges are closed (inclusive) on both ends. Either bound may be omitted
([0..], [..255]), in which case the missing bound defaults to the widest
value the inferred width allows.
NaN is outside every declared numeric range, including a range with an
omitted bound. NaN is also outside every step grid (§4.3); a grid admits only
finite values. A numeric range's omitted bound is the widest finite backing
value, so a range also excludes infinities. An unconstrained float retains its
backing representation's NaN and infinity values. Declaring a range or step
therefore changes validity, without globally restricting the float primitive
to finite values. These rules apply to named types and inline constraints. The
Rust backend enforces them in checked constructors for named scalars and in
payload verification for named and inline constraints. Public primitive fields
do not validate assignment. The reason is domain membership: NaN has no ordered
position in a closed interval and no integer grid index.
Exclusive bounds are not supported, and are not planned. An integer's exclusive bound is written as the closed bound one value inside it. A float's exclusive bound has no definable neighbour: §4.3 derives the width from the range, so "the next representable value" would depend on a width that depends on the bound itself. The forcing case that would reopen this: a published contract whose semantics genuinely require an open interval on a float.
5.6 Width Is Inferred, Never Written
typl v0.1 has no surface syntax for wire width. A range is the only width
control an author needs: integer [0..255] is a uint8, integer [0..65535]
is a uint16, and the compiler derives the narrowest safe width once (§4.2,
§4.3) and passes it to every backend. The ten concrete width names
(uint8 … float64) are not keywords, not primitives, and not writable anywhere
in a .typl source — writing one is a parse error (there is no production for
it).
Range-driven inference has one known hazard: widening [0..250] to [0..300]
silently flips the wire type uint8 → uint16 — a wire ABI break from a
semantically innocent edit (hardest on FlatBuffers and CAN, invisible on proto3
varint). v0.1 handles this with the ridl-diff gate alone: it classifies
every resolved-width change as breaking, so the flip is caught in CI. An
explicit width floor that would pre-empt the flip at the source — a wire
clause pinning headroom above the boundary — is deferred to v0.2; see §17.11.
A resolved width is part of the contract on every encoding, and part of the
memory layout on some. On an encoding with a fixed inline layout there is no
vtable to absorb a change of size, so a width change moves every later field.
The ridl-diff classification above covers both cases, because it is the
resolved width that it compares. A backend that emits a fixed inline layout must
therefore treat a width change as a layout break and must not rely on a
reader tolerating it.
5.7 Type Identity — Nominal
Every named type is nominally distinct — from its backing primitive and from
every other named type, even one with identical constraints. Speed and
Torque are both float-backed; they are not assignable to each other, and
neither accepts a bare float. A literal is accepted where a named type is
expected when it satisfies the type's constraints (this is how
const MAX_SPEED : Speed = 250.0 works); a value of another named type never
is. There is no implicit conversion anywhere in the family — explicit conversion
is expr/rmdl territory, where unit dimensions decide convertibility (§17.5).
Precedent: Ada derived types, Rust newtypes, Haskell newtype. Rationale:
nominal identity is what makes unit safety real — structural typing would let a
Torque flow into a Speed port because both are "float in a range", which is
precisely the class of error this language exists to prevent. Codegen realises
nominal types as distinct wrapper types where the target allows (Rust newtype,
Kotlin value class) and as documented aliases where it does not (proto3).
A language backend's wrapper type also carries the type's constraints: its value is built through a constructor that checks the range, step and length bounds of §5.1 to §5.5 and reports a violation instead of building the value, beside a constructor that does not check, for a value the payload validator has already checked. A backend may make the pattern check optional. A wire backend emits neither, because a schema cannot refuse a value when it is built. ADR-0013 decision 8 is the rule, and records which backends emit the checking constructor today.
5.8 Init Values
Every type has an init value — the value a consumer holds before anything
real arrives (the interaction layer's signal channels start from it; AUTOSAR
initValue is the precedent). It is either declared or derived:
Declared — a bare = value suffix, valid on type definitions as well as
struct fields. There is no keyword: the value simply follows the constraint.
type Speed : km/h [0.0..250.0 step 0.5] = 0.0
The bare = value form is the family's single init syntax at the vocabulary and
interaction layers (typl types, typl struct fields, ridl signal overrides). It
carries no keyword — not default (retired) and not init (which belongs
to rmdl alone, where init x = value seeds a memory recurrence and the keyword
is needed to disambiguate it from the equation).
Derived — when no = value is declared:
| Type | Derived init |
|---|---|
boolean | false |
| numeric / unit type | 0 (or 0.0) if admitted by both range and step, else the lower bound when declared |
string / bytes | empty if the bounds admit length 0; not derivable otherwise |
type with match pattern | not derivable (a pattern-valid value cannot be synthesised) — declare a = value |
enum | the value 0 if declared, else the lowest declared value |
enumset | empty set |
struct | each field's init, recursively; optional fields absent |
union | first arm's init |
| tuple | each field's init |
| array | min-bound count of element inits (empty when min = 0) |
| map | empty when min = 0; one key/value init when min = 1 and both are derivable; otherwise not derivable |
For a numeric range with no lower bound whose upper bound is negative, derive the upper bound when no step is declared. With a step, derive the greatest grid point at or below that upper bound, using the zero origin and exact decimal arithmetic. A derived floating-point init must fit the finite backing domain; when no finite grid point is available, the init is not derivable.
A map with a minimum count above one has no derived init: repeating a key's init would violate key uniqueness, and the toolchain does not synthesize distinct keys. Optional map fields without an override remain absent.
A declared init value must have the backing's kind and satisfy the type's
constraints, including step (TYPL-109). Floating-point init values and typed
floating-point constants must lie within the finite binary64 backing domain.
Integer and enum init values may use an exactly integral numeric spelling such
as 1.0; an enum value must name a declared discriminant. Constant references
are checked after resolution; an unresolved reference is an invalid init. A
string literal initializes string or its UTF-8 bytes initialize bytes, with
the corresponding character or byte length check. A boolean literal initializes
only boolean. The init syntax has no composite or enum-set literal, so a
literal or constant override on a struct, union, tuple, array, map or enum set
is rejected rather than stored as an untyped scalar. Derived composite inits
remain recursive as listed above. A type whose init is not derivable is simply
marked so in the IR (TYPL-115, info) — it becomes an error only where a consumer
requires an init (e.g. a ridl signal payload, ridl §4.4) and none is declared.
6. Constants
const defines a named compile-time constant — package-scoped, importable, and
reusable in range constraints, match patterns, and init (= value)
declarations. Higher profiles also use constants in require/ensure
expressions.
6.1 Value Constants
package veh.common
const MAX_SPEED : Speed = 250.0
const SPEED_LIMIT_EU : Speed = 130.0
const MAX_GEAR : integer = 6
const IDLE_RPM : RPM = 800.0
SCREAMING_SNAKE naming. The declared value must satisfy the constant's type
constraints (TYPL-108). Reuse:
type Speed : km/h [0.0..MAX_SPEED step 0.5]
struct GearState {
gear : integer [0..MAX_GEAR]
}
6.2 Regex Constants
A const may hold a regex literal, reusable in match constraints. The pattern
follows §2.7, and TYPL-106 or TYPL-220 is reported at the constant's
declaration, not at each match that names it. The Rust backend emits the
constant itself as text, not as a compiled pattern:
const VIN_PATTERN = /^[A-HJ-NPR-Z0-9]{17}$/
const ASCII_PATTERN = /^[\x20-\x7E]+$/
type Vin : string [17 match VIN_PATTERN]
type Email : string [1..254 match /^[^@]+@[^@]+\.[^@]+$/]
7. Structs
A struct is a named composite with a fixed, closed set of named fields.
There are no additional/unknown properties — a struct is closed by definition
(contrast JSON Schema's open-world default, Appendix G). A field name is unique
within its struct (TYPL-215): a name identifies one field, so a struct cannot
declare two.
struct DriverProfile {
name : Name
speed : Speed
override : Speed? // optional — may be absent
}
7.1 Optionality
The ? suffix marks a field as optional — may be absent, which is distinct
from "present with a null value"; typl has no null. Maps to optional in
proto3, Option<T> in Rust, T? in Kotlin.
7.2 Field Init Values
A field may carry a bare = value suffix — its init value, the same syntax a
type uses (§5.8). There is no attribute block and no keyword; typl carries no
other field attribute at the vocabulary layer.
struct RetryPolicy {
attempts : integer [0..10] = 3
backoff : Duration = 100.0
}
The init value must satisfy the field's type (TYPL-109). require/ensure are
expr-core attributes owned by higher profiles and are rejected in .typl
(TYPL-303).
7.3 No Recursive Shapes
A struct (or union) must not reference itself, directly or through any chain of composite references. Recursion makes the wire size unbounded, which contradicts the bounded-size guarantee every typl composite otherwise carries. Recursive composite reference is a compile error (TYPL-206). A genuine tree- or graph-shaped payload is modelled with explicit indices into a bounded array.
This is not provisional. The bounded-size guarantee is what the rest of the language rests on: §12 requires an explicit bound on every variable-size collection, §4.3 infers float width from a count of representable values, and an encoding with a fixed inline layout cannot exist at all for a shape whose size is not known at compile time. The forcing case that would reopen it: a published contract whose payload is genuinely tree-shaped and whose depth cannot be bounded at the source.
7.4 Field Identity and Evolution
Tag-based transports (proto3 field numbers, FlatBuffers field ids) need a stable
numeric identity per field. typl assigns it implicitly, by declaration order
(1-based), for struct fields and union arms alike. There is no explicit = N
tag syntax.
That makes evolution discipline part of the language:
- Append-only. New fields are added at the end of the struct or union.
Inserting or reordering fields shifts the ordinals of everything after them —
a wire break, rejected by
ridl-diff. - Delete by tombstone. A removed field keeps its ordinal slot occupied with
reserved:
struct DriverProfile {
name : Name
reserved legacyChecksum // was ordinal 2 — slot retired, never reused
speed : Speed
}
Re-declaring a field under a reserved name is a compile error (TYPL-210).
reserved in an enum body. Both forms are legal there, and they guarantee
different things. reserved 3 retires the wire value 3: it is never reused
with a new meaning. reserved Retired retires the name Retired: it is
never re-declared, and it says nothing about any wire value — a value is retired
only by the integer form, because the value a retired name once carried is not
recoverable from the source that no longer declares it. Re-declaring either
draws TYPL-210. A backend projects a name tombstone where its target has a name
reservation of its own — proto3's reserved "NAME"; — and drops it where its
target has none, as FlatBuffers does.
-
ridl-diffis the enforcement point. Ordinals are fully derivable from source — no sidecar state, no lockfile-assigned numbers. The CI plane (ridl-diffagainst the previous IR snapshot) rejects reorder, insertion, un-tombstoned deletion, and an append that the backward-compatibility decoder rule below cannot read, in published packages. -
Decoder rule for forward compatibility. The flip side of append-only: a decoder built against version n of a struct, receiving a payload from a version n+k encoder, must ignore unknown trailing fields (and should preserve them when acting as a proxy — the proto3 unknown-field lesson). This is native on proto3/FlatBuffers; on positional transports it requires length-prefixed framing (SOME/IP TLV) or is simply unavailable (CAN) — where unavailable, mixed-version communication is a deployment error rsdl must surface (§17.9).
-
Decoder rule for backward compatibility. The other direction: a decoder built against version n+k of a struct, receiving a payload from a version n encoder, finds every field appended since n absent. An absent optional field reads as absent. An absent non-optional field reads as the transport's default, 0, only when the field is a scalar, an enum or an enum set whose type allows the value 0: a range and
stepthat hold 0 (§4.3), an enum that declares a member with the value 0, or any enum set (0 is the empty set). Any other absent non-optional field — a string, bytes, struct, union, tuple, array or map field, or a scalar or enum whose type excludes 0 — has no value to read, and the decoder refuses the payload rather than invent one. Soridl-diffreports appending a non-optional field as breaking unless its type allows 0; the author declares the new field optional (field : T?) instead (driftsys/ridl#598). A type the diff cannot resolve is reported as breaking;ridl diffresolves types against the new snapshot and the built-inridl.std.
Rationale. Explicit proto-style tags were considered and rejected: they
impose a per-field numbering ritual on every struct — including the majority
bound to positional transports (CAN, SOME/IP) where the numbers mean nothing —
and gaps/vanity numbering must then be linted. Implicit ordinals keep the
surface small and the audit trail honest: the wire layout is readable from the
.typl file alone. The discipline this demands — append-only with tombstones —
is the discipline a contract language wants anyway. Trade-off on record:
reordering is legal syntax, and only ridl-diff makes it an error; publishing
without the CI gate is unprotected. Judged acceptable because ridlc --frozen +
ridl-diff are already mandatory in the ecosystem's CI plane.
8. Enums
An enum is a named set of discrete integer-backed values, for single-value
selection:
enum GearPosition {
PARK = 0
DRIVE = 1
REVERSE = 2
NEUTRAL = 3
}
- Backing type is
integer, implicit; all values explicitly assigned; values unique (TYPL-203) - A value name is unique within its enum (TYPL-216): a name identifies one value, so an enum cannot declare two
- First value conventionally
= 0for proto3 compatibility - String-backed enums are not supported in v0.1; planned for v0.2 (§17.1)
Identity is the value, not the position. An enum value's wire identity is
the explicit number it declares, and nothing about it depends on where the value
sits in the body. Reordering the members of an enum body therefore changes
nothing on any wire; ridl diff reports it as enum_reordered, compatible.
This is the opposite of a struct field or a union arm, whose identity is its
declaration order (§7.4), and it is why an enum value may be inserted in the
middle of a body while a struct field may not.
As built.
ridl diffcomparesenumvalues andenumsetbits by the number each declares. A member whose number changed is reported asmember_reorderedwith its old and new number, breaking. A body whose difference is only order — its values or bits reordered with no number changed, or anenum'sreservedlist reordered with no entry changed — is reported asenum_reordered, compatible: nothing moves on the wire. The catalog hash covers only the declarations an interface reaches; when an interface reaches theenumorenumset, the order of its values or bits, and of anenum'sreservedlist, is part of the hash, so the hash changes then.
9. Enum Sets
An enumset is a named bitfield — multiple flags active simultaneously. Backing
width inferred from the highest bit position.
An enumset bit's identity is the bit position it declares, not its position in
the body, so reordering the members of an enumset body changes nothing on any
wire, and ridl diff reports it as enum_reordered, compatible — the same rule
as §8's, for the same reason.
9.1 Standalone Form
enumset WarningFlags {
LOW_FUEL = 0 // bit 0
CHECK_ENGINE = 1 // bit 1
DOOR_OPEN = 2 // bit 2
SEATBELT = 3 // bit 3
}
- Bit positions unique (TYPL-207)
- A bit name is unique within its enumset (TYPL-218): a name identifies one bit, so an enumset cannot declare two
- A bit position is a number with an integer value (TYPL-219): a string, a
boolean, a fractional number, a regex, or a constant reference is refused,
1.0is accepted as bit 1, and a position outside bits 0..63 is TYPL-111
9.2 Derived Form
Derives bit positions from an existing enum — preferred when both single-value
and set use are needed:
enum Warning { LOW_FUEL = 0, CHECK_ENGINE = 1, DOOR_OPEN = 2, SEATBELT = 3 }
enumset WarningFlags : Warning
9.3 Width Inference
| Highest bit | Wire type |
|---|---|
| 0..7 | uint8 |
| 8..15 | uint16 |
| 16..31 | uint32 |
| 32..63 | uint64 |
Language layer — always int64. Transport layer — narrowest inferred.
10. Unions
A union is a discriminated (tagged) union — exactly one arm active at any
time:
union SensorResult {
ok : SensorReading
err : SensorFault
}
- Arms reference named types only — primitives not permitted directly (TYPL-204)
- An arm name is unique within its union (TYPL-217): a name identifies one arm, so a union cannot declare two
- Untagged unions and intersections (JSON Schema
anyOf/allOf) are deliberately not supported — see Appendix G - Maps to
oneofin proto3,unionin FlatBuffers,sealed classin Kotlin, Rustenum
10.1 Error Types
The error modifier marks a named composite as failure vocabulary:
error enum DiagError {
FILTER_INVALID = 0
STORAGE_BUSY = 1
ACCESS_DENIED = 2
}
error struct CalFault { // data-carrying failure
code : integer [0..255]
detail : Message
}
error union ServiceFault { // composes error types — all arms error-typed
diag : DiagError
cal : CalFault
}
erroris valid onenum,struct, anduniononly (TYPL-212); anerror union's arms must all be error-typed (TYPL-214)- An error type is otherwise an ordinary type — package-scoped, importable, nominally distinct, usable wherever data is legal (a fault-log struct may contain one; a signal may publish one). The marker adds exactly one bit of meaning: this shape describes a failure
- Codegen realises error types with the target's error material where it exists
(Rust
std::error::Errorimpl, Kotlin sealed failure hierarchies) and as plain data elsewhere
10.2 Result Unions
A union mixing error and non-error arms is a result union and must have
exactly two arms — one success arm (any non-error type) and one error arm (an
error type). Anything else that mixes the two kinds is malformed (TYPL-213).
Several failure kinds compose into one error union before entering the
result union.
union CalOutcome {
ok : CalReport
err : CalFault
}
A result union is the family's Result<T, E> — achieved without generics,
consistent with the no-parametric-types decision. In pure typl a result union is
simply a union; the interaction layer (ridl §10.1) recognises it at
interaction positions and maps the error arm onto transport-native error
channels. This is the family's entire functional-error mechanism: failure is
data, declared once in the vocabulary — there is no throws, no exception, no
second channel anywhere in the family.
11. Tuples
A tuple is an anonymous composite of named fields used inline — as struct fields at the typl layer; higher profiles additionally use tuples as query returns (ridl §7.1). A tuple is not an interaction parameter type: a parameter is a named type or a stream (ridl §6.1, §7.1).
struct SensorBounds {
range : (min: Speed, max: Speed)
}
- Fields are always named — positional access is not permitted
- A field name is unique within its tuple (TYPL-215), as in a struct (§7)
- The empty tuple
()is the unit type (its interaction role is defined by ridl) - The same tuple shape used in multiple places draws a linter warning: extract a
named
struct(TYPL-205)
12. Collections
Collections are finite, bounded aggregates. All variable-size collections require explicit bounds — no defaults, no exceptions (TYPL-201/202).
12.1 Array
readings : [Speed; 8] // fixed — exactly 8
faults : [FaultCode; 0..32] // bounded — 0 to 32
12.2 Map
Keys must be a named string type or primitive. Each key occurs at most once in a
map; a payload containing duplicate keys is invalid. Equality is value equality
in the key's declared type, including floating-point equality (+0.0 and -0.0
compare equal; NaN does not compare equal to itself). Validation must reject
duplicates rather than choose which entry to retain:
metadata : [Label : Name; 0..32]
sensors : [Label : Speed; 1..8]
A bare string or bytes key takes §4.4–§4.5's [0..256] length default, with
TYPL-103, the same as every other position — §15.3 keeps a bare string/bytes
out of a field position, but a map key is not one, so the default applies there
instead. A key therefore always carries a length bound, which is what lets a
projection state a finite size for the map.
A map has no declared entry order, and typl defines no canonical payload
encoding. A map is the only typl construct whose ordering is free: an array is
ordered, a struct field and a union arm are identified by ordinal (§7.4), and an
enum value and an enumset bit carry explicit numbers (§8, §9). So a
bit-reproducible byte sequence — what a signed payload, a content hash or a
replay plane needs — is a property of an encoding and of the frame that carries
it, not of the type. The rule that fixes it, including the entry order a map
serialises in, belongs to the frame specification and to each projection record
(§17.10).
12.3 Bound Rules
| Construct | Syntax | Bound |
|---|---|---|
| Exact-length array | [T; N] | exactly N — mandatory |
| Bounded array | [T; min..max] | min to max — mandatory |
| Bounded map | [K:V; min..max] | min to max entries — mandatory |
The stream container <T> is the deliberate exception to boundedness and lives
in the ridl profile (§1.3).
13. Comments
Discarded by the compiler.
// line comment
/* block comment — nesting not supported */
14. Doc Comments
A doc comment documents the next named declaration or member, called its
carrier. It is processed by documentation generators and editors and has no
semantic effect: the catalog hash clears every doc field, and ridl diff
classifies a change to one as DocOnly. No blank line may separate a doc
comment from its carrier (TYPL-404). The decisions behind this section are
ADR-0026.
14.1 Syntax
/// Current calibrated wheel radius
type WheelRadius : m [0.20..0.45 step 0.001]
/// lines are the house style. A block /** ... */ has the same meaning; a
leading * on each of its lines is decoration and is stripped. A project can
require /// with the doc-comment-style lint (TYPL-410, allow by default).
A doc is CommonMark, and a bracketed name in it is a link to a declaration or a member (§14.5). The carriers are listed in §14.4.
Examples are written under a # Examples heading with a fenced code block. This
is a convention: the compiler does not read it.
14.2 Tags
A tag is @word at the start of a line of the doc, after the comment markers
and leading whitespace. An @ anywhere else is prose. A tag line is not part of
the doc text. Each tag may appear more than once, and every value is kept.
| Tag | Value | Valid on |
|---|---|---|
@see | one qualified name, with an optional member; a link | all |
@since | a version, MAJOR.MINOR or MAJOR.MINOR.PATCH | all |
@labels | comma-separated classification labels (§14.3) | all |
@deprecated | "reason string" | all |
Any other tag draws TYPL-408. A @see or @since with a missing or malformed
value draws TYPL-409, and a @see target that does not resolve draws TYPL-401.
A @deprecated with no reason draws TYPL-405. Every tag is accepted on every
carrier, and @deprecated and @labels are stored in the IR on the carriers
that have the field — a declaration, a struct field, an interface, an
interaction and a service. On an enum value, an enumset bit, a union arm, a
parameter and an rsdl carrier the two tags are read and dropped, with no
diagnostic. Both stay doc tags: ADR-0028 decision 4 declines the general form's
plan to make deprecated and labels attribute keys.
14.3 Labels
@labels carries free-form SCREAMING_SNAKE classification identifiers
(optionally with a parenthesised suffix, e.g. MY_LABEL(D)). The core language
defines no vocabulary — an external profile validates labels and enforces
combinations. The compiler passes labels through to generated metadata
unchanged.
14.4 Carriers
| Carrier | A doc is required (TYPL-406) |
|---|---|
type, const, struct, enum, enumset, union, interface, service | when not internal |
| struct field, enum value, enumset bit, union arm, interaction | when the declaration is not internal |
parameter of a command or query | never |
reserved entry | never |
rsdl system, component, distribution, deployment, machine | always |
rsdl body line (offers, requires, a bare member reference) | never |
A package needs no doc. A doc made only of tags (§14.2) counts as missing. A
parameter's type is always a named type, which carries its own doc. A doc
comment in any other position — before package, an import, a return type or
an attribute block, an arm of an inline T | E return, or at the end of a file
or a body — is misplaced (TYPL-407).
14.5 Links
Three bracket forms are doc links:
[Name]and[pkg.Name];[`Name`], the code-span form;[text][Name], with an explicit label.
The bracket content is a link only when it is a qualified identifier with at
most one member suffix: [0..250] and [see below] are prose. A name inside a
code span or a fenced code block is not a link, and a [Name] that has a
CommonMark link reference definition in the same doc is an ordinary Markdown
link.
A link resolves in the scope of the file that holds it, with the rules of a type
reference (§3): a bare name is looked up locally, then among the imported names,
then in ridl.std. A qualified pkg.Name names any package the current package
can depend on (ADR-0002 §5) without an import; a remote package of [imports]
is reachable where an import of it resolves — the package's own [imports]
or, for a workspace member, the workspace [imports] pin its unit. One more
segment names a member of the declaration: a field, enum value, enumset bit,
union arm or interaction ([Gear.PARK], [CruiseControl.setLever]). When a
name could be a package path or a declaration followed by a member, the link
resolves as a type reference resolves the same name. A declaration that is
internal in another package cannot be linked to. A link that does not resolve
draws TYPL-401; each link that resolves is stored in the IR with its canonical
target.
15. Conventions
15.1 Naming
| Construct | Convention | Example |
|---|---|---|
type, struct, enum, enumset, union | CamelCase | VehicleSpeed, WarningFlags |
| Fields, tuple fields | camelCase | currentSpeed |
| Enum values, enumset bits, constants | SCREAMING_SNAKE | PARK, MAX_SPEED |
| Packages | lowercase.dot | veh.common |
15.2 Separators
Newline and comma interchangeable in all block constructs; trailing comma permitted; no semicolons.
15.3 Type Usage
stringandbytesmust not be used directly as field types — always define a namedtype(the ridl profile grants streams an element-type exception; there is no exception inside typl)- All variable-size types declare explicit bounds
unionarms reference named types only- Prefer named types over inline primitives for all domain concepts
15.4 Imports
Explicit single-type imports only (wildcards are an error, §3.2). Aliases only on genuine collision — linters flag gratuitous aliases.
16. Diagnostics
typl adopts stable diagnostic codes — a practice inherited from the markspec
typl catalogue (Appendix F). Codes are namespaced TYPL- and grouped by
hundreds: 0xx module, 1xx scalars & constants, 2xx composites, 3xx profile
boundary, 4xx documentation. Codes are never renumbered; retired codes are
marked deprecated and never reused.
The tables below are the TYPL- codes only. A .typl file also draws the two
namespaces no profile owns — FORM- (surface syntax: lexical, parse, and the
attribute-block rules) and MANI- (the manifest). Both are tabulated once in
the family overview §7 and are not restated here.
A .typl file also draws one RIDL- code, RIDL-149: two names in one scope
that collide after a pinned name transform (ADR-0016). Three of its scopes are
typl declarations — the fields of one struct, the arms of one union, and the
values of one enum — so LEVEL_10 beside LEVEL10 in one enum is refused, as
both become Level10. Its row, with the transform each scope is checked under,
is in ridl §16.4.
16.1 Module (TYPL-0xx)
| Code | Rule | Severity |
|---|---|---|
| TYPL-001 | more than one package declaration in a file | error |
| TYPL-002 | package name does not mirror the directory path | error |
| TYPL-003 | wildcard, relative, or re-exporting import | error |
| TYPL-004 | circular package imports | error |
| TYPL-005 | a public declaration exposes an internal type | error |
| TYPL-006 | conflicting imports without an alias | error |
| TYPL-007 | unused import | warning |
| TYPL-008 | import alias without an actual collision | warning |
| TYPL-009 | duplicate definition of the same name in a package | error |
| TYPL-010 | package name is reserved for a package the compiler provides | error |
| TYPL-011 | type reference names no visible declaration (§3.2, §3.3) | error |
16.2 Scalars and Constants (TYPL-1xx)
| Code | Rule | Severity |
|---|---|---|
| TYPL-101 | integer without a range constraint | warning; error if active profile requires |
| TYPL-102 | float without both a range and a step | warning; error if active profile requires |
| TYPL-103 | string/bytes without explicit bounds — default [0..256] applied | warning; error if active profile requires |
| TYPL-104 | range min > max | error |
| TYPL-105 | step type mismatch, non-positive, or larger than the range | error |
| TYPL-106 | invalid regex syntax in match or a regex const — a valid pattern the Rust regex crate refuses is TYPL-220 | error |
| TYPL-107 | regex contradicts declared character bound | warning |
| TYPL-108 | const value violates its declared type constraints | error |
| TYPL-109 | init = value incompatible with the type or field constraints | error |
| TYPL-110 | unknown or malformed UCUM unit expression | error |
| TYPL-111 | integer range bound outside the int64 domain ([-2⁶³..2⁶³−1]) | error |
| TYPL-112 | a concrete wire-width name (uint8 … float64) written in source — width is inferred, not written (§5.6) | error |
| TYPL-115 | type has no derivable init value and no declared = value (§5.8) | info — escalated by consumers that require an init |
| TYPL-116 | string/bytes bound above the advised transfer limit — a string bound above 4,032 characters or a bytes bound above 16,128 bytes (§4.4, §4.5), reported on the bound; the payload ceiling is 16,256 bytes, which the codecs do not yet enforce (driftsys/ridl#996) | warning |
16.3 Composites (TYPL-2xx)
| Code | Rule | Severity |
|---|---|---|
| TYPL-201 | array without explicit bounds | error |
| TYPL-202 | map without explicit bounds | error |
| TYPL-203 | enum values not unique or not explicitly assigned | error |
| TYPL-204 | union arm with a primitive type | error |
| TYPL-205 | same tuple shape used in multiple places | warning |
| TYPL-206 | recursive composite reference, direct or transitive | error |
| TYPL-207 | enumset bit positions not unique | error |
| TYPL-208 | string/bytes used directly as a field type | error |
| TYPL-209 | map key is not a named string type or a primitive | error |
| TYPL-210 | field, arm, or enum value re-declared under a reserved entry (a name or a value) | error |
| TYPL-211 | duplicate reserved entry — a dangling entry (a name or value never previously used) falls under this rule too, but its check needs the previous IR snapshot and is not built | warning |
| TYPL-212 | error modifier on a declaration other than enum, struct, union | error |
| TYPL-213 | union mixes error and non-error arms without the result-union shape (exactly one success + one error arm) | error |
| TYPL-214 | error union contains a non-error-typed arm | error |
| TYPL-215 | field name declared twice in one struct or one tuple | error |
| TYPL-216 | enum value name declared twice in one enum | error |
| TYPL-217 | union arm name declared twice in one union | error |
| TYPL-218 | enumset bit name declared twice in one enumset | error |
| TYPL-219 | enumset bit position is not a number with an integer value | error |
| TYPL-220 | regex pattern the Rust regex crate cannot compile (§2.7) — numbered in this range, but not a composite | error |
| TYPL-222 | one field name used with different units | info |
| TYPL-224 | duplicate declaration shape | info |
16.4 Profile Boundary (TYPL-3xx)
Emitted when a .typl file (or a package declared profile = "typl" in
ridl.toml) contains constructs of a higher layer. TYPL-301 is also emitted in
a .ridl file, as its row says:
| Code | Rule | Severity |
|---|---|---|
| TYPL-301 | stream type <T> outside interaction position — in a typl context, and in a ridl file anywhere but a parameter or a query return, such as a struct field, a collection, a type definition or a union arm, except a position diagnosed separately: a signal or event payload is RIDL-201, a fixed payload is FORM-102 and a command return is RIDL-104 (ridl §12.3) | error |
| TYPL-302 | timing annotation or duration literal in a typl context | error |
| TYPL-303 | require/ensure attribute in a typl context | error |
| TYPL-304 | interaction declaration in a typl context — one of the ridl interaction words at declaration start; an rmdl or rsdl word there is FORM-102 | error |
16.5 Documentation (TYPL-4xx)
| Code | Rule | Severity |
|---|---|---|
| TYPL-401 | doc link or @see target that does not resolve — the name is unknown, is internal in another package, or names no member of the declaration | warning |
| TYPL-402 | @labels identifier not recognised by active profile | info |
| TYPL-403 | @labels combination invalid per active profile | error |
| TYPL-404 | blank line between a doc comment and its carrier — a declaration or a member | warning |
| TYPL-405 | @deprecated doc tag without a reason string | warning |
| TYPL-406 | item without a doc comment — a declaration that is not internal, a member of one, or an rsdl declaration; a doc made only of tags is missing | warning |
| TYPL-407 | doc comment in a position that is not a carrier — before package, an import, a return type or an attribute block, or at the end of a file or a body | warning |
| TYPL-408 | doc tag other than @see, @since, @deprecated and @labels | warning |
| TYPL-409 | @see or @since with a missing or malformed value | warning |
| TYPL-410 | doc comment written as /** */ — allow by default | warning |
17. Open Questions
Every question this section has carried is disposed of. A question is either resolved — its rule has moved into the section of this reference that governs it, and it no longer appears below — or deferred to a named version, in which case it appears below with that version.
The numbers are stable. Other documents cite these questions by number, so a resolved question's number is retired rather than reused, and the ledger records where its rule now lives. A number with no entry after the ledger is resolved, except §17.10, which is resolved and keeps its entry because other documents cite that section.
17.0 Disposition Ledger
| # | Question | Disposition |
|---|---|---|
| 17.1 | String-backed enums | deferred to v0.2 |
| 17.2 | Exclusive range bounds | resolved — not supported and not planned; the rule and its reason are §5.5 |
| 17.3 | uniqueItems for arrays | deferred to v0.2, as an array constraint; the set container is rejected |
| 17.4 | Recursion policy | resolved — the hard error stands; the rule and its reason are §7.3 |
| 17.5 | Unit conversion semantics | deferred to the rmdl era |
| 17.6 | Scientific notation in float literals | deferred to v0.2 |
| 17.7 | Value-rule expressions | deferred to the family expr core |
| 17.8 | Standards-fixed invalid sentinel values | deferred to v0.2; the rest of the original question is resolved |
| 17.9 | Byte order | resolved for typl — byte order is not a type property, §4.6; the deployment surface is rsdl's, at rsdl v0.2 |
| 17.10 | Canonical (deterministic) encoding | resolved — not a type-layer property, §12.2; the frame specification §4 and each projection record own the rest |
| 17.11 | Explicit wire-width floor | split: the layout consequence is resolved in §5.6; the wire clause is deferred to v0.2 |
| 17.12 | Integer-backed unit types | deferred to v0.2 |
| 17.13 | What a string is made of, and how it is encoded | resolved — the rule is §4 and §4.4 |
| 17.14 | Value-aware enum and enum-set reorder comparison | resolved — the rule is §8 and §9 |
| 17.15 | A reorder in the same edit as an addition or a removal | deferred to v0.2, sequenced behind driftsys/ridl#302 |
One question outside this section was disposed of in the same pass: whether a
name-based reserved is legal in an enum body, and what it guarantees
(driftsys/ridl#245). It is legal and it retires the name; the rule is §7.4.
17.1 String-Backed Enums — deferred to v0.2
JSON Schema, TypeScript, and OpenAPI make string enums the common case; typl
v0.1 supports only integer-backed enums (wire efficiency, DBC/CAN mapping).
Candidate design: enum Region : string { EU = "eu", US = "us" } — integer wire
form with a declared string rendering.
Deferred to v0.2, with the candidate design above as the starting point. The
wire form stays integer, so nothing in §4.2's width table, in ridl-diff, or in
any encoding changes; what is added is a rendering per value, carried to the
targets that have somewhere to put it. It is the one gap in Appendix G's matrix
with named external consumers.
17.3 uniqueItems for Arrays — deferred to v0.2
Not expressible today. The set collection once suggested here
({T; min..max}) is rejected: no target this family emits has a set type —
proto3, FlatBuffers and podbuf each carry a vector plus a rule — so a set
container would add a surface with no projection, and every backend would
flatten it back into an array.
Uniqueness is instead a constraint on an array, alongside the range, step,
length bound and match: enforced by the generated validator and carried as
documentation on the wire targets. Deferred to v0.2, where the generated
validator is the enforcement point.
17.5 Unit Conversion Semantics — deferred to the rmdl era
UCUM units are currently labels with dimensional identity. Whether typl-core
defines convertibility (km/h ↔ m/s), or leaves conversion to codegen, is
open.
Deferred to the rmdl era — the question is claimed by rmdl §12 open question
8, where cross-unit arithmetic (Speed * Duration : Length) makes it concrete,
and RMDL-106's explicit-conversion rule is the stopgap until then. typl's own
commitment is unchanged and is not waiting on that answer: a unit is a label
with dimensional identity, and nominal identity (§5.7) forbids implicit
conversion in every profile.
17.6 Scientific Notation in Float Literals — deferred to v0.2
A v0.1 float literal must contain a decimal point and admits no exponent (§2.5). The restriction is at the family lexer, so it binds every profile, and rmdl is where it is felt: gains, tolerances and small time constants are where exponent notation earns its place.
Deferred to v0.2, as a family lexer change. Precondition: the exponent
form must not collide with the number-plus-unit-atom merge the same lexer
performs. That merge is restricted to the UCUM time atoms (us, ms, s,
min, h), none of which begins with e, but UCUM has atoms that do (eV).
The check before the pattern changes is that no literal position in the v0.2
grammar admits a bare unit atom after a number.
17.7 Value-Rule Expressions — deferred to the family expr core
typl v0.1 constraints are deliberately closed-form: literal/const ranges,
step, length bounds, and match patterns. Everything requiring a general
expression language is deferred to the family expr core and will arrive with
require/ensure: (a) arithmetic in constraint bounds
([0.0..MAX_SPEED * 0.5] — v0.1 bounds accept only literals and plain constant
references); (b) predicate constraints on a single value (value % 2 == 0);
(c) regex matching as a predicate operator — declarative match on string
types exists today; when expr lands, the same match keyword serves infix
in predicates (require vin match VIN_PATTERN): one keyword, one concept
(pattern conformance), two positions. (A ~ sigil was considered and rejected —
the family is deliberately sigil-poor, and match reads as English to
non-programmer audiences); (d) cross-field invariants on structs
(min <= max) — the likely surface is a struct-level invariant block
completing the Eiffel triad beside ridl's require/ensure.
Rationale for the deferral: closed-form constraints keep every typl type
decidable for width inference and mechanically derivable into property-test
generators; general expressions in constraint position need fencing rules
(const-evaluable subset only) that belong to the expr-core specification, not
here. TYPL-303 already rejects require/ensure in a .typl context, which is
the boundary this deferral protects.
The target is anchored. The expr core is the
expr-core Specification (family overview §2;
concept note open question 6), whose guaranteed subset ships with the interface
layer and whose function layer is not built. Sub-items (a) to (c) are
constraint-position expressions and are the expr core's entirely. Sub-item (d)'s
expression language is likewise the expr core's, but its surface — a
struct-level invariant block — is a typl declaration shape and lands in §7
when the expr core does.
17.8 Standards-Fixed Invalid Sentinel Values — deferred to v0.2
Most of the original question is resolved and lives elsewhere. The init half is
§5.8 (a declared bare = value, or derived). The invalid half is handled at the
interaction layer: ridl §4.5 propagates invalidity as channel state, with the
SNA sentinel as its CAN/AUTOSAR wire realisation. Where the codegen is free to
choose the value, ADR-0013
decision 7 settles it and no typl syntax is needed — ? declares the absence,
the backend picks a value the declared range does not use and does not surface
it in the generated API, and where the range has no room the backend fails with
a diagnostic.
What remains open is the case where the backend may not choose, because a published standard has fixed the value. Two sub-cases are known and are the inputs to the decision:
- a single fixed value, as in a brownfield DBC signal whose SNA encoding is already deployed;
- several fixed values with distinct meanings, as in the ETSI ITS vehicle-length data type, which separates "longer than this field can express" from "not measured". One reserved value cannot carry both, and a consumer can act on the first while it can only abstain on the second. This is a contract fact rather than a backend fact, which is why it is not pushed to a backend annotation.
Deferred to v0.2. It is answerable when a conformance target exists: modelling one published message end to end — a Cooperative Awareness Message is the candidate — is the way to test a proposal before it becomes syntax.
17.9 Byte Order — resolved for typl; the deployment surface is rsdl v0.2
CAN/DBC signals carry per-signal byte order (Intel/Motorola) and SOME/IP defaults big-endian, and typl was silent.
Resolved for typl: byte order is not a type property. The rule is §4.6.
Deferred, rsdl v0.2: the surface by which a deployment states the byte order of a transport that carries a choice belongs to rsdl, and lands with the rsdl rewrite at v0.2 (family overview decision ledger #36). A typl rule cannot be written against rsdl surface that does not exist yet, which is why this half is named rather than written here. The same deployment surface is what §7.4's mixed-version decoder rule needs on a transport that cannot carry unknown trailing fields.
17.10 Canonical (Deterministic) Encoding — resolved; the frame specification and each projection record own the rest of the rule
This question is resolved, and its entry stays below because the frame specification and ADR-0014 cite this section.
Signed payloads, content-hashing of data, and the replay/test plane all want a bit-reproducible encoding (protobuf's deterministic-marshal caveats are the cautionary tale; map ordering is the classic leak).
One correction to this question as it was first written. It guessed the answer was "likely an IR-spec concern". The IR's own canonical form is settled — ADR-0014 decision 9 makes canonical protobuf JSON the canonical encoding, with binary and prototext derived (that clause named binary until decision 9's 2026-09-22 amendment; the policy is the IR specification) — and that is a different artifact. This question is about the payload encoding a contract's values travel in, which ADR-0014 does not touch.
Resolved for typl: a canonical payload encoding is not a type-layer property, and the one ordering typl controls is a map's key order. The rule is §12.2.
Placed: a reproducible byte sequence for a transmitted unit is a property of an encoding and its frame, so the rule belongs to the frame specification and to each projection record, not to this reference. Written 2026-09-22: that document is the frame specification, whose §4 makes the frame header canonical and leaves the payload's part to each projection record.
17.11 Explicit Wire-Width Floor — a deferred wire clause, v0.2
v0.1 has no surface syntax for wire width (§5.6): a range is the width,
inferred once by the compiler, and the ten concrete width names
(uint8 … float64) are not writable anywhere. This leaves one hazard unhanded
at the source: widening a range across a width boundary silently flips the wire
type (uint8 → uint16), a wire ABI break that today only the ridl-diff CI
gate catches.
A future wire clause would let an author pin an explicit width floor
above the boundary — type Counter : integer [0..250] wire uint16 — reserving
evolution headroom so a later widening does not flip the transport encoding.
Design constraints already settled, for whenever it lands: the named width
must be at least the inferred width and of the same signedness class
(narrower or sign-incompatible is an error); it never changes the language-layer
type (int64/float64) or the value constraints — the range still validates,
only the transport encoding widens; and it is a floor, never a parallel type
system (ranges remain the semantic truth). Whether the floor should also be
declarable per-field, and how it interacts with the scaled-integer wire form of
quantized floats, are the two rules a real contract should shape.
Deferred to v0.2. The ridl-diff gate already prevents the silent break in
CI, and the clause is pure evolution ergonomics. The forcing case that reopens
it is ADR-0013 decision 6's: a
deployment that needs to widen a range on a FlatBuffers-bound contract without a
coordinated flag day.
One catalogue entry moves with the clause. TYPL-112 makes a concrete
wire-width name written in source an error (§16.2). A wire clause makes that
rule conditional — the name becomes writable in exactly one position — so the
two are reconciled together when the clause lands.
The half that is not deferred is the consequence a fixed inline layout
carries, which is now stated in §5.6: a resolved width change is a layout break
as well as a wire break, and ridl-diff classifies it breaking on both counts.
No encoding has to wait for the clause to know that.
17.12 Integer-Backed Unit Types — deferred to v0.2
A unit type's backing would be the literal type of its range: integer literals
give an integer backing, float literals give a float backing, and a unit type
with no range keeps today's float default, so a range-less declaration is
unchanged. Latency : ms [0..5000] is then an integer type carrying a unit, and
Timestamp : us and Duration : ms become declarable in typl as the integers a
runtime library already makes them.
The grammar already distinguishes int_lit from float_lit, and TYPL-105
already rejects a step whose literal type differs from the range's, so a
backing cannot flip unnoticed by editing one number wherever a step is
declared; where none is, ridl-diff is what sees the change, because the
backing reaches the IR. The domain type, the width and every encoding follow the
rules plain integer already has, and the unit algebra of §17.5 is untouched.
An explicit backing keyword was rejected: TYPL-105 already makes the flip
explicit, and this is the rule every language uses for 1 against 1.0.
Deferred to v0.2, and it is not a v0.1 change. §5.1 fixes the backing of a
unit type to float, so the new rule changes the backing, the inferred width
and the IR of every unit type already declared with integer literals, and
ridl-diff classifies each as a breaking width change. A point release that
turns every such published contract into a breaking diff is not a point release.
The corpus check runs before the change lands, not after: every unit type
written with integer literals today is either a wanted integer or a missing
.0. Recorded in
docs/wip/2026-09-12-release-scope-and-plugin-system-design.md
§3.10.
17.15 A Reorder in the Same Edit as an Addition or a Removal — deferred to v0.2
ridl-diff's walk reports member_reordered only when both bodies hold the
same member names; with a member added or removed, the report carries the
addition or removal alone and does not name the survivors that moved.
Until it is taken, the report is incomplete rather than wrong. A member
inserted in the middle of a struct or union body, with every later member left
in its written order, shifts the ordinal of every member after it and is
reported as one decl_added, which the classifier judges breaking under §7.4's
append-only rule — the classifier reads the body to judge the addition's
direction, rather than relying on the reorder walk. An enum value or enumset
bit inserted mid-body shifts no number and is judged by its value (§8, §9). A
member removed is one decl_removed, breaking, whether or not a tombstone
replaced it.
Deferred to v0.2, sequenced behind driftsys/ridl#302. Widening the case
reaches the name-keyed matching logic that crates/ridl-diff/src/walk.rs's
carried-debt comment guards, and #302 records why that logic cannot move alone:
on FlatBuffers a union arm's discriminant is implicit in declaration order, so a
tombstoned retirement that the widened logic would call compatible shifts every
later arm. #302 also records why the backend cannot move alone — explicit union
member values pin the discriminants, but flatc 25.12.19 accepts the form while
planus 1.3.0 rejects it, so taking it would put the union path outside the
validity oracle. The blocking constraint is that oracle gap, and no edit to this
reference removes it.
Appendix A — Standard Library
ridl.std is a pure typl package — the proof that typl stands alone. It is
implicitly imported in every file of every profile.
Inclusion criterion. ridl.std holds only definitions whose form is fixed
by a cross-industry standard and whose meaning does not depend on the domain the
system models. A definition that is meaningful in one industry and inert in the
others belongs in a domain package. The criterion follows from the implicit
import: every file of every profile carries whatever this package holds, so a
domain type here is a cost paid by every user who does not work in that domain,
and admitting one leaves no principled place to stop.
Applying that criterion removed the ISO 3779 Vin type and its VIN_PATTERN
constant, which this appendix had inherited unexamined from the RIDL Language
Reference v0.1. A vehicle program declares both in two lines of its own package,
and no other member of ridl.std is bound to a single industry.
Contents (normative):
package ridl.std
// ---------- Regex Constants ----------
/// Pattern of a UUID: five groups of 8, 4, 4, 4 and 12 lowercase hexadecimal digits
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
/// Pattern of a ULID: 26 Crockford base32 characters, the first one 0 to 7
const ULID_PATTERN = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/
/// Pattern of a URI: a scheme, `://`, then at least one character
const URI_PATTERN = /^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\/.+$/
/// Pattern of a URL: `http://` or `https://`, then at least one character
const URL_PATTERN = /^https?:\/\/.+$/
/// Pattern of an email address: a local part, `@`, and a domain that contains a dot
const EMAIL_PATTERN = /^[^@]+@[^@]+\.[^@]+$/
/// Pattern of an IPv4 address: four groups of 1 to 3 digits, separated by dots
const IPV4_PATTERN = /^(\d{1,3}\.){3}\d{1,3}$/
/// Pattern of an IPv6 address: lowercase hexadecimal digits and colons
const IPV6_PATTERN = /^[0-9a-f:]+$/
/// Pattern of a string of printable ASCII characters
const ASCII_PATTERN = /^[\x20-\x7E]+$/
// ---------- Identity Types ----------
/// RFC 4122 UUID — 36 characters including hyphens
type Uuid : string [36 match UUID_PATTERN]
/// ULID — 26 characters, lexicographically sortable
type Ulid : string [26 match ULID_PATTERN]
// ---------- Network Types ----------
/// RFC 3986 URI
type Uri : string [1..2048 match URI_PATTERN]
/// HTTP/HTTPS URL
type Url : string [1..2048 match URL_PATTERN]
/// Email address — RFC 5321
type Email : string [1..254 match EMAIL_PATTERN]
/// IPv4 address — dotted decimal notation
type IpV4 : string [7..15 match IPV4_PATTERN]
/// IPv6 address
type IpV6 : string [2..39 match IPV6_PATTERN]
// ---------- General String Types ----------
/// Short human-readable label — ASCII printable, max 64 characters
type Label : string [1..64 match ASCII_PATTERN]
/// Human-readable name — max 128 characters
type Name : string [1..128 match ASCII_PATTERN]
/// Short message or description — max 256 characters
type Message : string [1..256 match ASCII_PATTERN]
// ---------- Time Types ----------
/// Absolute point in time — platform time domain (ridl §3.1):
/// int64 microseconds since the PTP epoch, 1970-01-01 00:00:00 TAI.
/// Continuous and leap-second-free; convert to civil datetime only at
/// presentation edges.
type Timestamp : integer [0..9223372036854775807]
/// Elapsed time interval in milliseconds
type Duration : ms [0.0..9223372036854775807]
/// Calendar date — ISO 8601 YYYY-MM-DD
type Date : string [10 match /^\d{4}-\d{2}-\d{2}$/]
/// Time of day — ISO 8601 HH:MM:SS
type TimeOfDay : string [8..15 match /^\d{2}:\d{2}:\d{2}/]
// ---------- Version ----------
/// Semantic version — SemVer 2.0.0
type Version : string [5..32 match /^\d+\.\d+\.\d+/]
// ---------- Locale ----------
/// ISO 3166-1 alpha-2 country code
type CountryCode : string [2 match /^[A-Z]{2}$/]
/// ISO 639-1 language code with optional region — e.g. "en", "en-US"
type LanguageCode : string [2..5 match /^[a-z]{2}(-[A-Z]{2})?$/]
// ---------- Crypto / Security ----------
/// SHA-256 hash — 32 bytes
type Sha256Hash : bytes [32]
/// SHA-512 hash — 64 bytes
type Sha512Hash : bytes [64]
/// Generic cryptographic signature — 64 bytes
type Signature : bytes [64]
/// X.509 DER certificate
type Certificate : bytes [1..4096]
Appendix B — Full Example
A complete pure-typl package — veh/common/, profile-pure, generating types
across every backend with no interface content:
package veh.common
// ---------- Units and scalars ----------
/// Vehicle speed over ground
type Speed : km/h [0.0..MAX_SPEED step 0.5]
/// Coolant / ambient temperature
type Temperature : Cel [-40.0..125.0 step 0.1]
/// Engine crankshaft speed
type RPM : /min [0.0..8000.0 step 10.0]
/// Normalised ratio
type Ratio : % [0.0..100.0 step 0.1]
type Counter : integer [0..65535]
type Gain : float [0.0..1.0 step 0.01]
// ---------- Constants ----------
const MAX_SPEED : Speed = 250.0
const SPEED_LIMIT_EU : Speed = 130.0
const MAX_GEAR : integer = 6
const IDLE_RPM : RPM = 800.0
// ---------- Enums ----------
enum GearPosition {
PARK = 0
DRIVE = 1
REVERSE = 2
NEUTRAL = 3
}
enum Warning {
LOW_FUEL = 0
CHECK_ENGINE = 1
DOOR_OPEN = 2
SEATBELT = 3
}
enumset WarningFlags : Warning
// ---------- Composites ----------
struct SpeedLimitPayload {
limit : Speed
actual : Speed
}
struct DriverProfile {
name : Name // ridl.std
speed : Speed
override : Speed? // optional
gears : integer [0..MAX_GEAR] = 6
}
struct SensorBounds {
range : (min: Speed, max: Speed)
readings : [Speed; 8]
labels : [Label; 1..16]
meta : [Label : Name; 0..32]
}
union SensorResult { // result union — §10.2: one success arm, one error arm
ok : SensorReading
err : SensorFault
}
struct SensorReading {
value : Speed
timestamp : Timestamp // ridl.std
}
error struct SensorFault { // failure vocabulary — §10.1
code : Counter
message : Message // ridl.std
}
// package-private helper — not visible to importers, omitted from
// cross-package codegen artifacts
internal struct RawWheelFrame {
ticks : Counter
frame : bytes [8]
}
Appendix C — Standards References
| Standard | Used for |
|---|---|
| Unicode 15.0 | source encoding |
| RFC 3629 | UTF-8 |
| RFC 8259 | string escapes |
| ECMA-262 | regex literal syntax, restricted to the patterns the Rust regex crate also compiles (§2.7) |
| CommonMark | doc comment markdown |
| IEEE 754-2019 | floating point |
| ISO/IEC 9899:2018 (C17) | integer/float literal conventions |
| UCUM | physical unit expressions |
| ISO 8601 / RFC 3339 | time types |
| RFC 3986, RFC 4122 | Uri, Uuid |
| SemVer 2.0.0 | Version |
| ISO 3779, ISO 3166-1, ISO 639-1 | Vin, CountryCode, LanguageCode |
| JSON Schema 2020-12 | coverage benchmark (Appendix G) |
Appendix D — Codegen Targets
typl owns the type layer of every backend; interaction/behaviour codegen is specified by the higher profiles. Width mapping is resolved once by the compiler and passed to all backends (§4.2, §4.3, §9.3).
Language layer — always widest. The columns are the width rule per language, not a list of planned backends: Rust and TypeScript are built, Kotlin is an out-of-tree plugin, and C++ has no plan (ADR-0013 decision 1, as amended 2026-09-12).
| Canonical | Rust | Kotlin | C++ | TypeScript |
|---|---|---|---|---|
| any integer | i64 | Long | int64_t | bigint/number* |
| any float | f64 | Double | double | number |
| enumset | i64 | Long | int64_t | bigint |
*TypeScript uses number when the range fits in 2^53, bigint otherwise.
Transport layer — narrowest that safely fits:
| Canonical | proto3 | FlatBuffers | SOME/IP | CAN/DBC | AIDL |
|---|---|---|---|---|---|
uint8 | uint32* | uint8 | UINT8 | 8 bits | int |
uint16 | uint32* | uint16 | UINT16 | 16 bits | int |
uint32 | uint32 | uint32 | UINT32 | 32 bits | int |
int32 | sint32 | int32 | SINT32 | 32 bits signed | int |
int64 | sint64 | int64 | SINT64 | 64 bits signed | long |
float32 | float | float32 | FLOAT | 32 bits | float |
float64 | double | float64 | FLOAT64 | 64 bits | double |
*proto3 has no uint8/uint16 — varint encoding keeps small values small on
the wire.
Per-Target Compatibility Notes
proto3. Unsigned 8/16-bit map to uint32 varint — no wire penalty for small
values. Ranges containing negatives map to sint32/sint64 (ZigZag encoding);
plain int32 varint would cost 10 bytes for every negative value. Field numbers
are the typl ordinals (§7.4); reserved tombstones emit proto reserved
statements. Range widening never breaks proto wire (varint absorbs it) but
remains a contract-breaking change per ridl-diff.
FlatBuffers. Full uint8..uint64 palette — the cleanest width mapping.
Field ids are the typl ordinals emitted as (id: N) attributes. FlatBuffers is
the most width-brittle target: any resolved-width change is a hard wire break —
the primary motivation for the wire floor deferred to v0.2 (§17.11); until it
lands the ridl-diff gate is the sole guard, and it is also a layout break
rather than only a wire break (§5.6). A struct whose fields are all fixed-width
and non-optional may be emitted as a FlatBuffers struct (inline, zero
indirection) instead of a table; the IR carries a fixed_layout flag for
this. The built FlatBuffers projection does not take that allowance
(ADR-0019 decision 3):
a FlatBuffers struct has a fixed inline layout and no vtable, so after a
compatible field append a reader with the newer schema reads past what an older
writer wrote and returns the appended field fabricated from padding, where a
table reports it absent. Every struct is therefore emitted as a table. The
fixed_layout flag stays in the IR for a target where a fixed layout is safe.
AUTOSAR Classic (ARXML). The best structural fit: range → DataConstr,
quantized float → LINEAR CompuMethod (factor = step, offset = min) — the
scaled-integer wire form (§4.3) is native here. Two gaps become profile
rules: Classic sender-receiver records have no optional fields — a struct
containing ? bound to a Classic/CAN transport is a codegen error, never
silently default-filled; and Classic has no native tagged union — a union on a
Classic-bound payload is a codegen error in v0.1 (a generated tag + padded-body
convention is a possible later opt-in). A normative UCUM → AUTOSAR unit /
physical-dimension mapping table is required and does not yet exist — open
item.
CAN / DBC. Signals are scaled integers by construction — factor, offset, min, max, and bit length all derive from range + step. Strings, bytes, maps, and anything unbounded cannot bind to CAN; bounded arrays flatten to indexed signals. Optionality does not exist on CAN — same profile rule as Classic.
SOME/IP. Positional serialization — ordinals matter only as declaration
order, which §7.4 freezes anyway. Width flips shift the byte layout: breaking,
caught by ridl-diff. Quantized-float-as-scaled-integer is available as a
per-deployment choice (an rsdl concern, not a type property).
Schema targets. Because typl is a schema language, schema languages are
themselves backends: JSON Schema 2020-12 (each named type → $defs entry;
ranges → minimum/maximum, step → multipleOf, match → pattern, structs →
closed objects with additionalProperties: false), OpenAPI/AsyncAPI component
schemas, ARXML (<AR-PACKAGE> chain, CompuMethods from unit types),
DBC (scaling/offset derived from range and step). The JSON Schema emitter is
the round-trip check on Appendix G.
Appendix E — Formal Grammar (EBNF)
The typl profile grammar — the restriction of the family grammar accepted in
.typl files. Family constructs not derivable here (interactions, timing,
streams, behaviour, architecture) are rejected with TYPL-3xx.
(* Top-level *)
file = package { import } { definition } ;
package = "package" qualified_id ;
import = "import" qualified_id [ "as" id ] ;
(* no wildcards, no relative paths, no re-exports — ADR-0002 *)
definition = [ "internal" ] [ "error" ] ( type_def | const_def | struct_def
| enum_def | enumset_def | union_def ) ;
(* "error" valid on struct_def, enum_def, union_def only — §10.1 *)
(* ---------- Type ---------- *)
type_def = doc_comment? "type" CamelCase_id ":" type_backing constraint? init_value? ;
init_value = "=" literal ; (* declared init value, bare — §5.8 *)
type_backing = "integer" | "float" | "string" | "bytes" | "boolean" | ucum_unit ;
ucum_unit = (* UCUM expression: km/h, Cel, N.m, /min, %, bar, V, A ... *) ;
constraint = "[" constraint_spec "]" ;
constraint_spec
= scalar ".." scalar ( "step" scalar )?
| scalar ".."
| ".." scalar
| int_lit
| int_lit ".." int_lit
| int_lit "match" pattern
| int_lit ".." int_lit "match" pattern
| "match" pattern
;
pattern = regex_lit | SCREAMING_SNAKE_ID ;
scalar = int_lit | float_lit | SCREAMING_SNAKE_ID ; (* constants usable as bounds *)
(* ---------- Constants ---------- *)
const_def = doc_comment? "const" SCREAMING_SNAKE_ID ( ":" type_ref )? "="
( literal | regex_lit ) ;
(* ---------- Struct ---------- *)
struct_def = doc_comment? "struct" CamelCase_id "{" { ( field | reserved ) sep? } "}" ;
field = doc_comment? camelCase_id ":" field_type init_value? ;
reserved = "reserved" ( camelCase_id | int_lit ) ; (* tombstone — §7.4 *)
field_type = "boolean"
| ( "integer" | "float" ) constraint? (* inline constrained primitive;
string/bytes need a named type — §15.3 *)
| type_ref
| tuple_type
| field_type "?"
| "[" field_type ";" bound "]" (* array *)
| "[" key_type ":" field_type ";" bound "]" (* map *)
;
bound = int_lit | int_lit ".." int_lit ;
key_type = type_ref | "string" | "integer" ;
tuple_type = "(" named_field { "," named_field } ")" ;
named_field = camelCase_id ":" field_type ;
type_ref = qualified_id ; (* Type or pkg.Type *)
(* ---------- Enum ---------- *)
enum_def = doc_comment? "enum" CamelCase_id "{" { ( enum_value | reserved ) sep? } "}" ;
enum_value = doc_comment? SCREAMING_SNAKE_ID "=" int_lit ;
(* ---------- EnumSet ---------- *)
enumset_def = doc_comment? "enumset" CamelCase_id "{" { enumset_bit sep? } "}"
| doc_comment? "enumset" CamelCase_id ":" type_ref
;
enumset_bit = doc_comment? SCREAMING_SNAKE_ID "=" int_lit ;
(* ---------- Union ---------- *)
union_def = doc_comment? "union" CamelCase_id "{" { ( union_arm | reserved ) sep? } "}" ;
union_arm = doc_comment? camelCase_id ":" type_ref ;
(* ---------- Doc Comments ---------- *)
doc_comment = "/**" { doc_tag | markdown_text } "*/"
| { "///" markdown_text newline } ;
doc_tag = "@see" qualified_id
| "@since" version
| "@labels" label { "," label }
| "@deprecated" string_lit ;
version = int_lit "." int_lit [ "." int_lit ] ;
label = SCREAMING_SNAKE_ID [ "(" SCREAMING_SNAKE_ID ")" ] ;
(* ---------- Separators, Identifiers, Literals ---------- *)
sep = "," | newline ;
qualified_id = id { "." id } ;
id = CamelCase_id | camelCase_id ;
CamelCase_id = [A-Z][a-zA-Z0-9]* ;
camelCase_id = [a-z][a-zA-Z0-9]* ;
SCREAMING_SNAKE_ID = [A-Z][A-Z0-9_]* ;
int_lit = "-"? [0-9]+ ;
float_lit = "-"? [0-9]+ "." [0-9]+ ;
string_lit = '"' { utf8_char } '"' ;
regex_lit = "/" { regex_char } "/" ;
bool_lit = "true" | "false" ;
literal = int_lit | float_lit | string_lit | bool_lit ;
Appendix F — Prior Work: markspec typl
The first typl was drafted as a MarkSpec extension — a Type Specification DSL
binding $Name identifiers inside requirements entries to a kind and a shape
(markspec typl spec).
That work validated the core idea (a small, closed, range-first shape grammar
with a coded diagnostic catalogue) in a live toolchain (parser, LSP
hover/completion, JSON compile output with a corpus-level typeRegistry).
MarkSpec also carries uxil, the precursor of uxdl and rxdl — relevant to the
rxdl vocabulary workshop, not to this document.
This specification is the successor, not a superset. The mapping:
| markspec typl | family typl (this spec) | Disposition |
|---|---|---|
$Name : kind shape bindings with sigil | named declarations in packages (type, const, struct, …) | sigil dropped — names live in the module system, not in prose |
kind vocabulary: signal, event, command, stream | not typl — ridl's interaction keywords over the interact core | moved up a layer |
kind state | not typl — rmdl territory | moved up a layer |
kind config | ridl fixed (provisioned constant) | moved up a layer — fixed confirmed over config: it names the consumer contract (immutable, cacheable), not the provider workflow; see concept-note naming ledger |
kind const | const | kept |
kind value (default) | type / struct field | kept, made explicit |
kind document | struct | folded in |
kind namespace + $. relative refs + published tier | package / import (ADR-0002 ns core) | superseded — packages give declared-once, absolute citation, and ownership corpus-wide |
int[0..300], float[0..1.0] postfix ranges | integer [0..300], float [0.0..1.0 step s] | kept in spirit; family name : Type [constraint] syntax; step added |
string[3..6], bytes[32] length shapes | string [3..6], bytes [32] | kept |
pattern /re/ shape | match /re/ inside the string constraint | merged into the constraint; named regex constants added |
'low' | 'mid' | 'high' literal-union enums | enum (integer-backed) | not carried over — string enums are planned for v0.2 (§17.1) |
{ field: shape } records | struct | kept; structs are named and closed |
element[](min..max) arrays | [T; min..max] | kept; bounds made mandatory |
shape? optional | field : T? | kept |
typedef type Name = shape | type Name : backing [constraint] (scalars) / named composites | split by composite kind |
| entry-local scope tier | not applicable — no prose entries; packages are the only scope | superseded |
| TYPL-001…012 diagnostic catalogue | TYPL-0xx…4xx coded catalogue (§16) | practice adopted, codes renumbered by area |
| four Markdown surfaces (fence, bullet, inline, table) | not adopted — typl lives in .typl files | MarkSpec remains a consumer: a requirements corpus can cite typl symbols, and a future markspec bridge can validate ```ridl fences against a workspace |
What survives unchanged in spirit: closed vocabularies over extensible ones, ranges as first-class shape (not an afterthought annotation), a small shape grammar a reader can hold in their head, and machine-checked diagnostics with stable codes.
Appendix G — Coverage Analysis: JSON Schema and Other Schema Languages
G.1 Method
typl claims to be a schema language, so the benchmark is: can typl express what a contract author reaches for in JSON Schema 2020-12 — the latest published dialect (the in-progress "stable" rewrite does not change the validation vocabulary materially)? The matrix covers the entire 2020-12 validation and annotation vocabulary. ✓ covered, ≈ covered differently (usually stricter), ✗ not expressible (deliberate or open).
G.2 JSON Schema 2020-12 coverage matrix
| JSON Schema keyword | typl equivalent | Status |
|---|---|---|
type: boolean/integer/number/string | boolean, integer, float, string | ✓ |
type: null / nullable | ? optionality (absence; typl has no null value) | ≈ stricter |
type: array | [T; N], [T; min..max] | ✓ bounds mandatory |
type: object (free-form) | struct (closed) or [K:V; min..max] map | ≈ stricter |
minimum / maximum | [min..max] | ✓ |
exclusiveMinimum / exclusiveMaximum | — | ✗ deliberate — not supported and not planned (§5.5) |
multipleOf | step (quantization anchored at range min) | ✓ |
minLength / maxLength | string [min..max] | ✓ |
pattern | match /re/ or named regex const | ✓ + reusable named patterns |
format (uuid, email, uri, ipv4, date, …) | ridl.std named types (Uuid, Email, Uri, IpV4, Date, …) | ✓ and normative (JSON Schema format is annotation-only by default) |
contentEncoding / contentMediaType | bytes + doc comment | ≈ partial |
minItems / maxItems | [T; min..max] | ✓ mandatory |
uniqueItems | — | ✗ v0.2 (§17.3) — an array constraint, not a set container |
prefixItems (positional tuple) | named tuples (a: T, b: U) | ≈ named, not positional — deliberate |
contains / minContains / maxContains | — | ✗ deliberate (validation logic, not shape) |
properties + required | struct fields; optional via ? (required is the default) | ✓ inverted default, stricter |
additionalProperties: false | implicit — structs are closed | ✓ by construction |
additionalProperties: <schema> / patternProperties | bounded map [K:V; min..max] with match-constrained key type | ≈ |
propertyNames | map key = named string type with match | ✓ |
minProperties / maxProperties | map bounds | ✓ |
dependentRequired / dependentSchemas / if/then/else | — | ✗ deliberate: conditional shape is undecidable for width inference and hostile to certification; cross-field invariants belong to expr contracts in higher profiles (§17.7) |
enum (numeric) | enum | ✓ |
enum (string literals) | — | ✗ v0.2 (§17.1) — integer wire form with a declared rendering |
const (fixed value in schema position) | const declarations; fixed-value field via [N] exact forms | ≈ |
oneOf (discriminated) | union (tagged) | ✓ stricter — tag is structural |
anyOf / allOf / not | — | ✗ deliberate: open-world combinators break the faithful-codegen guarantee (no clean proto/FlatBuffers/DBC mapping) |
unevaluatedItems / unevaluatedProperties | irrelevant — structs closed, arrays homogeneous by construction | ✓ by construction |
$ref / $defs | named types + qualified imports | ✓ stronger: versioned, package-scoped, cycle-checked |
$id / $anchor / $dynamicRef | package + name is the identity | ≈ superseded by module system |
| recursive schemas | — | ✗ deliberate (§7.3): unbounded wire size |
title / description | doc comments | ✓ |
default | bare = v (init value, §5.8) | ✓ type-checked (JSON Schema does not validate default) |
deprecated | @deprecated "reason" | ✓ reason mandatory in practice |
examples | — (doc-comment code blocks) | ≈ |
readOnly / writeOnly | not a type property — direction lives on interactions (ridl fixed, signal direction) | ≈ relocated by design |
vocabularies / meta-schemas / $schema | one closed grammar + label profiles | ✗ deliberate — no user-extensible validation vocabulary |
Verdict. typl covers the JSON Schema constructs that describe data shape —
everything a codegen target can faithfully realise — and adds what JSON Schema
lacks: physical units, quantization (step as more than multipleOf — it
drives width inference), wire-width inference, bitfields (enumset), a real
module system with declared-once semantics, and type-checked init values. What
it refuses is JSON Schema's validation-logic stratum (conditionals,
combinators, contains, unevaluated*) — deliberately, because those constructs
have no faithful mapping onto proto/FlatBuffers/CAN and defeat static width/size
guarantees. Of the four gaps this appendix once tracked as open, two are now
closed by decision — exclusive bounds (§5.5) and recursion (§7.3) — and two are
planned for v0.2: string enums (§17.1) and uniqueItems as an array constraint
(§17.3).
G.3 Other prior art consulted
| Language | What typl takes / how it differs |
|---|---|
| ASN.1 (X.680) with constraints | the closest ancestor in spirit: subtype constraints (INTEGER (0..255)) driving encoding width (PER) is exactly typl's range→width inference. typl trades ASN.1's enormous surface for a closed 6-declaration grammar |
| CDDL (RFC 8610) | compact shape DSL over CBOR; ranges and .size/.regexp control operators parallel typl constraints; CDDL allows unbounded recursion — typl does not |
| XSD | facets (minInclusive, pattern, length) map to typl constraints; typl rejects XSD's inheritance/extension model in favour of composition |
| Protocol Buffers | the codegen contract baseline; typl is deliberately richer (ranges, units, bounds) precisely where proto is silent, and its emitter targets proto3 (Appendix D) |
| FlatBuffers / Avro / Thrift | fixed-width scalars and required bounds validate typl's transport-layer mapping; Avro's named-types + namespace model parallels the package system |
| Franca IDL / AUTOSAR ARXML | automotive precedent: ARXML CompuMethods and data constraints are the target of typl's unit types and ranges; Franca's typeCollection ≈ a pure typl package |
| DBC | signal scaling/offset/min/max are derivable from unit type + range + step — the rest-bus generation path |
| TypeSpec (Microsoft) | a modern "one schema source, many emitters" language — same architecture as typl→IR→backends; TypeSpec chooses an open decorator/extensibility model where typl chooses a closed grammar with label profiles |
| CUE | values-as-types and lattice unification are powerful but undecidable for width inference; typl keeps constraints first-class but static |
| Zod / valibot | runtime-validator ergonomics (.min(), .regex(), branded types) confirm the demand for constraint-first typing; typl moves the same checks to compile time and codegen |
Appendix H — Glossary
| Term | Definition |
|---|---|
| RIDL (capitals) | the platform and family name; typl is this language, the family's vocabulary layer and root of the lattice |
| family | the four languages — typl, ridl, rmdl, rsdl — sharing one grammar, one toolchain, one IR; rxdl is a profile and a spelling layer over ridl, not a fifth language |
| profile | the restriction of the family grammar accepted by a file extension; .typl accepts type declarations only; .rxdl is the total profile accepting every layer |
| profile purity | the ridl.toml policy restricting a package to one profile's declarations — enforced per package, not by the grammar |
| core | a reusable semantic unit beneath the surface languages: ns (namespacing), typl-core (types), expr (predicates), time (timing), interact (interactions) |
| vocabulary layer | typl's role: the types, units, ranges, constants, and composites every other layer describes things with |
| package | the unit of namespace, visibility, versioning, and codegen — a directory whose files share one package declaration (ADR-0002) |
| workspace | a coordinated set of packages with one lockfile and one [workspace] manifest |
| manifest / lockfile | ridl.toml (declares imports, profile purity, defaults) / ridl.lock (pins remote imports by content hash) |
| primitive | one of the five built-in scalars: boolean, integer, float, string, bytes — backing material, restricted as direct field types |
| named type | a type declaration: a constrained primitive or unit type carrying domain meaning; the normal building block for fields |
| backing | the primitive (or UCUM unit, itself float-backed) underneath a named type |
| unit type | a named type whose backing is a UCUM unit expression (km/h, Cel) — carries dimensional identity into every layer |
| UCUM | Unified Code for Units of Measure — the unit vocabulary standard (also used by OpenTelemetry, HL7 FHIR) |
| constraint | the [ ] clause on a type: range, step, length, match — closed-form by design (no general expressions, §17.7) |
| range | closed (inclusive) [min..max] bounds; the semantic truth from which wire width is inferred |
| step | quantization: valid values are min + n·step; drives float width inference and the scaled-integer wire form |
| quantized float | a float with range + step — losslessly representable as a scaled integer (raw = (value − min)/step), the CAN/AUTOSAR wire form |
match | the constraint keyword binding a string type to a regex pattern (named const or inline literal) |
| width inference | the compiler deriving the narrowest safe wire type from a declared range/step — computed once, passed to all backends |
| language layer | the generated in-code representation: always widest (int64/float64) so arithmetic cannot overflow storage |
| transport layer | the on-wire representation: narrowest width the range provably fits |
| wire type | a concrete transport width (uint8 … float64); inferred from the range and never written in source — the names are not typl keywords (§5.6) |
| width flip | the evolution hazard where widening a range crosses a width boundary and silently changes the wire type — caught by ridl-diff; a source-level pre-emption (a deferred wire floor) is §17.11 |
| nominal typing | §5.7: every named type is distinct from its backing and from all other types; no implicit conversion — the foundation of unit safety |
| struct | a named composite with a fixed, closed set of named fields — no unknown properties, ever |
optional (?) | a field that may be absent (typl has no null); inverted from JSON Schema — presence is the default |
| init value | the value a consumer holds before anything real arrives — declared via a bare = value suffix or derived per §5.8; the seed of every ridl signal channel. Carries no keyword at the typl/ridl layer (default retired; init is rmdl's memory-seed keyword only) |
| enum / enumset | closed set of integer-backed values (single selection) / named bitfield (multiple simultaneous flags, width from highest bit) |
| union | discriminated (tagged) composite — exactly one named-type arm active; never untagged |
| error type | an enum, struct, or union carrying the error modifier — failure vocabulary; ordinary data with one extra bit of meaning |
| result union | a two-arm union of one success arm + one error-typed arm — the family's Result<T, E> without generics; the entire functional-error mechanism |
| tuple | anonymous composite of named fields used inline; () is the unit type |
| bounded | the invariant that every typl composite has a statically known maximum wire size — why collections require bounds and recursion is an error |
| ordinal | a field's/arm's implicit 1-based declaration-order identity — the source of proto field numbers and FlatBuffers ids (§7.4) |
reserved | tombstone keeping a retired field's ordinal (or enum wire value) occupied so it is never reused |
| append-only | the evolution discipline: new fields at the end, deletions by tombstone, reorder = wire break caught by ridl-diff |
| one-definition rule | a public name is declared exactly once, in exactly one package, importable everywhere — no re-exports, no wildcards |
internal | package-private visibility; the opt-out from public-by-default |
| diagnostic code | a stable TYPL-nnn identifier for a compiler rule — never renumbered, never reused (practice inherited from markspec typl) |
| IR | the stable serialized intermediate representation — resolved names, checked types with ranges/units/widths intact — consumed by every backend and tool |
ridlc / ridl / ridl-diff | the compiler (plumbing, pure source→IR) / the toolchain facade (porcelain) / the IR-comparison breaking-change gate (plumbing-grade despite living in the facade) |
| profile (assurance) | an external plug-in validating @labels vocabulary and escalating optional rules (explicit ranges/bounds) to errors — distinct from grammar profile |
| SSOT | single source of truth — one vocabulary from which types, validators, schemas, and documentation derive across every backend |
End of typl Language Reference v0.1.0.