Documenting your API

A contract is read by more people than the ones who write it: the authors of each component that offers or requires it, the reviewers of a change, and the assistant that completes a line in an editor. A doc comment is where the contract says what a type, a field or an interaction means. This chapter covers where a doc comment goes, what it can hold, and the lints that check it. The rules are in the typl reference §14, and the decisions behind them in ADR-0026.

What is built. The checker reads the doc comment of every declaration and every member, resolves the links in it, and stores the doc, the resolved links and the tags in the IR. The language server shows them on hover and in completion. The doc lints of this chapter are reported by ridl check, ridl build, the language server and the MCP tool ridl_check, with the levels of the project's [lints] table.

The Rust and TypeScript backends write the docs, their links, the @see, @since and @deprecated tags and the item's rules into the generated code, as Using the generated Rust code describes.

What is not built. There is no ridl doc command that writes a documentation site.

A documented package

A doc comment is written on the lines directly above the item it documents. Here is a small package with every item documented:

package docs.cruise

/// A vehicle speed over ground, as the brake controller computes it from the
/// four wheel speed sensors.
/// @since 1.0
type Speed: km/h [0.0..250.0 step 0.5]

/// The position the driver moved the cruise control lever to.
enum LeverCmd {
  /// The lever is at rest.
  NONE = 0
  /// Raise the set speed by one step. The step is the `step` of [Speed].
  UP = 1
  /// Lower the set speed by one step.
  DOWN = 2
  /// Switch cruise control off. The set speed is kept for a later resume.
  CANCEL = 3
}

/// The state that [CruiseControl] publishes after each lever command.
struct CruiseState {
  /// Whether cruise control holds the vehicle at [CruiseState.setSpeed].
  engaged: Engaged
  /// The speed cruise control holds while it is engaged.
  setSpeed: Speed
}

/// Whether cruise control is engaged.
type Engaged: boolean

/// The cruise control function of one vehicle.
///
/// The interface accepts lever commands from the steering column switch and
/// publishes the resulting state.
/// @see docs.cruise.LeverCmd
interface CruiseControl {
  /// The current cruise control state. A consumer that subscribes late reads
  /// the last published value.
  signal status: CruiseState @[50ms..500ms]

  /// Applies one lever command. The outcome is published on
  /// [CruiseControl.status]; the command does not return it.
  command setLever(
    /// The lever position read from the switch.
    cmd: LeverCmd
  ) @[..100ms]
}

The rest of this chapter takes the parts of this example one at a time.

House style

  • Write doc comments as /// lines. The compiler also accepts the block form /** ... */ with the same meaning, and ridl fmt does not rewrite one form into the other. A project that wants only /// turns on the doc-comment-style lint (see The doc lints).
  • Start with one sentence that says what the item is. Add a blank /// line and more paragraphs when the reader needs them.
  • Say what the declaration itself cannot say: what a value means, where it comes from, and what happens when it changes. Do not repeat the range, the unit or the timing. The editor shows those from the declaration itself, in the Contract part of the hover (see In the editor).
  • Document an interface by its responsibility, and document each interaction on its own line. The interface doc does not list its members.

Where a doc comment goes

A doc comment documents the next named declaration or member. These are the positions that take a doc comment, called carriers:

CarrierMust have a doc
type, const, struct, enum, enumset, unionyes, unless internal
interface, serviceyes, unless internal
struct field, enum value, enumset bit, union armyes, unless its parent is internal
signal, event, command, query, fixedyes, unless its parent is internal
a parameter of a command or a queryno
a reserved entryno
rsdl system, component, distribution, deployment, machineyes
an rsdl body line (offers, requires, or a bare reference)no

"Must have a doc" is what the missing-docs lint checks. A package needs no doc.

A doc comment in any other position draws misplaced-doc-comment: before package, before an import, before a return type or an attribute block, and at the end of a file or of a body. Each of these positions either has no name to document or belongs to the item around it.

No blank line may separate a doc comment from its carrier. A blank line draws detached-doc-comment.

Parameters

A parameter takes a doc comment, as cmd does in setLever above, but never needs one. Every parameter type is a named type, and the named type carries its own doc and its own rules. When a parameter has no doc of its own, the editor shows the doc of its type instead, under the line "From TypeName:".

