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 by ridl-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 | E returns (§7, §10.1, Appendix A, Appendix C), generic min/max timing 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

  1. Scope and Position in the Family
  2. Lexical Additions
  3. The Interaction Model — families (§3.2), correspondence obligations (§3.3), availability (§3.4)
  4. Signal
  5. Event
  6. Command
  7. Query
  8. Fixed
  9. Timing Annotations
  10. Errors
  11. Interaction Identity and Evolution
  12. Streams
  13. Contracts — require / ensure
  14. Interfaces and Services
  15. Conventions
  16. Diagnostics
  17. Open Questions

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:

AdditionCoreSurface
interaction kindsinteractinterface, signal, event, command, query, fixed
timingtime@Xms, @[min..max], duration literals
contractsexprrequire, 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:

SuffixUnitExampleMicroseconds
usmicroseconds500us1
msmilliseconds10ms1 000
sseconds1s1 000 000
minminutes5min60 000 000
hhours1h3 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":

KeywordPatternSemanticInitiator
signalpub/subcontinuous state value — the latest sample mattersprovider publishes
eventpub/subdiscrete occurrence — every occurrence mattersprovider publishes
commandRPCfire-and-forget action — no reply channelconsumer calls
queryRPCrequest/response — reply mandatoryconsumer calls
fixedprovisionedimmutable for the software-instance lifetimeneither (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 fieldMeaning
timestampwhen the instance was published/raised/called — stamped by the sender, at the producing binding, before transmission
sequence numbermonotonic 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).

FamilyDirectionThe provider's promise
dispatchsystem → systemtransfer, nothing interpreted
presentationsystem → personI offer this to be perceived
intentperson → systemI capture what you meant
acquisitionworld → systemI report what is
controlsystem → worldI 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

