CLI reference
Two command-line binaries ship from this repository: ridl, the porcelain
facade, and ridlc, the plumbing compiler underneath it. ridl hosts the
language server as the ridl lsp subcommand and the Model
Context Protocol server as ridl mcp, both over stdio (see
What is built). The VS Code extension spawns
ridl lsp. Build both with:
cargo build --release
as Getting started describes.
This page assumes target/release/ridl and target/release/ridlc are on your
PATH, or called by path.
Only typl and ridl have a toolchain — see What is built. Every command and transcript below runs against a workspace built from those two layers; rxdl, rmdl, and rsdl accept no command here.
Every synopsis and transcript on this page is the literal output of the
binaries built from this repository, run against constructed fixtures. The
fixtures themselves are not reproduced here — this page documents command
behavior, not how to write a .ridl file, and a fixture built for one
transcript is often a one-line edit of the fixture built for the transcript
before it. Getting started is the tutorial that shows
complete, worked files; treat a transcript below as a faithful record of what
the tool printed against some input shaped as described, not as a listing you
can paste in whole. Every exit code stated below was observed by running the
command and reading its exit status, not read out of a help string or a
specification.
ridl --version and its short form ridl -V (and ridlc --version) print the
binary's own name and version and exit 0:
ridl --version
ridl X.Y.Z
X.Y.Z stands for the build's version: the crate version for a local build,
or the release tag when the release workflow sets RIDL_BUILD_VERSION. The
version does not name a commit, so it cannot answer "which commit is this" —
recorded as a known gap in ADR-0010 decision 8, which also records
a deferred fix.
ridl
ridl --help
The RIDL toolchain
Usage: ridl <COMMAND>
Commands:
check Type-check a file, package, or workspace (defaults to the current directory)
baseline Publish the current workspace as a baseline: one `<pkg-name>.ir.json` snapshot per package and one `<unit>.catalogs` file per unit with an interface, written to `.ridl/baseline/` at the workspace root
build Compile to the selected artifacts (defaults to the current directory)
test Run the property suite over a workspace: the range self-corpora and the contract-clause sampling (ridl §13). Exit 0 when every run passes, 1 on a self-corpus failure or an evaluation error, 2 on a compile error
fmt Reformat `.typl`, `.ridl` and `.rsdl` files in place (defaults to the current directory)
diff Compare two IR snapshots or source trees and classify the change: exit 0 compatible or identical, 1 breaking, 2 error
lock Allocate a number to every interface that has none and write each unit's `interfaces.lock`; with `--rename` or `--retire`, rewrite one unit's entries in place instead. Exit 0 when the file is written or nothing changes, 1 on a diagnostic error, 2 on a bad flag or a path or I/O failure. `ridl lock merge` is the git merge driver for the file
lsp Run the language server over stdio: exit 0 after `shutdown` then `exit`, 1 when the client ends the session without `shutdown`, 2 on a transport error. Editors spawn this. Stdio is the only transport
mcp Run the MCP server over stdio for an agent host: exit 0 on a clean shutdown, 2 on a transport error. It takes no flag of its own
init Scaffold a new unit in a directory that exists (defaults to the current directory): a package, or with `--workspace` a workspace and one member. Writes `ridl.toml` and one source file, and exits 2 without writing anything when a target file exists, the name is not a legal package name or holds a reserved word, or a `ridl.toml` above or below the directory would make a manifest inside a unit (MANI-013)
new Create a directory, with its parents, and scaffold a new unit in it, as `ridl init` does. Exits 2 without writing anything when the path exists
describe Print a catalog descriptor as strict JSON, after verifying it
vendor Copy every remote unit of the workspace's `[imports]` into `vendor/` at the manifest root, so a `--frozen` build needs no network and no cache. Every directory in `vendor/` that no `[imports]` key names is removed; files and hidden entries are left. Exit 0 when `vendor/` was written or there was nothing to change, 1 on a diagnostic error (`vendor/` is left untouched; `ridl.lock` may be updated), 2 on a missing or unreadable path, a fetch failure, or an I/O failure. With `--check`, compare `vendor/` with the imports and `ridl.lock` instead, writing nothing
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
Running ridl with no subcommand at all prints this same text to stderr
and exits 2; ridl --help prints it to stdout and exits 0 — the two
routes carry identical text but are not interchangeable in a script that
checks the exit code or reads the right stream.
ridl lsp and ridl mcp are the two stdio servers this binary hosts — the
language server an editor drives, and the Model Context Protocol server an
agent drives. ridl mcp takes no argument or flag; ridl lsp takes only the
two flags editor clients pass, which change nothing. Each is documented in its
own section below, ridl lsp and ridl mcp, and
each exits 0 when the client shuts it down and 2 when the transport ends
before the handshake or otherwise fails.
ridl check
ridl check --help
Type-check a file, package, or workspace (defaults to the current directory)
Usage: ridl check [OPTIONS] [PATH]
Arguments:
[PATH] [default: .]
Options:
--frozen Verify remote imports against `ridl.lock` without fetching or regenerating it (CI mode, ADR-0002 §7)
--baseline <DIR|FILE> Compare the checked workspace against a published baseline — a directory of `.ir.json` snapshots or one snapshot file — and warn (RIDL-407) on every interaction whose ordinal moved and every struct field or union arm change `ridl diff` gates on that concerns an ordinal: a member inserted, one removed, one moved in an edit that added or removed no member, and one appended beside such a change or to a result union. An append that is breaking only for its field's type moves no ordinal and draws no warning. Without the flag, `.ridl/baseline/` at the workspace root is used when it exists
--format <FORMAT> Output format for the report: text renders to stderr (the default); json goes to stdout instead — see the CLI reference (docs/book/cli-reference.md) for its schema; sarif writes one SARIF 2.1.0 log to stdout, for code-scanning viewers [default: text] [possible values: text, json, sarif]
-h, --help Print help
PATH is a .typl, .ridl or .rsdl file, a package directory, or a
workspace root, and defaults to the current directory. A .rxdl or .rmdl
file is refused as PATH when no ridl.toml is at or above it, as the next
paragraph says. A PATH inside a workspace member loads the whole workspace
whose members lists the member (ADR-0002 §4): the root's
[lints], the [defaults] keys and [imports] apply, imports of sibling members
resolve, and the report holds only the diagnostics of files under the member.
ridl build on a member also writes the whole workspace. An error in another
member makes it exit 1 with one error that says so; it then writes nothing, or,
for an RSDL-7xx error, leaves that deployment out of the system. The workspace
root's .ridl/baseline/ and ridl.lock serve a member entry too. --frozen is the same flag
ridlc check --frozen documents, forwarded unchanged; the two
commands share one implementation and render byte-identical diagnostics on the
same input. It now carries this description word for word under ridl too —
before ADR-0010, ridl check --help rendered it with a blank line
where the description belongs.
The loader compiles the .typl, .ridl and .rsdl files of a package
directory. A .rxdl or .rmdl file there is not compiled, because the rxdl
and rmdl profiles have no implementation (driftsys/ridl#68): each one draws the
warning RIDL-417 (unsupported-source-file) on the file itself, and its
declarations are not in the build. A bare .rxdl or .rmdl file given as the
entry with no ridl.toml at or above it is refused: every command that loads
source through the loader, such as ridl check, ridl build and ridl diff,
exits 2 with an error that names the extension as unsupported, and compiles
nothing.
It writes ~/.ridl/cache and ridl.lock, but only when the manifest
declares [imports]. ridl check loads, resolves, and checks the
workspace. When the checked-out manifest (or a package in it) has an
[imports] table, it also materializes each remote unit. It reads the unit from
vendor/<key>/ at the manifest root when that directory exists and ridl.lock
pins a tree for the unit's URL (see ridl vendor); otherwise
from ~/.ridl/cache (ADR-0002 §7, indexed by URL and by the fetched artifact's
SHA-256) or, on a cache miss outside --frozen, fetches it. Where ridl.lock
pins no tree for the unit, the cache or the fetch pins it first, and a
vendored directory, if present, is then verified against that pin and read in
place of the cache entry. It verifies the
tree hash of the tree it read, which for a vendored tree that no longer
matches is MANI-107, with a message that names ridl vendor; loads the unit
as a read-only dependency; and on a clean run regenerates ridl.lock at the
workspace root with both hashes. A remote unit's errors fail the check, and its
warnings are not shown. Every other fixture on this page has no [imports], so
none of them reach either write. The transcript below is an illustration, not a
recorded run: the URL names a local HTTP stub, and the two hashes are
placeholders that stand for the values a run computes.
ridl check && cat ridl.lock
# ridl.lock — generated by ridlc; do not edit by hand.
# Regenerated on every successful resolution (ADR-0002 §7).
[entries."http://127.0.0.1:8934/remotepkg.tar"]
sha256 = "<the SHA-256 of the fetched artifact>"
tree = "<the hash of the unpacked tree>"
ridl build and ridl baseline materialize remote units the same way, and
ridl build generates their packages beside the workspace's own. No IR dump,
catalog, baseline snapshot or ridl lock entry includes a remote unit.
--frozen verifies against the existing ridl.lock instead of fetching or
regenerating it, exactly as its description says: with no ridl.lock on disk
yet, ridl check --frozen over the same workspace fails closed (MANI-103,
exit 1) rather than writing one. A ridl.lock written before the tree hash
existed has no tree; under --frozen that is MANI-108, and a run without
--frozen adds the hash.
ridl lock, ridl test, ridl diff, ridl lsp and ridl mcp read
vendor/<key>/ and then the cache the way --frozen does: they never fetch and
never write ridl.lock. In a fresh clone, run ridl check once first, or run
ridl vendor; otherwise these commands report MANI-103 (no ridl.lock entry)
or MANI-104 (the pin is neither in vendor/ nor in the cache).
A ridl.lock written before the tree hash existed gives them MANI-108
too, and the same ridl check adds the hash.
Exit codes. 0 when nothing is wrong — a clean run prints nothing at all, to either stream. 1 when a diagnostic is an error, for example a range whose minimum exceeds its maximum:
ridl check
error[TYPL-104]: range minimum 250 is greater than maximum 0
┌─ ./demo.ridl:3:19
│
3 │ type Speed : km/h [250.0..0.0 step 0.5]
│ ^^^^^^^^^^^^^^^^^^^^^
The same run with --format json prints the JSON diagnostic contract, also
produced by ridl_core::diag::to_json, as a bare array:
ridl check --format json
[
{
"code": "TYPL-104",
"severity": "error",
"message": "range minimum 250 is greater than maximum 0",
"span": {
"path": "./demo.ridl",
"start": {
"line": 3,
"column": 19
},
"end": {
"line": 3,
"column": 40
}
},
"labels": [],
"fixes": []
}
]
A diagnostic whose code is a lint carries one more field, lint,
which holds the lint name, for example "lint": "unbounded-float" next to
"code": "TYPL-102". The field is absent for a code that is not a lint and
for a diagnostic with no code. severity is always the effective severity,
after the levels in the [lints] table of ridl.toml are applied, so a lint
set to deny reports "severity": "error" and a lint set to allow is not in
the array. The text report names the lint in a note after the snippet
(= lint: ...), as the examples below show.
--format sarif writes one SARIF 2.1.0
log to stdout, for code-scanning viewers such as GitHub code scanning. The log
holds one run. Its tool.driver.rules lists every catalogue code, errors
included, with the lint name as the rule name when the code is a lint and the
default level as defaultConfiguration.level. Each result has the code as its
ruleId, the effective level (error, warning, or note for an info), the
message, and one location; each label becomes a related location. Columns
count Unicode code points (columnKind is unicodeCodePoints). The log
carries no fix-its, which the JSON output carries, and no helpUri. A
diagnostic with no source file, such as a lockfile or remote-fetch diagnostic
(MANI-1xx), has no location. An uncoded diagnostic has no ruleId and no
ruleIndex. An artifact URI is relative to the current working directory,
with / separators and every segment percent-encoded; it carries uriBaseId
%SRCROOT%, which the run's originalUriBaseIds resolves to the working
directory as a file:// URI. A file outside the working directory is an
absolute file:// URI with no uriBaseId. The comparison is lexical, not
through the filesystem: a relative path is joined onto the working directory,
and an absolute path is compared with the working directory as the operating
system spells it, so an absolute path that reaches the working directory
through a symbolic link (/tmp on macOS, for example) counts as outside it.
Run ridl check --format sarif from the repository root, with the path to
check given relative to it, so that every URI is relative to the root, which
is what a code-scanning upload expects. The exit code is the same as for the
other formats.
2 when the workspace itself cannot be found:
ridl check
error: no `ridl.toml` found at or above `.`
An explicit --baseline naming a path that does not exist is the same exit
2, for the same reason — asking for a baseline that is not there is a mistake
worth reporting, while a missing default baseline is silently skipped:
ridl check --baseline ./nope
error: the baseline `./nope` does not exist
A baseline directory that holds IR artifacts but no .ir.json snapshot —
named with --baseline or auto-discovered at .ridl/baseline/ — is also
exit 2, with a message naming an artifact it found: a baseline stays
.ir.json (ADR-0014 decision 5), so such a directory is a
baseline in a refused encoding, not the silently skipped "no baseline
published yet" state. An empty directory is still that silent state when
auto-discovered; named explicitly with --baseline, it is a different
refusal (below). A snapshot-named entry inside the directory that cannot be
stat'ed — a symlink to a file that is gone — is exit 2 under either, naming
the entry, because skipping it would read the directory as one snapshot
short.
So is a directory whose .ir.json snapshots sit one level below it rather
than inside it — --baseline .ridl where .ridl/baseline was meant. The
message names the subdirectory it found and gives the path to pass instead.
Snapshots are read from one directory and never from the directories below
it, so a directory of this shape is a path aimed one level too high, not a
layout to descend into.
An explicit --baseline that yields no snapshot at all — an empty directory,
or one whose snapshots sit two or more levels down rather than one — is the
same exit 2: the message names the directory, says to point --baseline at
the directory that holds the snapshots (ridl baseline publishes them to
.ridl/baseline/ at the workspace root), and only then offers
ridl baseline --out <directory> for publishing a first baseline into that
directory. The offer is left out when the directory is a source tree — it
holds a ridl.toml, or a .typl, .ridl or .rsdl file, directly inside
it — since publishing a baseline there would write snapshots next to the
sources. Auto-discovery asserts nothing about a baseline being present, so a
.ridl/baseline/ directory found this way, empty or absent, keeps the
silent skip the two paragraphs above do not touch.
The baseline desk check. With .ridl/baseline/ present at the workspace
root — written by ridl baseline — ridl check compares
the workspace against it and warns (RIDL-407) on every interaction whose
declaration order moved, and every struct field or union arm change ridl diff gates on that concerns an ordinal. At its default level the warning does
not change the exit code; RIDL-407 is the lint ordinal-changed, so
ordinal-changed = "deny" in [lints] reports it as an error,
which makes the run exit 1, and ordinal-changed = "allow" removes it. The
warning for a field or arm
follows the verdict the gate reads, so the two agree: a member inserted, one
removed, one moved in an edit that added or removed no member, and one
appended beside such a change each draw one warning, and an append alone
draws none — not even one the gate reports breaking because the field's type
excludes 0 (driftsys/ridl#598) — except an arm added to a result union, whose arms are its
transport identity (ADR-0008 decision 4). A removal draws one whether or not
a reserved tombstone keeps the slot, because the diff does not yet read a
composite body's reserved entries. The diff reports no reorder beside an
addition or a removal, so the warning for the added or removed member names
the siblings whose ordinal changed. Reordering two events in a published
interface:
ridl check
warning[RIDL-407]: `doorOpened` has moved in `VehicleStatus` since the published baseline (position 1 there, position 2 here). Declaration order is the wire identity of an interaction (ridl §11), so a consumer built against the baseline would now bind this slot to a different interaction — put the declarations back in the baseline's order and add new ones at the end
┌─ ./demo.ridl:7:3
│
7 │ event doorOpened : DoorState @[100ms..1s]
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
│
= lint: `ordinal-changed` (set its level in `[lints]` in ridl.toml)
warning[RIDL-407]: `doorClosed` has moved in `VehicleStatus` since the published baseline (position 2 there, position 1 here). Declaration order is the wire identity of an interaction (ridl §11), so a consumer built against the baseline would now bind this slot to a different interaction — put the declarations back in the baseline's order and add new ones at the end
┌─ ./demo.ridl:6:3
│
6 │ event doorClosed : DoorState @[100ms..1s]
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
│
= lint: `ordinal-changed` (set its level in `[lints]` in ridl.toml)
That run exits 0: two RIDL-407 warnings and an otherwise clean compile stay
clean. The desk check runs only after a compile with no error diagnostic other
than RIDL-409 — a live interfaces.lock entry with no declaration, which
leaves nothing out of the IR the desk check compares — and other than a lint
raised to deny. A workspace with any other error draws no RIDL-407 warning in
addition to that error: it exits 1, exactly as it would with no baseline
present. Entered at a workspace member, the run reports only the diagnostics of
files under the member (ADR-0026 decision 10), and the desk check compares the
whole workspace: an error in another member is not reported, the desk check
does not run, and the run exits 0, exactly as it would with no baseline
present. Run ridl check at the workspace root to see that error and, once it
is fixed, the member's RIDL-407 warnings. A workspace whose only errors
are RIDL-409 still exits 1, and the desk check runs over it: when exactly one
declaration without an entry has the published shape of the orphan entry's
interface, the desk check adds a label to that RIDL-409 naming the
ridl lock --rename command to run. A lint raised to deny in
[lints] is an error for the exit code but does not stop the desk check: the
run reports the denied lint and the RIDL-407 warnings together, and exits 1.
ridl baseline
ridl baseline --help
Publish the current workspace as a baseline: one `<pkg-name>.ir.json` snapshot per package and one `<unit>.catalogs` file per unit with an interface, written to `.ridl/baseline/` at the workspace root
Usage: ridl baseline [OPTIONS] [PATH]
Arguments:
[PATH] [default: .]
Options:
--out <DIR> Write the snapshots and the catalog files here instead of `.ridl/baseline/`
-h, --help Print help
It writes one <package-name>.ir.json file per package in the workspace,
under .ridl/baseline/ at the workspace root or under --out when given —
and, exactly like ridl check, ridl.lock at the workspace
root when the manifest declares [imports] (ridl baseline builds through
ridlc build, non-frozen, so the same materialization step runs). Publishing
the snapshots is wholesale: the target directory ends up holding exactly the
snapshots the workspace declares now. Nothing else in that directory is touched
except the *.catalogs files, which publication replaces as
the catalog files below describes. The snapshots stay one
per source package; ridl diff groups them by
unit when it compares them. The workspace's interface numbers must be recorded first: a
provisional number is refused (RIDL-411, under the publication gate below), so
a package with interfaces runs plain ridl lock before its
first publication. A two-member workspace with no [imports], its locks
written, publishes two files and no ridl.lock:
ridl baseline && find .ridl/baseline -type f | sort
.ridl/baseline/veh.cluster.ir.json
.ridl/baseline/veh.common.ir.json
The catalog files. Beside the snapshots, ridl baseline writes one
<unit>.catalogs file per unit that has at least one interface shape, named
after the unit's manifest name, in the same directory. A file is a text file:
a first line that starts with #, then one catalog hash per line as 64
lowercase hexadecimal characters, newest first. An empty line and a line that
starts with # are skipped, and a hash is listed once. A line may end with
CRLF, as a checkout with core.autocrlf writes it; a line with any other
padding, such as a space, is refused. The first hash is the
catalog hash of the baseline being published, computed as
ridl build --emit catalog computes it. The hashes that follow
are the ones of the file being replaced, copied as they are and never
recomputed from a snapshot. They are carried over when ridl diff's
classifier, scoped to the unit, judges the change from the replaced snapshots to
the fresh ones compatible or identical, and when the new hash is the first hash
of the replaced file: the catalog did not change, so every hash the file lists
still applies, whatever the verdict. The scope is the unit's own source
packages and every declaration its interfaces reach, in the unit or in another
one, so a breaking change in a package that the unit does not reach leaves the
unit's chain as it was. A breaking change that changes the catalog restarts the
chain: the file then holds the one new hash. The file being replaced is carried
only when its first hash is the hash recomputed from the snapshots published
beside it; a file that does not describe those snapshots, as a partially
hand-edited baseline leaves behind, restarts the chain instead of carrying a
hash that may reach past a breaking change. A unit with no published snapshot,
or with no <unit>.catalogs file in the replaced baseline, also starts a chain
with the one new hash, and a file left under the name of a unit that had no
published snapshot is not carried, nor read. The file of a unit with a published
snapshot that cannot be read, or has a line that is neither a comment nor a
hash, or lists the same hash twice, stops the publication with exit 2: the
message names the file and the line, and says to repair the file or remove it
to start the unit's chain again.
Publication replaces the files in this order, so that an interrupted run never
leaves a history beside a snapshot it does not describe: every published
*.catalogs file is removed, then the fresh snapshots move in, then the fresh
*.catalogs files move in, and last the snapshots of packages the workspace no
longer declares are removed. A file left by a unit that no longer has an
interface shape is removed by the first step and not written again. The
histories are written only after the publication gate below has passed, so a
refused run leaves every published file as it was.
The publication gate. Before replacing the published snapshots,
ridl baseline compares the snapshot it is about to publish against the one
already there, and refuses to replace it when, in an interface body both
declare, the replacement breaks the tombstone rule of ridl §11 — RIDL-408,
exit 1, the published snapshots left as they were (the compile has already
run, so ridl.lock is written as on any other run). The refusal covers four
shapes: an interaction is gone from the source with no reserved line at
all; the source retires it with a reserved line, but at an ordinal other
than the one the interaction held; the baseline already retired the
interaction with a reserved line and the source has dropped that line; or
the source declares a live interaction under a name the baseline retires.
The interface level of the gate is the lock's: an interface whose number is
provisional — a declaration with no entry in the unit's interfaces.lock
— is refused (RIDL-411, exit 1, nothing published), on a first publication as
on a replacement, until plain ridl lock records the number; and a number the
published baseline holds that the fresh snapshot neither carries nor retires
is refused too (RIDL-412). That covers a lock line deleted by hand, since a
live entry with no declaration already fails the build with RIDL-409. It also
covers a package deleted, without retiring its numbers, from a unit that still
has other packages, and a whole unit deleted from the workspace. For a package
the message names the line to restore in the unit's interfaces.lock. For a
whole unit the lock left with it, so the message names the deliberate override
instead: delete the snapshots of that unit from .ridl/baseline/, so that the
next publication holds no number to lose. A published snapshot written before
the IR recorded the unit carries no unit; the gate compares it in the unit of the fresh package
of the same name, so the migration to one interfaces.lock per unit — delete
the per-package lock files, run ridl lock, run ridl baseline — passes
RIDL-412 when the unit's new numbering reaches every published number. When
it does not, because a legacy lock held retired entries, remove
.ridl/baseline/ and run ridl baseline again: a first publication is not
compared against a published number, and no flag overrides the gate. A whole
service removed from the source is reported by ridl diff as breaking but is
not refused here (ridl §17.14), and a named-form service's list is a set the
gate does not read. Deleting doorClosed outright, with doorOpened and
doorLocked still declared:
ridl baseline
error[RIDL-408]: `doorClosed` is gone from the source but the baseline being replaced still declares it in `VehicleStatus` (ordinal 2). Publishing would free its ordinal for a later interaction to reuse, with nothing left to record that it was ever taken. Retire it in place with `reserved doorClosed`.
┌─ ./demo.ridl:3:11
│
3 │ interface VehicleStatus {
│ ^^^^^^^^^^^^^
A first publication — the output directory absent, or present and holding no
.ir.json snapshot yet — has no published snapshot to compare against, so
RIDL-408 and RIDL-412 never refuse it. This is the same empty-directory shape
ridl check --baseline refuses when it is named explicitly:
ridl check --baseline refuses it because the user named the directory as a
baseline to compare against, while ridl baseline --out treats it as a first
publication because it is the directory being written. RIDL-411 still refuses
a first publication that holds a provisional interface number, because
RIDL-411 reads the fresh snapshot alone.
Exit codes. 0 on a clean publish. 1 when a diagnostic is an error, or when the publication gate above refuses the replacement — in both cases the existing baseline is left exactly as it was, because a re-publish must never destroy a good baseline with a broken one:
ridl baseline
error[TYPL-011]: unknown type name `Nope`
┌─ ./demo.ridl:4:22
│
4 │ event doorClosed : Nope @[100ms..1s]
│ ^^^^
2 when the workspace itself cannot be found, the same as ridl check, when
the output directory cannot be read or written, when a published .ir.json
snapshot fails to parse, when two published snapshots declare one package, or
when an entry of the output directory named like a snapshot cannot be
stat'ed:
ridl baseline /nonexistent/ridl/workspace
error: `/nonexistent/ridl/workspace` does not exist
A published snapshot that cannot be parsed is left untouched rather than
silently overwritten: replacing it would destroy whatever ordinal record it
held without any report. ridl cannot tell a damaged file from a snapshot
that a toolchain with a different IR schema wrote — the reader rejects an
unknown field (ADR-0014 decision 14) and a snapshot carries no
schema marker — so the message names a remedy for each cause, and neither
remedy discards the record unread:
ridl baseline
error: ./.ridl/baseline/veh.cluster.ir.json: the IR snapshot is not valid IR v2 JSON: expected value at line 1 column 1; the file is left as it is, because a record that cannot be read cannot be shown safe to replace. If the file is damaged, restore it from version control or resolve the merge conflict left in it. If a toolchain with a different IR schema wrote it, check the source against it with that toolchain (`ridl check --baseline`), then remove the file and run `ridl baseline` with this one
The same parse error on a ridl diff input, or on a file or directory named
with ridl check --baseline, is reported without that remedy: only the
published baseline that ridl baseline is about to replace is the record the
remedy protects.
Two published snapshots that declare one package are two records of the same
ordinals, and the gate has no rule for choosing between them, so it refuses
rather than compare against either. Both files are left as they are; the
message names the package and both files. Here zz-copy.ir.json is a copy of
the published snapshot left beside it:
ridl baseline
error: two published snapshots declare the package `veh.cluster`: `./.ridl/baseline/veh.cluster.ir.json` and `./.ridl/baseline/zz-copy.ir.json`; both files are left as they are, because the gate cannot tell which one is the published record. Remove the copy that is not the published record (restore the directory from version control if unsure), then run `ridl baseline` again
This refusal is ridl baseline's alone: ridl check --baseline and
ridl diff read such a directory as a snapshot set, matching packages by
name.
An entry of the output directory named like a snapshot whose metadata cannot
be read — a symlink to a file that is gone — is also exit 2, naming the entry.
Skipping it would read the directory as one snapshot short, which is a first
publication to the gate, and the publication would then replace the entry
with a snapshot that had dropped the ordinal record. The entry is left as it
is. ridl check reports the same entry the same way when it finds it under
.ridl/baseline/ by auto-discovery, and so does ridl diff over a directory
side:
ridl baseline
error: cannot read the snapshot ./.ridl/baseline/veh.cluster.ir.json: No such file or directory (os error 2)
ridl build
ridl build --help
Compile to the selected artifacts (defaults to the current directory)
Usage: ridl build [OPTIONS] [PATH]
Arguments:
[PATH]
[default: .]
Options:
--out-dir <OUT_DIR>
[default: out]
--emit <EMIT>
Possible values:
- rust: Idiomatic Rust source, written to `<base>.rs`
- ir-json: The lowered IR v2 as exact-decimal JSON, written to `<base>.ir.json`, and the lowered system to `<pkg.Name>.system.json`
- ir-text: The lowered IR v2 as prototext, written to `<base>.ir.txtpb`, and the lowered system to `<pkg.Name>.system.txtpb`
- ir-binary: The lowered IR v2 as protobuf binary, written to `<base>.ir.binpb`, and the lowered system to `<pkg.Name>.system.binpb`
- typescript: Idiomatic TypeScript source, written to `<base>.ts`, and the codec module `<base>.codec.ts` with `[backend.typescript] codec = true`
- proto: The proto3 schema, written to `<base>.proto`
- flatbuffers: The FlatBuffers schema, written to `<base>.fbs`
- podbuf: The podbuf C header, written to `<base>.h`
- codegen-model: The lowered codegen model (`ridl.codegen.v1`) as canonical protobuf JSON, written to `<base>.codegen.json`
- codegen-request: The whole codegen request a build hands a backend, as canonical protobuf JSON, written to `<base>.codegen-request.json`
- catalog: The catalog descriptor an engine reads, written to `<unit>.catalog.binfb` for every unit that declares an interface or a service with an inline body: a FlatBuffers file of the unit's interfaces, their members and their catalog hash
[default: rust]
--plugin <LANGUAGE[=PATH]>
A codegen plugin to run beside the emits, once per package: `<LANGUAGE>` runs `ridlc-gen-<LANGUAGE>` from PATH, `<LANGUAGE>=<PATH>` runs the executable at PATH. Repeatable
--plugin-timeout <SECONDS>
Seconds a plugin may run per package before it is killed
[default: 60]
--frozen
Verify remote imports against `ridl.lock` without fetching or regenerating it (CI mode, ADR-0002 §7)
--deployment <NAME>
The deployment to carry in each codegen request. With one deployment in the workspace it is selected without this flag; with several, none is carried unless named
-h, --help
Print help (see a summary with '-h')
--emit accepts a comma-separated list, so one invocation can write several
targets. --frozen is the same flag as on ridl check: it is
ridlc build --frozen, documented word for word since
ADR-0010.
--deployment NAME picks the deployment of the workspace's system that every
codegen request carries. Without it, the request carries the deployment when
the system declares exactly one, and none when it declares several or the
workspace has no system. A single declared deployment that an RSDL-7xx error
removed from the system is not carried either: the build reports that error and
exits 1. A NAME that the system does not declare exits 2 and
names the deployments it does declare, unless the build already has an error of
its own: that error takes precedence, and the build reports it and exits 1.
When no name can be right, the message says which reason it is — the workspace
declares no system, or the system declares no deployment. A deployment that an
RSDL-7xx error removed from the system is not unknown either: the build
reports that error and exits 1.
The compatible catalogs. When the build writes a catalog descriptor or
generates code, ridl build reads .ridl/baseline/ at the workspace root, as
ridl check finds it, and writes into the unit's descriptor and
into the catalog.compatible field of the unit's codegen model the earlier
catalog hashes it judges compatible with the current catalog. The list comes
from the unit's <unit>.catalogs file (see ridl baseline),
in the file's order, newest first, and it never holds the current catalog's own
hash. It is empty when the workspace has no baseline directory or the directory
holds no .ir.json snapshot, when the baseline holds no package of the unit,
when the unit has no <unit>.catalogs file (or the baseline has none at all), and when ridl diff's classifier,
scoped to the unit, judges the change from the baseline to the tree breaking
and the current catalog hash is not the file's first hash. When the two hashes
are equal the catalog did not change, and the list holds the file's other
hashes whatever the verdict. The file is used at all only when its first hash
is the hash recomputed from the baseline's own snapshots of the unit; a file
that does not describe those snapshots gives an empty list. The recorded file
is not changed by the build. An
IR dump reads no baseline. A baseline with at least one <unit>.catalogs file that cannot be loaded, or
a <unit>.catalogs file that cannot be read, is exit 2; a baseline with no
<unit>.catalogs file is not loaded. A compile with an error diagnostic gets no
list, and its baseline is not read. When the workspace names [imports] and a
list is computed, the build prints a note on stderr that a unit's list covers
the workspace's own packages only.
rust is a language backend, and it writes the whole generated surface of
a package in one file: the domain types (a struct, an enum, an enum set, a
union and a named scalar, each with its typl constraints enforced at
construction) always, and, when [backend.rust] encodings in the manifest names
flatbuffers or proto3, the codec of each encoding named — encode,
verify, decode and a MAX_SIZE bound — the interaction descriptors, with
one size table per encoding, and the interaction face. With no encodings list
the file holds the types only. Each
interface gets the parts its own interactions need: a
Client for the consumer side when it carries any interaction at all; a
Publisher for the producer side when it carries a signal or an event; and a
Provider trait the application implements plus a serve function whose
future settles the claims waiting on a port, when it carries a command or a
query; and, when it carries an event, a command or a query, a blocking
module under the crate's std feature, on by default, holding the same
Client, and the same serve when there is one, as blocking calls with a
timeout, each waiting call ridl_rt::task::block_on over the async form. So a
signal-only interface gets
a Client and a Publisher and nothing to settle with and nothing to block
on, a command-only interface gets a Client, a Provider, a serve and a
blocking module and no Publisher, and an interface carrying only fixed
declarations gets no face module at all, and neither does an internal
interface (the design record
gives the rule). Beside the per-package files it
writes a lib.rs crate root and a Cargo.toml naming ridl-rt with the
encoding's feature, and with ridl-rt/std behind the crate's own std.
Using the generated Rust code shows how a program uses
the crate, and lists its Cargo features.
Generated files are marked. The .rs, .ts, .proto and .fbs files, and
the lib.rs and Cargo.toml of the crate root, begin with a line comment that
says the file is generated:
// @generated by ridl from package veh.cabin. Do not edit.
veh.cabin is the dotted package name, also in single-file mode, where it is
the package the file declares and not the file stem. The lib.rs and
Cargo.toml of a standalone package build name that package in the same way.
Those of a workspace build belong to no one package, so their line reads
@generated by ridl. Do not edit.. Cargo.toml uses # as the comment token.
The marker is the first line because rustfmt looks for @generated only in
the first 5 lines of a file. It carries no version and no timestamp, so a
regeneration with the same inputs gives the same bytes. The TypeScript file has
/* eslint-disable */ after the comment block. The IR dumps (JSON,
prototext and binary), the <base>.codegen.json file and the
<unit>.catalog.binfb file carry no marker.
A licence header goes after the marker. [codegen] header-file in the
workspace root's ridl.toml (or in the ridl.toml of a standalone package)
names a text file, and ridl build writes its lines after the marker in every
file that carries one, then one blank line:
[codegen]
header-file = "LICENSE-HEADER.txt"
The path is relative to the manifest that declares it. The file holds plain
text without comment tokens, and each target gets its own comment syntax. ridl
accepts CRLF line ends and a lone CR as a line break, removes trailing
whitespace from each line and drops leading and trailing blank lines, so a file
with no text left means no header. A blank line inside the text becomes a bare
comment token. Single-file mode has no manifest and writes no header. MANI-011
is an error when the file cannot be read, when it is not UTF-8, and when it
contains a control character, which means a C0, DEL or C1 character (U+0085
included), U+2028 or U+2029, other than a tab and a line break; ridl check
reports it too. MANI-012 is an error when the manifest of a workspace member
sets the key: only the root sets it. MANI-013 is an error when a ridl.toml
sits inside the tree of a unit, and MANI-014 is an error when two units declare
the same source package.
A build overwrites a lib.rs or Cargo.toml in the output directory only when
the file begins with this marker, or with the marker an earlier release wrote
(// Generated by ridlc. Do not edit. and # Generated by ridlc. Do not edit.).
Any other file is taken to be hand-written and is not touched.
Mark the output in version control. ridl build does not write a
.gitattributes file. To collapse the generated files in review, add a line
such as this one, adapted to your output directory:
generated/** linguist-generated=true
The crate root allows two lints. The emitted lib.rs carries
#![allow(clippy::derivable_impls, clippy::module_inception)], after the
attribute that makes the crate no_std and before the line that links alloc
under the name std (Cargo features
says when each applies, and what a single-file consumer declares). These are the
two lints that the emitter draws on the examples/cabin crate under
cargo clippy: module_inception from the package and interface naming
(veh.cabin and Cabin), and derivable_impls. The list is closed for that
crate: the repository's just demo replaces allow with expect and runs
clippy with -D warnings on the generated crate for examples/cabin, so a new
lint, or a listed lint that no longer fires on it, fails that check. Single-file
mode writes no lib.rs. A consumer that includes the generated file with
include! puts #[allow(clippy::derivable_impls, clippy::module_inception)] on
the mod that wraps the include!. Explicit crate-root links in the generated
docs can be unresolved when the file is included inside another module
(Documentation in the generated code).
Two things it may leave out, each with a note in the source it writes. A
type whose size the compiler cannot bound — one that reaches itself, or a
stream — carries no codec, and gets a __RIDL_FB_NO_CODEC_<NAME> constant
whose documentation names the member and the reason. An interface the face
cannot carry — a parameter with no declared type, a stream in either
position, a query whose reply is a fallible T | E, or a contract clause
outside the form the translator accepts — is skipped along with its
descriptors, and gets a
__RIDL_NO_FACE_<NAME> constant naming the interface and the reason, and,
when an issue tracks the limit, that issue. An internal interface gets no
face in any case
(the design record
gives the rule). When its descriptors are refused, its note says that the
descriptors are absent and gives the reason. Neither is an error: the rest of
the package is emitted, and the build succeeds.
The face names the ridl-rt port traits and nothing else: the crate carries no
runtime and opens no socket, so an application supplies the ports. The one
runtime in this workspace is ridl-loopback, which runs in process.
examples/cabin/ is a worked example — a schema, and a consumer program
against the crate built from it — and just demo generates that crate and runs
the program, which prints one round trip per interaction kind through the
async client and the command and the query again through the blocking one.
The manifest says which encodings a backend emits. There is no flag for the
payload encoding. A [backend.<name>] table in the manifest holds an
encodings list for the backend of that name, and ridl build sends each
backend its own list:
[package]
name = "veh.cabin"
version = "1.0.0"
[backend.rust]
encodings = ["flatbuffers"]
The names are flatbuffers, proto3 and podbuf. An absent table, an absent
key and an empty list all mean types only. The Rust backend builds
each of the three, alone or together. A podbuf codec buffer starts on an
8-byte boundary, and the generated size tables hold Aligned buffers for it.
The generated Cargo.toml enables one ridl-rt
feature per named encoding, and none for types only. A single-file build has no
manifest, so it is types only. Each emitted codec names its encoding, as
::ridl_rt::encoding::FlatBuffers or ::ridl_rt::encoding::Proto3; the face
takes its encoding from its port
type instead, so a consumer that binds a generated face to a runtime it did not
write names an encoding only where it builds that runtime — over
ridl-loopback, Loopback::new for FlatBuffers and
Loopback::<E>::with_encoding for another. A program that writes its own port
names the encoding in that port's Encoded implementation instead
(Writing a port).
[backend.rust] wasm = true adds a module wasm to the generated crate,
written to __ridl_wasm.rs. The module lists the payload types that have a
FlatBuffers codec in WASM_PAYLOAD_TYPES, sorted by qualified name
(veh.cabin.Temperature; an induced tuple by its Rust name), and the typl rule
names in WASM_RULES, sorted. A type_id is an index into the list of the same
build. When the target is wasm32 and the generated crate's feature
wasm-exports is on, the module exports ridl_alloc, ridl_free,
ridl_catalog_hash, ridl_verify and ridl_type_id, and no other function.
ridl_type_id returns the type id of a payload type named by its qualified
name, or -1 for a name the list does not hold.
ridl_verify(type_id, ptr, len) returns 0 when the bytes verify as the type at
index type_id, 1 for a malformed buffer, 2 plus the index of the broken rule
in WASM_RULES, and -1 for an unknown type_id. A null ptr with a length,
and a range past the end of memory, return 1. ridl_alloc returns 0 when the
length is 0 or the allocation fails. The generated Cargo.toml declares the
feature wasm-exports = ["std"] and no crate type, so a Rust consumer of the
crate links no export. The wasm-exports feature gives the
command that builds the module. The module writes the catalog hash of one unit: the root
package's unit, or the one unit of the workspace's own packages when the
manifest is a [workspace]. A build with no root package and other than one
unit is refused. The option needs encodings to name flatbuffers, and the
Rust backend refuses it otherwise. A package whose first segment is wasm
refuses it too. A build with the option off removes a __ridl_wasm.rs that an
earlier build wrote, and a build with it on refuses to overwrite a
__ridl_wasm.rs that does not start with the generated marker. Single-file
mode has no manifest, so it has no wasm module.
[backend.typescript] codec = true writes <base>.codec.ts beside the
TypeScript module of every package that has a payload root with a codec (its
own, or one of another package that its interfaces name), and turns the wasm
module on when [backend.rust] writes
no wasm key. A written wasm = false, and a manifest with no
[backend.rust] table, are MANI-019. [backend.rust] encodings must still name
flatbuffers, and the Rust backend refuses an implied wasm module otherwise.
[backend.rust] transcode = true asks the Rust backend for the transcoding
functions between the encodings of the same table. The table must write at least
two encodings elements; every written element counts, valid or not. Fewer than
two written elements, or an absent encodings key, is MANI-020, reported on the
transcode value. A written element that is unknown or repeated is also
MANI-015. An encodings value that is not an array is MANI-015 only. The
ridlc request carries a transcode option with the value true for that
table, and no transcode option otherwise.
MANI-015 is an error when the encodings value is not an array of the known
names or repeats a name, when wasm, codec or transcode is not a boolean,
when a [limits] value is not an integer of 0 or more, or when [limits],
[limits.kind] or [limits.interaction] is not a table, and MANI-016 is an error when the manifest of a workspace member holds
a backend key or [backend.<name>] table: only the workspace root, or a
standalone package, sets it. Any other key under [backend.<name>] is
MANI-005. The table is not the backend. attribute namespace of an rsdl
declaration. The lint RSDL-807 link-encoding-not-emitted warns when a link of
the selected deployment carries an encoding that the emitted backend's list
does not name. It is compared once per emitted backend that can carry a codec:
the rust emit and each --plugin, and a backend named by both an --emit
and a --plugin is compared once. A schema emit (proto, flatbuffers), the
podbuf emit, the typescript emit and codegen-model are not compared, because their
backends take no encodings option, and codegen-request is not compared
either, because it writes its options into the request rather than emitting a
codec from them. A [backend.<name>] table whose
encodings list is empty declares that the backend emits no codec, and it is
not compared either; a table written with no encodings key at all is the same
declaration, as the manifest records the two forms alike. A backend with no
table at all is compared, so a build that declared no encodings is still
reported. An empty list is how a
schema-only plugin silences the lint for its own backend, where the [lints]
level silences it for every backend at once.
[limits] gives the interactions of a unit their payload limits, in bytes of
encoded payload (ADR-0030
decision 12). A unit sets it in its own ridl.toml; the table of a workspace
root does not apply to its members, and a package of a unit's subdirectory
takes the unit's table.
[limits]
default = 4096 # every kind with no entry below
[limits.kind]
signal = 256 # signal, fixed, event, command or query
event = 2048
[limits.interaction]
"Climate.temperature" = 64 # Interface.member, quoted because of the dot
The table of a remote unit that [imports] names is read too, so its
interactions record the owner's limits and the consumer computes the owner's
catalog hash. The consumer reports no RIDL-151 or RIDL-152 for it.
The limit of an interaction is the first that is found of: its entry in
[limits.interaction], the entry of its kind in [limits.kind], default,
and the payload ceiling of 16,256 bytes. No limit is above the ceiling. A key
of [limits.interaction] is the short name of the interface, or of the inline
service (the last segment of its dotted name), then ., then the member;
neither the package nor a dotted service name is part of it. When two
interfaces or inline services of one unit share a short name, a key that uses
the name is ambiguous.
An unknown kind in [limits.kind] is MANI-021, and a value above 16,256 is
MANI-022. A key that names no interaction of the unit, or that is ambiguous, is
RIDL-152, reported on the key. RIDL-151 is reported on an interaction whose
payload has a largest encoded size, in an encoding that a [backend.<name>]
table enables (FlatBuffers when no table names one), above the limit that an
entry of [limits] gives it, or no finite bound. A command or query
checks its request and its reply against its one limit, and a fixed is sized
too. Both are errors, so they have no name and no level in [lints]. A payload
above the ceiling under a declared limit draws RIDL-150 as well; where no entry
gives a limit, only RIDL-150 applies. A package with an error that stops
ridl build writing artifacts, or that depends on one, is not checked, and a
unit with such a package reports no RIDL-152. An error in a manifest stops
RIDL-150 in every package of the workspace, and RIDL-151 and RIDL-152 only in
the packages beneath that manifest.
A [limits] entry that applies to an interaction is recorded in the IR, so
adding, removing or editing one changes the catalog hash of the interaction's
unit. An interaction that no entry reaches records no limit, and a unit with no
[limits] table keeps its hash.
proto is a wire backend (ADR-0013 decision 2): it emits the typl
surface — structs, enums, enum sets and unions, projected to proto3 messages
and enums, with named-scalar constraints carried as comments — plus the
interaction identity table, one enum per interface giving each signal, event,
command, query and fixed its ordinal. It emits no service block, no call
face, and no value store; store and dispatcher generation is not part of this emit
(ADR-0018 decision 16).
flatbuffers is the second wire backend, with the same two tiers and the
same ceiling. Its projection rules differ from proto3's where the targets
differ — a union is isolated in a wrapper table, every struct is a table, a
map is a vector of generated entry tables with no (key), and enum values are
not prefixed because FlatBuffers scopes them inside their enum — and are
recorded in ADR-0019.
podbuf is the third wire backend: it writes the C header, <base>.h,
through which a C consumer reads a podbuf buffer that the gate has verified,
with no library. The header holds the preamble and descriptor structs, one head
struct per type with its pad members written out and _Static_assert checks
on every sizeof and offsetof, the constants of the enums and the union
tags, and inline accessor functions that read through the preamble's head size
and answer 0 for a field past it. It refuses a big-endian target with #error.
It takes no option. A package that lays out a struct, tuple or union of another
package inline defines that head again behind a guard macro, so the headers of
two packages can be included in one translation unit.
codegen-model is not a backend at all: it writes the lowered codegen
model — one package, resolved over the scope the build read, with the scalar
classes, the pinned name transforms, the resolved type references, the typl
init and closure rules, the tombstones in their slots, the FlatBuffers
projection and the interaction facts computed once — as canonical protobuf
JSON, in the schema ridl.codegen.v1. It is the payload a codegen request
carries (ADR-0020 decisions 8 and 9), the same JSON value as the request's
model, so a plugin's fixture is a file ridlc wrote. The rust
backend reads the model and nothing else. The other three backends above still
read the IR, and a drift test in each of them asserts that what the backend
derives and what the model states are the same fact.
codegen-request writes the whole request, to
<base>.codegen-request.json: the same canonical protobuf JSON a plugin reads
on its standard input, with schema, toolchain, model, options,
artifactBase, generatedMarker, header and the deployment section. It is
the artifact a plugin author debugs against — the request their plugin would
receive for this package, written without running the plugin. Its model is
the value codegen-model writes, so the two emits state one fact. Unlike
codegen-model, it accepts the options of its own [backend.codegen-request]
table and writes them into the request's options, because those options are
part of what a plugin receives and an emit that refused them could never show a
request carrying any.
--plugin runs a codegen plugin beside the emits — a backend that is an
executable rather than part of ridl, over the contract
generate(CodegenRequest) → CodegenResponse (ADR-0020 decisions 9
and 10; the as-built record is
docs/design/codegen-plugins.md).
The value is a language, or a language and a path:
--plugin kotlinrunsridlc-gen-kotlin, found by walkingPATHin order and taking the first directory that holds a file of that name (on Windows,ridlc-gen-kotlin.exeis also tried). No directory holding it is an error namingridlc-gen-kotlin, and — like a compile error — a build that writes nothing.--plugin kotlin=/opt/gen/bin/kt-genruns the executable at that path and skips the lookup.ridlstill reports it asridlc-gen-kotlin, with the path beside the name.
The flag repeats, once per plugin. Each plugin runs once per package the code
emits are written for — ridl.std included, under the same rule as above — and
receives on its standard input one ridl.codegen.v1.CodegenRequest in canonical
protobuf JSON: schema ("ridl.codegen.v1") and toolchain (this ridl's
version) first, then model (the same JSON value as the package's
codegen-model artifact, one indentation level deeper), options (one
encodings option when the root manifest's [backend.<language>] table names
an encoding, as described above; empty otherwise; no flag sets one),
artifactBase, the <base> of the
emit list above, generatedMarker (the marker line) and header (the header
text, an empty string when there is none), both always present, and
deployment, the deployment section — the one --deployment
names, or, with no flag, the one the source declares when it declares exactly
one — absent when no deployment is carried. It answers on its standard output with one
CodegenResponse: files,
each a path relative to --out-dir with a text or binary content, and
diagnostics, each a severity and a message. ridl writes the files; the
plugin never touches the filesystem, so --out-dir means for a plugin exactly
what it means for rust. A path that is absolute, has a .. or . or empty
component, or contains \ is refused, and no file of that response is written.
A diagnostic is reported prefixed with the plugin's name, at its severity; an
error-severity one is a build that exits 1 with none of that response's files
written, as an in-tree backend's failure is. A non-zero exit, a response that
does not parse, a plugin that cannot be started, and a plugin still running
after --plugin-timeout seconds (60 by default; the plugin is killed) are each
one error naming the plugin, and the build exits 1. The plugin's standard error
is passed through.
This repository builds two plugins for its test suite, and no release
installs either: ridlc-gen-model is --emit codegen-model as a process, and
ridlc-gen-rust is --emit rust as a process. A parity test runs each one
through this path and compares its output with the in-process backend's, byte
for byte. The TypeScript, proto3 and FlatBuffers backends run in process only.
Writing a codegen plugin is the guide for a plugin
author.
It writes one file per package per --emit target, under --out-dir
(out by default), and — exactly like ridl check —
ridl.lock at the workspace root when the manifest declares [imports],
non-frozen. The exception is catalog, which writes one file per unit, named
<unit>.catalog.binfb after the unit's manifest name, when any source
package of the unit declares an interface or a service with an inline body,
and no file for a unit that declares neither. <base> in the --emit list
above is the package name when PATH is a package directory or a workspace
root, and the input file's stem in single-file mode; the catalog of a single
file is named after the file's package, which is its own unit.
When a package names a type from ridl.std, the standard package is written
beside your own as one more file per --emit target — ridl.std.rs,
ridl.std.ts, ridl.std.codegen.json — because generated code refers to
standard types by package path and does not compile without it, and the model
is lowered over the same scope. ridl.std.codec.ts joins them under
[backend.typescript] codec = true only when a package's codec module imports
it, for a payload type that is a ridl.std scalar. The three IR targets —
ir-json, ir-text, ir-binary — get no such file: a direct IR dump
records the packages the workspace declares, and ridl.std ships with the
compiler rather than with the workspace (ADR-0007 decision 15).
catalog gets no such file either: ridl.std declares no interface, so no
ridl.std.catalog.binfb is written.
When the workspace declares a system (rsdl reference §3.1), each of the three
IR targets also writes the lowered system — the closure, and every deployment
with its placements, links, routes and surface set (rsdl reference §13) — as
one more file named after the system's qualified name:
veh.topology.Vehicle.system.json for system Vehicle in package
veh.topology under --emit ir-json, .system.txtpb and .system.binpb for
the other two.
Building a workspace of two packages that name no standard type, with no
[imports]:
ridl build --out-dir out && find out -type f | sort
out/Cargo.toml
out/lib.rs
out/veh.cluster.rs
out/veh.common.rs
The Cargo.toml and the lib.rs are the crate root: the per-package files
are flat, and the crate root is what gives them the module paths the generated
code refers to each other by. Single-file mode writes neither, because one
file is not a crate.
and a single .typl file with --emit rust,ir-json:
ridl build speed.typl --emit rust,ir-json --out-dir out && find out -type f | sort
out/speed.ir.json
out/speed.rs
Exit codes. 0 when the build compiles clean and every requested artifact is written. 1 when a diagnostic is an error — nothing is written for a package that fails to compile. An RSDL-7xx error is the one exception: it blocks only its own deployment (rsdl reference §13), so the build writes every artifact, leaves that deployment out of the lowered system, and still exits 1:
ridl build --out-dir out
error[TYPL-011]: unknown type name `Bogus`
┌─ ./demo.ridl:4:25
│
4 │ signal currentSpeed : Bogus @10ms
│ ^^^^^
2 when the workspace itself cannot be found:
ridl build /nonexistent/ridl/workspace --out-dir out
error: `/nonexistent/ridl/workspace` does not exist
ridl test
ridl test --help
Run the property suite over a workspace: the range self-corpora and the contract-clause sampling (ridl §13). Exit 0 when every run passes, 1 on a self-corpus failure or an evaluation error, 2 on a compile error
Usage: ridl test [OPTIONS] [PATH]
Arguments:
[PATH] [default: .]
Options:
--samples <SAMPLES> Random parameter tuples drawn per `require` clause (minimum 1). Each clause also runs its parameters' boundary corpus, which is drawn first and is not counted here, so the total per clause is larger [default: 256]
--format <FORMAT> Output format for the report [default: text] [possible values: text, json]
-h, --help Print help
ridl test runs two checks per package: every constrained named type's
boundary and violation corpus against the constraint validator (a
self-consistency check of the toolchain, not of the model), and every
require clause's satisfiability, sampled over each parameter's boundary
values plus --samples random draws. ensure clauses are listed as observer
stubs and never evaluated — there is no runtime to produce the result they
need. The report goes to stdout; a compile error's diagnostics go to
stderr instead, and no report prints at all.
It writes nothing, ever. The report is printed, never saved. ridl test
runs the plain compile only — never the materialization step that makes
ridl check write ridl.lock — so pointing the same
[imports]-declaring workspace at ridl test produces no lockfile.
A clean run over an interface with two require clauses:
ridl test --samples 8
package cli.demo
ranges
Speed ok — 4 boundary accepted, 2 violations rejected
requires
Cruise.setTargetSpeed.require[0] ok — 3 boundary + 8 random of 12 satisfied (speed > 0.0)
Cruise.setTargetSpeed.require[1] ok — 4 boundary + 8 random of 12 satisfied (speed <= 250.0)
ensures
(none)
summary — requires: 2 total, 2 evaluated; ensures: 0 listed
The same run with --format json prints one JSON object per package, on one
line:
ridl test --samples 8 --format json
[{"contracts":[{"boundary_samples":4,"detail":null,"discarded_samples":0,"id":"Cruise.setTargetSpeed.require[0]","random_samples":8,"samples":12,"satisfied":11,"source":"speed > 0.0","status":"ok"},{"boundary_samples":4,"detail":null,"discarded_samples":0,"id":"Cruise.setTargetSpeed.require[1]","random_samples":8,"samples":12,"satisfied":12,"source":"speed <= 250.0","status":"ok"}],"package":"cli.demo","ranges":[{"boundary":4,"status":"ok","type":"Speed","violations":2}],"summary":{"ensures_listed":0,"nothing_evaluated":false,"requires_constant_false":0,"requires_errored":0,"requires_evaluated":2,"requires_skipped":0,"requires_suspect":0,"requires_total":2}}]
Exit codes. 0 above. 1 when a range self-corpus fails, or — the case a
require clause reaches in practice — evaluating one raises an error, such as
a division by zero every sampled input hits:
ridl test
package cli.demo
ranges
Divisor ok — 4 boundary accepted, 2 violations rejected
requires
I.c.require[0] ERROR — division by zero while evaluating `100 / (d - d) > 1`
ensures
(none)
summary — requires: 1 total, 0 evaluated, 1 errored; ensures: 0 listed
WARNING: no require clause was evaluated — this run tested no precondition (0 skipped of 1)
2 when the workspace fails to compile, when the workspace cannot be found, or
when --samples 0 is given — sampling zero values is refused rather than
silently clamped, because it would report every clause as unsatisfiable and
call that a finding:
ridl test
error[TYPL-104]: range minimum 250 is greater than maximum 0
┌─ ./demo.ridl:2:20
│
2 │ type Broken : km/h [250.0..0.0]
│ ^^^^^^^^^^^^
warning[TYPL-102]: `float` without both a range and a `step`
┌─ ./demo.ridl:2:15
│
2 │ type Broken : km/h [250.0..0.0]
│ ^^^^
│
= lint: `unbounded-float` (set its level in `[lints]` in ridl.toml)
ridl test --samples 0
error: `--samples` must be at least 1
Note the overloaded meaning: exit 1 here is a test failure (the toolchain
found a problem worth failing the run over), not the breaking verdict
ridl diff uses the same code for, and not the diagnostic error ridl check uses it for. A suspect finding — no sampled input satisfies a
precondition — is reported in the same output but does not fail the run;
only a self-corpus failure or an evaluation error does.
ridl fmt
ridl fmt --help
Reformat `.typl`, `.ridl` and `.rsdl` files in place (defaults to the current directory)
Usage: ridl fmt [OPTIONS] [PATH]
Arguments:
[PATH] [default: .]
Options:
--check Do not write; exit 1 if any file would change
-h, --help Print help
It writes every .typl/.ridl/.rsdl file under PATH back to itself in
canonical form, unless --check is given or the file fails to parse. The walk
skips vendor/ at every manifest root it meets, including one below a PATH
that has no ridl.toml; ridl vendor fills that directory with
copies of remote units that belong to their own owners. A PATH that is such a
directory or lies under it is refused with exit 2. An .rsdl
file takes the file layout (the header, one blank line between declarations).
Systems, components, distributions, deployments and machines use canonical
brace bodies, references and attributes. Deployments keep for before header
attributes and nest machine blocks; source blank lines between machines remain.
Interfaces and inline services use the canonical brace layout
and member spacing. Named services keep commas between shapes, remove the
optional trailing comma, and break overlong shape lists after the colon. Interaction timing precedes attribute blocks. Predicate attributes use block
form; flag and assignment attributes stay inline when their code line fits.
Layout and line width. Formatted blocks use two spaces per indentation level. Parameter lists, tuple types and attribute value lists break into one item per line with commas between items when their code line exceeds the configured width. Overlong attribute blocks use one attribute per line; named service shape lists break after the colon. The default width is 100 characters. Width counts Unicode scalar values, including indentation. Trailing comments do not count towards the limit, and unbreakable code can exceed it. A comment embedded inside a single-line construct keeps that construct's source layout. Comments between interaction annotations stay with the preceding source annotation when timing moves first. If a moved annotation line comment would consume another trailing member comment, the whole member keeps its source layout.
A leading comment block is kept. A block of line comments before the
package line, such as a copyright and licence notice, stays where it is, with
or without a blank line between it and package. The formatter does not insert
or remove that blank line.
EditorConfig example. Editors that support these properties can use this
section in .editorconfig for the family's source files:
[*.{typl,ridl,rmdl,rsdl,rxdl}]
indent_style = space
indent_size = 2
max_line_length = 100
The CLI resolves max_line_length from .editorconfig for each file. The
language server resolves it from the document path and formats the current
buffer. An integer sets the width, off disables breaking, and absent, unset
or invalid effective values use 100. Indentation remains two spaces; the
formatter ignores indent_size, indent_style and client formatting options.
Compiler profiles for .rmdl and .rxdl are still pending; the glob includes
them for editor settings.
Exit codes. 0 when nothing needed rewriting, or the rewrite (without
--check) succeeded. 1 under --check when a file would change, without
writing it. Starting from speed.typl holding
type Speed : km/h [0.0..250.0 step 0.5] on one line, with no blank line
after the package declaration:
ridl fmt --check speed.typl; echo "exit: $?"
exit: 1
Reformatting it for real, then checking again:
ridl fmt speed.typl && cat speed.typl
package veh.common
type Speed: km/h [0.0..250.0 step 0.5]
ridl fmt inserted the blank line and tightened the colon; the shown text is
cat's, since ridl fmt itself printed nothing. A second --check pass is
now a fixed point:
ridl fmt --check speed.typl; echo "exit: $?"
exit: 0
and 1 — with or without --check — when a file has a parse error; the file
is never rewritten, and the parse diagnostics render to stderr:
ridl fmt broken.typl
error[FORM-101]: expected `]`
┌─ broken.typl:2:22
│
2 │ type X : integer [0..10ms]
│ ^^^^
error[TYPL-302]: duration literal in typl context
┌─ broken.typl:2:22
│
2 │ type X : integer [0..10ms]
│ ^^^^
2 when an existing file cannot be read or written — a permissions error, for example:
ridl fmt unreadable.typl
error: cannot read unreadable.typl: Permission denied (os error 13)
A missing PATH, or a directory the walk cannot read, is the same exit 2 —
naming the cause. This is a fix, recorded as ADR-0010 decision 6
and decision 1: before it, ridl fmt /nonexistent walked the missing path,
found nothing, and reported success, and an unreadable directory reached
partway through a larger walk was silently treated the same way — the one
subcommand on this page that did not fail closed on a bad path. Both now
report the directory read_dir failed on and the OS error:
ridl fmt /nonexistent/ridl/workspace
error: cannot read /nonexistent/ridl/workspace: No such file or directory (os error 2)
and, for a tree holding one ordinary file next to a subdirectory the walk cannot read into:
ridl fmt --check .
error: cannot read ./sub: Permission denied (os error 13)
Of the eight subcommands ADR-0010 decision 6 examined on 2026-07-27,
it found ridl fmt is the only one that reliably names the actual unreadable
path this way in every case it was tested against. ridl describe, added later,
names the path in every exit-2 message about a path it was given. ridl check,
ridl build, ridl baseline, ridlc check, and ridlc build still exit 2 on
the same inputs, but with the wrong cause or none: an unreadable workspace
root reports error: no `ridl.toml` found at or above `<path>` — confirmed
directly against this build — and an unreadable subdirectory nested inside an
otherwise-readable workspace reports a bare error: Permission denied (os error 13), naming no path at all — also confirmed directly. The other two
subcommands decision 6 examined name a path only in part: ridl diff names the
unreadable directory only when it is the argument given, and ridl test names
the workspace root, not the subdirectory that failed. Tracked as
issue driftsys/ridl#196, not fixed as of this page.
ridl diff
ridl diff --help
Compare two IR snapshots or source trees and classify the change: exit 0 compatible or identical, 1 breaking, 2 error
Usage: ridl diff [OPTIONS] [OLD] [NEW]
Arguments:
[OLD] The baseline: an `.ir.json` snapshot, a `.typl`/`.ridl` file, a package directory, or a workspace root
[NEW] The candidate, in the same forms as the baseline
Options:
--format <FORMAT> Output format for the report [default: text] [possible values: text, json]
--explain <CATEGORY> Print the classification rule for one change category and exit, instead of comparing snapshots. Takes a category exactly as the report prints it, e.g. `timing_changed`
--encoding <ENCODING> An encoding the change is judged for, as `[backend.<name>] encodings` names it; repeat the flag for several. A snapshot does not record the encodings a workspace builds. Only `podbuf` has a rule of its own: with it, a change under the heading "breaking under podbuf" is breaking; without it, compatible [possible values: flatbuffers, proto3, podbuf]
-h, --help Print help
OLD and NEW each take one of three forms: an .ir.json snapshot, a
directory of them (the shape .ridl/baseline/ takes), or source — a file, a
package directory, or a workspace root — compiled in process. Only IR
artifacts are recognised by name, and three inputs are refused rather than
compiled (exit 2): an .ir.txtpb or .ir.binpb file, because diffs and
baselines read .ir.json only (ADR-0014 decision 5); a directory
that holds IR artifacts but no .ir.json snapshot and no source — no
ridl.toml and no .typl/.ridl file — which is a snapshot directory in a
refused encoding, not a source tree; and a directory with no source whose
.ir.json snapshots sit one level below it, which is a path aimed one level
too high. A directory is also exit 2, naming the entry, when an entry inside
it named like a snapshot cannot be stat'ed — a symlink to a file that is gone
— because skipping the entry would compare against a set one snapshot short;
this is checked before the directory is read as a source tree, so a source
directory holding such an entry is refused too. Every other input reaches
the source compiler. Omit both and pass --explain <CATEGORY> instead to print that
category's classification rule without comparing anything.
--encoding <ENCODING> names an encoding the change is judged for, with the
names a manifest's [backend.<name>] encodings list takes (flatbuffers,
proto3, podbuf); repeat the flag for several. A snapshot does not record
the encodings a workspace builds, so the command does not read them from a
manifest. Only podbuf has rules of its own, three of them. A struct field
appended to a struct that another struct, a tuple or an exact-length array
holds inline anywhere in the new side — in a struct, in the argument struct a
call with several parameters induces, or in a tuple reply — is
podbuf_inline_append, breaking with --encoding podbuf and compatible
without it, because the inline copy cannot grow without moving the fields
after it. An enum value, an enum-set flag or a union arm appended after which
the podbuf integer of the enum, of the enum set or the tag of the union is a
different integer, by width or by signedness — a value above 255, a flag at
bit 8 or an arm past ordinal 255 widens a u8 to a u16, a value above 65535
or a flag at bit 16 a u16 to a u32, and a value above 4294967295 or a flag
at bit 32 a u32 to a u64; an enum's integer is signed when a value is
negative, and since a negative value appended to an enum that has values is an
insert below its highest value, a breaking decl_added, the integer turns
signed only when an enum with no value gains values — is
podbuf_width_change, reported once per declaration with the old and the new
integer, breaking with --encoding podbuf because every head that holds the enum, the enum set or the union lays out a
different integer; an append that keeps the integer draws no such line. Without
the flag the line is reported compatible, which holds under proto3; an enum-set
flag that widens the integer also widens the enum set's FlatBuffers scalar,
which ridl diff --encoding flatbuffers does not report yet
(driftsys/ridl#1120). An enum value or an enum-set flag appended is
podbuf_strict_enum_append, reported once per declaration with the appended
names, breaking with --encoding podbuf and compatible without it, because
every enum and enum set is strict and the verify of an older reader refuses
a value or a bit its declaration does not name; an enum or an enum set whose
append changes the integer draws only the podbuf_width_change line. The
podbuf_inline_append line names each inline use, a struct field as
<package>/<struct>/<field> and an interaction's request, reply or fixed
payload as <package>/<interface>/<interaction>, and sits under the heading
breaking under podbuf either way, so that a workspace that does not build
podbuf still sees it. The search mirrors the heads the encoder lays out, a
declaration and a named induced tuple: a struct that is a root of its own (a
single named parameter, a named reply, a signal, event or fixed payload), that
is used only behind a descriptor (the element of a bounded array, the key or
value of a map entry, a union arm), or only inside an exact-length array that
is itself a root (a fixed payload [S; 2..2], which has no head) draws no
such line: the root or the descriptor's stride gives the producer's size. A
tuple inside such a root is a head, and a struct inside that tuple is a use.
A name outside the three is a usage error,
exit 2. With Reading held
inline by Bounds and a field appended to it:
ridl diff --encoding podbuf v1.typl v2.typl
breaking
[compatible] decl_added veh.cluster/Reading/quality: (absent) -> quality
breaking under podbuf:
[breaking] podbuf_inline_append veh.cluster/Reading/quality: (absent) -> nested inline in veh.cluster/Bounds/range
It writes nothing. The report goes to stdout; a compile error's diagnostics go to stderr instead and no report prints.
Two identical sources:
ridl diff old.ridl old.ridl
identical
An appended interaction — compatible:
ridl diff old.ridl compatible.ridl
compatible
[compatible] interaction_appended veh.cluster/VehicleStatus/doorClosed: (absent) -> event doorClosed
A signal's payload type changed — breaking:
ridl diff old.ridl breaking.ridl
breaking
[compatible] decl_added veh.cluster/Speed2: (absent) -> type
[breaking] payload_changed veh.cluster/VehicleStatus/currentSpeed: Speed -> Speed2
A named-form service's list is a set of interfaces: an interface joining it
is service_interface_added, one leaving it service_interface_removed, a
reorder no change, and both are compatible, because an interface's number
comes from its unit's interfaces.lock and the routing key does not
contain the service. A removal is still visible in source — the
service.member addresses of that interface stop resolving under the service
— so the text report lists it under a heading, shared with interface_renamed, printed once as a
line ending in a colon, after every change that has no heading. The JSON
report carries the category word and no heading field. With interface K
added and J dropped from service veh.cluster.dash : I, J:
ridl diff dash-old.ridl dash-new.ridl
compatible
[compatible] decl_added veh.cluster/K: (absent) -> interface
compatible on the wire, visible in source:
[compatible] service_interface_removed veh.cluster/veh.cluster.dash/J: J -> (removed)
An interface is matched by its number from the unit's interfaces.lock,
within the unit, not by its name — a declared interface and a service's
inline shape alike. A
rename that keeps its number, recorded with ridl lock <unit dir> --rename Old=New,
is interface_renamed: compatible on the wire, because the number is the
routing identity, and visible in source, because the generated identity-table
names change, so it shares the heading above; the path carries the new name,
and a change inside the renamed interface is reported under the new name too.
A number gone from the new side is interface_retired when that side's lock
retires it, and decl_removed, breaking, otherwise. Against an old side whose
numbers are frozen, a declaration with no lock entry carries a provisional
number, which is no identity: it is decl_added, and it is never matched to an
old interface, so a rename the lock does not record is decl_removed plus
decl_added. Against a provisional old side, interfaces are matched by name,
as described below. Two sides with no lock
file — two bare source trees, or a snapshot published before the lock existed —
are matched by name. Interfaces are matched within the unit, and the names on
both sides of a change are catalog names, so an interface moved between two
sibling packages of one unit keeps its number and is reported as
interface_renamed, from the old catalog name to the new one. With J renamed to Jay on its number, the lock beside
each file recording it, and Jay no longer listed in
service veh.cluster.dash : I, J:
ridl diff old/dash.ridl new/dash.ridl
compatible
compatible on the wire, visible in source:
[compatible] interface_renamed veh.cluster/Jay: J -> Jay
[compatible] service_interface_removed veh.cluster/veh.cluster.dash/J: J -> (removed)
An interface matched by name across a provisional old side has its number and
its provisional flag compared, because the catalog hash covers both. When the
number is the same on both sides and only the new side is frozen — the number
ridl lock froze in place — the change is interface_frozen: compatible,
because the routing key did not move, and listed under the heading
"compatible on the wire, may change the catalog hash", which it shares with
enum_reordered, because the catalog hash changes and a generated face refuses
a port whose catalog hash is another one. With old a unit before ridl lock
and new the same unit after it:
ridl diff old new
compatible
compatible on the wire, may change the catalog hash:
[compatible] interface_frozen veh.cluster/Doors: 1 (provisional) -> 1
A number that differs between the two sides is interface_number_changed,
breaking, because the number is the routing key. Between two builds with no
lock, an interface added before Doors in byte order of the name moves its
provisional number:
ridl diff old new
breaking
[breaking] interface_number_changed veh.cluster/Doors: 1 (provisional) -> 2 (provisional)
[compatible] decl_added veh.cluster/Aux: (absent) -> interface
A number 0 on either side, from a snapshot written before the lock existed,
is not compared. A published baseline never holds a provisional number,
because ridl baseline refuses one (RIDL-411), so both categories are seen
between two builds, or in a reversed comparison whose old side is a build.
An enum value or an enum-set bit takes its identity from the explicit number
it declares, so a body whose difference is only order — its values or bits
reordered with no number changed, an enum's reserved list reordered with no
entry changed, or both — moves nothing on the wire. The change is
enum_reordered, compatible, on the container's path and under the same
heading. The catalog hash covers only the declarations an interface reaches:
when an interface reaches the enum or enum set, the order of its values or
bits, and of an enum's reserved list, is part of the hash, so the hash
changes then. With the values of enum GearPosition reordered:
ridl diff old.ridl new.ridl
compatible
compatible on the wire, may change the catalog hash:
[compatible] enum_reordered veh.cluster/GearPosition
A value or bit whose number changed is member_reordered, breaking, with the
old and the new number in the detail.
The breaking comparison above with --format json:
ridl diff old.ridl breaking.ridl --format json
{
"verdict": "breaking",
"changes": [
{
"path": "veh.cluster/Speed2",
"category": "decl_added",
"verdict": "compatible",
"before": null,
"after": "type"
},
{
"path": "veh.cluster/VehicleStatus/currentSpeed",
"category": "payload_changed",
"verdict": "breaking",
"before": "Speed",
"after": "Speed2"
}
]
}
At the system. When both sides are source whose workspace declares a
system, ridl diff also lists the changes to the lowered system that are not
contract changes, under two headings and with no verdict (rsdl reference §14):
placement changed for a deployment or a machine added or removed, a machine
made external, or an instance moved to another machine; composition
changed for a component added to or removed from the system, an offers or
requires line added or removed, instances changed, a component made
external, a distribution added or removed, or a member line added to or
removed from a distribution. The verdict and the exit code are the contracts'
alone.
Moving Panel from machine Front to machine Rear in deployment Desk:
ridl diff old new
identical
placement changed
Desk/Rear: (absent) -> machine
Desk/Front: machine -> (removed)
Desk/veh.demo.Panel.Unit: Front -> Rear
With --format json the headings are the keys placement_changed and
composition_changed, each a list of {"path", "before", "after"}, present
only when they hold a change. An .ir.json snapshot, or a directory of them,
carries no system — ridl baseline publishes package snapshots only — so a
diff against .ridl/baseline/ lists no system change.
--explain for one category:
ridl diff --explain payload_changed
payload_changed
A signal, event, or fixed payload type changed.
breaking any direction, a stream added or removed included
Exit codes. 0 for identical and compatible above. 1 for breaking
above — the verdict, not a tool failure. 2 when a side fails to compile:
ridl diff old.ridl broken.ridl
error[TYPL-011]: unknown type name `NoSuchType`
┌─ broken.ridl:4:25
│
4 │ signal currentSpeed : NoSuchType @10ms
│ ^^^^^^^^^^
2 when an input path does not exist:
ridl diff old.ridl does-not-exist.ridl
error: does-not-exist.ridl: `does-not-exist.ridl` does not exist
2 when neither both inputs nor --explain are given:
ridl diff old.ridl
error: `ridl diff` needs both an old and a new input, or `--explain <CATEGORY>`
and 2 for an --explain category that is not one of the categories the tool
knows, which the error lists in full:
ridl diff --explain not_a_real_category
error: unknown change category `not_a_real_category`
the categories `ridl diff` reports are:
decl_added
decl_removed
interface_renamed
interface_retired
interface_frozen
interface_number_changed
member_reordered
enum_reordered
interaction_appended
interaction_inserted
interaction_reordered
interaction_removed
interaction_retired
kind_changed
payload_changed
return_changed
params_changed
timing_changed
rpc_bound_changed
limit_changed
contract_changed
interaction_class_changed
width_changed
constraint_changed
init_changed
reserved_name_redeclared
service_changed
service_interface_added
service_interface_removed
doc_only
visibility_changed
podbuf_inline_append
podbuf_width_change
podbuf_strict_enum_append
ridl lock
ridl lock --help
Allocate a number to every interface that has none and write each unit's `interfaces.lock`; with `--rename` or `--retire`, rewrite one unit's entries in place instead. Exit 0 when the file is written or nothing changes, 1 on a diagnostic error, 2 on a bad flag or a path or I/O failure. `ridl lock merge` is the git merge driver for the file
Usage: ridl lock [OPTIONS] [PATH]
ridl lock <COMMAND>
Commands:
merge The git merge driver for `interfaces.lock`: a three-way merge over entries matched by number, written to OURS. Exit 0 when the merge is clean, 1 when entries disagree (they are left between conflict markers of MARKER_SIZE, and the file is RIDL-410 until resolved), 2 when an input cannot be read or does not parse (OURS is left as it was). Register it with `.gitattributes` and `git config` as the CLI reference documents
help Print this message or the help of the given subcommand(s)
Arguments:
[PATH] A unit directory, a workspace root, or a file. A directory named `merge` is spelled `./merge`, since the bare word is the subcommand [default: .]
Options:
--rename <OLD=NEW> Rewrite the live entry OLD to hold the key NEW, keeping its number (repeatable). NEW must be a declaration without an entry
--retire <NAME> Mark the live entry NAME retired, keeping its line and its number (repeatable). NAME must no longer be declared
-h, --help Print help
It writes one interfaces.lock per unit, in the directory of the unit's
ridl.toml — the line table that gives every interface of the unit its number
(ridl §11): a # header, next N, then one entry per interface, Name N,
with the word retired after the number when the interface is gone. An
interface of a subpackage is keyed by its name relative to the unit, such as
cluster.Speed. A service's inline shape is an interface too and is keyed
service: followed by the service's full dotted name. Only ridl lock writes the file: the
compiler reads it beside the sources, and ridl fmt never touches it.
Plain ridl lock is the only form that allocates. Every declared interface
whose name has no live entry gets the next free number, in byte order of the
name, and the file is written; a declaration that already has its entry is
left as it is. Over a unit holding interface Zone and interface Cabin
and no lock file yet:
ridl lock . ; echo "exit: $?"
allocated Cabin 1
allocated Zone 2
exit: 0
# interfaces.lock — written by ridl lock; do not edit by hand.
next 3
Cabin 1
Zone 2
Run again with nothing to allocate, it prints nothing, writes nothing and
exits 0. Over a workspace it writes each unit's own file, and each output
line is prefixed with the unit directory relative to PATH and a colon:
hvac: allocated Cabin 1. A PATH inside a workspace member compiles the
whole workspace and allocates in, edits and reports on that member only.
Until ridl lock has run, a declaration with no entry compiles with a
provisional number, which carries no identity. The catalog hash covers each
number and its provisional flag, so ridl lock changes it: ridl diff
reports a number frozen in place as interface_frozen, compatible, and a
number frozen as another one as interface_number_changed, breaking.
The reverse case — a live entry whose interface is gone from the source — is
RIDL-409 from the compiler, and plain ridl lock refuses to allocate over it:
nothing is written, exit 1. The two flags are that diagnostic's fix, and they
run with RIDL-409 present:
--rename OLD=NEWrewrites the entryOLDto hold the keyNEWin place, keeping its number, whenNEWis a declaration without an entry — the same interface under a new name. Printed asrenamed Old New N.--retire NAMEmarks the entry retired, keeping its line and its number, when nothing declaresNAMEany more. Printed asretired Name N.
Both are repeatable, neither allocates, and PATH must resolve to exactly one
unit. OLD, NEW and NAME are dotted names relative to the unit, such as
cluster.Old, so a rename across sibling packages is
ridl lock <unit dir> --rename cluster.Old=cluster.New. An interfaces.lock in
a subdirectory of a unit is not read and draws the warning RIDL-416
(lock-in-subdirectory). A rename keeps the number because the number, not the name, is the
interface's wire identity; the old name is then free for a later, unrelated
interface. Starting from the file above with interface Zone renamed to
interface Lane in the source:
ridl check . ; echo "exit: $?"
error[RIDL-409]: `Zone` is a live entry of `interfaces.lock` with no declaration in the unit: run `ridl lock . --rename Zone=New` when a declaration without an entry, `New`, is this interface under a new name, or `ridl lock . --retire Zone` when the interface is gone
┌─ ./interfaces.lock:4:1
│
4 │ Zone 2
│ ^^^^^^
exit: 1
ridl lock . --rename Zone=Lane ; echo "exit: $?"
renamed Zone Lane 2
exit: 0
The merge driver. Two branches that each edited interfaces.lock merge
through ridl lock merge, a three-way merge over entries matched by number
rather than over lines. git runs it when the repository registers it: one
versioned line in .gitattributes, and one git config line per clone,
which git does not version.
interfaces.lock merge=ridl-lock
git config merge.ridl-lock.driver "ridl lock merge %O %A %B %L"
ridl lock merge --help
The git merge driver for `interfaces.lock`: a three-way merge over entries matched by number, written to OURS. Exit 0 when the merge is clean, 1 when entries disagree (they are left between conflict markers of MARKER_SIZE, and the file is RIDL-410 until resolved), 2 when an input cannot be read or does not parse (OURS is left as it was). Register it with `.gitattributes` and `git config` as the CLI reference documents
Usage: ridl lock merge <BASE> <OURS> <THEIRS> <MARKER_SIZE>
Arguments:
<BASE> The common ancestor's file (`%O`); an empty file reads as `next 1`
<OURS> The current branch's file (`%A`); the result is written here
<THEIRS> The other branch's file (`%B`)
<MARKER_SIZE> The length of a conflict marker line (`%L`, 7 by default)
Options:
-h, --help Print help
At each number the driver applies git's own three-way rule: a change on one
side is taken, the same change on both sides is kept once, and two different
changes to one entry — a retire against a rename, or two renames — are left
between conflict markers, with every other entry written plain. One rule is
the driver's own: when both sides allocated one number to two different
interfaces, ours keeps the number and theirs is renumbered to the next free
one, which is safe because a branch never allocates. A live name on two
numbers — each side allocated the same interface on its own number — is a
conflict too. next is the maximum of the three sides plus one per
renumbered entry, so it is never lowered. With A 1, B 2 as the base, ours
retiring B and theirs renaming it to Bee:
# interfaces.lock — written by ridl lock; do not edit by hand.
next 3
A 1
<<<<<<< ours
B 2 retired
=======
Bee 2
>>>>>>> theirs
The file is malformed (RIDL-410) until an author keeps one side and deletes
the markers, and ridl check refuses it until then. git's union driver is
not a substitute: it discards the base, and resurrects a renamed or retired
entry silently. An empty BASE — what git passes when both branches created
the file — reads as next 1 with no entries. A package directory named
merge is spelled ./merge: the bare word is the subcommand.
Exit codes. 0 when the file is written or there is nothing to change. 1
on a diagnostic error over the source, with nothing written: a live entry with
no declaration when plain ridl lock is asked to allocate (RIDL-409), a
malformed lock file — a git conflict left in it included — (RIDL-410), or any
other compile error, in any package of the workspace. 2 when the path is
missing or unreadable, on a bad flag — --rename naming no live entry or a
NEW that is not a declaration without an entry, --retire naming an
interface that is still declared, either flag over more than one package — or
on an I/O failure writing the file. Every cell is confirmed against the built
binary by crates/ridl/tests/lock_cli.rs. ridl lock merge exits 0 when the
three sides merge clean and OURS is written; 1 when entries disagree — OURS is
written with the conflict markers and is malformed until resolved; 2 when an
input cannot be read or does not parse (OURS is left as it was), when
MARKER_SIZE is not a number from 1 up, or on an I/O failure writing OURS.
Every cell is confirmed by crates/ridl/tests/lock_merge.rs, which also
registers the driver in a temporary repository and merges two branches
through git.
ridl lsp
ridl lsp --help
Run the language server over stdio: exit 0 after `shutdown` then `exit`, 1 when the client ends the session without `shutdown`, 2 on a transport error. Editors spawn this. Stdio is the only transport
Usage: ridl lsp [OPTIONS]
Options:
--stdio Select the stdio transport. Accepted because editor clients pass it, as the LSP specification recommends; omitting it changes nothing
--clientProcessId <PID> The editor's process id, which the LSP specification recommends a server accept. Ignored: the `initialize` request carries it too
-h, --help Print help
ridl lsp hosts the language server: behavior lives in crates/ridl-lsp, and
this subcommand only wires the stdio transport. An editor spawns it and speaks
the Language Server Protocol over its stdin and stdout.
The server never fetches. It reads the remote units of [imports] from
vendor/ at the manifest root and then from the cache, as ridl check --frozen
does, so run ridl check or ridl vendor once in a fresh clone.
It loads the remote units once, when it loads the workspace, and does not
reload them when ridl.lock changes: after a ridl check that fetches a unit
or rewrites ridl.lock, restart the server. It publishes no diagnostics for a
file of a remote unit; it shows the errors of a remote unit once per load, in
one window message.
Flags. --stdio and --clientProcessId=<PID> are accepted and change
nothing. The LSP specification (3.17, "Implementation Considerations")
recommends that a server accept both, and editor clients pass them: the VS
Code client runs the server as ridl lsp --stdio. Stdio is the only
transport, so --pipe and --socket are refused with exit 2. The client's
process id also arrives in the initialize request.
Exit codes. They follow the LSP specification (3.17, "Exit
Notification"). 0 on a clean shutdown — the client sends shutdown then
exit. 1 when the client sends exit with no shutdown before it, or closes
stdin after the handshake with no shutdown. 2 when the transport ends before
the initialize handshake, or fails for any other reason. All four outcomes are
confirmed directly against the built binary by crates/ridl/tests/servers.rs.
ridl mcp
ridl mcp --help
Run the MCP server over stdio for an agent host: exit 0 on a clean shutdown, 2 on a transport error. It takes no flag of its own
Usage: ridl mcp
Options:
-h, --help Print help
ridl mcp serves the Model Context Protocol over stdio with nine tools:
ridl_checkchecks workspace files or a source text.ridl_explainexplains a diagnostic code or diff category.ridl_resolveresolves a declaration and its location.ridl_describe_typedescribes a non-interface declaration.ridl_list_interactionslists an interface's interactions.ridl_referenceslists declarations and interactions using a declaration.ridl_dependencieslists package dependencies and dependents.ridl_metricsreports package coupling and interface cohesion groups.ridl_diffcompares source workspaces or IR snapshots.
Behavior lives in crates/ridl-mcp; this subcommand builds the Tokio runtime
and wires the stdio transport. An agent host spawns it and speaks MCP over stdin
and stdout. The crate README documents input
and output schemas, tool errors and the read-only workspace contract. The command
takes no flag of its own.
Exit codes. 0 on a clean shutdown: the host closes the server's stdin,
which is how a stdio host ends an MCP session. 2 when the Tokio runtime fails
to build, when the transport ends before the initialize handshake, or when a
task the SDK runs for the session fails after it. A read failure on stdin
after the handshake is not distinguishable from the host closing stdin — the
SDK logs it and ends the session the same way — so it exits 0. There is no
exit 1: ridl mcp answers no question that can come back negative. The clean
shutdown and the transport ending before the handshake are confirmed directly
against the built binary by crates/ridl/tests/servers.rs; a session-task
failure cannot be provoked through the server, so its mapping is tested on its
own in crates/ridl-mcp.
ridl init
ridl init --help
Scaffold a new unit in a directory that exists (defaults to the current directory): a package, or with `--workspace` a workspace and one member. Writes `ridl.toml` and one source file, and exits 2 without writing anything when a target file exists, the name is not a legal package name or holds a reserved word, or a `ridl.toml` above or below the directory would make a manifest inside a unit (MANI-013)
Usage: ridl init [OPTIONS] [PATH]
Arguments:
[PATH] The existing directory to scaffold into [default: .]
Options:
--workspace Scaffold a workspace root and one member instead of a package
--name <NAME> The package name (the member name with `--workspace`); the directory name by default
-h, --help Print help
Writes the files of a new unit into a directory that already exists, and
nothing else. The default is one standalone package: a ridl.toml that holds a
[package] table with the version 0.1.0, and <name>.ridl in the same
directory, which declares package <name>, one unit-carrying type and one
interface with one signal. ridl check and ridl fmt --check draw nothing on
it. On success the command prints Created package <name> to stdout.
--workspace writes a root ridl.toml with [workspace] members = ["<name>"],
and the package above in the directory <name> under it. The command then
prints Created workspace <name>, and ridl check passes from the root and
from inside the member.
The name is the directory's basename when that is already a legal package name
(lowercase dot-separated segments, each a letter followed by letters or
digits). The basename is taken from the path as written, so a symbolic link is
not followed to find it. The command never rewrites a name: a basename that is
not legal exits 2 with an error that asks for --name, and so does a --name
value that is not legal. A name with a reserved word as a segment, such as
type or veh.signal, is also refused, because a source file that declares it
does not parse. With --workspace, --name names the member and the root
directory keeps its own name.
Exit codes. 0 when the files are written. 2, with an error: line on
stderr and nothing written, when a target file already exists, the path is not a
directory, the name is not legal or holds a reserved word, or a ridl.toml
above or below the package directory would make a manifest inside a unit
(MANI-013): the directory sits inside another unit or inside a workspace, or it
already holds a ridl.toml in a subdirectory that ridl check would also
reject. The search below is the loader's own: it skips directories whose name
starts with a dot and does not follow symbolic links. This command does not add a member
to an existing workspace. The search upward does not stop at a .git directory,
because MANI-013 does not, and it follows symbolic links. Every target is
checked before any file is written, the member files are written before the
workspace root manifest, and a write that fails removes the paths this command
created. There is no exit 1. The cases are confirmed against the built binary by
crates/ridl/tests/scaffold_cli.rs.
ridl new
ridl new --help
Create a directory, with its parents, and scaffold a new unit in it, as `ridl init` does. Exits 2 without writing anything when the path exists
Usage: ridl new [OPTIONS] <PATH>
Arguments:
<PATH> The directory to create; it must not exist
Options:
--workspace Scaffold a workspace root and one member instead of a package
--name <NAME> The package name (the member name with `--workspace`); the directory name by default
-h, --help Print help
Creates the directory at PATH, with its parents, and scaffolds into it exactly
as ridl init does, with the same flags, the same output and the
same refusals. A PATH that already exists exits 2 and leaves the file system
as it was. The name defaults to the last component of PATH.
ridl new vehicle-demo --name demo
ridl describe
ridl describe --help
Print a catalog descriptor as strict JSON, after verifying it
Usage: ridl describe <PATH>
Arguments:
<PATH> The `<unit>.catalog.binfb` file `ridl build --emit catalog` wrote
Options:
-h, --help Print help
Prints a catalog descriptor written by ridl build --emit catalog as JSON after
verifying it; a file that is not a descriptor, or is malformed, is rejected as a
whole with exit code 2.
The transcript below is abridged: it is the output for the test corpus in
crates/ridl/tests/baseline-corpus, with each ... line standing for lines
that were removed. The keys print in alphabetical order. First comes
compatible, the earlier catalog hashes the descriptor lists as compatible with
this one, each as its 32 bytes; it is empty here, because the corpus's
published baseline holds no corpus.baseline.catalogs file. Then come the 32
bytes of the catalog hash, one entry per interface, the unit name, the retired
numbers, the toolchain version that wrote the file, and the descriptor's schema
version.
ridl build crates/ridl/tests/baseline-corpus --emit catalog
ridl describe out/corpus.baseline.catalog.binfb
{
"compatible": [],
"hash": [
105,
123,
139,
...
],
"interfaces": [
{
"members": [
{
"kind": "Signal",
"limit": null,
"name": "currentSpeed",
"ordinal": 1,
"payloads": [
{
"max_sizes": [
{
"bytes": 46,
"cause": "Unspecified",
"encoding": "FlatBuffers",
"state": "Bounded"
}
],
"role": "value",
"type_name": "Speed"
}
],
"timing": {
"max_us": "10000",
"min_us": "10000",
"mode": "StrictPeriodic"
}
},
...
],
"name": "VehicleStatus",
"number": 1,
"provisional": true,
"reserved_ordinals": []
},
...
],
"name": "corpus.baseline",
"retired": [],
...
"version": 1
}
Exit codes. 0 when the descriptor was printed. 2 for a missing or
unreadable path, a file that is not a catalog descriptor, a version this
toolchain does not read, a malformed buffer, or an I/O failure writing to
stdout (a pipe whose reader has gone); the message names the path and the
cause. There is no exit 1: ridl describe answers no question that can come
back negative. The exit-0 outcome, the missing path, the foreign file, a
malformed buffer (three fixtures), a version the toolchain does not read, and the write
failure on stdout are confirmed against the built binary by
crates/ridl/tests/describe_cli.rs.
ridl vendor
ridl vendor --help
Copy every remote unit of the workspace's `[imports]` into `vendor/` at the manifest root, so a `--frozen` build needs no network and no cache. Every directory in `vendor/` that no `[imports]` key names is removed; files and hidden entries are left. Exit 0 when `vendor/` was written or there was nothing to change, 1 on a diagnostic error (`vendor/` is left untouched; `ridl.lock` may be updated), 2 on a missing or unreadable path, a fetch failure, or an I/O failure. With `--check`, compare `vendor/` with the imports and `ridl.lock` instead, writing nothing
Usage: ridl vendor [OPTIONS] [PATH]
Arguments:
[PATH] A unit directory, a workspace root, or a file; the manifest root above it is the one vendored [default: .]
Options:
--check Compare `vendor/` with `[imports]` and `ridl.lock` instead of writing: no network, no write. Exit 0 when `vendor/` matches, 1 on drift or another diagnostic error, 2 on a missing or unreadable path
-h, --help Print help
ridl vendor makes a project build with no network and no cache; the
Remote units and ridl vendor chapter is the guide. PATH is a
unit directory, a workspace root or a file, and defaults to the current
directory; the command acts on the manifest root above it, found as ridl check
finds it, so vendor/ is at the workspace root whichever member the command
runs from. It then does three things:
- It takes the union of the
[imports]of the root manifest and of every member. - It materializes each unit as
ridl checkdoes, from the cache or by a fetch, and rewritesridl.lock. It reads the cache and nevervendor/, so a URL that changed is fetched and its directory replaced. - It brings
vendor/to exactly that set. Each unit is written whole into a staging directory and renamed into place under its[imports]key, with the bytes of its files unchanged. Every directory directly undervendor/whose name is no[imports]key is removed, whether or notridl vendorwrote it, so that, after a run, no directory other than a remote unit's remains among the non-hidden entries ofvendor/; a file, a symbolic link and a hidden entry (a name that starts with.) are left alone. Nothing invendor/changes when the run has an error.
Each changed unit prints one line, vendored <key> or removed <key>; a run
that changes nothing prints vendor/ is up to date; nothing to change. A unit's
own vendor/ directory is copied with the unit, because it is part of the tree
hash, and is skipped when the unit is read.
The tree hash is computed over the bytes of each file, so a checkout that
converts line endings changes the bytes of a vendored file and makes its unit
MANI-107. Mark the directory -text in .gitattributes with the line
vendor/** -text, which stops line-ending conversion.
ridl vendor
After that, ridl check --frozen reads vendor/<key>/ first, only when
ridl.lock pins a tree for the unit. For any other unit it reads the cache
first, which gives the pin, and a vendored directory, if present, is then
verified against that pin and read in place of the cache entry. A vendored tree
that no longer matches its tree hash is MANI-107 and names ridl vendor.
ridl vendor exit codes.
| Exit | Meaning |
|---|---|
| 0 | vendor/ was written, or there was nothing to change |
| 1 | a diagnostic error, including MANI-102, MANI-105, MANI-106, MANI-107 and a source error in a remote unit; vendor/ is left untouched, while ridl.lock may be updated, as it is by every command that reads remote units without --frozen: it is rewritten when every remote unit was fetched and verified, even if a source error follows |
| 2 | a missing or unreadable path, no ridl.toml at or above it, a bad flag, an I/O failure, or a fetch failure (MANI-101) |
ridl vendor applies no [lints] levels, and a lint that is allow by default
is left out of its output, as for ridl lock.
ridl vendor --check compares vendor/ with the imports and ridl.lock.
It fetches nothing, reads no cache and writes nothing, so it is the check to run
in CI. It reports one diagnostic per unit:
MANI-109, an imported unit with no directory invendor/;MANI-110, a directory invendor/that no[imports]key names;MANI-107, a vendored tree that does not match itstreehash;MANI-103orMANI-108, a unit thatridl.lockdoes not pin or pins with notree, because its directory cannot be verified.
A manifest that does not parse is reported too, so a broken manifest is never a
clean vendor/.
ridl vendor --check exit codes.
| Exit | Meaning |
|---|---|
| 0 | vendor/ matches |
| 1 | drift (MANI-107, MANI-109 or MANI-110), a unit that ridl.lock does not verify (MANI-103 or MANI-108), or another diagnostic error |
| 2 | a missing or unreadable path, no ridl.toml at or above it, or a bad flag |
ridlc
ridlc --help
The RIDL family compiler (plumbing)
Usage: ridlc <COMMAND>
Commands:
check Type-check a file, package directory, or workspace
build Compile to the selected artifacts
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
ridlc has no humane defaults: PATH is required on both subcommands, and
build's --out-dir is required rather than defaulted to out. Both are
usage errors — exit 2 — courtesy of clap, not application code:
ridlc check
error: the following required arguments were not provided:
<PATH>
Usage: ridlc check <PATH>
For more information, try '--help'.
ridlc check
ridlc check --help
Type-check a file, package directory, or workspace
Usage: ridlc check [OPTIONS] <PATH>
Arguments:
<PATH> The `.typl` file, package directory, or workspace root
Options:
--frozen Verify remote imports against `ridl.lock` without fetching or regenerating it (CI mode, ADR-0002 §7)
-h, --help Print help
It writes ridl.lock under the same condition ridl check does — the
manifest declares [imports], the run is non-frozen, and materialization
found no error — because ridl check calls this very function
(ridlc::run_check) directly, forwarding its own PATH and --frozen.
Otherwise it reads the workspace, renders diagnostics to stderr, and touches
no file.
Exit codes. 0 clean, 1 a diagnostic is an error, 2 an input/output or
usage error — identical in shape to ridl check, and, on the same input,
identical in rendered text:
ridlc check .
error[TYPL-104]: range minimum 250 is greater than maximum 0
┌─ ./demo.ridl:3:19
│
3 │ type Speed : km/h [250.0..0.0 step 0.5]
│ ^^^^^^^^^^^^^^^^^^^^^
run over the same directory as the type Speed : km/h [250.0..0.0 step 0.5]
example under ridl check above — byte for byte the same
diagnostic, confirmed by diffing the two runs' stderr. A workspace ridlc
cannot find is the same exit 2 as ridl check:
ridlc check /nonexistent/ridl/workspace
error: `/nonexistent/ridl/workspace` does not exist
ridlc build
ridlc build --help
Compile to the selected artifacts
Usage: ridlc build [OPTIONS] --out-dir <OUT_DIR> <PATH>
Arguments:
<PATH>
The `.typl` file, package directory, or workspace root
Options:
--out-dir <OUT_DIR>
The directory to write generated artifacts into
--emit <EMIT>
The artifacts to emit: `rust` (default), `ir-json`, `ir-text`, `ir-binary`, `typescript`, `proto`, `flatbuffers`, `podbuf`, `codegen-model`, `codegen-request`, `catalog`
Possible values:
- rust: Idiomatic Rust source, written to `<base>.rs`
- ir-json: The lowered IR v2 as exact-decimal JSON, written to `<base>.ir.json`, and the lowered system to `<pkg.Name>.system.json`
- ir-text: The lowered IR v2 as prototext, written to `<base>.ir.txtpb`, and the lowered system to `<pkg.Name>.system.txtpb`
- ir-binary: The lowered IR v2 as protobuf binary, written to `<base>.ir.binpb`, and the lowered system to `<pkg.Name>.system.binpb`
- typescript: Idiomatic TypeScript source, written to `<base>.ts`, and the codec module `<base>.codec.ts` with `[backend.typescript] codec = true`
- proto: The proto3 schema, written to `<base>.proto`
- flatbuffers: The FlatBuffers schema, written to `<base>.fbs`
- podbuf: The podbuf C header, written to `<base>.h`
- codegen-model: The lowered codegen model (`ridl.codegen.v1`) as canonical protobuf JSON, written to `<base>.codegen.json`
- codegen-request: The whole codegen request a build hands a backend, as canonical protobuf JSON, written to `<base>.codegen-request.json`
- catalog: The catalog descriptor an engine reads, written to `<unit>.catalog.binfb` for every unit that declares an interface or a service with an inline body: a FlatBuffers file of the unit's interfaces, their members and their catalog hash
[default: rust]
--plugin <LANGUAGE[=PATH]>
A codegen plugin to run beside the emits, once per package: `<LANGUAGE>` runs `ridlc-gen-<LANGUAGE>` from PATH, `<LANGUAGE>=<PATH>` runs the executable at PATH. Repeatable
--plugin-timeout <SECONDS>
Seconds a plugin may run per package before it is killed
[default: 60]
--frozen
Verify remote imports against `ridl.lock` without fetching or regenerating it (CI mode, ADR-0002 §7)
--deployment <NAME>
The deployment to carry in each codegen request. With one deployment in the workspace it is selected without this flag; with several, none is carried unless named
-h, --help
Print help (see a summary with '-h')
It writes the same artifacts as ridl build — and ridl.lock under the
same [imports] condition — under the --out-dir you must now name
explicitly. --plugin, --plugin-timeout and --deployment are the same three
flags as on ridl build, spelled and documented identically.
ridlc reads no baseline, so its descriptors and codegen requests carry an
empty compatible list; ridl build is the command that fills it:
ridlc build . --out-dir out && find out -type f | sort
out/Cargo.toml
out/cli.demo.rs
out/lib.rs
Exit codes. 0/1/2 in the same shape as ridl build — clean, a diagnostic
is an error, or an input/output error such as a missing --out-dir (usage,
exit 2) or a workspace that cannot be found (exit 2).
How ridl and ridlc relate
ridlc is the plumbing: two subcommands, check and build, both with a
required PATH and no defaults — stable flags meant for CI and build
scripts. ridl check and ridl build call straight into ridlc's own
library functions and add nothing to the compile itself; what they add is
humane defaults (PATH defaults to ., ridl build's --out-dir defaults
to out) and, on check only, the baseline desk check described
above, which has no ridlc equivalent. On identical input the
two render byte-identical diagnostics, confirmed earlier on this page.
ridl baseline, ridl test, ridl fmt, ridl diff, ridl lock,
ridl describe, and ridl vendor have no ridlc counterpart at all — ridlc's surface is check and build, full stop, as
its own --help shows. Reach for ridl unless you are scripting the
compiler directly and want its stable, default-free flags.
Exit codes across the toolchain
| Command | 0 | 1 | 2 |
|---|---|---|---|
ridl check / ridlc check | clean (warnings included) | a diagnostic is an error | the workspace cannot be found, or — for ridl check only — a --baseline problem: absent, wrongly encoded (not .ir.json), unreadable, a snapshot-named entry inside it that cannot be stat'ed, its snapshots nested one level too deep, empty when named explicitly, or a snapshot that fails to parse |
ridl build / ridlc build | clean, every requested artifact written | a diagnostic is an error, nothing written — except for an RSDL-7xx error, which leaves only its deployment out of the lowered system | the workspace cannot be found, or (for ridlc build) a missing --out-dir |
ridl baseline | clean, snapshot(s) published | a diagnostic is an error, or the publication gate refuses: the replacement under the tombstone rule (RIDL-408), a provisional interface number (RIDL-411), or a published number the fresh snapshot neither carries nor retires (RIDL-412); the existing baseline is left untouched | the workspace cannot be found, the output directory cannot be read or written, a published .ir.json snapshot fails to parse, two published snapshots declare one package, or a snapshot-named entry in the output directory cannot be stat'ed |
ridl test | every range self-corpus and sampled require passed | a self-corpus failure, or a clause raised an evaluation error | the workspace fails to compile, cannot be found, or --samples 0 |
ridl fmt | nothing under --check would change, or the rewrite succeeded | a file under --check would change, or has a parse error | the path does not exist, is vendor/ at the manifest root or lies under it, or a directory the walk reaches is unreadable — named in the message, unlike ridl check, ridl build, ridl baseline, ridl lock, ridlc check and ridlc build, which name no path at all |
ridl diff | the change is compatible, or the two sides are identical | the change is breaking | a side fails to compile, an input is missing, or neither --explain nor both inputs were given |
ridl lock | the file is written, or there is nothing to change | a diagnostic error over the source, nothing written: a live entry with no declaration under plain ridl lock (RIDL-409), a malformed lock file (RIDL-410), or any other compile error | the path is missing or unreadable; a bad flag — --rename naming no live entry or a NEW that is not a declaration without an entry, --retire naming a still-declared interface, either flag over more than one package; an I/O failure writing |
ridl lock merge | the three sides merge clean, and the result is written to OURS | entries disagree: OURS is written with conflict markers around only the disagreeing entries, and is malformed (RIDL-410) until resolved | an input cannot be read or does not parse (OURS is left as it was), MARKER_SIZE is not a number from 1 up, or an I/O failure writing OURS |
ridl init / ridl new | the files are written | a target file exists, new was given an existing path, the path is not a directory, the name is not a legal package name or holds a reserved word, or a ridl.toml above or below the package directory would make a manifest inside a unit (MANI-013); nothing is written | |
ridl describe | the descriptor was printed | a missing or unreadable path; a file that is not a catalog descriptor; a version this toolchain does not read; a malformed buffer; an I/O failure writing to stdout | |
ridl vendor | vendor/ was written, or there is nothing to change; with --check, vendor/ matches the imports and ridl.lock | a diagnostic error (including MANI-102, MANI-105, MANI-106, MANI-107 and a source error in a remote unit), with vendor/ left untouched and ridl.lock possibly updated; with --check, drift (MANI-107, MANI-109, MANI-110) or a unit that ridl.lock does not verify (MANI-103, MANI-108) | a missing or unreadable path, no ridl.toml at or above it, a bad flag, an I/O failure writing vendor/, or a fetch failure (MANI-101) |
This table is this repository's own taxonomy, recorded in
ADR-0010: 0 succeeded, or the verdict is affirmative; 1 a
real answer that happens to be negative; 2 the tool could not answer at
all. clig.dev, which the ADR quotes rather than paraphrases, says
nothing about a multi-valued exit code — it treats exit status as a binary
success/failure signal. ridl diff's breaking-change exit 1,
ridl fmt --check's would-reformat exit 1, and ridl test's evaluation-fault
exit 1 rest on the external diff(1)/grep(1) convention of a specific
non-zero code being part of the answer, not on any clig endorsement of it —
clig is silent here, not permissive.
Exit code 1 means something different in almost every row: for ridl diff it
is the breaking verdict, a statement about the change, not a failure of the
tool; for ridl test it is a test failure — a self-corpus problem or an
evaluation error, where a suspect finding is not enough to fail the run; for
ridl check, ridl build, and ridl baseline it is an ordinary diagnostic
error; for ridl fmt it is would change or unparsable. Reading a bare
$? of 1 without knowing which subcommand produced it says nothing on its
own.
Exit code 2 is the one genuinely shared meaning — an input the command could
not use at all, including ridl fmt's missing or unreadable path since
ADR-0010 closed the gap documented under
ridl fmt above. A clap-level usage error — a missing
required argument, an unrecognized flag, or no subcommand at all — is also
exit 2 on both binaries, which happens to agree with the applications' own
convention rather than being one of their exit paths:
ridl check --bogus-flag
error: unexpected argument '--bogus-flag' found
tip: to pass '--bogus-flag' as a value, use '-- --bogus-flag'
Usage: ridl check [OPTIONS] [PATH]
For more information, try '--help'.
--help itself, on any subcommand of either binary, always exits 0 — and so
does --version/-V, covered above.
Seven of the subcommands that take a path also share a lesser-known gap:
issue driftsys/ridl#196 records that when the workspace root
itself is unreadable, ridl check, ridl build, ridl baseline,
ridl lock, ridlc check, and ridlc build all report
error: no `ridl.toml` found at or above `<path>` — exit 2 is right, the
cause is wrong, confirmed directly against this build — and when a
subdirectory nested inside an otherwise-readable workspace is unreadable,
the same six report a bare error: Permission denied (os error 13), naming
no path at all, also confirmed directly. ridl test reaches the same code
path and wraps it with the top-level path it was given
(error: <path>: no ridl.toml found… in the first case,
error: <path>: Permission denied… in the second — also confirmed directly),
so it names a path but the wrong one in the second case: the workspace
root, not the subdirectory that actually failed. ridl fmt is the one
subcommand that names the actual cause in both cases (see
above); ridl diff names it only when the unreadable directory
is the argument passed to it directly, not one nested inside a readable
argument.