rsdl

The five rsdl declarations are carriers, and so is each line of their bodies:

/// The cruise control computer in the front zone.
component CruiseEcu {
  /// The function the driver controls with the steering column lever.
  offers docs.cruise.control
}

The docs of the declarations, of the system and distribution member lines, and of the offers and requires lines are stored in the system IR. A placement line in a machine takes a doc too, and the editor shows it on hover, but the system IR has no field for it.

What a doc comment holds

A doc is CommonMark: paragraphs, lists, emphasis, code spans and fenced code blocks.

A name in square brackets is a link to a declaration or a member. Three forms are links:

FormExampleText shown
[Name][Speed], [docs.cruise.Speed]the name
[`Name`][`Speed`]the name, as code
[text][Name][the set speed][CruiseState.setSpeed]the text

The bracket content is a link only when it is a name: one or more dotted segments. [0..250] and [see below] are prose. A name inside a code span or a fenced code block is not a link. A [Name] that has a CommonMark link reference definition ([Name]: https://...) in the same doc is an ordinary Markdown link.

A link resolves in the scope of the file that holds it, by the rules of a type reference:

  • A bare Name is looked up as a type name is: a declaration of the package, then an imported name (an import alias leads to the declaration it names), then ridl.std.
  • A qualified pkg.Name reaches any package that the current package can depend on — another member of the workspace, or a package of [imports] — whether or not the file imports it. A doc link never makes you add an import that the code does not use. A remote package of [imports] is reachable where an import of it would resolve: the package's own [imports], or the workspace [imports] for a member, must pin its unit. A link into a remote package no such pin reaches draws broken-doc-link.
  • One more segment names a member of the declaration: a field ([CruiseState.setSpeed]), an enum value ([LeverCmd.UP]), an enumset bit, a union arm, or an interaction ([CruiseControl.status]).
  • A declaration that is internal in another package cannot be linked to, because documentation of the other package could not show it. A link to an internal declaration of the same package resolves.

A link that does not resolve draws broken-doc-link, and the IR does not store it.

Tags

A tag is a word that starts with @, at the start of a line of the doc. Four tags exist:

TagValueChecked
@seeone name, with an optional member, as a link takesresolved like a link
@sincea version: MAJOR.MINOR or MAJOR.MINOR.PATCHits form only
@deprecateda reason in double quotesa missing reason draws deprecated-without-reason
@labelscomma-separated SCREAMING_SNAKE labelspassed through unchecked (typl §14.3)

A tag may appear more than once; every value is kept. An @ that is not at the start of a line is prose. A tag line is not part of the doc text: a doc made only of tags has no text, and missing-docs treats it as missing.

@see and @since change only documentation. @deprecated and @labels are stored in the IR beside the doc on a declaration, a struct field, an interface, an interaction and a service, and a backend that emits deprecation or label metadata reads them there. On an enum value, an enumset bit, a union arm, a parameter and an rsdl declaration or body line the two tags are accepted and dropped, with no diagnostic (ADR-0026 decision 4).

package docs.history

/// A request to change the gear.
/// @since 1.2
/// @see docs.history.GearBox.setGear
/// @labels SIL_2
struct GearRequest {
  /// The gear to change to, from 1 to 6.
  gear: Gear
}

/// A gear of the six-speed gearbox.
type Gear: integer [1..6]

/// The gearbox controller.
interface GearBox {
  /// Requests a gear change.
  command setGear(request: GearRequest) @[..20ms]

  /// The previous gear request, kept for clients built against version 1.0.
  /// @deprecated "use setGear"
  command requestGear(request: GearRequest) @[..20ms]
}

Examples

Write a usage example under a # Examples heading, with a fenced code block. This is a convention, not a tag: the compiler does not read it, and a name inside the code block is not a link.

package docs.examples

/// A distance to the vehicle ahead.
///
/// # Examples
///
/// ```ridl,ignore
/// signal gap: Gap @[50ms..200ms]
/// ```
type Gap: m [0.0..250.0 step 0.1]

The doc lints

Each doc lint is a TYPL- code, because doc comments are part of typl and every language of the family shares them. All of them are warnings; doc-comment-style is allow by default, so it is reported only when a project sets its level. The lints page explains levels and the [lints] table.

LintCodeDefaultRaised when
broken-doc-linkTYPL-401warna link or @see target does not resolve, or is internal in another package
detached-doc-commentTYPL-404warna blank line separates a doc comment from its carrier
deprecated-without-reasonTYPL-405warn@deprecated has no reason
missing-docsTYPL-406warna carrier that must have a doc has none
misplaced-doc-commentTYPL-407warna doc comment is in a position that is not a carrier
unknown-doc-tagTYPL-408warna tag other than @see, @since, @deprecated and @labels
malformed-doc-tagTYPL-409warn@see or @since has a missing or malformed value
doc-comment-styleTYPL-410allowa doc comment is written as /** */

The examples below each draw one documentation lint on purpose. Some also draw the design lints explained beside their source blocks.

missing-docs

missing-docs is reported once per item, at the item's name. Its quick fix in the editor inserts an empty /// line above the item. Here Odometer has no doc, and internal exempts Raw and its field:

package docs.missing

type Odometer: km [0.0..1000000.0 step 0.1]

/// A raw sensor sample, for the use of this package only.
internal struct Raw {
  value: Odometer
}

The inconsistent-unit lint also reports value here. It uses km, while fields named value in other book examples use km/h. The book checks its whole-file examples as one workspace.

When you check from inside a workspace member, ridl check, ridl build, ridl lock and the MCP tool ridl_check report only the diagnostics of files under that member, so an undocumented item in a sibling member does not appear in that report. The language server reports every consumer file it loaded, and publishes no diagnostics for a file of a remote unit. A remote package of [imports] is checked: its errors fail the run, and the language server shows them in one window message per load; its warnings and infos, missing-docs included, are dropped before any [lints] level is applied.

A project that does not want to document every item yet sets the level in its ridl.toml:

[lints]
missing-docs = "allow"
package docs.broken

/// The heading in whole degrees, converted from a [Compass] reading.
type Heading: integer [0..359]

Compass is declared nowhere, so the link does not resolve.

detached-doc-comment

package docs.detached

/// The cabin air temperature.

type CabinTemperature: Cel [-40.0..85.0 step 0.5]

deprecated-without-reason

package docs.reason

/// The fan level of the first climate control version.
/// @deprecated
type FanStep: integer [0..7]

misplaced-doc-comment

A doc comment at the end of a body has no item after it:

package docs.misplaced

/// The wiper speed.
enum WiperSpeed {
  /// The wipers are off.
  OFF = 0
  /// The wipers run once every few seconds.
  SLOW = 1
  /// The wipers run continuously.
  FAST = 2
  /// A fourth speed is planned.
}

unknown-doc-tag and malformed-doc-tag

package docs.tags

/// The tyre pressure of one wheel.
/// @author the chassis team
/// @since next
type TyrePressure: kPa [0.0..500.0 step 1.0]

@author is not one of the four tags, and next is not a version.

doc-comment-style

doc-comment-style reports every /** */ doc comment, so a project that sets it to warn or deny requires ///:

[lints]
doc-comment-style = "deny"

Its quick fix in the editor rewrites the comment as /// lines. The lint checks in one direction only: no setting requires /** */.

In the editor

The language server reads the docs from the same compile as the diagnostics, so it shows what ridl check reads:

  • Hover on a declaration, a member, a parameter, an rsdl declaration or body line, or a use of any of them shows the signature, the doc, a Contract list, and the @since versions, the labels and the deprecation reason. The Contract list is read from the IR: the range, step, unit, length, pattern and collection bound of the type, the timing of a signal or event, the response bound of a command or query, the error type of a fallible query, and each require and ensure clause. A link in the doc is shown as a link to the target's file and line. Hover on a member used in a contract clause shows that member.
  • Completion items carry the doc of the item they insert, except the member items offered after . in a doc link. Inside a doc comment, [ and . complete link targets, and @ at the start of a line completes the four tags.
  • Go to definition, find references and rename work on doc links and @see targets in .typl, .ridl and .rsdl files. Renaming a type updates every link and @see that names it, in every member of the workspace.
  • Quick fixes: missing-docs inserts a /// line, and doc-comment-style rewrites a /** */ comment as /// lines.

There is no signature help: the language has no call expression for it to serve.