Familycontinuous (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:

ShapeMeaningParameters
activateinvokenone
toggleflip a binary statenone
selectchoose one from a setexactly one — the key
adjustset a value within a rangeexactly one — a ranged/unit type
dismisscancel or close a transientnone

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.

FamilyReferenceRealisation
presentationthe provider's valuewhat the person perceives
intentwhat the person meantthe captured value
acquisitionthe physical quantitythe reported value
controlthe commanded valuethe resulting world state

The four obligations

ObligationPresentationAcquisitionControl
relationshipnever under-reads; quantisedtransfer function; calibrationauthority limits; slew rate
uncertaintydisplay resolutiontolerance per readingpositioning tolerance
latency of correspondenceperception delaysample instant ≠ publication instantactuation lag
failure to correspondshown but not perceivableplausible but falsecommanded 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 termHere
measurandwhat an acquisition interaction names
calibration — the relation between quantity values and indicationsthe relationship obligation
measurement uncertaintythe uncertainty obligation
indication — the value a measuring instrument providesa presentation-family value
detector — indicates presence when a threshold is exceededan acquisition occurrence
metrological traceability — an unbroken chain of calibrations, each contributing to the uncertaintythe 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.

SourceExampleCarried by
modeonly while the system is in ERRORduring — a state gate
datadisabled until a value validatesa predicate over declared state
in progressalready running, cannot be re-invokeda predicate over a declared progress value
policynot permitted in this conditiona predicate over declared policy state
provisioningthis variant does not have the interactionfixed — 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 = value or derived). A signal may override it with the same bare = value idiom, 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 @Xms is 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 named struct (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, use query
  • 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 query returning T | E — the language forces that honesty
  • require permitted (§13); ensure not (nothing to observe)
  • A range timing annotation is permitted, and an undeclared response bound draws a warning (§9.3): the max bound is a response bound on acceptance — the delivery acknowledgment below, never execution — and the min bound 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 → observe currentGear). 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, use command (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
  • require and ensure permitted (§13); ensure constrains result
  • Functional failure is expressed in the return type: the inline T | E form makes the query fallible — §10.1. There is no throws clause in ridl
  • A range timing annotation is permitted, and an undeclared response bound draws a warning (§9.3): the max bound is a response bound on the reply, and the min bound 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 fixed is free of the query machinery — bindings expose it as a plain accessor, populated at binding initialization
  • Naming decision on record: fixed, not final and not config — one word for one concept at every boundary (ADR-0011); fixed is family-neutral, because a provisioned constant carries no correspondence anywhere. final was the earlier spelling and misled: a Java or Kotlin reader takes it for a compile-time constant, which this is not. config connotes 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:

BoundOn a signal (state)On an event (occurrence)On a command / query (call)
min — rate floordebounce — a faster update is coalesced into the next samplethrottle — the provider must not raise occurrences fastercall throttle — the caller must not call faster
max — staleness boundrefresh ceiling — re-publish even if unchangedTTL — an occurrence older than max is discardedresponse 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-diff compares 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:

  • min is the call throttle — the minimum interval between calls. It constrains the caller, not the provider. That is not an inconsistency: min always 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.
  • max is the response bound — the provider must respond within it. What responding means is derived per kind. For a query it is the reply. For a command it 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 | E in 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 union before 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 inline T | E
  • command remains 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:

CategoryTriggerRPC surface (caller sees)Pub/sub surface
INVALID_VALUEpayload violates typl constraints (range, step, bounds, pattern) at a boundarystandard error, distinct from any result-union error arm — gRPC INVALID_ARGUMENT, SOME/IP E_MALFORMED_MESSAGEsignal: channel transitions to invalid state, propagated to subscribers (§4.5); event: occurrence delivered marked invalid (§5.1)
PRECONDITION_FAILEDa require clause evaluates falsestandard error — gRPC FAILED_PRECONDITION, SOME/IP E_NOT_OKcommand: negative ack to calling runtime + observability (§6.2)
CONTRACT_BROKENan ensure clause evaluates false — a provider bugstandard error to caller; incident-grade telemetry on providern/a
UNKNOWN_INTERACTIONordinal/version mismatch between peers (§11)standard error — SOME/IP E_UNKNOWN_METHODsubscription 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 | E return — 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-rt supervision hooks), informed by @labels assurance 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
  • reserved tombstones 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 baseline refuses 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 no reserved line at all; it is retired with a reserved line, but at an ordinal other than the one it held; the baseline already retired it and the source has dropped the reserved line; 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 by ridl 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 diff and 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 diff says so and exits 1, and ridl check warns 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.lock retires 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 diff reports 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 diff reports the removal and the form switch as 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, and ridl-diff decides 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-diff per 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

PositionDirection
return typeprovider produces, consumer consumes
parameterconsumer produces, provider consumes
bothbidirectional, 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
]
AttributeMeaningValid on
require <expr>precondition over parameters and the interface's own signalscommand, 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 (a require may read currentSpeed — 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/ensure written 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 as service.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 reserved tombstones only — type declarations live at package level (they are typl), never inside an interface (RIDL-107)
  • Flat: there is no interface inheritance. extends was 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 names camelCase
  • @labels classification 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 addressed service.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 { for struct 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.engaged an unambiguous system-wide address (the VSS/uProtocol model).
  • A service is always public: it takes no internal modifier, and every shape it lists must be public too. Publishing an internal interface at a global address is RIDL-143, reported against the offending list element — the address would name a contract no importer can implement. Drop internal from 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, keyed service: 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, and ridl diff reports an addition as service_interface_added and a removal as service_interface_removed, both compatible. A removed interface is retired in the lock — ridl lock <pkg> --retire Name — never in the list, and reserved in 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.status with 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, and ridl-diff classifies 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.cruise can compile down to static CAN signals or a discovered service, no rewrite. One constraint follows from transport physics: a command/query member (RPC) cannot be realized on a pure static bus — buses carry dataflow, not calls — so a service deployed statically realizes only its signal/event members; 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 offers a 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 its service: 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:

TransportHow the grouping is preserved
shared memoryone versioned block per group, one generation counter
DDSGROUP-scope PRESENTATION with coherent_access
SOME/IPone 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 max staleness 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 as get…/stream…
  • Reuse error types across queries whose failure domains genuinely coincide — not one error enum per query by reflex; compose several with an error union
  • Prefer @[min..max] change-driven signals over strict @Xms unless 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)

