Skip to content

Value

A value is an expression, in the context of a statement, that provides a value to that statement. Values, statements and conditions work together to express business logic at an appropriate level of abstraction — detailed enough to be meaningful, but not so specific as to constrain implementation.

RIDDL 2.0 replaced what had been a mostly-opaque quoted string with a real value-expression system. A literal string is still accepted everywhere, so pseudo-code remains available where structure would be false precision.

The Eleven Forms

Form Syntax Meaning
Literal "some text" Opaque pseudo-code, or a literal constant
Empty empty, none, empty Tags The minimum-cardinality inhabitant of a type — the absence of a value
Value reference order.total A field, state field, function input, or let local
Constructor OrderPlaced(id, total = x) Builds a message or record
Get get from input SignupForm Reads a UI input or an entity state
Call call function Pricing.Total(a, b) Invokes a pure function for its result
Prompt prompt("compute the discount") [as <type>] A value computed by AI at generation time; the ascription states its type
Boolean a > b and not c A structured boolean expression
Ask ask query GetInfo of entity Catalog A query paired with the reply that answers it
Initiate initiate entity Order Brings an instance into being; yields its Id
Self self, self.id The instance executing right now

Value References

A value reference names something in scope. Four sources are consulted, in order:

  1. the on clause's message binding — bare ord is the whole message, ord.field reaches into it
  2. a field of the handled message, the entity state, or a function's requires input
  3. a definition reached by a qualified path, such as GState.active
  4. a named constant

A let local is also a value reference, but resolves lexically rather than through the symbol table: it is visible only after its declaration and is shadowed inside nested blocks.

Constructors

A constructor builds a message or record inline, rather than requiring it be assembled elsewhere first:

yield event OrderPlaced(orderId, total = cart.total, currency = "USD")
morph entity Order to state Done with record DoneData(note = "fulfilled")

Arguments are positional first, then named. Count, names, ordering and (best effort) types are all checked against the target's fields.

Get

get from reads a value from a UI input or an entity state:

let email = get from input SignupForm
record BasketData is { total is Natural }
command AddItem is { sku is String }

entity Basket is {
  state Filling of record BasketData is {
    handler BasketHandler is {
      on command AddItem {
        let current = get from state Filling
      }
    }
  }
}

The two forms cannot appear in the same clause. An input exists only inside an application context, and state may be read only inside the entity that owns it — reading another definition's state is an Error as of RIDDL 2.0.

Call

call invokes a function — and only a function, since functions are the only definitions guaranteed pure — and produces its result:

let total = call function Pricing.CalculateTotal(subtotal, taxRate = rate)

A call is value-producing rather than a bare statement, so it composes with let, set, return and constructor arguments. Calling something that declares no returns is an Error.

Prompt

prompt("...") denotes a value computed by AI at generation time:

set field recommendation to prompt("suggest a complementary product")

It is distinguished from the do statement by its parentheses. The statement describes an action for a human to implement; the value denotes something AI computes.

Multi-line prompts

Guidance long enough to need more than one line goes in braces, as a sequence of strings with no commas or other separator between them:

let sentiment = prompt({
  "rate the customer's sentiment from the note on the order,"
  "returning a value between 0.0 and 1.0"
}) as Real

The braces are the only multi-line form — a bare prompt("…") takes exactly one string. That restriction is deliberate: RIDDL statements have no terminator, so allowing two juxtaposed strings would leave nothing but the next keyword to mark where the statement ended.

Typed holes: prompt("…") as <type>

prompt(...) may carry an optional type ascription:

let price = prompt("a plausible price for this item") as Real
constant Greeting: String = prompt("a friendly greeting")

That is a typed hole, and it is the seam between RIDDL's two tiers: the type is known and checkable at compile time, while the computation that produces a value of that type is prose an AI fills in at generation time. The deterministic tier parses, validates and type-checks; the AI tier is the string inside the parentheses.

The ascription restates the position's type; it never overrides it. A let takes its type from the ascription, but in a position that already determines the type — a constructor argument, a field — the ascription must agree with it. Writing one is opt-in: unascribed prompt(...) is unchanged and still valid, and is the right form wherever the position already says enough.

When written, the ascription is a type name. A bare predefined type such as Real or TimeStamp is a name; String(1,30) or Recipe* is a type expression, and ascribing one is the Error value-ascription-not-a-name — declare the type with type and name it instead.

Empty and None

empty denotes the minimum-cardinality inhabitant of a type: no value at all, written where a value is expected.

