Back to Blog

RIDDL 2.3.0 Released

RIDDL 2.3.0 is now available.

release riddl compiler

RIDDL 2.3.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.3.0 is a language release. It answers riddl-generator’s eight-feature request in full: bounded arithmetic, repository storage statements, schema keys, collection predicates, a log statement, and two new lints — every one of them a sentence that models previously had to write as prompt("…") prose for an AI to fill in. Thirteen new value and statement kinds, nine new keywords, fifteen new diagnostics.

Nothing that parsed on 2.2.0 stops parsing: all 191 models of the riddl-models corpus still validate with zero errors, and every retired spelling still produces the same AST. The BAST binary format moved 25 → 30, so .bast files written by 2.2.x must be regenerated with riddlc bastify.

Features

  • Arithmetic, bounded on purpose — + - * / on numbers, + on strings, timestamp arithmetic (system.now + 30 days, t - opened), duration literals (30 days, 1.50 hours), comparisons whose operands are full expressions, and constants that are expressions (constant Bonus: Natural = PointsPerDollar * 2). This reverses the 2026-08-23 “RIDDL does no arithmetic” ruling within stated bounds: power, roots and every math-library function remain prompt("…"), because they are system-dependent. Every expression now has a real type, not a category: a boolean expression is Boolean, and a numeric expression takes the smallest constrained numeric type that contains its operands and its result — Natural + Natural is Natural, Natural - Natural is Integer (it may go negative), Natural / Natural is Whole, and Integer / Integer is Integer, truncating toward zero. Anything off the table is an Error, never a silent coercion.
  • Repository storage statements — store, upsert, update … set … where … and delete from … where …, legal only in a repository handler, plus query [one] <table> [where …] as a VALUE that composes with let and reply. Everything is typed against the schema’s stored record, so column names and types are checked rather than invented; a bare name inside a where or a set is a ROW field and shadows a same-named message field. upsert requires the schema to declare a key, since without one there is no row identity to update by.
  • Schema keys and history — key on field R.id declares a UNIQUE natural key (distinct from index on, which only speeds retrieval), and of tickets as record R with history asks the generator to maintain an append-only history of every stored version. Several keys are several independent constraints; there is no composite key.
  • Collection predicates — all of xs as e where p, any of …, none of …, the filter xs as e where p, count of xs and xs contains x. The element is bound explicitly and is scoped to the predicate. all of an empty collection is TRUE; count of binds tighter than arithmetic. map is deliberately absent: a per-element expression is a lambda, and a lambda is general computation.
  • log <value> — record a value for humans: a literal, a field, the bound message under on other as m, or an expression. Deterministic (a generator emits it to its logging facility, never through the AI tier), not state, not a message, legal everywhere a statement is.
  • m.<field> under on other as m — the binding stays the message’s ENVELOPE (A57), and a field the envelope does not have now resolves to a field that EVERY message able to reach the clause carries, with the same type. Partial coverage is an Error naming the members that differ. kind of m needs no syntax: it is m.type, the envelope’s CloudEvents attribute.
  • Two new lints — an Advisory on a prompt(…) used as a yield argument (430 in the corpus; the hole an AI fills worst), and a Style warning on do prose that validates a field against a range a TYPE could express (do "validate partySize is between 1 and 20").

Bug Fixes

  • riddlc dump --json threw a MatchError and emitted a zero-byte document for any model using the new log or storage statements, while riddlc validate on the same model was clean. The projection’s statement dispatch is total by hand in a module compiled with --no-warnings, so nothing reported the five missing arms. Fixed, and a fixture sweep (DumpProjectionFixturesTest) now runs every **/input/** fixture through the projection so the next missing arm fails the build instead of reaching a consumer. (Note for script authors: riddlc exits non-zero on such a failure, but riddlc … | wc -c reports the pipe’s last status — use set -o pipefail.)
  • A pure sink below an application context could never be reached. stream-sink-reached-by-no- source treated a chain origin as “bears an outlet and has no inbound edge”, and an application that consumes results always has an inbound edge — so a model whose commands originate in its application could not place a sink anywhere below it without giving the sink an outlet it should not have. Origination is now about what a processor RECEIVES: an outlet, and no inlet admitting a command or query. This also tightens the rule in the other direction — a processor that receives a command with nothing feeding it is no longer an origin.
  • A let with a keyword-qualified type lost the keyword through JSON. let x: record R round-tripped as let x: type R, the same defect a schema’s data entry had in 2.0; both spellings now read and the keyword survives.
  • ConstantRef degraded to a ValueRef when rebuilt from JSON — it formatted identically, so nothing caught it. It rebuilds as itself now.
  • Finder could not see a lookup’s indices, an empty’s type ascription, or a constant’s value, so anything walking the AST through Finder silently returned shorter lists.

Improvements

  • Ordering comparisons (< > <= >=) now accept timestamps and durations as well as numbers. t < system.now was silently unchecked before, because a timestamp had no comparison category.
  • A28’s parse-time refusal of literal comparison operands is reversed: count > "5" and count > true now parse, and validation decides whether the two sides may be compared.
  • A repository’s key on satisfies the “queried repository declares no index” completeness warning — a key is an index.
  • riddlc find -type learned the new kinds (arithmetic-expression, duration-literal, log-statement, store-statement, upsert-statement, update-statement, delete-statement, query-value, collection-predicate, collection-filter, count-value, membership-value).

Internal

  • BAST FORMAT_REVISION 25 → 30 (value tags 14–21, statement sub-kinds 24–28, two schema sequences). A revision-25 reader refuses a 30 file cleanly rather than misreading it.
  • Fifteen new rule ids, all appended to the published ledger; no code was retired or reused.
  • The Computational Semantics document records every ruling in this release, including the two boundaries that were deliberately NOT crossed: math-library functions and map.
  • ComparisonExpression.left/right widened from Comparand to Value. Constructing one is unchanged; a consumer that assigns ce.left to a Comparand-typed value will need to widen it. Comparand still types a match case’s comparison pattern.