CodeRuleSeverity
RIDL-100signal or event without a timing annotation — default [100ms..1000ms] (or configured [defaults].timing) appliedwarning; error if active profile requires explicit timing
RIDL-101timing 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-101error
RIDL-102zero or negative timing durationerror
RIDL-103strict-periodic @Xms on a kind other than signal (§9.2, §9.3) — widened from event only by ADR-0015error
RIDL-104explicit return type on a commanderror
RIDL-105query returning ()error
RIDL-106timing 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-0015error
RIDL-107type declaration inside an interface or service body — raised at parse time, where the declaration is recognised and recoverederror
RIDL-108degenerate 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-108warning
RIDL-109signal payload has no derivable init and no = value override (§4.4)error
RIDL-110signal = value init override violates the payload constraints (kind, finite float backing domain, range, step, length, pattern, enum discriminant, or supported value spelling)error
RIDL-112command 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], appliedwarning; 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)

CodeRuleSeverity
RIDL-201stream <T> on a signal or event payloaderror
RIDL-202stream element type not a named type, string, or byteserror

16.3 Contracts and Errors (RIDL-3xx)

CodeRuleSeverity
RIDL-301require or ensure on signal, event, or fixederror
RIDL-302ensure on commanderror
RIDL-303fallible query return with no success path — a bare error type in return position, or an error-typed left arm of an inline T | Eerror
RIDL-304error-typed or result-union parameter on a command or query — failure flowing toward a providerwarning
RIDL-305ensure clause that never references resultwarning
RIDL-306require/ensure expression outside the guaranteed subset (expr-core specification §8)error
RIDL-307contract-error category name declared in an error enum (INVALID_VALUE, …)warning
RIDL-308named result union in query return position — the inline T | E spelling is canonical there (§10.1)warning

16.4 Evolution and Profile (RIDL-4xx)

CodeRuleSeverity
RIDL-401interaction re-declared under a reserved nameerror
RIDL-402duplicate interaction name in one interaction body — an interface or a service's inline shapeerror
RIDL-403behaviour, user-interaction, or architecture declaration in a ridl contexterror
RIDL-404query 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-405one error type shared across unrelated failure domains — it is the failure arm of queries in 3 or more interaction scopes (heuristic)info
RIDL-406payload 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 legitimateinfo
RIDL-407interaction, 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 warningwarning
RIDL-408interaction 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 losterror
RIDL-409live 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 shapeerror
RIDL-410interfaces.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 lockerror
RIDL-411provisional 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 publisherror
RIDL-412published 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-413parameter 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 sameerror
RIDL-414interface members form disconnected type-sharing groupsinfo
RIDL-416interfaces.lock in a subdirectory of a unit is not read — the unit's lock is the file beside its ridl.tomlwarning
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 insteadwarning
RIDL-140duplicate service name across the workspace — the service catalog is a flat global namespaceerror
RIDL-141service names a type that is not an interface — and has no inline shapeerror
RIDL-143service publishes an internal interface — a global published address must name a public shape (§14.5)error
RIDL-144duplicate member name across a service's interfaces — flat addressing (§14.5) cannot give service.member two referentserror
RIDL-145the 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 sloterror
RIDL-146retired 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 tombstoneretired
RIDL-147retired 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 collideretired
RIDL-148retired 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 errorretired
RIDL-149two 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-150payload 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-151payload 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-152a [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-153type 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 insteaderror
RIDL-154array 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 encodingserror
RIDL-155deprecated 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 encodingserror
RIDL-156struct 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-orderinfo

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)