set field nickname to empty
set field tags to none

none is a synonym, not a second construct. Both spellings produce the identical AST, and prettify converges them on empty. Use whichever reads better where it appears — none often suits an optional scalar and empty a collection — but expect formatted output to say empty.

Only where the minimum cardinality is zero

empty is meaningful only for a type that admits having no value: an optional T?, a sequence T*, or an explicit range starting at zero, T{0,n}. A bare T or a T+ requires at least one value, so empty is not an inhabitant of it. A bare empty in such a position is the value-empty-not-allowed Error, and ascribing such a type (below) is value-empty-needs-zero-cardinality. Type names are read through, so empty fits a type Tags is String* just as it fits String*.

Where its type comes from

Every value in RIDDL is typed. empty does not say which type in its own spelling, so it takes the type of its position — the field it is assigned to, a constructor or call argument, a let's declared type, an output, a function's returns, a stored row, or the operand on the other side of a comparison:

let t: Tags = empty
when tags == empty then ??? end

The type ascription

Where no position supplies a type, write one after empty. It must be a type name, never a type expression:

let noTags = empty Tags
let unset  = none Nickname

Here Tags is String* and Nickname is String?, each declared with type. Writing empty String* instead is the Error value-ascription-not-a-name, and a bare empty with nothing to type it is value-empty-untyped.

An ascription restates the position's type and never overrides it: where a position does supply one, the ascription must be syntactically that declared type, or it is value-empty-ascription-contradicts. A field declared inline, like tags is String*, has no name to restate, so it takes only a bare empty.

Why the ascription cannot be followed by just anything

A type name is a bare path, and RIDDL statements are separated by whitespace with no terminator. Without a guard, set x to empty followed by set y to … would read the second statement's set as the first's ascription, and put empty to … would read to. So the parser stops an ascription at any word that can follow a value: a statement keyword, a readability word such as to, in or where, or any reserved word that cannot begin a type.

Boolean Expressions

Precedence runs or < and < not < comparison < atom, with parentheses to group:

when order.isPaid and not (order.isCancelled or order.isRefunded) then ??? end

and, or, not, true and false are context-sensitive: they are recognized only inside a boolean expression, so they remain legal identifiers everywhere else in the language.

Comparisons are type-CHECKED, not reference-only

Either operand of a comparison may be any expression — a value reference, a get from, a named constant, a literal, arithmetic, a count of. What may sensibly be compared is validation's question rather than the grammar's, so count > "5" is reported as the Error value-ordering-needs-numeric instead of failing to parse.

A literal operand is legal and draws the style warning value-literal-comparison-style. Naming the value is still better style, because the name records the intent. The constant is a definition, declared alongside the other definitions of its context:

constant MaxItems is Natural = 100

and the comparison is a statement, written inside an on-clause:

when cart.itemCount > MaxItems then error "too many items" end

The point is to remove magic constants from models and to make every comparison check the types on both sides. == and != require operands of the same category; <, >, <= and >= require an ordered numeric type on both sides.

true and false remain valid boolean atoms — usable with and, or, not, and standalone — just not as comparison operands.

Initiate

initiate brings a new instance of an entity into being, and its value is that instance's identity:

let fresh = initiate entity ExampleEntity
tell command ExampleWelcome(target = fresh) to fresh

It is not a second way to exist — construction still completes only when on init finishes. But without it no Id value could ever come into being, so nothing could be addressed.

initiate and terminate are entity-only and are both effects: banned in a function body, in on activate/on passivate, and in a correlation fold; legal in a saga step.

Discarding the id is a warning

The id is the only thing initiate produces, so binding it and never using it means the instance was created and immediately made unreachable.

Self

self is the instance executing right now. Its type is a synthesized record carrying id and version:

let me  = self
let who = self.id

Because that type is an ordinary record, self.id resolves by the same path walk as any other value — which is why no resolution rule has to know self exists.

The type is not user-nameable, though: self.id is Id(Order) in an Order handler and Id(Shipping) in a Shipping one. So let me: T = self has no T to write, and self cannot be assigned into a message field — pass self.id. The field set is closed; adding to it is a language change.

Where Values Are Used

Statement Operand
set the assigned value
let the bound expression
put the published value
return the returned value
send, tell, yield a message reference or constructor
morph a record reference or constructor
when, require, invariant a condition
match the subject, and each comparison pattern's comparand

Occurs In

Contains

Values may contain other values — a constructor's arguments, a call's arguments, and a boolean expression's operands are all themselves values.