Skip to content
hekla

Keywords and annotations

The closed sets. 32 hard keywords, 20 soft ones, and the seven annotations.

Every list on this page is closed and derived from the compiler’s own tables, so it cannot drift from what the language accepts.

Hard keywords

Thirty-two words, reserved everywhere. None of these can be a field, parameter or local name.

Keyword
as binds the whole event in an effect arm or a projector handler
command a declaration
const a declaration
delete a projector write
effect a declaration
else with if
emit a command statement
entity inside a projector
enum a declaration, module or projector scoped
event a declaration
false a literal
fn a declaration, module or effect scoped
fold a read declaration
for iteration
guard a declaration, and its use site
if branching
in with for
invalid an outcome
invoke an effect calling a command
let the only binding form
none the absent optional
on an arm, and a fold’s slice
patch a projector write
projector a declaration
put a projector write
record a declaration
refusal a declaration
reject an outcome
return
test a declaration
true a literal
update a projector write

Soft keywords

Twenty words claimed only positionally, so each stays usable as an ordinary name everywhere else. An entity column called given, a parameter called no and a field called subject are all legal.

subject and under are claimed only where a top-level declaration begins, and @subject(...) is an annotation the lexer reads apart from either.

Keyword Claimed in
deliver a test action
erase an effect arm
erased a test setup
expect a test expectation
fail an effect arm
given a test log
http an effect arm
latest an effect arm’s delivery
live an effect arm’s delivery
log an effect arm
no a test expectation
nothing a test expectation
project a test action
respond a test setup
run a test action
secret a declaration, and a test setup
skipped a test expectation
subject a declaration
timeout a test setup
under a subject declaration, naming its parent

There is a third group the tables above deliberately leave out: bare identifiers resolved by the callable dispatch rather than by the lexer or a positional check. reveal and now, plus the constructor roots Uuid, Json, Map, Timestamp, Money and Decimal, and the three rounding words HalfUp, HalfEven and Down. They are builtins, not keywords.

Annotations

Seven, and the set is closed. An unrecognised one is unknown-annotation; a recognised one in the wrong place is bad-annotation.

Annotation Legal on
@subject(field) an event field only seals the value under a key scoped to that subject
@max(n) an event, entity or record field bounds a String or String? and nothing else
@absent(literal) an event or record field what the field reads as on a payload written before it existed
@no_index an event field only keeps the field out of the tag set, so it is not foldable
@key an entity field, or an effect arm’s trigger destructure the primary key, or the lane
@index an entity field only a one-column index, so the scan route can filter on it
@default an enum variant only the zero value, which is what patch materialises

Two forms behave like annotations without the @:

Form Where
index (a, b) entity level a composite index; any prefix of it filters, and order_by names it
= <literal> an entity field a default for a column a write may omit

An annotation name may never contain a ..

The subtle rules

@max applies to String and String? and nothing else. @max on an Int, a container or a record is an error naming the type, because a length there would have nothing to bound. It bounds rather than truncates: a value that does not fit fails where it is written, and .truncate(n) (see Standard library) is how a computed one is made to fit.

@subject(x) constrains x four ways, all checked where the annotation is written: x is a field of the same event, its type is a declared subject, it is not optional, and it is not itself sealed. A subject id is the name a key is filed under, so a missing one is no question at all rather than a missing key.

@subject on a record field is a dedicated error, not a generic one. The annotation names a sibling field holding the id, and a field inside a record has no sibling to name. A record still carries personal data: @subject on an event field whose type is a record seals the whole record, which is Personal data.

@absent refuses four shapes, all at parse time: an optional field, a @subject field, a literal longer than the field’s own @max, and a literal that is not of the field’s type. An optional has already said absence is possible, sealed content has no plaintext literal to stand in for it, and a literal that could not be written could not be read back either.

An entity may have at most one @key. A second is bad-annotation.

An effect arm’s @keys are ordered. A composite key is a sequence, so { @key a, @key b } and { @key b, @key a } are different lanes.

= none on an optional column is rejected, because an optional is already absent by default.

Symbols

{ } a block, a record literal, a JSON object
( ) grouping, parameters, arguments, a type scale
[ ] a list literal, a comprehension, a test row key
=> a fold arm’s result
-> a fn return type
? an optional type, and an optional secret
@ an event path, and an annotation
. member access, and a projector’s stored value
= binding, and a named argument
== != < <= > >= comparison, non-associative
+ - * / % arithmetic. % is Int only, and there is no + on strings
&& || ! boolean
, separator

There are no semicolons anywhere in the language, and statements are separated by newlines.

What none of this includes

while, break, continue, match, switch, var, import, module, class, struct, interface, async, await, try, catch, throw, null, nil, undefined.

None of them is a reserved word, because none of them is a concept the language has. See Containers and control flow for why the loop forms stop where they do.