CodeRuleSeverity
RIDL-501interaction 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 tableerror
RIDL-502operation shape's parameter rule violated (§3.2.1) — parameters on activate/toggle/dismiss, or not exactly one on select/adjusterror
RIDL-503intent operation with no shape — the shape set is closed and has no generic form (§3.2.1)error
RIDL-504non-dispatch interaction declaring no correspondence obligation (§3.3) — a family tag with nothing behind itwarning
RIDL-505availability predicate on an intent interaction referencing state the consumer cannot evaluate (§3.4)error
RIDL-506obligation attribute on a dispatch interaction — between software peers the datum is the truth, so there is nothing to correspond toerror
RIDL-507derives names an interaction in a family that cannot be its reference (§3.3 direction rule)error
RIDL-508a 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.

  1. Selective broadcasts. Resolved — subscription granularity is the interaction (§5.1). The question was whether Franca's broadcast 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.

  2. 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. Composing DiagBlock beside 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).

  3. 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.

  4. 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: a command for the goal whose bound covers acceptance alone, a progress signal so a late subscriber reads where the operation is now, a completion event for the outcome, and a query beside it where the result must be fetchable afterwards. Sugar is not taken: an action keyword 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.

  5. 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 policyridlverdict
    RELIABILITYthe 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 max staleness boundcovered
    LIVELINESS§9 freshness SLO, §10.3 detectioncovered
    TIME_BASED_FILTER§9 min rate floor, derived as debouncecovered
    LIFESPAN§9 max, derived as event TTL (§5.2)covered
    OWNERSHIP / OWNERSHIP_STRENGTH§4.2 — exactly one owning provider per flowcovered by construction
    DESTINATION_ORDER§3.1 envelope timestamp and per-channel sequence numbercovered
    RESOURCE_LIMITS (payload)typl bounded collections and length boundscovered, in the type system
    HISTORY (KEEP_LAST N)§4 signal is latest-value; §5.1 rules out event replayout of scope, deliberately
    DURABILITY (PERSISTENT)persist, reserved by ADR-0008 decision 3reserved
    LATENCY_BUDGETa delivery-delay hint permitting batching; the DDS specification is explicit it is not a commitmentno analogue needed, none wanted
    RPC reply timeout (DDS-RPC)@[..max] response bound on command/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.

  6. 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 2 INVALID_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.

  7. Reflection/discovery service. Resolved — no normative ridl.reflect package. 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), where ridl describe prints 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.

  8. 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-rt runtime spec; assurance policy via @labels profiles. 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.

  9. 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.

  10. The explicit one-time read. The acquisition request/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.

  11. 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.

  12. 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 with reserved b at ordinal N, replaced by a source that retires it with reserved b at ordinal M, reports InteractionReordered, not InteractionRemoved, and the RIDL-408 gate reads InteractionRemoved changes and the interaction-level ReservedNameRedeclared, never InteractionReordered. 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 diff reports it and exits 1, and ridl check warns with RIDL-407 at the desk. §11 now states the division of labour between the two.

  13. A whole package removed or renamed bypasses the publication gate (§11). Resolved — the interaction level does not refuse it, the interface level does. ridl-diff reports one DeclRemoved for 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's interfaces.lock retires 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 diff reports the removal as breaking.

  14. 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-diff reports one DeclRemoved for a service present only on the baseline side and one ServiceChanged for 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 its interfaces.lock number, 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 by ridl 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.

