Formal EBNF Grammar¶
Below is the formal Extended Backus-Naur Form (EBNF) grammar for RIDDL.
This grammar provides a precise definition of RIDDL syntax and can be used as a
reference when constructing valid RIDDL expressions. This grammar is
automatically extracted from the riddl-language library and kept in sync
with the reference grammar written in Scala/fastparse form.
RIDDL 2.0
This is the grammar for RIDDL 2.0. Notable additions over 1.x include the
generic processor rule with as <shape> ascription, the context
intention prefix, the value and boolean_expression sub-languages, the
foreach/put/return/yield statements, the version and copyright
definitions, and the figma metadata reference. Rules that are retained
only for backward compatibility carry a comment saying so.
One rule is worth finding first: undefined = {comment} "???". It
replaces what used to be 23 separate inline "???" sites, and it is why a
comment may introduce a stub — see
Undefined Bodies. Each body tries
undefined before its content alternative, because a PEG would otherwise
consume the comments as content and never reach the marker.
(* RIDDL Grammar in EBNF *)
(* Last updated: July 27, 2026 *)
(* This grammar is automatically included in the riddl language module *)
(* and can be loaded from the classpath at: riddl/grammar/ebnf-grammar.ebnf *)
(* A `???` stub, optionally introduced by comments saying what belongs there. Comments AFTER the
marker are NOT part of the stub: `???` ends the body. The comments are kept as the container's
contents so a stub can document its own intent without losing it on the next prettify. *)
undefined = {comment} "???" ;
(* Basic Elements *)
letter = /[A-Za-z]/ ;
digit = /[0-9]/ ;
iso_country_code = /[A-Z]{3}/ ;
string_char = /[^"\\]/ ;
newline = /\n/ ;
any_char_except_newline = /[^\n]/ ;
any_char_except_end_comment = /(?:[^*]|\*(?!\/))+/ ;
any_char_except_triple_backtick = /(?:[^`]|`(?!``)|``(?!`))+/ ;
identifier = simple_identifier | quoted_identifier ;
simple_identifier = /[A-Za-z][A-Za-z0-9_-]*/ ;
quoted_identifier = /'[-A-Za-z0-9_+\\|\/@$%&,: ]+'/ ;
path_identifier = dotted_path_identifier | quoted_path_identifier ;
dotted_path_identifier = identifier { "." identifier } ;
quoted_path_identifier = /'[-A-Za-z0-9_+\\|\/@$%&,: .]+'/ ;
literal_string = '"' { string_char | escape_sequence } '"' ;
escape_sequence = /\\[\\\"aefnrt]/ | hexEscape | unicodeEscape ;
hexEscape = /\\x[0-9a-fA-F]{2,8}/ ;
unicodeEscape = /\\u[0-9a-fA-F]{4}/ ;
integer = /[+-]?[0-9]+/ ;
natural = /[0-9]+/ ;
(* Readability Keywords - these are always optional *)
is = ["is" | "are" | ":" | "="] ; (* optional readability word for definitions *)
byAs = ["by" | "as"] ; (* optional readability word for attribution *)
briefly = "briefly" | "brief" ;
described = "description" | "explanation" | "described" | "explained" ;
(* Comments - using single regex tokens to avoid whitespace-skipping issues *)
comment = inline_comment | end_of_line_comment ;
inline_comment = /\/\*(?:[^*]|\*(?!\/))*\*\// ;
end_of_line_comment = /\/\/[^\n]*/ ;
(* Main Structure *)
root = {root_content}+ ;
root_content = bast_import | root_definition | module | root_include ;
root_definition = domain | author | version | copyright | comment ;
root_include = "include" literal_string ;
(* An import plucks definitions out of an already-compiled .bast file. The full form takes *)
(* everything the file's root Module holds; the selective form takes exactly one named *)
(* definition, optionally renamed with "as". *)
bast_import = selective_bast_import | full_bast_import ;
full_bast_import = "import" literal_string ; (* path must end with .bast *)
selective_bast_import = "import" importable_kind identifier "from" literal_string ["as" identifier] ;
importable_kind = "domain" | "context" | "entity" | "type" | "epic" | "saga" | "adaptor"
| "function" | "projector" | "repository" | "streamlet" | "author" | "module"
| "user" | "connector" | "constant" | "invariant" ;
(* Module - a FLAT collection: any top-level definition, in any order, no hierarchy enforced *)
(* at this level. The internal rules of each contained definition still apply. *)
module = "module" identifier is "{" {module_content | module_include}+ "}" [with_metadata] ;
module_content = bast_import | adaptor | author | comment | connector | constant | context
| domain | entity | epic | function | invariant | module | projector
| relationship | repository | saga | streamlet | type_def | user | version
| copyright ;
module_include = "include" literal_string ;
(* DEPRECATED: an anonymous "nebula" - a whole-input sequence of module_content with no *)
(* `module <id> is { ... }` wrapper. Still parsed (it emits one [deprecated] message) and *)
(* yields a Module with the synthetic id `nebula`. Use `module` instead. *)
nebula = {module_content} ;
(* Domain *)
domain = "domain" identifier is "{" domain_body "}" [with_metadata] ;
domain_body = undefined | domain_definitions ;
domain_definitions = {domain_content}+ ;
domain_content = vital_definition_contents | author | context | domain | user | epic | saga | repository | connector | version | copyright | bast_import | domain_include | comment ;
domain_include = "include" literal_string ;
(* Context *)
intention = "application" | "external" | "gateway" | "service" ;
context = [intention] "context" identifier [as_shape] is "{" context_body "}" [with_metadata] ;
context_body = undefined | context_definitions ;
context_definitions = {context_definition}+ ;
context_definition = processor_definition_contents | entity | adaptor | group | saga | streamlet | projector | repository | connector | bast_import | context_include | comment ;
context_include = "include" literal_string ;
(* Entity *)
entity = "entity" identifier [as_shape] is "{" entity_body "}" [with_metadata] ;
entity_body = undefined | entity_definitions ;
entity_definitions = {entity_content}+ ;
entity_content = processor_definition_contents | state | entity_include ;
(* `of` is the canonical introducer for the record reference. `is` is DEPRECATED and, because
`is` is itself optional, so is omitting the introducer altogether; both still parse and both
draw a deprecation. `is` introduces a definition's BODY everywhere else, as state_body shows. *)
state = ["initial"] "state" identifier ("of" | is) record_ref [state_body] [with_metadata] ;
state_body = is "{" (undefined | {state_content}+) "}" ;
state_content = handler | invariant | comment ;
entity_include = "include" literal_string ;
(* Processor Definition Contents *)
processor_definition_contents = vital_definition_contents | constant | invariant | function | handler | streamlet | processor | connector | relationship | inlet | outlet | version | copyright ;
vital_definition_contents = type_def | comment ;
(* Type Definition *)
type_def = def_of_type | def_of_type_kind_type ;
def_of_type = "type" identifier is type_expression [with_metadata] ;
def_of_type_kind_type = aggregate_use_case identifier ["yields" message_ref] (scala_aggregate_definition | (is (aliased_type_expression | aggregation))) [with_metadata] ;
aggregate_use_case = "type" | "command" | "query" | "event" | "result" | "record" | "graph" | "table" ;
(* Type Expressions *)
type_expression =
("many" ["optional"] | "optional") type_expression_alternatives |
type_expression_alternatives ["?" | "*" | "+"] ;
type_expression_alternatives = predefined_types | pattern_type | unique_id_type | enumeration | sequence_type | mapping_from_to | a_set_type |
graph_type | table_type | replica_type | range_type | decimal_type | alternation | entity_reference_type |
aggregation | aggregate_use_case_type_expression | aliased_type_expression ;
(* Predefined Types *)
predefined_types = string_type | currency_type | url_type | integer_predef_types | real_predef_types | time_predef_types | zoned_predef_types | decimal_type | other_predef_types ;
string_type = "String" ["(" [integer] "," [integer] ")"] ;
currency_type = "Currency" "(" iso_country_code ")" ;
url_type = "URL" ["(" literal_string ")"] ;
integer_predef_types = "Boolean" | "Integer" | "Natural" | "Whole" ;
real_predef_types = "Current" | "Length" | "Luminosity" | "Mass" | "Mole" | "Number" | "Real" | "Temperature" ;
time_predef_types = "Duration" | "DateTime" | "Date" | "TimeStamp" | "Time" ;
zoned_predef_types = ("ZonedDate" | "ZonedDateTime") "(" [zone] ")" ;
(* "Abstract" is the DEPRECATED spelling of "Anything"; it still parses to the same
node but emits a [deprecated] message. *)
other_predef_types = "Abstract" | "Anything" | "Location" | "Nothing" | "UUID" | "UserId" ;
zone = /[A-Z0-9:.+-]{2,}/ ;
(* Type Expressions *)
pattern_type = "Pattern" "(" {literal_string} ")" ;
unique_id_type = "Id" "(" ["entity"] path_identifier ")" ;
enumeration = "any" ["of"] "{" enumerators "}" ;
enumerators = undefined | {enumerator [","]} ;
enumerator = identifier [enum_value] [with_metadata] ;
enum_value = "(" integer ")" ;
sequence_type = "sequence" "of" type_expression ;
mapping_from_to = "mapping" "from" type_expression "to" type_expression ;
a_set_type = "set" "of" type_expression ;
graph_type = "graph" "of" type_expression ;
table_type = "table" "of" type_expression "of" "[" integer {"," integer} "]" ;
replica_type = "replica" "of" replica_type_expression ;
replica_type_expression = integer_predef_types | mapping_from_to | a_set_type ;
range_type = "range" "(" [integer] "," [integer] ")" ;
decimal_type = "Decimal" "(" integer "," integer ")" ;
alternation = "one" ["of"] "{" alternation_contents "}" ;
alternation_contents = undefined | [aliased_type_expression {("|" | "or" | ",") aliased_type_expression}] ;
entity_reference_type = "reference" ["to"] ["entity"] path_identifier ;
aggregation = "{" aggregate_definitions "}" ;
aggregate_definitions = undefined | {aggregate_content [","]} ;
aggregate_content = field | method | comment ;
aggregate_use_case_type_expression = aggregate_use_case aggregation ;
aliased_type_expression = [aggregate_use_case] path_identifier ;
scala_aggregate_definition = "(" {field [","]} ")" ;
(* Fields and Methods *)
field = identifier is field_type_expression [with_metadata] ;
method = identifier "(" [arguments] ")" is field_type_expression [with_metadata] ;
arguments = {method_argument [","]} ;
method_argument = identifier ":" field_type_expression ;
field_type_expression =
("many" ["optional"] | "optional") field_type_alternatives |
field_type_alternatives ["?" | "*" | "+"] ;
field_type_alternatives = predefined_types | pattern_type | unique_id_type | enumeration | sequence_type | mapping_from_to | a_set_type |
graph_type | table_type | replica_type | range_type | decimal_type | alternation | aggregation |
aliased_type_expression | entity_reference_type ;
(* Functions *)
function = "function" identifier is "{" function_body "}" [with_metadata] ;
function_body = [func_input] [func_output] function_definitions ;
func_input = "requires" ( aggregation | type_ref ) ;
func_output = "returns" ( aggregation | type_ref ) ;
function_definitions = {undefined | (vital_definition_contents | function | function_include | statement)} ;
function_include = "include" literal_string ;
(* References *)
type_ref = [aggregate_use_case] path_identifier ;
field_ref = "field" path_identifier ;
constant_ref = "constant" path_identifier ;
message_ref = command_ref | event_ref | query_ref | result_ref ;
command_ref = "command" path_identifier ;
event_ref = "event" path_identifier ;
query_ref = "query" path_identifier ;
result_ref = "result" path_identifier ;
record_ref = "record" path_identifier ;
adaptor_ref = "adaptor" path_identifier ;
entity_ref = "entity" path_identifier ;
function_ref = "function" path_identifier ;
handler_ref = "handler" path_identifier ;
state_ref = "state" path_identifier ;
context_ref = "context" path_identifier ;
outlet_ref = "outlet" path_identifier ;
inlet_ref = "inlet" path_identifier ;
(* The keyword is the streamlet's SHAPE, mirroring Keywords.streamlets in the parser — not the
literal word "streamlets", which nothing accepts and no model writes. *)
streamlet_ref = ("source" | "sink" | "merge" | "split" | "void") path_identifier ;
projector_ref = "projector" path_identifier ;
repository_ref = "repository" path_identifier ;
saga_ref = "saga" path_identifier ;
epic_ref = "epic" path_identifier ;
user_ref = "user" path_identifier ;
output_ref = output_aliases path_identifier ;
input_ref = input_aliases path_identifier ;
group_ref = group_aliases path_identifier ;
author_ref = "by" "author" path_identifier ;
processor_ref = adaptor_ref | context_ref | entity_ref | projector_ref | repository_ref | streamlet_ref ;
any_interaction_ref = processor_ref | saga_ref | input_ref | output_ref | group_ref | user_ref ;
(* Handlers *)
handler = ["initial"] "handler" identifier is "{" handler_body "}" [with_metadata] ;
handler_body = {undefined | handler_contents} ;
handler_contents = {on_clause | comment} ;
on_clause = on_init_clause | on_other_clause | on_term_clause | on_activate_clause | on_passivate_clause | on_event_clause | on_message_clause ;
on_init_clause = "on" "init" is pseudo_code_block [with_metadata] ;
on_other_clause = "on" "other" is pseudo_code_block [with_metadata] ;
on_term_clause = "on" "term" is pseudo_code_block [with_metadata] ;
on_activate_clause = "on" "activate" is pseudo_code_block [with_metadata] ;
on_passivate_clause = "on" "passivate" is pseudo_code_block [with_metadata] ;
(* A55: an optional local name may be bound to the handled message with type ascription:
`on foo: command Foo { ... }`. Within the body the name denotes the whole message. *)
on_event_clause = "on" [identifier ":"] event_ref ["from" [identifier ":"] message_origins] is pseudo_code_block [with_metadata] ;
on_message_clause = "on" [identifier ":"] message_ref ["from" [identifier ":"] message_origins] is pseudo_code_block [with_metadata] ;
message_origins = inlet_ref | processor_ref | user_ref | epic_ref ;
(* Statements *)
(* Core statements available in all contexts *)
statement = when_statement | match_statement | foreach_statement | send_statement | tell_statement |
yield_statement | reply_statement | the_set_statement | let_statement | put_statement |
return_statement | prompt_statement | code_statement | error_statement | require_statement |
morph_statement | become_statement | comment ;
(* Control flow *)
when_statement = "when" when_condition "then" pseudo_code_block ["else" pseudo_code_block] "end" ;
(* A28/A17: a condition is an opaque pseudo-code literal, a negated/bare let-binding identifier, a
structured boolean_expression, or (A17) a first-class bare boolean value_ref (a single name or a
dotted path, resolved and validated to be Boolean-typed). *)
(* literal_string is DEPRECATED as a condition: a bare string means a literal everywhere else,
so a natural-language condition is written prompt("...") and evaluated by an AI. *)
when_condition = prompt_value | literal_string | "!" identifier | boolean_expression | value_ref ;
(* A29: `match <subject> { case <pattern> [when <boolean_expression>] { <statements> } ...
[default { <statements> }] }`. The subject is a runtime value ref / `get` / legacy literal. A
pattern is a type-case (a bare type_ref), an explicit value comparison (`<op> <comparand>`, the
subject is the implicit left operand), or a legacy pseudo-code literal. *)
match_statement = "match" match_subject "{" {match_case}+ ["default" "{" {statement} "}"] "}" ;
match_subject = get_value | literal_string | value_ref ;
match_case = "case" match_pattern ["when" boolean_expression] "{" {statement} "}" ;
match_pattern = comparison_operator comparand | literal_string | type_ref ;
(* A25: `foreach <element> in <collection> { <statements> }`; `field <path>` collection is a
FieldRef, a bare identifier is a `let`-bound local *)
foreach_statement = "foreach" identifier "in" (field_ref | identifier) "{" {statement} "}" ;
(* Message operations. A54: the message operand is a bare ref or an inline constructor. *)
message_value = constructor | message_ref ;
(* A30: `outlet` is the canonical `send` target (emit on your own outlet; a connector routes it
to a downstream inlet). The `inlet_ref` form is DEPRECATED (soft, removed in 3.0) but still
parses; use `tell` for direct delivery to a processor. *)
send_statement = "send" message_value "to" (outlet_ref | inlet_ref) ;
tell_statement = "tell" message_value "to" processor_ref ;
yield_statement = "yield" message_value ;
(* `reply` is the deprecated synonym for `yield`; both parse to the same node *)
reply_statement = "reply" message_value ;
(* Variable operations. A54: `set`/`let` operands are now full value expressions. *)
the_set_statement = "set" (field_ref | state_ref) "to" value ;
let_statement = "let" identifier [":" type_ref] "=" value ;
(* A54: value expressions. A value is pseudo-code text, an AI-computed `prompt(...)`, a message/record
constructor, a `get from` read of a UI input or entity state, or (A28) a boolean expression whose
bare atom is a value reference or literal. *)
value = literal_string | prompt_value | call | constructor | get_value | boolean_expression ;
prompt_value = "prompt" "(" literal_string ")" ;
(* A24: call a pure function to get a result. "Functions only" — the callee is a function_ref. *)
call = "call" function_ref "(" [constructor_arg {"," constructor_arg}] ")" ;
constructor = (message_ref | record_ref) "(" [constructor_arg {"," constructor_arg}] ")" ;
constructor_arg = (identifier "=" value) | value ;
get_value = "get" "from" (input_ref | state_ref) ;
value_ref = path_identifier ;
(* A28: the boolean-expression sub-language, lowest to highest precedence: or < and < not <
comparison < atom. `and`/`or`/`not`/`true`/`false` are context-sensitive operators — legal
identifiers everywhere except these rules. A boolean_atom with no comparison operator is a plain
value. A28 s3: comparisons are TYPE-SAFE — their operands are ref-only `comparand`s (a value_ref,
a get_value, or a constant_ref), never a literal/constructor/boolean_literal — so a comparison is
always between two typed references. *)
boolean_expression = and_expression {"or" and_expression} ;
and_expression = not_expression {"and" not_expression} ;
not_expression = "not" not_expression | comparison ;
comparison = comparand comparison_operator comparand | boolean_atom ;
comparand = get_value | constant_ref | value_ref ;
comparison_operator = "==" | "!=" | "<=" | ">=" | "<" | ">" ;
boolean_atom = boolean_literal | "(" boolean_expression ")" | get_value | value_ref ;
boolean_literal = "true" | "false" ;
(* A45/A57: boundary value operations. `put` (application/context handlers only) publishes a value
to a UI output; `return` (function bodies only) returns the function's result value. *)
put_statement = "put" value "to" output_ref ;
return_statement = "return" value ;
(* General statements. A54: `do` is canonical; the `prompt` statement is a deprecated synonym. *)
prompt_statement = ("do" | "prompt") literal_string ;
code_statement = "```" ("scala" | "java" | "python" | "mojo") code_contents "```" ;
code_contents = {any_char_except_triple_backtick} ;
error_statement = "error" literal_string ;
require_statement = "require" (literal_string | invariant_ref | boolean_expression) ; (* A28 *)
invariant_ref = "invariant" path_identifier ;
(* Entity state transitions. A54: the morph value is a bare record ref or an inline constructor. *)
record_value = constructor | record_ref ;
morph_statement = "morph" entity_ref "to" state_ref "with" record_value ;
become_statement = "become" entity_ref "to" handler_ref ;
(* Pseudo Code Block *)
pseudo_code_block = undefined | "{" pseudo_code_contents "}" | {statement} ;
pseudo_code_contents = [undefined] {statement} ;
(* Constants and Invariants *)
constant = "constant" identifier is type_expression "=" literal_string [with_metadata] ;
invariant = "invariant" identifier is [literal_string | boolean_expression] [with_metadata] ; (* A28 *)
(* Version - A53: a scope's version component is EITHER a name OR a natural number, never both. *)
(* Composing a definition's versioned ancestors root-to-leaf and joining with "." yields its *)
(* precise version, e.g. Garibaldi.4.3. At most one version per scope. *)
version = "version" (natural | identifier) [with_metadata] ;
(* Copyright - A47: a NAMED notice carried verbatim, including the (c) symbol, year and holder. *)
(* Inheritance is NEAREST-WINS, not composed: the applicable notice is the one from the nearest *)
(* ancestor scope that declares one. At most one copyright per scope. *)
copyright = "copyright" identifier is literal_string [with_metadata] ;
(* Relationship *)
relationship = "relationship" identifier "to" processor_ref byAs relationship_cardinality ["label" byAs literal_string] [with_metadata] ;
relationship_cardinality = "1:1" | "1:N" | "N:1" | "N:N" ;
(* Streamlet-related *)
as_shape = "as" ( "void" | "source" | "sink" | "flow" | "cascade" | "merge" | "fanin" | "split" | "broadcast" | "fanout" | "router" ) ;
processor = "processor" identifier [as_shape] is "{" ( {processor_definition_contents}+ | undefined ) "}" [with_metadata] ;
(* `streamlet` mirrors StreamingParser.streamlet exactly: the deprecated shape keywords PLUS the *)
(* generic `processor <id> [as <shape>]` form. Without `processor` here, a processor written *)
(* directly in a module (where `module_content` reaches streamlets only through this rule) would *)
(* parse with fastparse but not with this grammar. *)
streamlet = source | sink | flow | merge | split | router | void | processor ;
source = "source" identifier is "{" streamlet_body "}" [with_metadata] ;
sink = "sink" identifier is "{" streamlet_body "}" [with_metadata] ;
flow = "flow" identifier is "{" streamlet_body "}" [with_metadata] ;
merge = "merge" identifier is "{" streamlet_body "}" [with_metadata] ;
split = "split" identifier is "{" streamlet_body "}" [with_metadata] ;
router = "router" identifier is "{" streamlet_body "}" [with_metadata] ;
void = "void" identifier is "{" streamlet_body "}" [with_metadata] ;
streamlet_body = {undefined | streamlet_definition} ;
streamlet_definition = {inlet | outlet | streamlet_include | processor_definition_contents} ;
streamlet_include = "include" literal_string ;
inlet = "inlet" identifier is type_ref [with_metadata] ;
outlet = "outlet" identifier is type_ref [with_metadata] ;
connector = "connector" identifier is connector_definitions [with_metadata] ;
connector_definitions = ["{" "from" outlet_ref "to" inlet_ref "}" | "from" outlet_ref "to" inlet_ref] ;
(* Group-related *)
group = group_aliases identifier is "{" {undefined | group_definitions} "}" [with_metadata] ;
group_definitions = {group | contained_group | shown_by | group_output | group_input | comment} ;
contained_group = "contains" identifier byAs group_ref [with_metadata] ;
group_output = output_aliases identifier presentation_aliases (literal_string | constant_ref | type_ref) [output_definitions] [with_metadata] ;
group_input = input_aliases identifier acquisition_aliases type_ref [input_definitions] [with_metadata] ;
output_definitions = [is "{" {undefined | (group_output | type_ref)} "}"] ;
input_definitions = [is "{" {undefined | group_input} "}"] ;
group_aliases = "group" | "page" | "pane" | "dialog" | "menu" | "popup" | "frame" | "column" | "window" | "section" | "tab" | "flow" | "block" ;
output_aliases = "output" | "document" | "list" | "table" | "graph" | "animation" | "picture" ;
input_aliases = "input" | "form" | "text" | "button" | "picklist" | "selector" | "item" ;
presentation_aliases = "presents" | "shows" | "displays" | "writes" | "emits" ;
acquisition_aliases = "acquires" | "reads" | "takes" | "accepts" | "admits" | "enters" | "provides" | "selects" | "chooses" | "picks" | "initiates" | "submits" | "triggers" | "activates" | "starts" ;
(* Repository-related *)
repository = "repository" identifier [as_shape] is "{" repository_body "}" [with_metadata] ;
repository_body = {undefined | repository_definitions} ;
repository_definitions = {processor_definition_contents | schema | repository_include} ;
repository_include = "include" literal_string ;
schema = "schema" identifier is schema_kind {data} {link} {index} [with_metadata] ;
schema_kind = "flat" | "relational" | "time-series" | "graphical" | "hierarchical" | "star" | "document" | "columnar" | "vector" | "other" ;
data = "of" identifier byAs type_ref ;
link = "link" identifier byAs field_ref "to" field_ref ;
index = "index" "on" field_ref ;
(* Adaptor-related *)
adaptor = "adaptor" identifier adaptor_direction context_ref [as_shape] is "{" adaptor_body "}" [with_metadata] ;
adaptor_direction = "from" | "to" ;
adaptor_body = {undefined | adaptor_contents} ;
adaptor_contents = {processor_definition_contents | handler | adaptor_include} ;
adaptor_include = "include" literal_string ;
(* Projector-related *)
projector = "projector" identifier [as_shape] is "{" projector_body "}" [with_metadata] ;
projector_body = {undefined | projector_definitions} ;
projector_definitions = {processor_definition_contents | updates | projector_include} ;
projector_include = "include" literal_string ;
updates = "updates" repository_ref ;
(* Saga-related *)
saga = "saga" identifier is "{" saga_body "}" [with_metadata] ;
saga_body = [func_input] [func_output] {saga_definitions} ;
saga_definitions = {saga_step | inlet | outlet | function | saga_include} ;
saga_include = "include" literal_string ;
saga_step = "step" identifier is pseudo_code_block "reverted" ["by"] pseudo_code_block [with_metadata] ;
(* Epic-related *)
epic = "epic" identifier is "{" epic_body "}" [with_metadata] ;
epic_body = user_story {epic_definitions} ;
epic_definitions = {vital_definition_contents | use_case | shown_by | epic_include} ;
epic_include = "include" literal_string ;
use_case = "case" identifier is "{" user_story {undefined | interactions} "}" [with_metadata] ;
user_story_verb = "wants" | "must" | "shall" | "should" | "may" | "will" | "can" ;
user_story = user_ref user_story_verb ["to"] literal_string "so" ["that"] literal_string ;
interactions = {interaction | comment} ;
interaction = parallel_interactions | optional_interactions | sequential_interactions | step_interactions ;
parallel_interactions = "parallel" "{" interactions "}" ;
optional_interactions = "optional" "{" interactions "}" ;
sequential_interactions = "sequence" "{" interactions "}" ;
step_interactions = "step" (focus_on_group_step | direct_user_to_url | select_input_step | refusal_step |
take_input_step | show_output_step | self_processing_step | send_message_step |
arbitrary_step | vague_step) ;
focus_on_group_step = "focus" user_ref "on" group_ref [with_metadata] ;
direct_user_to_url = "direct" user_ref "to" http_url [with_metadata] ;
select_input_step = user_ref "selects" input_ref [with_metadata] ;
refusal_step = any_interaction_ref "refuses" user_ref literal_string [with_metadata] ;
take_input_step = "take" input_ref "from" user_ref [with_metadata] ;
show_output_step = "show" output_ref "to" user_ref [with_metadata] ;
self_processing_step = "for" any_interaction_ref is literal_string [with_metadata] ;
send_message_step = "send" message_ref "from" any_interaction_ref "to" processor_ref [with_metadata] ;
arbitrary_step = "from" any_interaction_ref literal_string ["to"] any_interaction_ref [with_metadata] ;
vague_step = is literal_string literal_string literal_string [with_metadata] ;
(* User-related *)
user = "user" identifier is literal_string [with_metadata] ;
(* Author-related *)
author = "author" identifier is "{" [(undefined | ("name" is literal_string "email" is literal_string
["organization" is literal_string] ["title" is literal_string] ["url" is http_url]))] "}" [with_metadata] ;
(* URLs *)
http_url = ("http" | "https") "://" host_string [":" port_num] "/" [url_path] ;
host_string = /[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*/ ;
port_num = /[0-9]+/ ;
url_path = /[A-Za-z0-9_.~!$&'()*+,;=\/-]*/ ;
(* Metadata *)
with_metadata = ["with" "{" {undefined | {meta_data}} "}"] ;
meta_data = brief_description | description | term | option | author_ref | figma_ref | attachment | comment ;
brief_description = briefly byAs literal_string ;
description = described (byAs doc_block | ("at" http_url) | ("in" "file" literal_string)) ;
term = "term" identifier is doc_block ;
figma_ref = "figma" literal_string "node" literal_string ;
option = "option" is option_name ["(" {literal_string} ")"] ;
option_name = /[a-z0-9_-]*/ ;
(* The three attachment forms share ONE "attachment" keyword. Listing the ULID form as a
separate meta_data alternative made it unreachable in the fastparse parser, whose keyword
combinator cuts. *)
attachment = "attachment" (ulid_attachment_body | named_attachment_body) ;
ulid_attachment_body = "ULID" is literal_string ;
named_attachment_body = identifier is mime_type (("in" "file" literal_string) | (byAs literal_string)) ;
doc_block = "{" {markdown_lines | literal_strings | undefined} "}" | literal_string ;
markdown_lines = {markdown_line}+ ;
markdown_line = "|" /[^\n]*\n?/ ;
literal_strings = {literal_string}+ ;
shown_by = "shown" "by" "{" {http_url} "}" ;
mime_type = ("application" | "audio" | "example" | "font" | "image" | "model" | "text" | "video") "/" /[a-z.*-]*/ ;