Built. ridl has a working toolchain in this repository: the five interaction kinds, timing annotations, contracts, interfaces and services, with Rust and TypeScript code generation and
ridl diff. There is no runtime you can run a contract over from a command in this book — of the transport bindings below, the WebSocket one is implemented byridl-transport-ws, which a program embeds; the others and their delivery semantics are specified and not implemented, and the provider-side contract enforcement that is implemented is not reachable from any command. See what is built.
ridl Language Reference
Reactive Interface Description Language — the system-interaction layer of
the RIDL family: transport-neutral reactive contracts (signal · event ·
command · query · fixed) over the typl vocabulary.
Version: 0.2.0
Provenance and supersession. This document supersedes the interaction half of the RIDL Language Reference v0.1 (§12 Streams, §13 Interfaces, and the interaction rows of §16–§17). The vocabulary half (§1–§11) is now the typl Language Reference, which this document builds on and never restates. v0.1 errata applied here: wildcard imports are an error (ADR-0002), no semicolons, float width inference is count-based (typl §4.3). New in v0.2, from the prior-art review (Appendix F): the error model (§10), the last-value subscription guarantee (§4.4), and interaction identity & evolution (§11).
Reconciled with the shipped toolchain at the close-out of the interface layer (ADR-0008 decisions 1 and 16 to 21). Four supersessions from general form §6 are now absorbed rather than pending: inline
T | Ereturns (§7, §10.1, Appendix A, Appendix C), genericmin/maxtiming with the per-kind table as a derivation (§4.3, §5.2, §9), ordinal drift reported at the desk (RIDL-407), and the Stratum-3 wording "infrastructure failure — detected, undeclared" (§10.3). §2.1's duration table, §9.2's@[X..X]rule, §16.1's RIDL-108 and RIDL-110 rows, and Appendix C's grammar are corrected to the compiler.
Table of Contents
- Scope and Position in the Family
- Lexical Additions
- The Interaction Model — families (§3.2), correspondence obligations (§3.3), availability (§3.4)
- Signal
- Event
- Command
- Query
- Fixed
- Timing Annotations
- Errors
- Interaction Identity and Evolution
- Streams
- Contracts — require / ensure
- Interfaces and Services
- Conventions
- Diagnostics
- Open Questions
- Appendix A — Full Example
- Appendix B — Codegen Targets
- Appendix C — Formal Grammar (EBNF)
- Appendix D — Standards References
- Appendix E — Prior Art Survey
- Appendix F — Coverage Analysis: Other Interface Languages
- Appendix G — Glossary
1. Scope and Position in the Family
1.1 What ridl is
ridl answers one question: what does a service produce, consume, and
guarantee? It is the single source of truth for the contract boundary of a
reactive system, and it is transport-neutral by design — the same .ridl
contract binds to SOME/IP, proto/gRPC, DBC, AIDL, MQTT/AsyncAPI, or an
in-process broker without modification.
ridl is a profile of the one family grammar. A .ridl file accepts
interaction declarations plus everything typl accepts — types and their
interfaces naturally travel together; profile purity per package remains a
ridl.toml policy (typl §1.2). What .ridl rejects: behaviour declarations
(rmdl), architecture declarations (rsdl).
1.2 What ridl adds to typl
Exactly four things, each owned by a family core:
| Addition | Core | Surface |
|---|---|---|
| interaction kinds | interact | interface, signal, event, command, query, fixed |
| timing | time | @Xms, @[min..max], duration literals |
| contracts | expr | require, ensure attribute clauses |
| stream container | — (ridl-owned) | <T> in interaction position |
Everything else — types, units, ranges, constants, composites, packages, visibility, doc comments, diagnostic-code practice — is typl, unchanged.
Doc comments follow typl §14 in every ridl file. ridl adds carriers: an
interface, a service, each interaction, and each parameter of a command or
query. An interface's doc states the interface's responsibility and does not
list its members; each interaction is documented on its own line, and
missing-docs (TYPL-406) covers the interface and each interaction. A parameter
doc is allowed and never required, because a parameter's type is a named type
with its own doc (typl §14.4). A doc link can name an interaction as a member,
[CruiseControl.setLever]. A doc comment before a return type, before an arm of
an inline T | E return, or before an attribute block is misplaced (TYPL-407).
1.3 ridl describes every boundary
The five interaction kinds are named, typed, directed interactions on a contract
boundary — any boundary. ADR-0012 retired uxdl as a separate language
and gave ridl an interaction family on every declaration: dispatch (system
to system), presentation and intent (the person boundary), acquisition and
control (the physical boundary). Only dispatch carries no correspondence
obligation, because there the datum and its referent are the same thing.
The kinds below are family-neutral: a signal is a continuous state value
whoever reads it. What a non-dispatch family adds is the four correspondence
obligations, not a different kind. Readable per-family spellings — present,
measure, actuate and the rest — are the rxdl reference's, and add no
semantics to what is defined here.
2. Lexical Additions
ridl inherits the family lexical conventions (typl §2) and activates two token classes typl rejects:
2.1 Duration Literals
A positive integer followed by a time-unit suffix. Used exclusively in timing annotations and time-typed contract expressions. The five suffixes below are the complete set:
| Suffix | Unit | Example | Microseconds |
|---|---|---|---|
us | microseconds | 500us | 1 |
ms | milliseconds | 10ms | 1 000 |
s | seconds | 1s | 1 000 000 |
min | minutes | 5min | 60 000 000 |
h | hours | 1h | 3 600 000 000 |
The suffixes are UCUM time atoms, but they are a proper subset of the UCUM
atom table typl uses for unit types (typl §5.1): d is a unit atom there and is
not a duration suffix here, so @[1min..1d] is a parse error.
Zero duration is not permitted (RIDL-102). Fractions are not supported — use a
smaller unit (500us, not 0.5ms); a fractional literal is FORM-102.
2.2 The @ Sigil
@ introduces a timing annotation and is used for nothing else in the family.
2.3 Keywords
Keywords used by the ridl profile, beyond typl's set:
interface service signal event command query fixed
interface names the abstract contract shape; service names a global
published declaration of one or more of them (§14). Both are ridl's — the
contract SSOT — while offering services and requiring interfaces is rsdl's.
A signal's init override carries no keyword: it is a bare = value suffix
(§4.4), the same idiom typl uses for a type or field init (typl §5.8). There is
no init keyword in ridl — init belongs to rmdl alone, where init x = value
seeds a memory recurrence and the keyword is needed to disambiguate it from the
equation.
There is deliberately no error keyword: functional errors are typl
vocabulary (error types, result unions — typl §10.1–10.2), not interaction
syntax (§10.1).
(reserved and the evolution machinery are inherited from typl §7.4.
require/ensure are expr-core words activated here. Every registry word
remains reserved in every profile, and the registry as built is the list in typl
§1.4. The ADR-0012 domain spellings — present, trigger and the rest — are
not in it: each is a keyword only as the first token of an interface member, in
a profile that implements it, and an identifier everywhere else — the
member-start rule, typl §1.4.)
3. The Interaction Model
Five interaction kinds, each a distinct answer to "who initiates, and what does delivery mean":
| Keyword | Pattern | Semantic | Initiator |
|---|---|---|---|
signal | pub/sub | continuous state value — the latest sample matters | provider publishes |
event | pub/sub | discrete occurrence — every occurrence matters | provider publishes |
command | RPC | fire-and-forget action — no reply channel | consumer calls |
query | RPC | request/response — reply mandatory | consumer calls |
fixed | provisioned | immutable for the software-instance lifetime | neither (provisioned) |
The signal/event split is the load-bearing distinction, borrowed from the
automotive tradition (AUTOSAR isQueued): state may be sampled, coalesced,
and cached — missing an intermediate value is acceptable; occurrences are
queued and each one is meaningful. Everything else in this document (timing
semantics, late-joiner behaviour, error handling) follows from which side of
that line an interaction sits on.
Payloads are always named typl types — the interaction layer never defines shapes, it references vocabulary. This is the family's anti-coupling rule (concept note §7.1).
3.1 The Implicit Envelope
Every interaction instance — a signal publication, an event occurrence, a command call, a query request and reply — carries an envelope supplied by the runtime and never declared in the contract:
| Envelope field | Meaning |
|---|---|
| timestamp | when the instance was published/raised/called — stamped by the sender, at the producing binding, before transmission |
| sequence number | monotonic counter (frame number) assigned by the sender: per channel per provider instance on a signal or an event; on a command or a query, per caller over every request it sends, and per provider over its responses (frame specification §7) |
Timestamping is a sender-side act, always: no broker, relay, or receiver ever (re)stamps the envelope. Under the synchronized time base (below) this is what makes the semantics right — an event's TTL measures age since it was raised, not since it was forwarded; freshness measures the provider's publication, not the broker's hop; and one-way latency is simply receive-side clock minus envelope timestamp. Receive time is a local runtime observable, never envelope data.
The envelope is what the machinery runs on: TTL, debounce, and freshness (§9)
are evaluated on envelope timestamps; duplicate suppression of a retried call
(§6.1) on the caller's identity plus the request's sequence number, so two
callers are never merged under one sequence number, and a retry resends the
request with its envelope unchanged
(frame specification §7); a sequence gap makes loss
detectable on an event channel (a Stratum 3 detection, feeding §10.4
management) — a gap on a signal channel is not a loss, because intermediate
samples may be missed (§4); seq 0 is the envelope of a signal channel that has
had no publication; AUTOSAR E2E protection consumes the counter; deterministic
replay (concept note §9.3) is ordered by it; observability spans derive from
both. Generated subscriber/caller APIs expose the envelope alongside the value
(value + provenance + envelope); request/reply correlation is likewise
runtime-internal.
System time. The platform assumes one synchronized time base across the system — gPTP/PTP (IEEE 802.1AS in vehicle networks) or an equivalent shared realtime clock. Envelope timestamps are stamped in that domain and are therefore comparable system-wide: cross-node latency, end-to-end freshness, and global event ordering are meaningful by assumption, not by luck. The concrete mechanism and sync topology are deployment configuration, which rsdl does not declare (rsdl §3.4); that a synchronized base exists is a platform assumption every contract may rely on. Loss of synchronization is a detectable system failure like any other — surfaced by the runtime, handled by failure management (§10.4), never silently absorbed.
Epoch. Platform time is int64 microseconds since the PTP epoch —
1970-01-01 00:00:00 TAI — the PTP timescale: continuous, leap-second-free,
monotonic. This is the domain of every envelope timestamp, and ridl.std's
Timestamp type shares the same epoch and resolution, so domain time in
payloads and transport time in envelopes subtract cleanly. Civil datetime
(UTC/local, leap seconds, time zones) is a presentation conversion at the
edges — never the computation domain.
Because the envelope always exists, contract payloads should not re-declare
it: a payload field carrying publication time or a frame counter draws an info
lint (RIDL-406; §16.4 names the eight field spellings it matches). The
legitimate exception is domain time distinct from transport time — a
FaultEvent.timestamp recording when the fault occurred belongs in the
payload, because the envelope of a streamed history reply timestamps delivery,
not occurrence.
3.2 Interaction Families
Every interaction declares a family — who is on the other side of the boundary. The counterparty set is closed, because there are only three kinds of thing a system can talk to: another piece of software, a person, the physical world (ADR-0012 decision 2).
| Family | Direction | The provider's promise |
|---|---|---|
| dispatch | system → system | transfer, nothing interpreted |
| presentation | system → person | I offer this to be perceived |
| intent | person → system | I capture what you meant |
| acquisition | world → system | I report what is |
| control | system → world | I cause this to happen |
dispatch is the default and the baseline: an interaction that declares no
family is dispatch, and everything in §4 through §14 reads unchanged for it.
The other four are where the datum and the thing it stands for come apart, and
they carry the obligations of §3.3.
The family is a property of the declaration, not of the file, the package,
or the deployment, and it is selected by the declaring keyword — there is no
family clause. signal, event, command, and query are dispatch, so a
.ridl file declares nothing else. The non-dispatch spellings belong to the
rxdl reference and lower to these same kinds carrying a family; this document
owns what a family means and every rule one can violate, and rxdl owns only
the words. The family reaches the IR as a closed enum alongside the kind.
The agent asymmetry
Two families are epistemic (presentation, acquisition) and two volitional (intent, control), and the agent swaps sides: at the person boundary the person is the agent and the system informs; at the world boundary the system is the agent and the world informs.
Every structural rule below follows from that one fact, so it is worth stating plainly rather than deriving each rule separately.
Which kinds each family admits
| Family | continuous (signal) | occurrence (event) | operation (command) | request/response (query) |
|---|---|---|---|---|
| dispatch | ✓ | ✓ | ✓ | ✓ |
| presentation | ✓ | ✓ | — | — |
| intent | — | ✓ | ✓ — a shape (§3.2.1) | — |
| acquisition | ✓ | ✓ | — | reserved |
| control | ✓ | — | ✓ | — |
A combination outside this table is RIDL-501. Each absence is a consequence of the agent asymmetry, not an unfilled gap:
- presentation admits no operation. Nothing can be invoked on an agent. A provider may present, and the person may then act; it may not call them.
- intent admits no continuous value. A person's continuous state is knowable
only by measuring a physical proxy, and measuring a physical proxy is
acquisition. An accelerator pedal is an acquisition signal, not an intent one: there is no interpretation gap between a foot's position and its meaning, and all four obligations instantiate exactly as they do for a wheel-speed sensor. A person therefore appears at two boundaries — as an agent that means things, and as a physical object that is measured. - intent admits no query. A provider cannot block on a human. A confirmation is a presentation followed by an operation — two interactions, not a request/response.
- acquisition admits no operation. Commanding a device to recalibrate acts
on the world, so it is
control. - control admits no occurrence. Nothing leaves a system uninvited.
- control admits no query. Effect is never returned; it is observed by measuring back, which is a separate acquisition interaction paired with the control one (§3.3).
The reserved acquisition query is the explicit one-time read — see §17.10.
3.2.1 Operation shapes
An intent operation declares a shape, which constrains its parameters:
| Shape | Meaning | Parameters |
|---|---|---|
activate | invoke | none |
toggle | flip a binary state | none |
select | choose one from a set | exactly one — the key |
adjust | set a value within a range | exactly one — a ranged/unit type |
dismiss | cancel or close a transient | none |
Violating a shape's parameter rule is RIDL-502.
The set is closed and has no generic form. A person performs one act at a
time; a multi-parameter operation is a function call, not a gesture. Sending a
message is an occurrence carrying the body, a select of the recipient, and an
activate — three interactions, each separately observable and testable.
Repositioning an item, stripped of modality, is select then adjust.
An intent operation without a shape is RIDL-503. The world boundary has no
such restriction: a control operation takes whatever parameters it needs,
because nothing there is an agent constrained to single gestures.
Shapes are machine-readable gesture semantics. They drive interface scaffolding, accessibility roles, and the test plane's parameter generation — which is what earns them keywords rather than an attribute, and what a generic escape hatch would quietly destroy.
3.3 Correspondence Obligations
Between two software peers the datum is the truth: both sides agree on a number and there is nothing behind it. At every boundary with the non-software world, the datum and its referent come apart.
The canonical case is a speedometer. A vehicle's true speed and the number on
its cluster are deliberately different quantities: the indication is legally
constrained never to read below the true value, is quantised with hysteresis, is
damped, and is unit-switched per market. Declaring both as a plain signal
erases the distinction that matters, and the wrong one eventually reaches the
cluster.
Direction rule. In every non-dispatch family one side causes the other. The causally upstream side is the reference; the downstream side is the realisation; the obligation is that the realisation corresponds to the reference within declared bounds.
| Family | Reference | Realisation |
|---|---|---|
| presentation | the provider's value | what the person perceives |
| intent | what the person meant | the captured value |
| acquisition | the physical quantity | the reported value |
| control | the commanded value | the resulting world state |
The four obligations
| Obligation | Presentation | Acquisition | Control |
|---|---|---|---|
| relationship | never under-reads; quantised | transfer function; calibration | authority limits; slew rate |
| uncertainty | display resolution | tolerance per reading | positioning tolerance |
| latency of correspondence | perception delay | sample instant ≠ publication instant | actuation lag |
| failure to correspond | shown but not perceivable | plausible but false | commanded but not achieved |
Two are load-bearing enough to state separately.
Latency of correspondence is not delivery latency. The envelope is sender-stamped at publication (§3.1). For a transducer with a response time, the value describes the world as it was before that stamp, so any model computing with the time of the cause is silently wrong by the transducer's lag unless the contract declares it. The obligation has two forms: an instant offset, and a span for a value acquired over an interval rather than at a point — a swept sensor frame corresponds to the world across its sweep, which is why motion distortion exists in such frames.
Failure to correspond is not invalidity. §4.5's invalid state detects a malformed or unavailable value. A sensor can report a well-formed, in-range, perfectly fresh value that is false — a wheel spinning on ice. Nothing in the datum reveals it, which is why plausibility must be declared rather than inferred.
Obligations relate two declarations
An obligation frequently qualifies a pair rather than a single declaration: raw against indicated speed, commanded against achieved angle, a toggle against the state it flips, a set value against the value shown back. The pair is where the obligation lives, and one mechanism serves every family.
This is also a real failure mode with no other expression: a person acts, the indication does not follow, the person acts again — and both declarations were individually satisfied.
Obligations compose along a path
Correspondences chain, and the chain is the point:
true wheel speed
│ acquisition — measurement error, sensor lag
▼
the provider's speed value
│ relationship — legal bias, quantisation, damping
▼
the indicated value
│ presentation — refresh rate, legibility, perception delay
▼
what the driver believes
The legal requirement is stated at the end of that chain, so it is verifiable only if every hop declares its own contribution. An end-to-end budget is computable exactly when the path is fully declared, and not otherwise.
Metrology is the reference
These are metrology's concepts, and the correspondence with VIM (JCGM 200 / ISO-IEC Guide 99) is term for term:
| VIM term | Here |
|---|---|
| measurand | what an acquisition interaction names |
| calibration — the relation between quantity values and indications | the relationship obligation |
| measurement uncertainty | the uncertainty obligation |
| indication — the value a measuring instrument provides | a presentation-family value |
| detector — indicates presence when a threshold is exceeded | an acquisition occurrence |
| metrological traceability — an unbroken chain of calibrations, each contributing to the uncertainty | the composition property above |
Where this layer needs to name something further, VIM is the reference to reach for before inventing a word. One VIM concept has no expression here yet — the influence quantity, which does not affect the measurand but does affect the relation between indication and result, such as ambient temperature shifting a pressure sensor's characteristic. It is neither uncertainty (which is dispersion) nor the relationship (it is what perturbs the relationship); see §17.11.
Declaration
Obligations are attributes in the tail block (general-form §4.2), because they are authored, heterogeneous, and mix the assignment and predicate forms. The working spellings are below; they are fixed with the obligation implementation and an author should expect them to settle rather than change in substance.
The example below is written in the rxdl spellings, because a .ridl file
spells only dispatch and these are non-dispatch interactions. measure is a
signal in the acquisition family and present a signal in presentation;
both lower to the kinds and attributes this section defines.
interface Cluster {
measure wheelSpeed: Speed @10ms [
tolerance = 0.5 km/h,
lag = 40ms,
plausible abs(delta) < 10.0
]
present clusterSpeed: IndicatedSpeed @[50ms..200ms] [
derives = wheelSpeed,
resolution = 1 km/h,
ensure clusterSpeed >= wheelSpeed
]
}
A non-dispatch interaction that declares no obligation at all draws RIDL-504: the family exists precisely to make these facts explicit, and a family tag with nothing behind it is a declaration wearing a label.
3.4 Availability
An interaction may be unavailable, and unavailability has five distinct
sources. during, a reserved keyword whose grammar is not decided yet, covers
the first; the rest are predicates.
| Source | Example | Carried by |
|---|---|---|
| mode | only while the system is in ERROR | during — a state gate |
| data | disabled until a value validates | a predicate over declared state |
| in progress | already running, cannot be re-invoked | a predicate over a declared progress value |
| policy | not permitted in this condition | a predicate over declared policy state |
| provisioning | this variant does not have the interaction | fixed — and the interaction is absent |
The first four render as present but refused; provisioning renders as not there at all. Those lead a consumer — and a person — to different conclusions, and they are different tests.
The consumer-evaluability rule
At a presentation-obligated boundary, an availability condition must be evaluable by the consumer.
A person must perceive that something is unavailable before attempting it, not
discover it by rejection. A renderer cannot disable a control whose condition it
cannot evaluate, so an availability predicate on an intent interaction may
reference only declared, consumer-visible state — never provider-internal
state. Violating this is RIDL-505.
At the dispatch and control boundaries the rule does not apply: a software
caller or an actuator may simply be rejected, and §13's ordinary require
semantics hold unchanged.
Availability is general; pre-visibility is not. Every family has all five sources — an actuator is mode-gated, derated, mid-stroke, policy-gated, and fitted or not. Only the person boundary requires that unavailability be perceivable in advance.
Field-level absence is a sixth thing, and is not on this list
All five sources above are properties of an interaction. A field inside a
payload may separately carry no value, written with typl's ? (typl §7.1) — a
CAM-style status message whose acceleration was not measured this cycle is
well-formed, its channel is live, and one field is empty.
On an interaction payload ? has exactly one meaning: the provider has no
value for this field right now. It never means "this variant does not have
it", because that is the provisioning source above, and provisioning renders as
the interaction being absent — a variant with no accelerometer omits the
interaction rather than publishing a permanently empty field.
Absence is realised per target, not per contract: a target that can represent it structurally does so, and one that cannot takes a value the range does not use, without surfacing that value to consumers (ADR-0013 decision 7).
4. Signal
A continuous volatile state value. The latest sample is the truth; intermediate samples may be missed.
signal currentSpeed : Speed @10ms
signal engineTemp : Temperature @[20ms..100ms]
signal warnings : WarningFlags @[50ms..1s]
4.1 Rules
- Single named-type payload
- Timing annotation optional:
@Xms(strict periodic) or@[min..max](change-driven with bounds). An untimed signal receives the configurable default@[100ms..1000ms]— a warning, and concrete bounds in the IR either way (§9.1) - Optional bare
= <value>suffix overriding the payload type's init value (§4.4) - Stream
<T>not valid (RIDL-201) - No
require/ensure— a signal has no call site; validity is the typl constraint (RIDL-301). Error-typed payloads are legal — errors are data (§10.1)
4.2 Direction
The interface provider publishes; consumers subscribe. (Consumer-published flows belong to the consuming component's own provided interface — every flow has exactly one owning provider.)
4.3 Publication Semantics
With @Xms, the provider publishes every X ms regardless of change. With
@[min..max], the provider publishes on change, no faster than the rate floor
min and at least every staleness bound max even unchanged. On a signal those
two generic bounds derive as debounce and refresh ceiling, because state may be
coalesced and survives being unchanged — see §9.
4.4 The Channel Is Never Empty — Init Value and Last-Value
Normative: a signal channel holds a value from the moment it exists. Subscribing delivers a value immediately (within the transport's delivery latency): the init value before the provider's first publication, the latest published value thereafter. A signal is state — state exists even while unchanged, and it exists before anything happens.
- Init value. Defaults to the payload type's init value (typl §5.8 — a
declared bare
= valueor derived). A signal may override it with the same bare= valueidiom, written directly after the payload type — one syntax for init everywhere at the vocabulary and interaction layers, no keyword:
signal targetSpeed : Speed = SPEED_LIMIT_EU @[20ms..500ms]
The init value is the channel-level counterpart of the type's own init: a
property of this signal, layered over the type's default. A payload type with
no derivable init and no = value override is a compile error (RIDL-109); an
override violating the payload constraints likewise (RIDL-110).
- Last-value cache. The binding/broker maintains it per signal, seeded with
the init value at channel creation — this is the contract's demand on every
transport mapping (SOME/IP field with initial value, MQTT retained message
seeded at provisioning, DDS
TRANSIENT_LOCAL; Appendix B). - Application code can always distinguish init from published — the channel state carries provenance (init / live / invalid, §4.5), so consumers that must not act on a mere init value can tell.
- Events carry none of this (§5) — occurrences are not state.
Precedent: AUTOSAR initValue, DDS durability, MQTT retained, SOME/IP field
semantics.
4.5 Invalid Values Propagate — As Invalid State
A payload violating the typl constraints is never hidden. The malformed value itself is not delivered, but its invalidity is: the channel transitions to the invalid state, propagated to every subscriber like any other state change. Silent quarantine — subscribers unknowingly holding stale "last good" data — is exactly the failure mode a safe system must not have.
The generated subscriber API therefore exposes the channel as: value +
provenance, where provenance is init | live | invalid (with the last good
value still accessible in the invalid state). What the application does on
invalid — hold last-good, apply failsafe, degrade — is failure management
(§10.4), not contract semantics; the contract's job is that invalidity is
visible, typed, and timely.
Transport realisation: on CAN/AUTOSAR, channel invalidity is carried in-band
as the SNA/invalid sentinel (0xFF-style, AUTOSAR invalidValue) — which is
what typl's sentinel open question (typl §17.8) becomes at this layer; on
SOME/IP/DDS/MQTT it is a marked update or metadata flag per binding.
Observability hooks record every invalid transition (§10.3). There is no error
channel back to a publisher on pub/sub — a publisher emitting invalid values is
a provider bug surfacing through telemetry, not through consumers.
5. Event
A discrete occurrence. Every occurrence matters; occurrences are queued, not coalesced.
event doorOpened : DoorPayload @[50ms..500ms]
event speedLimitExceeded : SpeedLimitPayload @[100ms..2000ms]
5.1 Rules
- Single named-type payload
- Timing annotation optional:
@[min..max]only — strict periodic@Xmsis meaningless for occurrences (RIDL-103). An untimed event receives the configurable default@[100ms..1000ms](§9.1) - Stream
<T>not valid (RIDL-201) - No
require/ensure(RIDL-301) - No late-joiner delivery: subscribing to an event delivers only occurrences raised after the subscription. No cache, no replay — an occurrence that happened before you were listening did not happen to you. (Replaying history is the test/observability plane's job, not the contract's.)
- Subscription granularity is the interaction. A consumer subscribes to an event, not to a group of them and not to a set chosen per consumer.
- An invalid occurrence is delivered, marked invalid. An occurrence whose payload violates its typl constraints (Stratum 2, §10.2) or is not a well-formed encoding (Stratum 3, §10.3) reaches the subscriber with an invalid marker in place of the value, together with its envelope. The occurrence is never withheld, because nothing fails silently (§10.3, §10.4), and never delivered as a value, because the contract does not admit the value. Nothing is sent back to the provider, as on a signal (§4.5) (frame specification §9.2)
Who receives an occurrence is not a contract term. An occurrence is raised to every consumer subscribed to that event; the contract says what is raised and when, never to whom. Two neighbouring mechanisms sit outside it for the same reason:
- Per-client delivery — Franca's
broadcast selective, where the provider directs an occurrence at one named client — puts the consumer's identity in the provider's contract, and a contract that names its consumers is no longer one contract. Where the recipient is part of what is being said, it is part of the payload: raise the occurrence with the subject as a field, and let each consumer read the subject it cares about. - Subscription granularity below the interface — SOME/IP eventgroups, where a consumer takes a subset of an interface's events — is a transport-level grouping for subscription efficiency. Appendix B maps an eventgroup to an interface, and a finer grouping is a deployment matter the binding derives, not a fact the contract carries: the same contract is realized with one eventgroup per interface or with several, and no consumer can tell from the contract which it got. Grouping for efficiency is rsdl's and the binding's.
A signal subscription has the same granularity, for the same reason: §4.2 gives each flow one owning provider, and who observes it is not the provider's to declare.
5.2 Timing Semantics
min is the rate floor and max the staleness bound, as everywhere (§9). On an
event they derive as a throttle (the provider must not raise occurrences
faster than min) and a TTL (an occurrence processed later than max after
being raised is stale and is discarded by the binding), because an occurrence is
individually meaningful and cannot be coalesced into its successor.
6. Command
A fire-and-forget action request. The consumer requests; the provider acts; there is no reply.
command setGear(position: GearPosition) [
require position != GearPosition.PARK || currentSpeed == 0.0
]
command resetFaults()
command uploadFirmware(data: <FwBlock>)
6.1 Rules
- Parameters are named typl types; stream
<T>permitted on parameters. A tuple is not a parameter type — pass a namedstruct(a tuple is a query return type, §7.1) - A parameter name is unique within the parameter list (RIDL-413): a name identifies one parameter, so a command cannot declare two
- Always returns
()— writing a return type is an error (RIDL-104); if the caller needs a result or a completion signal, usequery - No functional-error channel, by construction: a command has no return, so
there is nowhere for a fallible return. A command whose outcome the caller
must know about is a
queryreturningT | E— the language forces that honesty requirepermitted (§13);ensurenot (nothing to observe)- A range timing annotation is permitted, and an undeclared response bound
draws a warning (§9.3): the
maxbound is a response bound on acceptance — the delivery acknowledgment below, never execution — and theminbound is a call throttle on the caller. RIDL-112 reports a command that declares no response bound and takes the default (§9.1) - Fire-and-forget describes the contract, not the wire. The runtime protocol carries a delivery acknowledgment beneath every command: the receiving binding confirms received and accepted for execution (payload valid, precondition passed) or negatively acknowledges with the Stratum 2 category (§10.2). The ack carries no functional payload, never reaches the contract surface, and is not application-visible as a return value — it exists so the runtime can implement retries, delivery supervision, and duplicate suppression (keyed on the caller's identity plus the envelope sequence number, §3.1), and so a rejected command is a detected event in a safety context rather than a silent one
- A command is therefore fallible like everything else — invalid payload or
failed precondition (Stratum 2, negative ack), or undelivered (Stratum 3, ack
timeout) — all visible to the calling runtime, none to the contract. What
the contract does not promise is execution outcome: observable results of a
command travel back as state (e.g.
setGear→ observecurrentGear). This is the CQRS discipline: commands mutate, state reports.
6.2 Provider-Side Rejection
The provider binding validates parameters (typl constraints) and require
clauses before invoking application code. A violating command is rejected —
never partially executed — and the rejection surfaces twice: as a negative
acknowledgment carrying the Stratum 2 category back to the calling runtime
(§6.1), and through the provider's observability hooks. Application code on both
sides stays uninvolved.
7. Query
A request/response interaction. The reply is mandatory; failure is expressible.
query getAverageSpeed(window: Duration): Speed [
require window > 0ms
ensure result >= 0.0
]
query getMinMax(window: Duration): (min: Speed, max: Speed)
query streamFaults(filter: DiagFilter): <FaultEvent>
query calibrate(axle: Axle): CalReport | CalError // inline T | E — fallible query, §10.1
7.1 Rules
- Must return non-void —
()return is an error, usecommand(RIDL-105) - Return type: named type, named-field tuple, inline
T | E, or stream<T>; streams also permitted on parameters (bidirectional streaming supported) - A parameter name is unique within the parameter list (RIDL-413): a name identifies one parameter, so a query cannot declare two
requireandensurepermitted (§13);ensureconstrainsresult- Functional failure is expressed in the return type: the inline
T | Eform makes the query fallible — §10.1. There is nothrowsclause in ridl - A range timing annotation is permitted, and an undeclared response bound
draws a warning (§9.3): the
maxbound is a response bound on the reply, and theminbound is a call throttle on the caller. RIDL-112 reports a query that declares no response bound and takes the default (§9.1)
7.2 Concurrency and Idempotence
The contract does not serialize queries — providers may answer concurrently and
out of order. Queries should be read-only or idempotent; a state-mutating
request belongs to command (with state observation) unless the mutation
inherently needs a result (e.g. allocate, calibrate). Linters flag
verb-named queries (set…, reset…) as probable commands; §16.4 names the six
verbs RIDL-404 matches.
8. Fixed
A value provisioned externally — build, factory, FOTA — and immutable for the lifetime of the running software instance.
fixed vin : Vin
fixed softwareVersion : Version
fixed capabilities : [Label; 0..32]
- Single named-type payload (collections permitted); no timing, no attribute block (RIDL-106)
- Read-only: no interaction can mutate it; safe to cache unconditionally
- Reading a
fixedis free of the query machinery — bindings expose it as a plain accessor, populated at binding initialization - Naming decision on record:
fixed, notfinaland notconfig— one word for one concept at every boundary (ADR-0011);fixedis family-neutral, because a provisioned constant carries no correspondence anywhere.finalwas the earlier spelling and misled: a Java or Kotlin reader takes it for a compile-time constant, which this is not.configconnotes hot-reload and is reserved vocabulary space for rsdl. See the concept-note naming ledger
Maps to: Android ro.* properties, AUTOSAR CalibrationParameter, SOME/IP
field with getter only.
9. Timing Annotations
Timing is the time core surfacing in ridl. The @ annotation is part of the
contract, not documentation — it generates freshness monitoring, drives rmdl
base clocks, and defines "late" for the observability plane.
Strict periodic — signal only:
signal currentSpeed : Speed @10ms
Range — signal, event, command, and query:
@[20ms..100ms] // both bounds
@[20ms..] // lower bound only
@[..100ms] // upper bound only
The bounds have one generic meaning, whatever the declaring keyword (general form §6.2):
min= rate floor — the minimum interval between publications.max= staleness bound — the maximum age, measured on envelope sender timestamps (§3.1), before the value or occurrence is stale.
So the naive reading of @[20ms..500ms] — "not faster than 20ms, not staler
than 500ms" — is the correct one, and it is the same reading on a signal and on
an event.
What differs per kind is what the runtime does at each bound, and that is derived from the semantics of the declaring keyword (§3), not from the annotation. The familiar names are the derivation, not the definition:
| Bound | On a signal (state) | On an event (occurrence) | On a command / query (call) |
|---|---|---|---|
min — rate floor | debounce — a faster update is coalesced into the next sample | throttle — the provider must not raise occurrences faster | call throttle — the caller must not call faster |
max — staleness bound | refresh ceiling — re-publish even if unchanged | TTL — an occurrence older than max is discarded | response bound — the provider must respond within |
A state value that is stale is refreshed and a fast one coalesced, because state survives being unchanged and only the latest sample matters; an occurrence that is stale is discarded and a fast one throttled, because every occurrence is individually meaningful and cannot be merged into its successor. A call has no sample to refresh or discard: its two bounds oblige the two ends of the call directly — §9.3 (ADR-0015). Editor hover expands the per-kind consequence from the declaring keyword.
9.1 Default Timing
A signal or event with no @ annotation receives the default range
@[100ms..1000ms] — a 100ms rate floor and a 1s staleness bound, derived
per kind as above. The default is configurable at compile time in ridl.toml:
[defaults]
timing = "[100ms..1000ms]" # applies to untimed signals and events
command_timing = "[..1s]" # applies to a command with no response bound
query_timing = "[..3s]" # applies to a query with no response bound
Resolution follows the ADR-0002 precedence: package-level [defaults] shadows
workspace-level, which shadows the built-in — [100ms..1000ms] for timing,
[..1s] for command_timing, [..3s] for query_timing. Each key resolves on
its own. Three properties keep this safe:
- The IR always carries concrete bounds. The default is applied at compile time, so every downstream consumer — freshness SLOs, observability conventions, rmdl, test generators — sees a fully timed contract; "untimed" does not exist beyond the parser.
- An untimed interaction draws a warning (RIDL-100: default applied); an active profile may escalate to an error and require explicit timing — the same idiom as typl's default string bounds (typl §4.4). Safety-graded packages should require explicit timing.
- Changing the configured default is a contract change. Because
ridl-diffcompares resolved IR, editing a[defaults]key flips the bounds of every untimed interaction of the kinds it covers in the package and is flagged accordingly — the default is a convenience, not a loophole.
Strict periodic @Xms is never defaulted — an isochronous rate is always an
explicit engineering decision (it drives rmdl base clocks).
A command or query takes a default response bound (ADR-0015 decision 4).
An RPC with no declared response bound draws RIDL-112 (warning; an active
profile may escalate it to an error, the same two-step as RIDL-100 above) and
receives the command_timing or query_timing default: [..1s] for a command
and [..3s] for a query unless a manifest says otherwise. The default fills
max only. A member written @[20ms..] keeps its min and takes max from
the default; a default's min applies only to a member with no annotation. The
IR records default_applied as true. A default is a value the package declares
and both sides read from the catalog, so it is not an invented promise. To
require every call to carry its own bound, set missing-response-bound = "deny"
in [lints]: an untimed call is then an error.
9.2 Validity Rules
Rules: @0ms is an error (RIDL-102); @[X..Y] with X > Y is an error
(RIDL-101); @[X..X] draws a warning (RIDL-108) on every kind that admits the
range — it is a degenerate range, a rate floor equal to its staleness bound,
which is almost always a mistake. Neither RIDL-101 nor RIDL-108 runs on a range
that has a bound drawing FORM-102 (a duration that is not a whole number of time
units): the rejected literal is the only ordering report until the author
rewrites it. It is not a spelling of the strict period @Xms: a strict
period is a separate mode, admitted on signals only, never defaulted (§9.1),
recorded in the IR beside the bounds, and a change between the two modes is
breaking whatever the bounds do.
Timing belongs to every kind but fixed. A signal takes either form; an
event, a command, and a query take the range form only, so a strict period
@Xms on any of the three is RIDL-103 — an isochronous rate is meaningless for
occurrences, and a caller is not isochronous by contract (ADR-0015 decision 5).
An @ annotation on a fixed is RIDL-106. The grammar admits the annotation on
every interaction kind so that each narrowing is a semantic rule with a semantic
message; none is a parse error.
A signal's @Xms or @[..max] is an alertable freshness SLO: a subscriber
that has not seen a publication within the bound may treat the value as stale —
generated bindings expose staleness, and the observability conventions map the
bound to an OTel attribute. Every timing bound — the rate floor, the staleness
bound, and the derived debounce, refresh, throttle and TTL behaviour — is
evaluated on envelope timestamps (§3.1), never on payload content.
DDS students will recognise max as the DEADLINE QoS and staleness as
LIVELINESS — ridl puts them in the contract instead of a QoS profile; see
Appendix F.
9.3 RPC Bounds — the Call Throttle and the Response Bound
command and query admit the range form, and only the range form (ADR-0015
decisions 2, 3, and 5). The two bounds keep their generic meaning; the per-kind
consequence is derived from the declaring keyword, exactly as it is for state
and occurrences:
minis the call throttle — the minimum interval between calls. It constrains the caller, not the provider. That is not an inconsistency:minalways constrains whoever initiates, and on an RPC the initiator is the consumer. It is enforceable at the provider's admission point, and it is what a rate-limiting binding already implements.maxis the response bound — the provider must respond within it. What responding means is derived per kind. For aqueryit is the reply. For acommandit is acceptance — the delivery acknowledgment of §6.1 — since §6.1 promises no execution outcome: a command's bound covers admission and queueing at the provider, and not execution.
The response bound is a bound on the response, not on delivery. Delivery latency is a property of the link, so a per-interaction delivery bound would declare the same number on every declaration in the package and carry no information. Response time is what varies per interaction — which is why gRPC, DDS-RPC, and AIDL all bound the whole call.
The half-open spellings carry the two partial cases: @[..100ms] is a response
bound with no throttle, and @[20ms..] a throttle with no response bound. An
RPC whose response bound is undeclared — a bare declaration, or the @[20ms..]
spelling — draws RIDL-112 and takes the default response bound (§9.1). The
warning is about max specifically, not about the annotation; a missing min
draws nothing, because an unbounded call rate is the sensible default and is
what every RPC has today.
In the IR the declared bounds reuse the Timing message unchanged (ADR-0015
decision 7): the mode is always the range mode, min_us is the call throttle,
max_us the response bound (always present), and default_applied true when a
bound came from a default. The response bound is for RPC what §10.3 calls the
freshness machinery for pub/sub: the contract-level view of transport health.
10. Errors
New in v0.2. The error model has three strata. None of them adds interaction syntax: the first is expressed as data from the typl vocabulary; the other two never appear in source at all.
10.1 Stratum 1 — Functional Errors Are Data
ridl has no error syntax — no throws, no exceptions, no status codes. A
query that can fail as part of its domain semantics says so in its return
type, with vocabulary declared in typl:
error enum CalError { // typl §10.1 — failure vocabulary
SENSOR_UNAVAILABLE = 0
VEHICLE_MOVING = 1
OUT_OF_RANGE = 2
}
query calibrate(axle: Axle): CalReport | CalError // inline T | E — the canonical spelling
Rules:
- An inline
T | Ein query-return position makes a fallible query — the canonical spelling (general form §6.1). The left arm is any non-error named type (success); the right arm is exactly one error type. Both arms stay named typl types; only the union container is structural, and its transport identity is synthesized from the interface, the interaction ordinal, and the ordered arm types (ADR-0008 decision 4) - Bindings split it mechanically: the success arm is the reply payload; the
error arm maps to the transport's native error channel — SOME/IP return
code, gRPC rich status, AIDL exception — and to
Result<T, E>(Rust) / sealed result (Kotlin) in language targets, with exhaustive handling enforced wherever the target can express it - Several failure kinds compose into one
error unionbefore appearing as the error arm (typl §10.2) — one query still carries one closed failure set - A named result union (typl §10.2) remains legal typl data — for storing an outcome in a struct or a log — but in return position it draws RIDL-308, a lint steering to the inline spelling
- A query with no success path is an error (RIDL-303): a bare error type in
return position, or an
error-typed left arm of an inlineT | E commandremains failure-free by construction: it has no return, so there is nowhere for a fallible return — nothing to ban. A command's observable failure is state, like every other command outcome: publish a fault signal (§6.1)- Because errors are ordinary vocabulary, pub/sub can carry them:
signal lastFault : ServiceFault @[1s..]is legal and idiomatic for fault reporting — structurally impossible in throws-based designs, where failure lives in a control-flow channel that pub/sub does not have
Rationale (decided v0.2, replacing a draft throws clause). One concept
instead of two: failure shapes are typl types like every other shape — nominally
typed, importable, reusable across interfaces, evolvable under the same ordinal
rules. ridl adds zero error surface; the runtime keeps its own stratum below
without the contract pretending to model it. The cost — naming the failure type
on the signature — is exactly the honesty wanted: the failure set is part of the
signature, as data. The inline spelling (general form §6.1) puts it at the
signature rather than one hop away in a named union.
10.2 Stratum 2 — Contract Errors: implicit, standardized, derived
Invalid values and violated preconditions are contract violations, not
functional errors. They are never declared — the typl constraints and
require clauses already describe them completely — and never occupy an error
type. The spec defines the categories once; bindings implement them uniformly:
| Category | Trigger | RPC surface (caller sees) | Pub/sub surface |
|---|---|---|---|
INVALID_VALUE | payload violates typl constraints (range, step, bounds, pattern) at a boundary | standard error, distinct from any result-union error arm — gRPC INVALID_ARGUMENT, SOME/IP E_MALFORMED_MESSAGE | signal: channel transitions to invalid state, propagated to subscribers (§4.5); event: occurrence delivered marked invalid (§5.1) |
PRECONDITION_FAILED | a require clause evaluates false | standard error — gRPC FAILED_PRECONDITION, SOME/IP E_NOT_OK | command: negative ack to calling runtime + observability (§6.2) |
CONTRACT_BROKEN | an ensure clause evaluates false — a provider bug | standard error to caller; incident-grade telemetry on provider | n/a |
UNKNOWN_INTERACTION | ordinal/version mismatch between peers (§11) | standard error — SOME/IP E_UNKNOWN_METHOD | subscription fails at bind time |
Provider bindings evaluate constraints and require before application code
runs, and ensure after; application code never sees a contract-violating
input, and callers can always distinguish "the service said no" (Stratum 1) from
"the call was ill-formed" (Stratum 2).
10.3 Stratum 3 — Transport Errors: infrastructure failure — detected, undeclared
Timeouts, broker loss, serialization failures, connection resets, and a
command's missing delivery acknowledgment (§6.1): these exist for every
interaction identically and say nothing about the contract, so the language
never declares them. The normative phrasing is "infrastructure failure —
detected, undeclared" (general form §6.4), and both halves are load-bearing.
Detected: the runtime observes every one of them — acks, timeouts, staleness —
and nothing fails silently (§10.4). Undeclared: the contract language has no
vocabulary for them, because a contract author has no knowledge to express. This
is the opposite of undefined behaviour, where a system may do anything; the
generated code names the stratum in exactly these words. Generated caller-side
types carry a transport-error variant supplied by the runtime (ridl-rt), not
by the .ridl source — including command delivery status for callers that
choose to supervise it. The freshness machinery (§9) is the contract-level view
of transport health for pub/sub; the response bound (§9.3) is the same view for
RPC.
The invariant across all three strata: what the contract declares is exactly the failure knowledge a domain engineer possesses (Stratum 1); what the contract implies is derived mechanically and enforced uniformly (Stratum 2); what the contract cannot know, it does not pretend to know (Stratum 3).
10.4 Failure Management Is Out of Scope — Deliberately
Detection is not management. This section specifies how failures are detected and classified; what a system does about them — failsafe states, fallbacks, degraded modes, health monitoring, halt management — is a safety/quality-management concern, specified separately and never expressed in ridl syntax. The division of labour:
- ridl declares which failures a fallible query can answer with (the error
arm of its
T | Ereturn — the vocabulary itself is typl's, §10.1) and the bounds whose violation defines failure (typl constraints,require/ensure, freshness) - the runtime detects every failure across all three strata — quarantines, negative acks, ack timeouts, staleness — nothing fails silently; this total detection is precisely what makes failure management implementable
- the management layer decides — supervision policies, fallback wiring,
degradation ladders, health state machines. Its natural homes are the
deployment topology (rsdl: which component supervises which, what replaces a
failed provider) and the runtime specification (
ridl-rtsupervision hooks), informed by@labelsassurance levels
Planned direction — properties, not mechanisms. The family is expected to
grow declarative safety/high-availability properties across its layers: what
must hold, never how the runtime achieves it. Candidate surfaces, gated through
the @labels/profile system first and promoted to keywords only when earned:
typl — invalid/SNA sentinel values (typl §17.8); ridl — per-signal failsafe
values, availability requirements on interactions; rmdl — degraded-mode models
and observers acting as safety monitors; rsdl — arbitration over the redundant
provider sets it derives from instances (rsdl §7), supervision topology,
fallback wiring. Deferred as a body of work of its own (§17.8) — in v0.2 the
contract's job ends at making every failure visible and typed.
11. Interaction Identity and Evolution
typl §7.4's model, applied to interfaces. Tag-based transports need stable numeric identities per interaction (SOME/IP method/event IDs, proto RPC identity).
- Implicit ordinals by declaration order (1-based) across all interactions of an interface, one sequence regardless of kind
- Append-only: new interactions go at the end; insert/reorder shifts
ordinals — wire break, rejected by
ridl-diff reservedtombstones retire removed interactions by name:
interface VehicleStatus {
signal currentSpeed : Speed @10ms
reserved legacyTemp // was ordinal 2 — retired
event doorOpened : DoorPayload @[50ms..500ms]
}
- Publication enforces the tombstone.
ridl baselinerefuses to replace a published baseline when, in an interface body both declare, the replacement breaks the rule above in any of four shapes — RIDL-408, exit 1, the published baseline left as it was: an interaction is gone with noreservedline at all; it is retired with areservedline, but at an ordinal other than the one it held; the baseline already retired it and the source has dropped thereservedline; or the source declares a live interaction under a name the baseline retires. The published snapshot is the only record that the ordinal was taken, so publication is the last point at which the change can be refused. The gate covers the interaction level; the interface level is the lock's, one level up below: a whole interface removed with its lock entry live fails the build (RIDL-409), a provisional number is refused at publication (RIDL-411), and so is a published number the fresh snapshot neither carries nor retires (RIDL-412). RIDL-407 is unchanged: it remains the desk-time warning that an ordinal moved, emitted byridl check, and it neither classifies nor gates. - What the publication gate is for, and what it is not for. The gate
protects the record of a retirement: the four shapes above are the ones
that would publish a baseline in which an ordinal's history is no longer
written down anywhere. It is not a second compatibility gate —
ridl diffand its exit-code contract are that one, and they report every breaking change, including the shapes publication lets through, and the one below it refuses at the interface level only:- An ordinal that moves. An interaction inserted mid-body, a live
interaction reordered, or an existing tombstone moved from the ordinal it
held (§17.12) leaves the record complete: every name in the published
snapshot still carries an ordinal, and a later comparison reads it. The move
is breaking,
ridl diffsays so and exits 1, andridl checkwarns RIDL-407 at the desk. Publication does not refuse it. - A whole package removed or renamed (§17.13). The interaction level does
not refuse it, and the snapshot goes with every tombstone it held. The
interface level does: an interface number the removed package's snapshot
holds is refused at publication (RIDL-412) unless the unit's
interfaces.lockretires it or another package of the unit still declares it; a number 0, published before the lock existed, is never refused. This reverses the earlier position, which rested on the language having no package-level retirement. That premise no longer holds for a package removed from a unit that remains: the unit's lock retires its numbers (ridl lock --retire), which is the remedy the author writes. For a whole unit removed or renamed, the refusal catches the accidental case — a moved directory or a changed manifest — that would otherwise drop the unit's snapshots and lose the record without a sign. The deliberate case costs one deletion, of that unit's snapshots from.ridl/baseline/, which the message names. What would reopen the question is a unit-level retirement construct.ridl diffreports the removal as breaking. - A whole service removed, or a service whose form switches between inline
and named (§17.14). Neither moves an identity of its own: an interface is
identified by its number, a named-form service carries no number, and its
list is a set (§14.5) the gate does not read. Every case where an identity
would be lost is already refused one level down — a live lock entry with no
declaration is RIDL-409 at the build, and an inline shape's own number is
RIDL-411 or RIDL-412 at publication.
ridl diffreports the removal and the form switch as breaking.
- An ordinal that moves. An interaction inserted mid-body, a live
interaction reordered, or an existing tombstone moved from the ordinal it
held (§17.12) leaves the record complete: every name in the published
snapshot still carries an ordinal, and a later comparison reads it. The move
is breaking,
- Transport IDs derive deterministically from ordinals (e.g. SOME/IP: method ID = ordinal for RPC kinds, event ID = ordinal with the event flag bit; Appendix B) — readable from source, no sidecar state
- No in-language version block. Franca's
version { major minor }was considered and rejected: the package version (ridl.toml, ADR-0002) is the one version, andridl-diffdecides compatibility mechanically — a hand-maintained version pair inside the source is a second source of truth that drifts. Major-version coexistence is a deployment topology question (rsdl), not a contract question - Changing an interaction's kind (e.g. event → signal), payload type, timing
bound, or a fallible return's error arm in a breaking direction is detected by
ridl-diffper its category rules; the diff exit-code contract (concept note §9.1) applies
One level up, an interface's identity is its number, and the number lives
outside the source. Every interface of a unit — a declared interface and a
service's inline shape (§14.5) alike — carries an interface number from the
unit's interfaces.lock. A unit is one ridl.toml with a [package] table
plus every source package in its directory tree (ADR-0002 §1), and the lock is
one line table in the manifest directory, beside the root source package, that
only ridl lock writes: a next N line, then one entry per interface,
Name N, keyed by the interface's catalog name or, for an inline shape, by
service: and the service's dotted name. The catalog name of a declared
interface is its source package relative to the unit, then its name, joined by
.: cluster.SpeedDisplay for an interface of the source package
com.example.hmi.cluster in the unit com.example.hmi, and Session for an
interface of the root source package. The key is therefore one identifier or a
dotted path of identifiers ending in the interface name. The number is 1-based,
per unit, and is the interface's routing identity; a rename keeps it, and an
entry is never removed or renumbered — a retired interface keeps its line with
the word retired, and its number is never allocated again. A declaration with
no entry compiles with a provisional number, taken from next upward in
byte order of the name — the lock key without the service: prefix of an inline
shape, with an interface before an inline shape of the same name — over the
whole unit. A provisional number carries no identity until plain ridl lock
allocates and records it. The compiler reads the file as a unit input and
refuses the departures it cannot resolve: a live entry with no declaration is
RIDL-409, fixed on the branch that made the change with
ridl lock <path> --rename Old=New or ridl lock <path> --retire Old, where
<path> is any path inside the unit and Old and New are catalog names —
with a published baseline, ridl check names the one --rename when exactly
one declaration without an entry has the old interface's shape, member for
member — and a malformed file, a merge conflict left in it included, is
RIDL-410. Publication refuses what the build cannot see: ridl baseline
refuses a provisional number (RIDL-411) and a published number that is
absent from the fresh side and not retired (RIDL-412). An interaction's
ordinal space stays local to its interface — a binding keys the spaces on the
unit and the interface number, never on a name or on a position in a
service's list (§14.5) — so the two levels never renumber each other.
The lock is the unit's, and only the file beside the manifest is read. An
interfaces.lock in a subdirectory of a unit is not read and draws the warning
RIDL-416. An interface that moves to another source package of its unit
keeps its number when ridl lock --rename records the move from its old catalog
name to its new one, so a move between sibling packages is a rename and not a
removal. A retired entry belongs to the source package its catalog name names.
The IR spells each retired entry (Package.retired[].name) as its lock key, the
catalog name, and the unit's anchor package carries the entries that name no
source package of the unit, and every service: entry. The anchor package is
the source package named like the unit when it exists, and otherwise the first
source package of the unit in byte order of name; it also reports the unit's
RIDL-409 orphans. In single-file mode, outside any manifest, the unit is the
file's package name and the lock beside the file is the unit's lock.
12. Streams
The stream container <T> is ridl-owned (typl §1.3): an unbounded sequence
valid only in interaction position — command/query parameters and query
returns.
query streamFaults(filter: DiagFilter): <FaultEvent> // server produces
command uploadFirmware(data: <FwBlock>) // client produces
query pipe(samples: <SensorSample>): <ProcessedSample> // bidirectional
12.1 Direction by Position
| Position | Direction |
|---|---|
| return type | provider produces, consumer consumes |
| parameter | consumer produces, provider consumes |
| both | bidirectional, full-duplex |
12.2 Element Type
A named typl type, or raw string/bytes when the element is genuinely
unstructured (the one exception to typl §15.3, inherited from v0.1). The element
type's bound describes one logical element; the stream itself has no bound —
framing, buffering, and backpressure are transport concerns.
12.3 Restrictions
<T> on signal/event (RIDL-201), struct fields, or collections (typl
TYPL-301 territory) is an error. A signal is the better stream for state; an
unbounded push of occurrences is an event; <T> exists for RPC-scoped
transfers with a beginning and an end.
Maps to: gRPC streaming, Kotlin Flow, Rust Stream, WIT stream<T> (WASI
0.3), SOME/IP segmented transfer.
12.4 Errors on Streams
Functional failure around a stream is data, like everywhere else: a failure the producer knows about is expressed by making the element type a result union.
An element that violates its typl constraints ends the stream. It is a
Stratum 2 INVALID_VALUE (§10.2), and a stream lives inside an RPC (§12.3), so
it takes the RPC surface of that stratum: a standard error to the caller,
distinct from any result-union error arm, and the call — with the stream inside
it — is over. Dropping the element and continuing is the one behaviour ruled
out, for the reason §4.5 gives for state: a consumer that silently receives
fewer elements than the producer sent cannot tell a complete transfer from an
incomplete one, and silent quarantine is the failure mode a safe system must not
have.
What the element type records is which stratum a failure lands in, and that
is the whole of the contract-level choice. With a plain T, a failing element
is a contract violation and the call ends. With T | E, the failure is declared
data: it is Stratum 1, it does not end the call, and the consumer handles it
element by element. What the producer does after emitting an error element —
close the stream, or go on producing — is behaviour, not contract, and a
consumer writes for both, because a stream may end at any point in any case. A
producer that means the error element to be the last one closes after it, and a
consumer reads the close, not the arm, as the end.
No channel-level policy, keyword, or per-binding setting is needed for either half, and none exists. Transport interruption remains Stratum 3.
13. Contracts — require / ensure
The expr core surfacing in ridl. Attribute blocks [ ] on command and
query:
command setRange(min: Speed, max: Speed) [
require min < max
require max <= MAX_SPEED
]
query getAverageSpeed(window: Duration): Speed [
require window > 0ms
ensure result >= 0.0
]
| Attribute | Meaning | Valid on |
|---|---|---|
require <expr> | precondition over parameters and the interface's own signals | command, query |
ensure <expr> | postcondition over result (and parameters) | query only |
Both predicates are rows of the attribute registry (ridl_core::attributes,
ADR-0012 decision 8): owner core, form predicate, sites as the table above
says, consumer the clause check, and a change to either reported by ridl diff
as contract_changed. The registry is the one allow-list the checker reads for
a plain key: a plain key on a member draws FORM-107 when the registry has a row
for it on an rsdl site and FORM-106 when it has none. No plain key is accepted
on a member today.
- Expressions are type-checked, unquoted, side-effect-free; they reference
parameters,
result, constants, enum values, and the interface's own signals (arequiremay readcurrentSpeed— the provider's latest published state) - Violations are Stratum 2 errors (§10.2) — never the error arm of a
T | E - The full expression grammar is the expr-core specification. This document
fixes the positions and the checking discipline; the expr-core
specification §3.1 fixes the grammar of the guaranteed subset —
comparison, boolean connectives, arithmetic, enum access, tuple-field access,
and duration comparison, over parameters,
result, constants, enum values and the interface's own signals — and §8 fixes the RIDL-306 boundary that rejects everything outside it - One assertion, four executions (concept note §9.2): static where decidable,
property test in CI, online observer on live flows, synchronous observer in
the rmdl reference oracle.
require/ensurewritten here are the source for all four
14. Interfaces and Services
ridl names contracts at two levels — the shape and its global declaration — the split the whole SDV industry converged on (AUTOSAR Adaptive ServiceInterface vs Service Instance; VSS branch type vs instance; uProtocol interface vs UUri endpoint):
interface— the abstract shape: a reusable, identity-less group of interactions. A contract type. Defined once, realized by many services. The analogue of a class or a proto service definition.service— a global, named, published declaration of one or more interfaces: the SSOT catalog entry that gives contracts concrete identity in the system, addressable asservice.member. The analogue of a global named instance. What actually gets offered, deployed, and (optionally) discovered.
interface : service :: type : global-named-instance. This section covers both.
14.0 Interface — the abstract shape
An interface groups interactions under a named, reusable contract shape. It
has no identity and no location — it is not a runtime unit, not addressable,
not deployed. It exists to be realized by services (§14.5) and to type them.
Multiple services may share one interface (a second vehicle variant, a family of
sensors). Within one rsdl system each interface is listed by one service,
because the routing key names no service (rsdl §8, RSDL-408); whether ridl
rejects a shared interface across a whole workspace is open (rsdl §17, item 10).
/**
* Main vehicle status contract shape.
* @labels SIL_B, CAL_2, PRIVATE
*/
interface VehicleStatus {
signal currentSpeed : Speed @10ms
signal warnings : WarningFlags @[50ms..1s]
event doorOpened : DoorPayload @[50ms..500ms]
command setGear(position: GearPosition) [
require position != GearPosition.PARK || currentSpeed == 0.0
]
query getAverageSpeed(window: Duration): Speed [
require window > 0ms
ensure result >= 0.0
]
fixed softwareVersion : Version
}
14.1 Rules
- Contains interactions and
reservedtombstones only — type declarations live at package level (they are typl), never inside an interface (RIDL-107) - Flat: there is no interface inheritance.
extendswas considered and rejected (Franca/CORBA precedent notwithstanding): inheritance couples the derived interface's wire identity to base evolution (a base insertion renumbers every child), complicates audit ("what is this service's complete contract?" requires closure computation), and has no faithful mapping to proto services or DBC. Shared shapes are typl; a shared interaction set is composed at the service — a service lists several interfaces (§14.5), so a recurring block keeps its own ordinal space beside every contract it joins and is never flattened into one (§17.2, ADR-0015) - Interface names are
CamelCase; interaction namescamelCase @labelsclassification and profiles as in typl §14.3 — assurance labels (SIL_B, …) gate the test plane's injection rights (concept note §9.2)
14.5 Service — the global published declaration
A service is a global, named declaration that contracts of the given
interface shapes exist in the system. It is the SSOT catalog entry:
system-visible at design time, addressable by name, and the unit that an rsdl
component offers (§14.6) and that deployment realizes.
service veh.adas.cruise : CruiseControl
service veh.body.doors : DoorControl, DiagBlock
service veh.powertrain.motor : MotorControl
- A service has a dotted global name (reverse-domain, like packages) and a
comma-separated set of interfaces after
:(ADR-0015 decision 12, as amended by the lock design): the list's order carries nothing. Its members are addressedservice.member—veh.adas.cruise.engaged,veh.adas.cruise.setLever. - Commas are required between shapes — the one list in the family whose
separators are not optional (ADR-0015 decision 13). The reason is structural:
every other list is terminated by a closing token, and this one is not — it
ends where the next declaration begins. Without required commas the list would
parse greedily, and a mistyped declaration on a following line (
Struct Foo {forstruct Foo {) would be absorbed as another shape, with the error surfacing at the{and saying nothing about the mistake. A trailing comma stays optional. - The name is unique across the system (RIDL-140) — the service catalog is a
flat global namespace, the SSOT every component agrees on. This is what makes
veh.adas.cruise.engagedan unambiguous system-wide address (the VSS/uProtocol model). - A service is always public: it takes no
internalmodifier, and every shape it lists must be public too. Publishing aninternalinterface at a global address is RIDL-143, reported against the offending list element — the address would name a contract no importer can implement. Dropinternalfrom the interface, or give the service an inline shape. A shape that is not an interface at all is RIDL-141, likewise per element. - A service may instead declare one inline shape, for a one-off global contract not worth a reusable interface — never both forms in one declaration (ADR-0015 decision 14):
service veh.hvac.cabin {
signal temperature : Temperature @[1s..10s]
command setTarget(t: Temperature)
}
Composing interfaces — numbers, flat addressing, and the rules
A service composing several interfaces names each in its list, and the list is a set (rsdl decision D-7; the lock design §7 and §9):
-
An interface's number comes from its unit's
interfaces.lock(§11), not from its place in a service's list. An inline shape is an interface too and has its own entry there, keyedservice:followed by the service's dotted name. The list holds no slot and no tombstone: adding an interface to the set, removing one, or reordering the list moves no wire identity, andridl diffreports an addition asservice_interface_addedand a removal asservice_interface_removed, both compatible. A removed interface is retired in the lock —ridl lock <pkg> --retire Name— never in the list, andreservedin a service's list is a parse error. -
Ordinals stay per-interface, and a binding keys the ordinal spaces on (unit, interface number) (ADR-0015 decision 17, as amended by the lock design), not on the interface name and not on list position. Renumbering interactions across a service was rejected: an interface's wire identity would then depend on what else the service happens to carry — the coupling §14.1 rejected inheritance to avoid. Appendix B already maps a SOME/IP eventgroup to an interface, so a multi-interface service maps to several transport-level groupings under one logical name, each keyed on the number.
-
Addressing stays flat (ADR-0015 decision 16): members remain
service.member, so every address written before composition existed stays valid. A member name duplicated across a service's interfaces is a compile error (RIDL-144) —service.statuswith two referents cannot be expressed. The accepted cost is stated rather than discovered: two independently written interfaces that share a member name cannot be composed into one service without renaming one of them. -
The same interface named twice in one service is RIDL-145 — its own code rather than one RIDL-144 per member, which would bury the actual mistake; the duplicate listing is dropped.
-
Extracting an inline shape into a named interface is breaking, by design. The interface's number survives the refactor — the lock entry is renamed,
ridl lock <pkg> --rename service:veh.hvac.cabin=Cabin— but a fallible return's transport identity derives from the enclosing interface name (ADR-0008 decision 4), and an inline shape uses the service's dotted name instead — so extraction rewrites the identity of every fallible query in the shape, andridl-diffclassifies the form switch as breaking. Making that refactor compatible would be a wire-identity decision in its own right (ADR-0015 decision 15). -
Posture-neutral by design. A service declaration says nothing about how it is realized on the wire. The posture — static (its signals/events packed into bus frames, Classic/CAN) or discovered (SOME/IP/DDS/uProtocol, Adaptive) — is a deployment matter, so the same declaration can be realized either way. This is the whole point of ridl being the bus SSOT:
service veh.adas.cruisecan compile down to static CAN signals or a discovered service, no rewrite. One constraint follows from transport physics: acommand/querymember (RPC) cannot be realized on a pure static bus — buses carry dataflow, not calls — so a service deployed statically realizes only itssignal/eventmembers; its control API requires the discovered posture. Deriving the posture per deployment, and enforcing that constraint at deploy time, are deferred: rsdl reserves both (rsdl §12, RSDL-803). -
Offering and redundancy are rsdl concerns (§14.6): a component
offersa service, and exactly one component of a system offers each service (rsdl §8, RSDL-502). Redundancy is derived from that component's instances, never declared by a second offerer (rsdl §7).
Coherence — the published set is simultaneous
The signals of one provided interface are published coherently: the set a provider publishes in one step is a set of values it held simultaneously. A consumer reading two or more of them observes such a set wherever the binding preserves the grouping; where a binding cannot, that is a deploy-time constraint, not a weaker contract. The group is the provided interface, identified by its number in the unit's
interfaces.lock(§11) — an inline shape included, under itsservice:entry. The interface's name, or the service's dotted name for an inline shape, is how the group is written, not what identifies it: a rename keeps the number, and keeps the group.
The rule states production coherence (ADR-0015 decision 9). The three rules below establish that the provider's published set is simultaneous; they establish nothing about what survives an arbitrary transport, which is why the consumer sentence is conditional on the binding. What a consumer observes under a given binding is delivery coherence, treated separately below.
The rule is implicit, not declared — there is no coherent keyword. Three
rules the family already states produce it: §4.2 gives every flow exactly one
owning provider; a provider computes its outputs in one step (rmdl's topological
schedule, rmdl §6); and each provider realizes the service it publishes as a
whole — a component offers a service whole (rsdl §3.2), and one component offers
it (rsdl §8, RSDL-502). So the values a provider publishes in one step are a
simultaneous state by construction. Signals publishing at different rates do not
break this: in a given step some cells are written and others are not, but every
value present is one that provider held at that step, so the observed set
remains a state that existed.
Redundancy does not weaken the rule. A redundant provider set is several instances of the one component that offers the service (§14.6 above, rsdl §7), and the rule then holds of each instance's published set — which is what a consumer reads, since a consumer reads one provider at a time. How a consumer chooses between the instances is arbitration, which rsdl reserves (rsdl §12).
Declaring coherent would declare a consequence of how the platform executes,
which the general form §4.1 deletion test exists to reject. It would also be
false in the one place it appeared to help: marking one interface coherent would
imply the others are not, when all of them are.
Production coherence and delivery coherence
Production coherence and delivery coherence are different, and the difference is a demand on every transport mapping (ADR-0015 decision 10) — in the same sense the §4.4 last-value guarantee already is. The provider producing a coherent set is implicit; whether a consumer observes it coherently depends on the binding:
| Transport | How the grouping is preserved |
|---|---|
| shared memory | one versioned block per group, one generation counter |
| DDS | GROUP-scope PRESENTATION with coherent_access |
| SOME/IP | one notifier per field; preserved only within a field |
| static bus (CAN) | preserved only within one frame; not across frames |
Two consequences follow:
- Where a consumer needs the guarantee to survive an arbitrary binding, the answer is the struct idiom (§17.3): make the values one struct, so they are one payload on one channel — atomic on every transport.
- Where a binding cannot preserve the grouping, that is a deploy-time constraint, not a weaker contract. The precedent is the posture constraint above — a statically deployed service's control API requires the discovered posture — and the owner is the same: the deployment. rsdl lowers only the crossing kind of each link, and the transport is the implementer's configuration (rsdl §10); the check of feasibility against the contract at deploy time is reserved with posture derivation (rsdl §12, RSDL-801).
Appendix B carries the per-target realization.
The generation unit and the addressing unit
A provided interface is the generation unit (ADR-0015 decision 11). Coherence (above), ordinals (§11), and group identity all key on the same boundary, and that convergence is what makes a store and a dispatcher generatable from the IR alone. From one provided interface:
- one store — its signals, laid out as one coherent block behind a single
generation counter; each cell seeded with the payload's init value (§4.4),
carrying provenance and the envelope (§3.1), and evaluated against its own
maxstaleness bound (§9); - one dispatcher — its events, commands, and queries, routed by the interface's single ordinal sequence (§11), with the typl constraints checked before the handler and the response bound applied per its per-kind derivation (§9.3) — around the reply for a query, around acceptance for a command.
The service is the addressing unit: it contributes the global name and the
service.member addresses, never the shape of what is generated. The store and
the dispatcher multiply with the interfaces a service publishes, not with the
service — one store and one dispatcher per published interface, under one
logical name.
14.6 How components relate to services (forward reference)
Interfaces and services are ridl (the contract SSOT); offering services and
requiring interfaces is rsdl. In brief (full treatment in rsdl §3.2 and §8): a
component offers a service whole (implements the members of every
interface the service lists — produces their signals/events, accepts their
commands/queries) and requires each interface it consumes, one per line. An
inline-shape service is required by its dotted name, which names its one
interface. The verbs differ because the sides differ: the service is the unit of
publication and addressing, the interface the unit of contract. A requires
line never names a provider; the compiler resolves it to the one component that
offers the owning service. A pure behaviour (model, rmdl) knows nothing of any
of this — it is a contract-blind reaction; binding a reaction to a component's
interface members is reserved until rmdl is scheduled (rsdl §12). This keeps the
layers clean: ridl declares contracts, rmdl computes, rsdl connects.
15. Conventions
- Signals for state, events for occurrences — if you are tempted to publish an "event" carrying full current state, it is a signal
- Commands mutate, queries read; observable command outcomes return as state (signals), not as return values
- Name signals and fixed values as nouns (
currentSpeed), events as past-tense occurrences (doorOpened), commands as imperatives (setGear), queries asget…/stream… - Reuse error types across queries whose failure domains genuinely coincide —
not one
error enumper query by reflex; compose several with anerror union - Prefer
@[min..max]change-driven signals over strict@Xmsunless a consumer genuinely needs isochronous samples (rmdl clocks do)
A long-running operation is a triple, not a construct. An operation that runs long enough for a consumer to watch it — a calibration, a software update, a route computation, a long user action at the person boundary — is written as three interactions on one interface:
interface Calibration {
command startCalibration(mode: CalMode) @[..50ms] // the goal
signal calibrationProgress : CalProgress @[..1s] // the feedback
event calibrationFinished : CalOutcome @[..1s] // the result
}
The command carries the goal and its response bound covers acceptance alone
(§9.3) — starting the work, not finishing it. The signal carries progress as
state, so a consumer that subscribes late reads where the operation is now
(§4.4) rather than missing the steps that passed before it subscribed. The
event carries the outcome once. Where the result must be fetchable afterwards,
add a query beside the event rather than replacing it: an event is not
replayed (§5.1). Cancellation is a second command.
This is convention, and deliberately not sugar. An action keyword would
generate exactly these three interactions with exactly these ordinals, so it
would add no fact to the IR and no obligation a consumer could rely on — the
general form §4.1 deletion test rejects it. It would also fix three decisions
the author should make per operation: whether progress is state or occurrences,
whether the outcome is an event or a query, and what each of the three bounds
is. What would earn the sugar is a second fact a triple cannot express — a
relation between the three declarations that a generator must know — and the
mechanism for that is the correspondence-obligation pair (§3.3), not a keyword.
16. Diagnostics
Coded RIDL-, grouped by hundreds, same lifecycle rules as typl §16 (codes
never renumbered or reused). The tables below are the RIDL- profile codes
only. A .ridl 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.
16.1 Timing (RIDL-1xx)
| Code | Rule | Severity |
|---|---|---|
| RIDL-100 | signal or event without a timing annotation — default [100ms..1000ms] (or configured [defaults].timing) applied | warning; error if active profile requires explicit timing |
| RIDL-101 | timing range @[X..Y] with X > Y, also an @[X..] on a command or a query whose default maximum is shorter than X (§9.3), unless the range did not parse whole: such a range draws FORM-101 and no RIDL-101 from the default maximum; a range with a bound that drew FORM-102 draws no RIDL-101 | error |
| RIDL-102 | zero or negative timing duration | error |
| RIDL-103 | strict-periodic @Xms on a kind other than signal (§9.2, §9.3) — widened from event only by ADR-0015 | error |
| RIDL-104 | explicit return type on a command | error |
| RIDL-105 | query returning () | error |
| RIDL-106 | timing annotation on fixed, or attribute block on fixed — fixed is the one kind that carries no timing (§9) and no attribute block (§8); narrowed by ADR-0015 | error |
| RIDL-107 | type declaration inside an interface or service body — raised at parse time, where the declaration is recognised and recovered | error |
| RIDL-108 | degenerate timing range @[X..X] — the rate floor equal to its staleness bound (§9.2); every kind that admits the range; also an @[X..] on a command or a query whose default maximum equals X (§9.3), unless the range did not parse whole: such a range draws FORM-101 and no RIDL-108 from the default maximum; a range with a bound that drew FORM-102 draws no RIDL-108 | warning |
| RIDL-109 | signal payload has no derivable init and no = value override (§4.4) | error |
| RIDL-110 | signal = value init override violates the payload constraints (kind, finite float backing domain, range, step, length, pattern, enum discriminant, or supported value spelling) | error |
| RIDL-112 | command or query takes the default response bound (§9.3) — a bare declaration, or the half-open @[min..]; [defaults].command_timing or query_timing, else [..1s] or [..3s], applied | warning; error if active profile requires a response bound |
Init validation — RIDL-110. Signal overrides use the same declared-init validation as typl types and struct fields (typl §5.8). Literal kinds and resolved constant kinds must match the backing, and unresolved references are invalid; numeric values must satisfy range and step constraints, and floats must fit the finite binary64 backing domain; strings must satisfy length and pattern constraints. An enum override must be an exactly integral declared discriminant. Struct, union, tuple, collection and enum-set overrides are rejected because the init syntax defines no value spelling for them. This closes the M2 portion of driftsys/ridl#172; the other items in that issue retain their own dispositions.
16.2 Streams (RIDL-2xx)
| Code | Rule | Severity |
|---|---|---|
| RIDL-201 | stream <T> on a signal or event payload | error |
| RIDL-202 | stream element type not a named type, string, or bytes | error |
16.3 Contracts and Errors (RIDL-3xx)
| Code | Rule | Severity |
|---|---|---|
| RIDL-301 | require or ensure on signal, event, or fixed | error |
| RIDL-302 | ensure on command | error |
| RIDL-303 | fallible query return with no success path — a bare error type in return position, or an error-typed left arm of an inline T | E | error |
| RIDL-304 | error-typed or result-union parameter on a command or query — failure flowing toward a provider | warning |
| RIDL-305 | ensure clause that never references result | warning |
| RIDL-306 | require/ensure expression outside the guaranteed subset (expr-core specification §8) | error |
| RIDL-307 | contract-error category name declared in an error enum (INVALID_VALUE, …) | warning |
| RIDL-308 | named result union in query return position — the inline T | E spelling is canonical there (§10.1) | warning |
16.4 Evolution and Profile (RIDL-4xx)
| Code | Rule | Severity |
|---|---|---|
| RIDL-401 | interaction re-declared under a reserved name | error |
| RIDL-402 | duplicate interaction name in one interaction body — an interface or a service's inline shape | error |
| RIDL-403 | behaviour, user-interaction, or architecture declaration in a ridl context | error |
| RIDL-404 | query named like a mutation — the name begins with set, reset, clear, apply, write, or update followed by an upper-case letter (setGear, resetCounters) | warning |
| RIDL-405 | one error type shared across unrelated failure domains — it is the failure arm of queries in 3 or more interaction scopes (heuristic) | info |
| RIDL-406 | payload struct re-declares envelope metadata (§3.1) — a signal or event payload struct declaring timestamp, time, seq, seqNo, sequence, sequenceNumber, frameCounter, or frameNo; domain time or a domain counter distinct from transport metadata is legitimate | info |
| RIDL-407 | interaction, struct field, or union arm ordinal changed against the published baseline (§11; typl §7.4) — the desk-time drift check, emitted by ridl check, never by ridlc; for a field or arm the desk reads the verdict ridl diff gates on: an insertion, a removal (with or without a tombstone, until the diff reads a body's reserved entries), a reorder or tombstone shift in an edit that adds or removes no member, and an append beside such a change each draw it, and an append alone does not — even a struct field ridl diff reports breaking for its type (typl §7.4), which moves no ordinal — except an arm added to a result union (ADR-0008 decision 4); an enum value's or enum-set bit's reorder changes nothing on any wire (typl §8, §9; ridl diff reports it as enum_reordered, compatible) and draws no warning | warning |
| RIDL-408 | interaction removed, its tombstone dropped or moved, or its retired name redeclared — refused at publication (§11) — an interaction removed with no tombstone, retired with a tombstone at an ordinal other than its own, whose existing tombstone was dropped, or whose retired name is declared live again; emitted by ridl baseline alone, for the interaction level only (the interface level is the lock's — RIDL-409 at the build, RIDL-411 and RIDL-412 at publication — and a whole service removed is not refused, §17.14), which refuses to replace a baseline whose record of the ordinal would be lost | error |
| RIDL-409 | live interfaces.lock entry with no declaration (§11) — the interface it numbers was renamed or removed and the lock does not record which; emitted by the compiler on the entry's own line of the lock file, ridlc and ridl alike; the fix is ridl lock <pkg> --retire Old when the interface is gone, or ridl lock <pkg> --rename Old=New when a declaration without an entry is the same interface under a new name — the message names --retire alone when the package has no declaration without an entry, and ridl check adds a label naming the one --rename when the published baseline shows exactly one declaration without an entry with Old's shape | error |
| RIDL-410 | interfaces.lock is malformed — no next line, next not above every entry's number, one number on two entries, one live key on two entries, or a line that does not parse, git conflict markers included (§11) — emitted by the compiler on the offending line of the lock file, ridlc and ridl alike; resolve the conflict or restore the file from version control, then run ridl lock | error |
| RIDL-411 | provisional interface number refused at publication (§11) — an interface in the snapshot ridl baseline is about to publish has no entry in interfaces.lock; a provisional number is no identity, so the snapshot would record nothing a later comparison can hold the interface to; emitted by ridl baseline alone, at the declaration's name, for a first publication as for a replacement; run plain ridl lock to record the number, then publish | error |
| RIDL-412 | published interface number dropped without a retired entry (§11) — a number the published baseline holds is absent from the fresh snapshot and not among its interfaces.lock retired entries, which is a lock line deleted by hand (a live entry with no declaration is RIDL-409 at the build) or a package or a whole unit removed without retiring its numbers; publishing would lose the only record that the number was allocated; emitted by ridl baseline alone, for a published number other than 0 (a snapshot published before the lock existed carries 0 and is matched by name); restore the entry's line from version control, with retired after the number when the interface is gone, or, for a whole unit removed on purpose, delete that unit's snapshots from .ridl/baseline/ | error |
| RIDL-413 | parameter name declared twice in one parameter list (§6.1, §7.1) — a name identifies one parameter; distinct from RIDL-149, which fires only when the two source names collide after a pinned name transform, because these two source names are already the same | error |
| RIDL-414 | interface members form disconnected type-sharing groups | info |
| RIDL-416 | interfaces.lock in a subdirectory of a unit is not read — the unit's lock is the file beside its ridl.toml | warning |
| RIDL-417 | .rxdl or .rmdl file is not compiled: its profile is not supported — the loader compiles .typl, .ridl and .rsdl files and the rxdl and rmdl profiles have no implementation; emitted by the loader on a .rxdl or .rmdl file in a package directory, on disk or as an overlay, once per file. A lone .rxdl or .rmdl entry is a load error instead | warning |
| RIDL-140 | duplicate service name across the workspace — the service catalog is a flat global namespace | error |
| RIDL-141 | service names a type that is not an interface — and has no inline shape | error |
| RIDL-143 | service publishes an internal interface — a global published address must name a public shape (§14.5) | error |
| RIDL-144 | duplicate member name across a service's interfaces — flat addressing (§14.5) cannot give service.member two referents | error |
| RIDL-145 | the same interface named twice in one service — its own code, so the mistake is one diagnostic rather than one RIDL-144 per member; the duplicate listing holds no slot | error |
| RIDL-146 | retired by the lock (2026-09-15; ADR-0015 decision 18 as amended) — was: interface re-declared under a service-level reserved name; a service's list holds no tombstone | retired |
| RIDL-147 | retired by the lock (2026-09-15; ADR-0015 decision 24 as amended) — was: two shapes of one service whose interface names collide even though their references differ; a binding keys the ordinal spaces on (unit, interface number), so two names may collide | retired |
| RIDL-148 | retired by the lock (2026-09-15; ADR-0015 decision 24 as amended) — was: service-level reserved tombstone without an interface name; reserved in a service's list is a parse error | retired |
| RIDL-149 | two names in one scope collide after a pinned name transform — no transform is injective and no case-folding transform can be, so two names distinct in source can project to one identifier; scoped to the members of one interface, the parameters of one interaction, the fields of one struct, the arms of one union, and the values of one enum. The first three are checked under snake_case; a union's arms are checked under snake_case and camel_case both, because an arm reaches both namespaces and the two collision sets are incomparable; an enum's values are checked under pascal_case alone, because its collision set contains snake_case's. The message names the transform that collided (ADR-0016 decisions 3 and 4, as amended) | error |
| RIDL-150 | payload above the payload ceiling or with no finite bound — the largest encoded size of a signal, event, command or query payload, in an encoding that any [backend.<name>] table of the workspace manifest enables through encodings (FlatBuffers when no table names one), is above 16,256 bytes or has no finite bound; a fallible reply with a success type and an error type is checked through both types; reported on the interaction that carries the payload, so a type no interaction carries is not reported. The payload ceiling is 16,256 bytes, which the codecs do not yet enforce (driftsys/ridl#996; ADR-0030 decisions 2 and 7) | warning |
| RIDL-151 | payload above its declared limit or with no finite bound — a [limits] entry of the unit gives the interaction a limit (its [limits.interaction] entry, else the entry of its kind, else default), and the largest encoded size of its payload, in an encoding that any [backend.<name>] table of the workspace manifest enables (FlatBuffers when no table names one), is above that limit or has no finite bound; a fixed is sized too, which RIDL-150 does not do; a command or query checks its request and its reply against its one limit; reported on the interaction. Where no entry gives a limit, only RIDL-150 applies. A payload above the 16,256-byte ceiling under a declared limit draws both RIDL-150 (warning) and RIDL-151. A package that RIDL-150 skips because it has an error, or depends on a package that has one, gets neither RIDL-151 nor RIDL-152. An error in a manifest stops RIDL-150 in every package, and RIDL-151 and RIDL-152 only in the packages beneath that manifest (ADR-0030 decisions 12 and 14) | error |
| RIDL-152 | a [limits] entry names no interaction of its unit — a [limits.interaction] key must be Interface.member, the interface name, or the last segment of the dotted name of an inline service, without the package, then ., then the member; a key may name an interface of any package of the same unit; a key that names an interface of another unit, one written package.Interface.member, and one whose name is shared by two shapes of the unit (reported as ambiguous) are reported on the key in the unit's ridl.toml (ADR-0030 decision 12) | error |
| RIDL-153 | type too large for podbuf — a struct, tuple or union whose largest podbuf encoding, computed from its declared bounds, is above u32::MAX (4,294,967,295) bytes, because a u32 offset cannot address all of it; reported on the declaration name, naming the type, or, for the argument struct a call with several parameters induces and for a tuple reply, on the interaction, naming its request or reply. Raised only when a [backend.<name>] table of the workspace manifest lists podbuf in its encodings. A type that has no bound only because an array or map inside it is past a descriptor limit draws RIDL-154 instead | error |
| RIDL-154 | array or map exceeds the podbuf descriptor limits — a field with an array or map that declares more than 65,535 elements, or whose element or entry is larger than 65,535 bytes, because the descriptor records the count and the element size as u16; reported on the field, naming the type and the field. Raised only when a [backend.<name>] table lists podbuf in its encodings | error |
| RIDL-155 | deprecated field has no valid podbuf value — a non-optional struct field with a @deprecated doc tag, no declared init value, no derivable init value (typl §5.8) and a type that excludes 0, so the podbuf encoder has no valid value to write for it; reported on the field, naming the type and the field. Raised only when a [backend.<name>] table lists podbuf in its encodings | error |
| RIDL-156 | struct field order wastes podbuf padding — a struct whose declared field order lays out in a podbuf head larger than the head of its fields sorted by descending alignment (declaration order kept among fields of equal alignment), when no baseline snapshot declares the struct; with no baseline every struct is unreleased. Reported on the struct name, naming both head sizes and the saving order. Raised by ridl check only, and only when a [backend.<name>] table of the workspace manifest lists podbuf in its encodings. Lint podbuf-padding-order | info |
RIDL-141 and RIDL-143 apply per interface in the service's list (ADR-0015 decision 18): neither rule changed when composition arrived, only the span each reports against — the offending list element, not the reference position as a whole.
(Classifying a reorder, insert, or delete as breaking or compatible is
ridl-diff's jurisdiction, not the compiler's — typl §7.4 discussion applies.
RIDL-407 is the desk-time warning that an ordinal moved at all: it is emitted by
the ridl check facade against a workspace-local baseline, which is outside
ridlc's source-to-IR function, and it neither classifies nor gates.)
A public interface exposing an internal typl declaration is TYPL-005
(typl §3.3), not a RIDL code: it is the same rule the vocabulary layer states,
applied to the interaction positions — a signal, event or fixed payload, a
command or query parameter, a query return, a tuple-return field, an array or
stream element, either arm of an inline T | E, a collection length bound, and
a constant or enum type named by a require/ensure clause. A service is
RIDL-143 instead, because what leaks there is an interface rather than a type
and a service takes no internal modifier.
The two clause kinds do not expose the same names, because they do not bind the
same names: a require reads the interface's own signals (§13), so a signal
spelled like a package constant shadows it, while an ensure reads no signal
and the same spelling is the constant.
16.5 Families and Obligations (RIDL-5xx)
| Code | Rule | Severity |
|---|---|---|
| RIDL-501 | interaction kind not admitted by its family (§3.2) — an operation on presentation, a continuous value on intent, an occurrence on control, and the rest of that table | error |
| RIDL-502 | operation shape's parameter rule violated (§3.2.1) — parameters on activate/toggle/dismiss, or not exactly one on select/adjust | error |
| RIDL-503 | intent operation with no shape — the shape set is closed and has no generic form (§3.2.1) | error |
| RIDL-504 | non-dispatch interaction declaring no correspondence obligation (§3.3) — a family tag with nothing behind it | warning |
| RIDL-505 | availability predicate on an intent interaction referencing state the consumer cannot evaluate (§3.4) | error |
| RIDL-506 | obligation attribute on a dispatch interaction — between software peers the datum is the truth, so there is nothing to correspond to | error |
| RIDL-507 | derives names an interaction in a family that cannot be its reference (§3.3 direction rule) | error |
| RIDL-508 | a control interaction with no paired acquisition observing its effect (heuristic, §3.3) | info |
17. Open Questions
Each item below is either resolved — its rule now lives in the section it changes, and the row is struck through with a record of what was asked and what answered it — or deferred to a named version, with the observation that reopens it. After the disposition pass of 2026-09-16 three remain deferred, all to ridl v0.3: items 8, 10 and 11. Nothing in this section is normative; the sections it points at are.
-
Selective broadcasts.Resolved — subscription granularity is the interaction (§5.1). The question was whether Franca'sbroadcast selective(per-client delivery) and SOME/IP eventgroups (subscription granularity below the interface) need a ridl surface. Neither is a contract term under the general form §4.1 deletion test. Who receives an occurrence is not the provider's to declare: where the recipient is part of what is being said, it is part of the payload, and a contract that names its consumers is no longer one contract. Grouping for subscription efficiency is a transport-level arrangement a binding derives — the same contract is realized with one eventgroup per interface or with several, and no consumer can tell which from the contract — so it belongs to the binding and to rsdl. §5.1 states the rule and both exclusions; Appendix B keeps the eventgroup mapping. -
Interaction-set reuse.Answered by ADR-0015 — composition, not mixins. Flat interfaces (§14.1) mean recurring interaction patterns — the heartbeat, version and diagnostics triad — used to be duplicated, and the candidate recorded here was compile-time flattening mixins (include DiagBlock). Composition (§14.5: a service carries a comma-separated list of interfaces) is the better answer, and the reason is the identity model: mixins flatten, and ordinals are per-interface (§11), so one shared block folded into three interfaces would get three unrelated ordinal sets and editing the block would renumber all three. ComposingDiagBlockbeside each service's own interface leaves every ordinal space intact and independent of what it sits beside. The accepted cost is §14.5's: interfaces written to be composed must not collide on member names (RIDL-144). -
Signal groups / atomic multi-signal updates.Closed by ADR-0015 — the struct idiom is confirmed. The provisional answer here — a coherent multi-value sample is "make the payload a struct" — holds, and the rule it sits inside is now stated in §14.5: the signals of one provided interface are published coherently, and whether a consumer observes the grouping depends on the binding. A struct payload is one payload on one channel, so it is atomic on every transport — the form to use where the guarantee must survive an arbitrary binding. Where a binding cannot preserve the interface grouping, that is a deploy-time constraint (§14.5), not a weaker contract. -
Actions (long-running operations).Resolved — documented convention, and no sugar (§15). The question was whether the goal + feedback + result composition — ROS 2's action, and the same triple the person boundary needs for a long user operation — is convention or deserves sugar. §15 now carries the worked triple: acommandfor the goal whose bound covers acceptance alone, a progresssignalso a late subscriber reads where the operation is now, a completioneventfor the outcome, and aquerybeside it where the result must be fetchable afterwards. Sugar is not taken: anactionkeyword would expand to exactly those interactions with exactly those ordinals, so it adds no IR fact and no obligation a consumer could rely on — the §4.1 deletion test — while fixing three decisions an author should make per operation. What would earn it is a declared relation between the three declarations, and the mechanism for that is the correspondence-obligation pair (§3.3), not a keyword. -
QoS beyond timing.Answered by ADR-0015 — the absorption principle. The earlier wording here — QoS "deliberately absent from the contract" — described exclusion, and reading it literally is what produced the recurring request for a QoS block. The boundary it drew was correct; the description of it was not. The principle is:ridl expresses QoS as semantic obligation, never as a transport knob. A binding maps each obligation onto its native QoS; a transport lacking the mechanism either satisfies the obligation by construction or fails at bind time.
That is what preserves the §1.1 transport-neutrality claim: the same contract binds to SOME/IP, proto/gRPC, DBC, AIDL, MQTT, or an in-process broker without carrying any of their vocabulary, because it carries none of their mechanisms — only the outcomes a consumer may rely on. Mapping the contract-bearing DDS policy set against ridl:
DDS policy ridl verdict RELIABILITY the interaction kind is the reliability class (§3) covered by construction DURABILITY (TRANSIENT_LOCAL) §4.4 last-value guarantee; events explicitly carry none (§5.1) covered DEADLINE §9 maxstaleness boundcovered LIVELINESS §9 freshness SLO, §10.3 detection covered TIME_BASED_FILTER §9 minrate floor, derived as debouncecovered LIFESPAN §9 max, derived as event TTL (§5.2)covered OWNERSHIP / OWNERSHIP_STRENGTH §4.2 — exactly one owning provider per flow covered by construction DESTINATION_ORDER §3.1 envelope timestamp and per-channel sequence number covered RESOURCE_LIMITS (payload) typl bounded collections and length bounds covered, in the type system HISTORY (KEEP_LAST N) §4 signal is latest-value; §5.1 rules out event replay out of scope, deliberately DURABILITY (PERSISTENT) persist, reserved by ADR-0008 decision 3reserved LATENCY_BUDGET a delivery-delay hint permitting batching; the DDS specification is explicit it is not a commitment no analogue needed, none wanted RPC reply timeout (DDS-RPC) @[..max]response bound oncommand/querycovered — §9.3 (ADR-0015) The table is the contract-bearing subset — PARTITION, TRANSPORT_PRIORITY, the writer/reader lifecycle policies and the reader-side content filters are deployment or reader-local concerns (rsdl). The last row was the one genuine gap, and it is not a core DDS QoS policy at all: the reply timeout lives in DDS-RPC, and its relatives are gRPC's deadline, SOME/IP's configured timeout, and AIDL's transaction timeout. §9.3 closed it.
-
Mid-stream invalid elements(§12.4). Resolved — the element type decides, and it already can. The question was quarantine-and-continue versus abort, and whether the choice is a per-binding policy or a contract-level one. It is neither a policy nor a new construct. An element violating its typl constraints is a Stratum 2INVALID_VALUE, a stream lives inside an RPC (§12.3), and that stratum's RPC surface ends the call (§10.2) — so the stream ends, which is also what §4.5 requires: a consumer that silently receives fewer elements than were sent cannot tell a complete transfer from an incomplete one. A producer whose stream must survive a failing element declares that failure as data by making the element type a result union, which moves it to Stratum 1, where it does not end the call and the consumer handles it element by element. What the element type records is which stratum a failure lands in; whether the producer closes after an error element is behaviour, not contract, and a consumer writes for both because a stream may end at any point in any case. §12.4 states both halves, and the word "quarantined" is gone from it. -
Reflection/discovery service.Resolved — no normativeridl.reflectpackage. The question was whether the spy/control bridge (concept note §9.2) needs a generated meta-interface in the language, to be specified once the IR specification landed. It landed (ADR-0014), and with it the answer moved out of the language: the enumeration surface is the catalog descriptor — a per-unit artifact carrying each interface, its members and its numbers in a form an engine reads without decoding, specified in the runtime-descriptors design and built (described in the catalog descriptor design record), whereridl describeprints it at the desk. Nothing about it is a language construct, and a bridge that exposes that catalog at runtime is an ordinary component with an ordinary ridl service, written where the bridge lives; the bridge itself is a runtime concern the roadmap parks. No name is reserved for it. -
Failure management and safety/HA properties (§10.4). Deferred to ridl v0.3. Failsafe states, fallbacks, degraded modes, health/halt management — a safety/quality-management layer over the runtime's total failure detection. Likely split: supervision topology and fallback wiring in rsdl; supervision hooks in the
ridl-rtruntime spec; assurance policy via@labelsprofiles. The family may later grow declarative property surfaces for this — e.g. a signal's failsafe value in ridl, degraded-mode models in rmdl, failover and voting requirements over the redundant provider sets rsdl derives (rsdl §7) — always properties (what must hold), never mechanisms (how). Reopened by a consumer whose safety argument needs a property the §10.4 division of labour cannot carry. It needs its own document before any keyword lands; start via profile vocabulary, promote to syntax only when earned. -
uxdl divergence budget.Closed by ADR-0012. The question was which rules here are shared with uxdl and which are ridl-only. The answer is that none diverge: uxdl is retired, every rule in this document is family-neutral, and what a non-dispatch family adds is the four correspondence obligations rather than a different rule set. Transports remain a binding concern, not a family one. -
The explicit one-time read. The
acquisitionrequest/response cell is reserved (§3.2). Deferred to ridl v0.3. What separates it from a continuous acquisition is maintenance, not duration: a continuous acquisition obliges the provider to hold a current value, and forcing an on-request read into it would make the binding poll forever for something wanted only occasionally. Periodic and on-change acquisition need no new cell —@[min..max]already distinguishes rate floor from staleness bound (§9.2). Reopened by a world-boundary contract that needs an on-request read of a quantity too expensive to hold current, such as a diagnostic measurement. Until then the cell stays reserved and the combination is RIDL-501; rxdl §11 carries the same question and moves with this one. -
The influence quantity (VIM). A quantity that does not affect the measurand but does affect the relation between indication and result — ambient temperature shifting a pressure sensor's characteristic. It is neither uncertainty, which is dispersion, nor the relationship, because it is what perturbs the relationship. Whether it becomes a fifth obligation or a qualifier on the relationship (§3.3) is deferred to ridl v0.3, because the obligation attributes are themselves working spellings that settle with the obligation implementation, and deciding this one ahead of them would fix a shape with nothing to check it against. Reopened by an acquisition contract whose transfer function depends on a declared environmental condition.
-
Whether the publication gate should refuse a moved existing tombstone(§11). Resolved — it does not, and §11 says why. A baseline that retires an interaction withreserved bat ordinal N, replaced by a source that retires it withreserved bat ordinal M, reportsInteractionReordered, notInteractionRemoved, and the RIDL-408 gate readsInteractionRemovedchanges and the interaction-levelReservedNameRedeclared, neverInteractionReordered. The same is true of a live interaction inserted mid-body or reordered, so a moved tombstone is not a special shape. The gate is not a second compatibility gate: it protects the record of a retirement, and a move leaves that record complete — every name in the published snapshot still carries an ordinal. The move is breaking,ridl diffreports it and exits 1, andridl checkwarns with RIDL-407 at the desk. §11 now states the division of labour between the two. -
A whole package removed or renamed bypasses the publication gate(§11). Resolved — the interaction level does not refuse it, the interface level does.ridl-diffreports oneDeclRemovedfor a package whose whole unit is present only on the baseline side and does not descend into it; for a package removed from a unit that remains it reports each shape on its own line. Either way the RIDL-408 gate sees no refused interaction, and publication deletes the package's snapshot along with every ordinal record it held. The interface numbers the package held are refused at publication (RIDL-412) unless the unit'sinterfaces.lockretires them or another package of the unit declares them. This reverses the earlier resolution, which rested on the language having no package-level retirement: with one catalog per unit, the unit's lock retires the numbers of a package removed from a unit that remains. For a whole unit removed or renamed, the refusal catches the accidental case (a moved directory, a changed manifest) that would otherwise lose the record silently; the deliberate case costs one deletion of that unit's snapshots from.ridl/baseline/, which the message names. What would reopen the question is a unit-level retirement construct.ridl diffreports the removal as breaking. -
A whole service removed, or a service whose form switches, bypasses the publication gate(§11). Resolved — neither is refused, because neither moves an identity.ridl-diffreports oneDeclRemovedfor a service present only on the baseline side and oneServiceChangedfor a service whose form switches between inline and named (ADR-0015 decision 15), neither of which the RIDL-408 gate reads. The interface half of this item closed with the lock (2026-09-15): an interface is identified by itsinterfaces.locknumber, so a whole interface removed with its entry live fails the build (RIDL-409), and a published number the fresh snapshot neither carries nor retires is refused at publication (RIDL-412) — an inline shape's number included, so an inline-form service removed whole is refused for its shape's number unless the entry is retired, while the service itself is not refused. The service half closes the same way: a named-form service carries no number of its own, its list is a set (§14.5) the gate does not read, and a form switch that leaves the inline shape's lock entry behind with no declaration is already RIDL-409 at the build — with the entry renamed instead, it is the sanctioned refactor, still breaking on the wire for the fallible-return identity (ADR-0008 decision 4) and reported so byridl diff.
Appendix A — Full Example
package veh.cluster
import veh.common.Speed
import veh.common.Temperature
import veh.common.MAX_SPEED
import veh.common.GearPosition
import veh.common.WarningFlags
// --- vocabulary local to this contract (typl declarations, package level) ---
struct DoorPayload {
sensorId : integer [0..15]
isOpen : boolean
}
struct DiagFilter {
severity : integer [0..5]
category : Label?
}
struct FaultEvent {
code : integer [0..65535]
message : Message
timestamp : Timestamp
}
error enum DiagError { // failure vocabulary — typl §10.1
FILTER_INVALID = 0
STORAGE_BUSY = 1
ACCESS_DENIED = 2
}
struct FaultPage {
faults : [FaultEvent; 0..64]
}
// --- the contract ---
/**
* Main vehicle status interface.
* @labels SIL_B, CAL_2, PRIVATE
*/
interface VehicleStatus {
/// Current vehicle speed — isochronous, drives rmdl clocks downstream
signal currentSpeed : Speed @10ms
/// Engine temperature — change-driven, 100ms freshness SLO
signal engineTemp : Temperature @[20ms..100ms]
/// Active warnings — last-value delivered on subscribe (§4.4)
signal warnings : WarningFlags @[50ms..1s]
/// Raised on every door state change; stale after 500ms
event doorOpened : DoorPayload @[50ms..500ms]
/// Request a gear change — outcome observed via currentGear, not returned;
/// callers throttle to one request per 20ms, acceptance within 200ms (§9)
command setGear(position: GearPosition) [
require position != GearPosition.PARK || currentSpeed == 0.0
] @[20ms..200ms]
reserved resetCounters // retired ordinal — never reused
/// Sliding-window average — replied within 50ms (§9)
query getAverageSpeed(window: Duration): Speed [
require window > 0ms
ensure result >= 0.0
] @[10ms..50ms]
/// Fault history as a finite stream
query streamFaults(filter: DiagFilter): <FaultEvent> @[..1s]
/// Paged fault snapshot — fallible query via inline `T | E` (§10.1)
query getFaultPage(filter: DiagFilter): FaultPage | DiagError @[..100ms]
fixed softwareVersion : Version
fixed capabilities : [Label; 0..32]
}
Appendix B — Codegen Targets
Interaction mapping per target. Type/width mapping is typl Appendix D; the two compose.
| ridl | SOME/IP | proto3 / gRPC | AIDL | DDS | MQTT / AsyncAPI |
|---|---|---|---|---|---|
signal | field notifier (+ auto-derived getter from last-value cache) | server-streaming RPC or pub/sub sidecar | callback / oneway listener | topic, TRANSIENT_LOCAL durability, DEADLINE = max | retained message on channel |
| coherent-set delivery (§14.5) | per-field only — no grouping across notifiers | per-message only — no grouping across streams | per-parcel only — one callback per signal | GROUP-scope PRESENTATION with coherent_access | per-message only, per channel |
event | event (eventgroup) | server-streaming RPC | callback | topic, VOLATILE durability | non-retained publish |
command | request w/ empty response (= ack, §6.1) | unary RPC → Empty (= ack) | oneway + runtime ack shim | reliable-QoS request topic (DDS ack) | publish QoS 1 (puback = ack) |
query | request/response method | unary/streaming RPC | method | request/reply (RPC over DDS) | request/reply channel pair |
RPC response bound, max (§9.3) | derived request/response timeout, supervised by the binding | server-side deadline; per-call client override stays Stratum 3 | transaction-timeout supervision in the runtime shim | DDS-RPC reply timeout | reply-channel timeout |
RPC call throttle, min (§9.3) | admission-side rate limit at the provider | server-side admission rate limit | admission check in the runtime shim | requester-side rate limit | inbound publish rate limit |
fixed | field with getter only | unary getter RPC (cacheable) | constant/property | — | retained provisioning channel |
| result-union error arm (§10.1) | method return code table | google.rpc.Status + typed detail | ServiceSpecificException code | reply union arm | error payload schema |
| ordinals (§11) | method ID = ordinal; event ID = ordinal + event flag; eventgroup = interface, keyed on (unit, interface number) | RPC name (identity is nominal) | in the runtime's binder contract, as code or field | topic name suffix | channel path segment |
| Stratum 2 (§10.2) | E_MALFORMED_MESSAGE / E_NOT_OK / E_UNKNOWN_METHOD | INVALID_ARGUMENT / FAILED_PRECONDITION / UNIMPLEMENTED | IllegalArgumentException mapping | reply status | error topic convention |
Notes. The command delivery acknowledgment (§6.1) is realised with each
transport's cheapest confirmed primitive, as shown in the command row — where a
transport's fire-and-forget primitive has no confirmation (SOME/IP fire&forget,
AIDL oneway), the binding uses the confirmed variant or adds a runtime shim;
the ack never surfaces in generated application APIs as a return value. The AIDL
column names what a runtime's binder contract can use: ridl specifies no Binder
layout and no transaction code, so the ordinal stays stable whether that
contract carries it as a transaction code or as a field
(frame specification §11.2, which records that
driftsys/ridl#516 reversed a per-interface binding with the ordinal as the
transaction code). The §4.4 last-value guarantee is what makes the SOME/IP
getter derivable — a ridl signal generates the full SOME/IP field triple minus
setter (setters are explicit commands, by design). DBC/CAN binds signals only
(event/command/query do not exist on classic CAN — profile error), and a
coherent set survives only within one frame (§14.5); a set that must stay
simultaneous on every target is one struct payload (§17.3). The WASM component
target maps query streams to WIT stream<T> (native in WASI 0.3) — the
rmdl-behind-an-interface story stays whole in the browser.
Appendix C — Formal Grammar (EBNF)
The ridl profile adds to the typl grammar (typl Appendix E — definition gains
interface_def and service_def; everything else inherited):
definition = [ "internal" ] ( typl_definition | interface_def )
| service_def ; (* a service takes no `internal` — §14.5 *)
interface_def = doc_comment? "interface" CamelCase_id "{" { interaction sep? } "}" ;
service_def = doc_comment? "service" dotted_name
( ":" service_shape { "," service_shape } [ "," ]
(* the list, a set — §14.5; commas are
REQUIRED between shapes, the one
non-optional separator in the family
(ADR-0015 decision 13): the list has
no closing token, so it ends where
the next declaration begins *)
| "{" { interaction sep? } "}" ) ; (* one inline shape — §14.5; never both
forms (ADR-0015 decision 14) *)
service_shape = type_ref ; (* a composed interface — §14.5; the
list is a set with no tombstone: a
removed interface is retired in
`interfaces.lock` (§11) *)
dotted_name = camelCase_id { "." camelCase_id } ; (* reverse-domain global name,
every segment lowercase — §14.5 *)
interaction = signal_def | event_def | command_def | query_def | fixed_def | reserved ;
signal_def = doc_comment? "signal" camelCase_id ":" type_ref init_value? timing?
attr_block? ;
event_def = doc_comment? "event" camelCase_id ":" type_ref init_value? timing?
attr_block? ;
(* the parser admits an init; writing one is FORM-102 *)
(* untimed → configurable default [100ms..1000ms], §9.1 *)
(* There is no family clause. The family (§3.2) is selected by the
declaring keyword, and `.ridl` spells only `dispatch` — so every
production here is a dispatch interaction. The non-dispatch
spellings, and the operation shapes, are the rxdl reference's;
they lower to these same kinds carrying a family and a shape.
The rules they can violate are still this document's, reported
as RIDL-501 through RIDL-508 (§16.5). *)
init_value = "=" ( literal | SCREAMING_SNAKE_ID ) ; (* bare init override — §4.4 *)
command_def = doc_comment? "command" camelCase_id "(" param_list ")" ( ":" return_type )?
timing? attr_block? ;
(* the parser admits a return type; writing one is RIDL-104, §6.1 *)
query_def = doc_comment? "query" camelCase_id "(" param_list ")" ":" return_type
timing? attr_block? ;
(* range form only on command and query — a strict period is RIDL-103, §9.3 *)
(* no error syntax — a fallible_type return makes a query fallible, §10.1 *)
fixed_def = doc_comment? "fixed" camelCase_id ":" fixed_type init_value? timing?
attr_block? ;
(* the parser admits an init and both annotations; an init is
FORM-102; either annotation is RIDL-106, §8, §9.2 *)
param_list = "" | param { "," param } ;
param = doc_comment? camelCase_id ":" param_type ;
param_type = type_ref | stream_type ;
return_type = type_ref | tuple_type | fallible_type | stream_type ;
fallible_type = type_ref "|" type_ref ; (* inline T | E — §10.1, gf §6.1;
both arms are named types, the
right one an `error` type *)
fixed_type = type_ref | array_type ; (* array_type per typl grammar *)
(* correspondence obligations (§3.3) are ordinary attributes in the tail
`attr_block` — assignment form for tolerance, resolution, lag, span and
`derives`; predicate form for `plausible` and `ensure`. They are legal
only on a non-dispatch family — RIDL-506 *)
stream_type = "<" ( type_ref | "string" | "bytes" ) ">" ;
(* ---------- Timing ---------- *)
timing = "@" duration
| "@" "[" timing_range "]" ;
timing_range = duration ".." duration | duration ".." | ".." duration ;
duration = int_lit ( "us" | "ms" | "s" | "min" | "h" ) ; (* §2.1 *)
(* ---------- Attribute Block ---------- *)
attr_block = "[" { attribute sep? } "]" ;
attribute = "require" expr | "ensure" expr ;
expr = (* expr-core specification §3.1 — the guaranteed subset; §13 *) ;
(* reserved, sep, type_ref, tuple_type, identifiers, literals: typl Appendix E *)
Appendix D — Standards References
Additions over typl Appendix C:
| Reference | Used for |
|---|---|
| AUTOSAR Classic R22-11 | signal/event split (isQueued), CalibrationParameter, signal groups |
| SOME/IP (AUTOSAR PRS) | field/method/event mapping, return codes, IDs |
| Franca IDL | attribute/method/broadcast precedent, error enums, rejected version block |
| OMG DDS 1.4 / DDS-RPC | durability, deadline, liveliness — QoS-in-contract comparison |
| gRPC / google.rpc.Status | status model, streaming RPC |
| Android AIDL | oneway, transaction codes |
| Fuchsia FIDL | method ordinals (hash model rejected), protocol evolution |
| WASM Component Model / WIT | stream<T>/future<T> (WASI 0.3), resource model |
| AsyncAPI 3.0 | channel/operation vocabulary for the MQTT binding |
| ROS 2 | msg/srv/action decomposition |
| Eclipse uProtocol / COVESA uServices | contemporary transport-neutral automotive service layer |
| Eiffel, SPARK Ada | Design by Contract |
Appendix E — Prior Art Survey
What each interface language contributed to, or was rejected from, this design:
| Language | Taken / rejected |
|---|---|
| CORBA IDL | the interface grouping and the cautionary tale: inout params, exceptions as control flow, and interface inheritance all rejected here as evolution and codegen hazards — data-carrying failures are error structs in result unions instead |
| Franca IDL | closest European-automotive ancestor: attributes ≈ signals, broadcasts ≈ events, fireAndForget ≈ command, error enums → typl error types. Rejected: extends, in-language version, selective broadcasts (→ §5.1), deployment .fdepl files (rsdl + ridl.toml instead) |
| AUTOSAR Classic | the signal/event split (isQueued), CalibrationParameter → fixed, signal groups → the struct idiom (§14.5). ridl exists partly to be the legible source ARXML is generated from |
| SOME/IP | field triple (getter derivable from §4.4, setter as explicit command, notifier = signal), return codes → Stratum 2 mapping, method/event IDs → ordinal derivation |
| DDS | the QoS lesson: DEADLINE/durability/liveliness are contract-relevant — ridl promotes exactly the state-vs-occurrence and freshness subset into the language (@, §4.4) and leaves reliability/history to deployment (§17.5). DDS proves both that QoS matters and that 22 orthogonal QoS policies on one topic is too many degrees of freedom for a contract |
| gRPC / proto | streaming model (§12 direction-by-position), google.rpc.Status → Stratum 2 categories, Empty-returning unary → command mapping. Rejected: errors as open-ended status strings — ridl functional errors are closed typl types, and errors are data, not a status channel |
| AIDL | oneway validates fire-and-forget as a first-class kind; transaction-code stability → ordinals |
| FIDL | strict/flexible interaction evolution and hashed method ordinals studied; hashing rejected (renames silently break wire, IDs unreadable from source) in favour of positional ordinals + reserved |
| WIT / component model | stream<T>/future<T> (now native in WASI 0.3) confirm the stream container at the interface boundary; the WASM backend maps ridl streams onto them directly |
| AsyncAPI | naming for the MQTT/broker binding; validates "schema language + interaction language as siblings over one type vocabulary" (its schemas are JSON Schema — structurally the typl/ridl split) |
| ROS 2 | msg/srv ≈ signal/query; actions (goal+feedback+result) written as the composition idiom → §15 |
| OPC-UA | industrial precedent for typed nodes with subscriptions; its reference model is heavier than a contract language needs — profiles/labels carry the assurance metadata instead |
| MQTT | retained messages are the §4.4 last-value guarantee in the wild; Sparkplug's birth/death certificates prefigure provider-activation semantics (initial publish, §4.4) |
| uProtocol / COVESA uServices | the contemporary validation: transport-neutral pub/sub+RPC over SOME/IP·Zenoh·MQTT with proto-defined services. ridl differs by owning the vocabulary layer (typl ranges/units vs bare proto types), timing in the contract, and contracts/behaviour above it |
Appendix F — Coverage Analysis: Other Interface Languages
Method as in typl Appendix G: can ridl express what a contract author reaches for in each major interface language? ✓ covered, ≈ covered differently (usually stricter), ✗ not expressible (deliberate or open).
| Foreign construct | ridl equivalent | Status |
|---|---|---|
| CORBA/Franca attribute (readable, subscribable state) | signal + §4.4 last-value | ✓ |
| attribute setter | explicit command (never implicit) | ≈ deliberate — mutation is always a visible verb |
| Franca broadcast | event | ✓ |
| Franca selective broadcast (per-client) | — (the recipient is a payload field, §5.1) | ✗ deliberate §5.1 |
| Franca/AIDL fireAndForget / oneway | command | ✓ first-class |
| method with error (Franca error enum, SOME/IP return code) | result-union return; error arm → native error channel | ✓ |
| exception hierarchies with payloads (CORBA, Java) | error struct / error union arms in result unions | ≈ deliberate — closed sets, no hierarchies, errors are data |
| gRPC unary / server-stream / client-stream / bidi | query T / query : <T> / command(<T>) or query(<T>) / query(<T>): <U> | ✓ |
| gRPC deadline | @[..max] response bound on command/query (§9.3); the per-call override stays Stratum 3 | ✓ in-contract (ADR-0015) |
| DDS DEADLINE QoS | @[..max] refresh ceiling / freshness SLO | ✓ in-contract |
| DDS durability (TRANSIENT_LOCAL) | §4.4 last-value guarantee (signals) | ✓ normative, not tunable |
| DDS reliability / history depth | — (transport/rsdl) | ✗ deliberate §17.5 |
| DDS liveliness | staleness via freshness bounds + observability | ≈ |
| SOME/IP field get/set/notify | signal (get derived, notify native) + command (set) | ✓ decomposed |
| SOME/IP eventgroups | no equivalent: a consumer subscribes per interaction (§5.1), and Appendix B maps an eventgroup to an interface | ✗ deliberate — grouping for subscription efficiency is the binding's |
| AUTOSAR signal groups (atomic sample) | struct payload on one signal | ≈ idiom, confirmed (§14.5, ADR-0015) |
| AUTOSAR client/server + sender/receiver ports | query/command + signal/event | ✓ |
| AIDL in/out/inout parameters | parameters in, tuple returns out | ≈ deliberate — no out-params |
| FIDL events (server-initiated on protocol) | event | ✓ |
| FIDL strict/flexible evolution | ordinals + reserved + ridl-diff categories | ≈ |
| ROS 2 action (goal/feedback/result) | command + progress signal + result query/event | ≈ composition idiom §15 |
| WIT resource (capability handle with methods) | — | ✗ open — relevant when rmdl/WASM host interfaces mature |
| WIT future / stream | query return / <T> | ✓ |
| AsyncAPI channel + operation | interaction + transport binding | ✓ |
| MQTT retained message | §4.4 | ✓ |
| OPC-UA historical access | — (test/observability plane, not contract) | ≈ relocated |
| service discovery (SOME/IP-SD, DDS discovery) | rsdl topology + the catalog descriptor (§17.7) | ≈ relocated |
interface versioning (Franca version, SOME/IP major/minor) | package version + ridl-diff | ≈ relocated, mechanical |
Verdict. ridl covers the working set of every surveyed interface language
through five kinds plus two orthogonal clauses (timing, contracts) — where the
others reach for per-feature keywords (field triples, selective broadcasts,
exception clauses, QoS matrices), ridl decomposes into the same five primitives,
expresses failure as vocabulary, and pushes tunables to deployment. The honest
gap is WIT-style resource handles; per-client delivery and the action idiom's
lack of sugar are decided exclusions rather than gaps (§5.1, §15), and neither
blocks v0.2. What no surveyed language has that ridl does: timing as first-class
contract with derived freshness SLOs, the three-strata error model with
errors-as-data (no error syntax at all) and derived Stratum 2, typl vocabulary
(units/ranges) under every payload, and one evolution mechanism (ordinals +
reserved + diff) uniform from struct fields to interface methods.
Appendix G — Glossary
| Term | Definition |
|---|---|
| RIDL (capitals) | the platform and family name; ridl (lowercase) is this language, the family's interaction layer and flagship member |
| 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 |
| interaction family | who is on the other side of a boundary — dispatch · presentation · intent · acquisition · control (§3.2). A property of the declaration, a closed enum in the IR |
| correspondence obligation | what a non-dispatch contract must declare about the gap between a datum and its referent: relationship, uncertainty, latency of correspondence, failure to correspond (§3.3) |
| reference / realisation | in a non-dispatch family, the causally upstream and downstream sides; the realisation must correspond to the reference within declared bounds (§3.3) |
| operation shape | activate · toggle · select · adjust · dismiss — a closed set constraining an intent operation's parameters and carrying machine-readable gesture semantics (§3.2.1) |
| indication | metrology's term (VIM) for the value a measuring instrument provides; a presentation-family value is one (§3.3) |
| acquisition span | the interval over which a value acquired by sweeping corresponds to the world, as against an instant offset (§3.3) |
| profile | the restriction of the family grammar accepted by a file extension; .ridl accepts interactions + typl declarations; .rxdl is the total profile accepting every layer |
| core | a reusable semantic unit beneath the surface languages: ns (namespacing), typl-core (types), expr (predicates), time (timing), interact (interaction primitives) |
| interaction | a named, typed, directed exchange on a contract boundary; ridl defines five kinds |
| signal | pub/sub interaction carrying a continuous state value — latest sample matters, intermediate samples may be missed |
| event | pub/sub interaction carrying a discrete occurrence — every occurrence matters, queued not coalesced |
| command | fire-and-forget RPC at the contract level — no functional reply; the runtime carries a delivery acknowledgment beneath it |
| delivery acknowledgment (ack) | runtime-level confirmation that a command was received and accepted (validated, precondition passed) — or negatively acknowledged with a Stratum 2 category; enables retries and supervision, never visible in the contract |
| query | request/response RPC — reply mandatory; an inline T | E return makes it fallible |
| fixed | a value provisioned externally (build/factory/FOTA), immutable for the software-instance lifetime, safe to cache |
| provider | the component that owns an interface — the one component that offers the service listing it (rsdl §3.2, §8): publishes its signals/events, executes its commands/queries |
| consumer | any component bound to an interface it does not own: subscribes, calls |
| state vs occurrence | the load-bearing distinction behind signal/event: state exists while unchanged and may be cached; an occurrence happens once and is meaningful individually |
| timing annotation | the @ clause making publication timing part of the contract: @Xms strict periodic or @[min..max] |
| rate floor / staleness bound | the one generic meaning of min / max on any timing range (§9): minimum interval between publications / maximum age on envelope timestamps before the value or occurrence is stale |
| debounce / refresh ceiling | the signal derivation of rate floor / staleness bound (§9): coalesce updates faster than min / re-publish at least every max even unchanged |
| throttle / TTL | the event derivation of rate floor / staleness bound (§9): the provider must not raise faster than min / an occurrence older than max is discarded |
| default timing | the compile-time-configurable range ([100ms..1000ms], ridl.toml [defaults].timing) applied to untimed signals and events, with command_timing and query_timing ([..1s], [..3s]) for the response bound of a command or query; always resolved to concrete bounds in the IR |
| freshness SLO | the alertable staleness bound derived from a signal's timing — the contract's definition of "late" |
| last-value guarantee | §4.4: a signal channel is never empty — subscribing delivers a value immediately (init before first publication, latest published value after); the normative demand behind broker caches, MQTT retained, DDS TRANSIENT_LOCAL |
| late joiner | a subscriber that binds after publication began; served by the last-value guarantee on signals, receives nothing retroactive on events |
| quarantine | withholding a constraint-violating payload from application code, with the violation recorded. No quarantine is silent: an invalid stream element ends the call (§12.4), a signal's invalidity is channel state (§4.5), and an invalid event is still delivered (§5.1) |
| functional error (Stratum 1) | a domain-level failure expressed as data — the error arm of a fallible query's return; the provider answered: no |
| contract error (Stratum 2) | an implicit, standardized violation derived from the contract itself: INVALID_VALUE, PRECONDITION_FAILED, CONTRACT_BROKEN, UNKNOWN_INTERACTION — never declared, never an error-type value |
| transport error (Stratum 3) | infrastructure failure — detected, undeclared (general form §6.4): a timeout, broker loss or reset, observed by the runtime and carried by runtime types, with no vocabulary in the contract language |
| error type | a typl enum/struct/union carrying the error modifier — failure vocabulary, ordinary data (typl §10.1) |
fallible query / inline T | E | a query whose return names a success type and one error type — the family's Result<T, E>, written at the signature (§10.1, general form §6.1); bindings map the error arm to native transport error channels, exhaustively handled in every codegen target |
| result union | the two-arm named union (one success arm, one error arm) of typl §10.2 — legal data anywhere, but in query-return position it draws RIDL-308 steering to the inline spelling |
require / ensure | pre-/postcondition contract clauses (expr core); violations are Stratum 2, and each assertion also runs as CI property test, online observer, and rmdl oracle check |
| stream | the <T> container: an unbounded element sequence, valid only in interaction position; direction determined by position (parameter = consumer produces, return = provider produces) |
| ordinal | an interaction's implicit 1-based declaration-order identity within its interface; source of transport IDs (typl §7.4 model) |
reserved | tombstone keeping a retired interaction's ordinal slot occupied so wire identities are never reused |
| append-only | the evolution discipline implied by ordinals: new interactions at the end, deletions by tombstone, reorder = wire break |
| interface | the abstract contract shape — a reusable, identity-less group of interactions; a contract type, realized by services (interface : service :: type : instance) |
| service | a global, named, published declaration of an interface — the SSOT catalog entry, addressed service.member; posture-neutral (can deploy static or discovered; deriving the posture is reserved, rsdl §12); what a component offers |
| service catalog | the flat global namespace of all service declarations — the system-wide SSOT of contracts |
| posture | how a service is realized on the wire — static (bus signals/events, Classic) or discovered (SOME/IP/DDS/uProtocol, Adaptive); a deployment matter, not in the contract; deriving it per deployment is reserved (rsdl §12) |
| binding | generated per-transport code realising a contract: validation, caching, error mapping, (de)serialization |
| envelope | runtime-supplied metadata on every interaction instance — timestamp + sequence number (scoped per interaction kind) — never declared, never in payloads; powers timing evaluation, dedup, loss detection, E2E counters, and replay (§3.1) |
| system time | the platform's one synchronized time base (gPTP/PTP or shared realtime clock) — an assumed platform property; envelope timestamps live in it and are comparable system-wide (§3.1) |
| epoch (platform) | 1970-01-01 00:00:00 TAI (the PTP epoch); platform time = int64 microseconds since it — continuous, leap-second-free; civil datetime is presentation only |
| init value | the value a signal channel holds before the provider's first publication — the payload type's init (typl §5.8) or the signal's bare = value override (§4.4); no keyword — init is rmdl's alone |
| invalid state | the propagated channel state entered when a received payload violates typl constraints — visible to all subscribers with last-good value retained; realised as SNA sentinels on CAN (§4.5) |
| provenance (channel) | the subscriber-visible origin of a signal's current value: init / live / invalid |
| broker | the asynchronous message plane between components; ridl contracts cross it, rmdl models never do (the sync/async wall) |
| IR | the stable serialized intermediate representation — resolved names, types, timings, ordinals — consumed by every backend and tool |
ridl-diff | the IR-comparison tool classifying contract changes as breaking/compatible; plumbing-grade CI gate enforcing the evolution rules |
| profile (assurance) | an external plug-in validating @labels vocabulary and escalating optional rules (explicit timing, explicit bounds) to errors — distinct from grammar profile |
| SSOT | single source of truth — the design goal: one contract file from which bindings, docs, tests, and topologies derive |
End of ridl Language Reference v0.2.0.