ridlSOME/IPproto3 / gRPCAIDLDDSMQTT / AsyncAPI
signalfield notifier (+ auto-derived getter from last-value cache)server-streaming RPC or pub/sub sidecarcallback / oneway listenertopic, TRANSIENT_LOCAL durability, DEADLINE = maxretained message on channel
coherent-set delivery (§14.5)per-field only — no grouping across notifiersper-message only — no grouping across streamsper-parcel only — one callback per signalGROUP-scope PRESENTATION with coherent_accessper-message only, per channel
eventevent (eventgroup)server-streaming RPCcallbacktopic, VOLATILE durabilitynon-retained publish
commandrequest w/ empty response (= ack, §6.1)unary RPC → Empty (= ack)oneway + runtime ack shimreliable-QoS request topic (DDS ack)publish QoS 1 (puback = ack)
queryrequest/response methodunary/streaming RPCmethodrequest/reply (RPC over DDS)request/reply channel pair
RPC response bound, max (§9.3)derived request/response timeout, supervised by the bindingserver-side deadline; per-call client override stays Stratum 3transaction-timeout supervision in the runtime shimDDS-RPC reply timeoutreply-channel timeout
RPC call throttle, min (§9.3)admission-side rate limit at the providerserver-side admission rate limitadmission check in the runtime shimrequester-side rate limitinbound publish rate limit
fixedfield with getter onlyunary getter RPC (cacheable)constant/property—retained provisioning channel
result-union error arm (§10.1)method return code tablegoogle.rpc.Status + typed detailServiceSpecificException codereply union armerror 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 fieldtopic name suffixchannel path segment
Stratum 2 (§10.2)E_MALFORMED_MESSAGE / E_NOT_OK / E_UNKNOWN_METHODINVALID_ARGUMENT / FAILED_PRECONDITION / UNIMPLEMENTEDIllegalArgumentException mappingreply statuserror 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:

ReferenceUsed for
AUTOSAR Classic R22-11signal/event split (isQueued), CalibrationParameter, signal groups
SOME/IP (AUTOSAR PRS)field/method/event mapping, return codes, IDs
Franca IDLattribute/method/broadcast precedent, error enums, rejected version block
OMG DDS 1.4 / DDS-RPCdurability, deadline, liveliness — QoS-in-contract comparison
gRPC / google.rpc.Statusstatus model, streaming RPC
Android AIDLoneway, transaction codes
Fuchsia FIDLmethod ordinals (hash model rejected), protocol evolution
WASM Component Model / WITstream<T>/future<T> (WASI 0.3), resource model
AsyncAPI 3.0channel/operation vocabulary for the MQTT binding
ROS 2msg/srv/action decomposition
Eclipse uProtocol / COVESA uServicescontemporary transport-neutral automotive service layer
Eiffel, SPARK AdaDesign by Contract

Appendix E — Prior Art Survey

What each interface language contributed to, or was rejected from, this design:

LanguageTaken / rejected
CORBA IDLthe 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 IDLclosest 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 Classicthe 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/IPfield triple (getter derivable from §4.4, setter as explicit command, notifier = signal), return codes → Stratum 2 mapping, method/event IDs → ordinal derivation
DDSthe 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 / protostreaming 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
AIDLoneway validates fire-and-forget as a first-class kind; transaction-code stability → ordinals
FIDLstrict/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 modelstream<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
AsyncAPInaming 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 2msg/srv ≈ signal/query; actions (goal+feedback+result) written as the composition idiom → §15
OPC-UAindustrial precedent for typed nodes with subscriptions; its reference model is heavier than a contract language needs — profiles/labels carry the assurance metadata instead
MQTTretained 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 uServicesthe 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 constructridl equivalentStatus
CORBA/Franca attribute (readable, subscribable state)signal + §4.4 last-value✓
attribute setterexplicit command (never implicit)≈ deliberate — mutation is always a visible verb
Franca broadcastevent✓
Franca selective broadcast (per-client)— (the recipient is a payload field, §5.1)✗ deliberate §5.1
Franca/AIDL fireAndForget / onewaycommand✓ 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 / bidiquery 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 livelinessstaleness via freshness bounds + observability≈
SOME/IP field get/set/notifysignal (get derived, notify native) + command (set)✓ decomposed
SOME/IP eventgroupsno 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 portsquery/command + signal/event✓
AIDL in/out/inout parametersparameters in, tuple returns out≈ deliberate — no out-params
FIDL events (server-initiated on protocol)event✓
FIDL strict/flexible evolutionordinals + 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 / streamquery return / <T>✓
AsyncAPI channel + operationinteraction + 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

