Back to Blog

RIDDL 2.2.0 Released

RIDDL 2.2.0 is now available.

release riddl compiler

RIDDL 2.2.0 is now available. This release of the RIDDL compiler and language tooling includes the following changes:

What’s New

What’s New

RIDDL 2.2.0 is a language release: three new pieces of syntax, a new message kind, and a substantial tightening of what riddlc can say about the streaming and messaging structure of a model. Every model that parsed on 2.1.1 still parses; the changes below are additive syntax and new or corrected diagnostics. The BAST binary format moved to revision 25, so .bast files written by 2.1.x must be regenerated with riddlc bastify.

Features

  • on quiescence <window> — a handler clause that fires when nothing has arrived at a processor instance within the window (on quiescence 30 minutes is { … }). The clock restarts on every handled message; inside a State’s handler it is armed only while that state is active. Legal on any handler-bearing processor, at most one per handler. Before this, “no payment within 30 minutes, cancel the order” had to live in a projector correlation, and riddl-models counted 18 time-caused events raised by a command nothing could send.
  • send <message> to <portlet> at <instant> — a delivery scheduled for a time. at states an instant (a TimeStamp, DateTime or ZonedDateTime value), not a mechanism; the timer, delay queue or scheduler is the generator’s choice. There is no cancellation construct — the idiom is to schedule to yourself and decide at fire time, and that loop connector is legal.
  • append and remove — collection statements. An event-sourced entity’s fold can now say what it appends to and removes from a collection field of its state: append added.item to field Cart.Data.items, remove tag from field Cart.Data.tags (every element equal to the value), and the keyed form remove from field Cart.Data.items where itemId == removed.itemId (every element whose named field matches). The target must be a collection field; the value is typed against the element type; both statements obey every rule set obeys (scope, refusals-first, pure functions, event-sourcing). Arithmetic deliberately gets no syntax — RIDDL does none, and set field S.balance to prompt("balance + points") is the intended spelling.
  • Advisory — a new message kind. An advisory reports a structural fact that is consistent with the model as written and inconsistent with what such a declaration usually means; it never blocks code generation and never names a fix as required — the modeller may dismiss it by design. Severity 1 (with Style), not a warning: its own --show-advisories flag and show-advisories config key, default on, independent of -w. adaptor-direction-advisory had been a plain Warning that blocked generation; it is the first rule on the new kind.
  • Four counts about what an entity does with its journal. riddlc cannot judge whether an entity ought to be event-sourced, but it can count: entity-event-sourced-unread-history (one state, no transitions, no other processor reads its events — the journal has no reader), entity-crud-with-transitions-consumed (its inverse), entity-event-sourced-snapshot-events (every event carries the whole state and every fold copies it back) — all three advisories — and entity-event-sourced-prose-folds (Completeness: a fold written only as prose has no replay semantics). A set of a stated field from a prompt(…) counts as a stated fold.

Language rulings now enforced

  • Implied adaptor ports are abolished. A port is declared or absent, for every processor kind. A processor whose handlers receive messages but declares no inlet, or transmit but declares no outlet, is incomplete — a Missing warning naming the types (stream-processor-no-inlet / -no-outlet; an Entity keeps entity-no-inlet / -no-outlet) — and every rule that would read the missing side abstains until it exists, exactly as rules abstain from a ??? body. A connector endpoint that names an adaptor rather than one of its portlets is ref-wrong-kind again. Adaptors take whatever shape their ports give them (as merge on two inlets is legal).
  • The adaptor is the boundary for its pair, in both directions. A cross-context connector may terminate on an adaptor’s declared portlet at either end, so an asking adaptor owns both legs of its ask — the request outlet and the reply inlet — and the reply has a legal path.
  • An ask needs a modelled path both ways (msg-ask-target-unreachable, msg-ask-reply-unreachable): the question leg and the reply leg must each be carried by connectors. The mechanism of the reply is the generator’s; the path is the model’s.
  • on other is case _. It fires for exactly the message types no on <message> clause handles, and it receives whatever its body — an error-only body is a refusal of those messages, which is business logic, not non-reception. A Context or Projector with an inlet and no handler at all is now reported (stream-inlet-not-received), the error-sink inlet excepted.
  • A tell into an unrelated domain is a modelling error (msg-tell-crosses-unrelated-domains) whose remedy is to restructure — put both domains under a common parent — since no connector between unrelated domains is legal and “add a connector” was advice that could not be followed.
  • stream-graph-cycle forbids an infinite message loop, not a connector ring: a message of type X that travels the network and re-enters the clause that transmits it. The schedule-to-yourself idiom is no longer flagged.
  • A handler-less processor is a stream tail whatever its shape — an opaque processor lets no rule assert what it does with a message.
  • A saga step that tells nothing is an Error (saga-step-no-tell): a step that effects nothing yet promises to compensate it is a self-contradiction. saga-no-timeout stays a Completeness warning but no longer blocks code generation.
  • A handler may declare at most one of each special on-clause (on other, on init, on term, on activate, on passivate, on quiescence) — two catch-alls with no rule for which runs used to validate clean.

Bug Fixes

  • Prettify wrote URL("https") as URL"https", which does not parse — a second copy of the type’s own format had drifted. Fixed; both spellings round-trip.
  • find -type on-quiescence refused a kind the language produces; find -type append-statement / remove-statement likewise, caught by the release gate.
  • adaptor-direction-advisory now counts a far-context reference anywhere in the adaptor (handled types, transmitted operands, let ascriptions, portlet types), and resolves a qualified referent correctly; it used to reward the unmigrated shape.
  • Root2JsonCorpusTest stripped only one spelling of an embedded location, so the first corpus error carrying a file(line:col->col) read as a round-trip regression.

Improvements

  • validate reports advisories separately in its summary line and lists advisories on|off beside the warning classes.
  • The JavaScript ErrorInfo.kind union in index.d.ts now lists the kinds by the names riddlc actually renders (Severe, Error, Warning, Deprecation, Completeness, Usage, Missing, Style, Advisory, Tip, Info); the old spellings remain for compatibility.
  • The Messages scaladoc records the admission test between Missing and Completeness: Missing = the author owes something unwritten; Completeness = the written things do not connect.

Internal

  • BAST FORMAT_REVISION 24 → 25 (statement sub-kinds 22 append, 23 remove; the earlier bump to 24 in this cycle carried on quiescence and send … at). A revision-24 reader refuses a 25 file cleanly.
  • adaptor-implied-outlet-ambiguous (five days old, from the implied-ports design) is retired; its code will not be reused. Two new rule ids for the collection statements, four for the event-sourcing counts, two for the incompleteness checks.
  • The Computational Model (RIDDL-Computational-Model.md) records every ruling above, with the reversed 2026-09-06 paragraphs kept as history.
  • CLAUDE.md was split: per-construct reference now lives in docs/claude/language-constructs.md and build/API notes in docs/claude/build-and-api.md.