Skip to content

Release 2.3.0

Choose a tag to compare

@reid-spencer reid-spencer released this 24 Sep 23:05
· 1 commit to main since this release

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.