TermDefinition
RIDL (capitals)the platform and family name; ridl (lowercase) is this language, the family's interaction layer and flagship member
familythe 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 familywho 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 obligationwhat 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 / realisationin a non-dispatch family, the causally upstream and downstream sides; the realisation must correspond to the reference within declared bounds (§3.3)
operation shapeactivate · toggle · select · adjust · dismiss — a closed set constraining an intent operation's parameters and carrying machine-readable gesture semantics (§3.2.1)
indicationmetrology's term (VIM) for the value a measuring instrument provides; a presentation-family value is one (§3.3)
acquisition spanthe interval over which a value acquired by sweeping corresponds to the world, as against an instant offset (§3.3)
profilethe restriction of the family grammar accepted by a file extension; .ridl accepts interactions + typl declarations; .rxdl is the total profile accepting every layer
corea reusable semantic unit beneath the surface languages: ns (namespacing), typl-core (types), expr (predicates), time (timing), interact (interaction primitives)
interactiona named, typed, directed exchange on a contract boundary; ridl defines five kinds
signalpub/sub interaction carrying a continuous state value — latest sample matters, intermediate samples may be missed
eventpub/sub interaction carrying a discrete occurrence — every occurrence matters, queued not coalesced
commandfire-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
queryrequest/response RPC — reply mandatory; an inline T | E return makes it fallible
fixeda value provisioned externally (build/factory/FOTA), immutable for the software-instance lifetime, safe to cache
providerthe 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
consumerany component bound to an interface it does not own: subscribes, calls
state vs occurrencethe load-bearing distinction behind signal/event: state exists while unchanged and may be cached; an occurrence happens once and is meaningful individually
timing annotationthe @ clause making publication timing part of the contract: @Xms strict periodic or @[min..max]
rate floor / staleness boundthe 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 ceilingthe signal derivation of rate floor / staleness bound (§9): coalesce updates faster than min / re-publish at least every max even unchanged
throttle / TTLthe event derivation of rate floor / staleness bound (§9): the provider must not raise faster than min / an occurrence older than max is discarded
default timingthe 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 SLOthe 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 joinera subscriber that binds after publication began; served by the last-value guarantee on signals, receives nothing retroactive on events
quarantinewithholding 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 typea typl enum/struct/union carrying the error modifier — failure vocabulary, ordinary data (typl §10.1)
fallible query / inline T | Ea 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 unionthe 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 / ensurepre-/postcondition contract clauses (expr core); violations are Stratum 2, and each assertion also runs as CI property test, online observer, and rmdl oracle check
streamthe <T> container: an unbounded element sequence, valid only in interaction position; direction determined by position (parameter = consumer produces, return = provider produces)
ordinalan interaction's implicit 1-based declaration-order identity within its interface; source of transport IDs (typl §7.4 model)
reservedtombstone keeping a retired interaction's ordinal slot occupied so wire identities are never reused
append-onlythe evolution discipline implied by ordinals: new interactions at the end, deletions by tombstone, reorder = wire break
interfacethe abstract contract shape — a reusable, identity-less group of interactions; a contract type, realized by services (interface : service :: type : instance)
servicea 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 catalogthe flat global namespace of all service declarations — the system-wide SSOT of contracts
posturehow 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)
bindinggenerated per-transport code realising a contract: validation, caching, error mapping, (de)serialization
enveloperuntime-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 timethe 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 valuethe 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 statethe 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
brokerthe asynchronous message plane between components; ridl contracts cross it, rmdl models never do (the sync/async wall)
IRthe stable serialized intermediate representation — resolved names, types, timings, ordinals — consumed by every backend and tool
ridl-diffthe 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
SSOTsingle 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.