Grammar
CASC is defined by a PEG grammar written in Aether, the grammar language of Ichor. Both parsers — Elixir and Praxis — are generated from it rather than written by hand.
Statements
The rules below are quoted from priv/grammar/casc.aether in the Cooper repository, with comments removed. Upper-case rules are tokens, matched longest-first; lower-case rules are the syntax.
file := version_header statement* TRIVIA?
version_header := "#" "@" kw:IDENT "="? version_number
statement := disabled_statement | conditional_statement | real_statement
disabled_statement := "#" real_statement
conditional_statement := env_guard real_statement
env_guard := "${?" name:IDENT "}"
real_statement := var_decl | for_statement | import_statement
| sigil_kv_statement | delete_statement
var_decl := "@" private:"*"? name:IDENT "="? value
import_statement := import_kw:IDENT path:import_path
import_path := DQ_STRING | SQ_STRING
for_statement := for_kw:IDENT binding ("," binding)*
((from_kw:IDENT template:key_path as_kw:IDENT dest:key_path)
| (as_kw:IDENT dest:key_path))
block
binding := "@" name:IDENT (in_kw:IDENT iterable:iterable_value)?
sigil_kv_statement := sigil:("~" | "+" | "-")? key_path "="? rhs
delete_statement := "-" key_path
rhs := block | value
block := "{" (statement ","?)* "}"
key_path := key_segment ("." key_segment)*
key_segment := secret:"*"? seg
seg := IDENT | DQ_STRING | SQ_STRINGValues and references
value := NIL_KW | TRUE_KW | FALSE_KW | INF_KW | DATETIME | DATE | TIME | IPV6 | IPV4
| DURATION | BYTES | FLOAT | INTEGER | TRIPLE_STRING | DQ_STRING | SQ_STRING
| list | tuple | at_ref | env_ref | config_ref | resolver_ref | tagged_ref
| atom_value
list := "[" (value ","?)* "]"
tuple := "(" (value ","?)* ")"
atom_value := colon_atom | IDENT
colon_atom := ":" atom_word
atom_word := IDENT | NIL_KW | TRUE_KW | FALSE_KW | INF_KW
at_ref := "@{" name:ref_name idx_suffix? ref_suffix? filters? "}"
env_ref := "${" name:ref_name env_bracket? ref_suffix? filters? "}"
config_ref := "%{" path:key_path idx_suffix? ref_suffix? filters? "}"
ref_name := IDENT | DQ_STRING
resolver_ref := raw:RESOLVER_REF_RAW
tagged_ref := "!" name:IDENT "(" arg:value ")"
idx_suffix := "[" INTEGER "]"
env_bracket := "[" INTEGER? "]"
ref_suffix := (":" "?" msg:DQ_STRING) | (":" "+" alt:value) | (":" default:value)
filters := ("|" filter)+
filter := name:IDENT (":" arg:filter_arg)?
filter_arg := DQ_STRING | SQ_STRINGTokens
COMMENT := "#" !("@" | ALPHA | "*") (!"\n" .)*
IDENT := ALPHA (ALNUM | "_" | "+" | "-")* ("?" | "!")?
INF_KW := ("+" | "-")? "inf"
INTEGER := ("+" | "-")? ( "0x" HEX (HEX | "_")* | "0o" [0-7] ([0-7] | "_")*
| "0b" [01] ([01] | "_")* | DIGIT (DIGIT | "_")* )
FLOAT := ("+" | "-")? DIGIT (DIGIT | "_")*
( "." DIGIT (DIGIT | "_")* (("e" | "E") ("+" | "-")? DIGIT+)?
| ("e" | "E") ("+" | "-")? DIGIT+ )
DATE := DIGIT{4} "-" DIGIT{2} "-" DIGIT{2}
TIME := DIGIT{2} ":" DIGIT{2} ":" DIGIT{2} ("." DIGIT+)?
DATETIME := DATE "T" TIME (("Z") | (("+" | "-") DIGIT{2} ":" DIGIT{2}))?
IPV4 := DIGIT{1,3} "." DIGIT{1,3} "." DIGIT{1,3} "." DIGIT{1,3} ("/" DIGIT{1,3})?
IPV6 := (HEX_GROUP (":" HEX_GROUP){7}
| (HEX_GROUP (":" HEX_GROUP)*)? "::" (HEX_GROUP (":" HEX_GROUP)*)?)
("/" DIGIT{1,3})?
DURATION_UNIT := "ns" | "us" | "µs" | "ms" | "d" | "h" | "m" | "s"
DURATION := DIGIT+ ("." DIGIT+)? DURATION_UNIT (DIGIT+ DURATION_UNIT)*
BYTE_UNIT := "PiB"i | "TiB"i | "GiB"i | "MiB"i | "KiB"i
| "PB"i | "TB"i | "GB"i | "MB"i | "kB"i | "B"i
BYTES := DIGIT+ ("." DIGIT+)? BYTE_UNIT
TRIPLE_STRING := "\"\"\"" (!"\"\"\"" .)* "\"\"\""
DQ_STRING := "\"" ("\\" . | !"\"" .)* "\""
SQ_STRING := "'" (!"'" .)* "'"Interpolation inside double-quoted strings has a grammar of its own (casc_interp.aether): a string is a sequence of text chunks and embedded @{…}, ${…}, %{…}, !{…} and !tag(…) references, each ending at its first closing brace or parenthesis.
Where the implementation differs
The specification is ahead of the reference implementation in a few places. Until they meet, these are the portable choices:
| Specification | Currently | Write |
|---|---|---|
| Comments and blank lines may precede the header | Parse error | Header on line 1 |
key "value" without = | Read as an import | Always = |
| A file’s own statements win over its imports | Strictly by position | Imports first |
_ separators in durations (1_000ms) | Mis-lexed | 1000ms, 1s |
| Interpolated quoted keys anywhere | Resolved only inside loops | Use them in loop bodies and destinations |
A \ at the end of a line continues a bare value | Not implemented | A triple-quoted string |
List merge by key (~items[id] = […]) is reserved in the specification and deliberately not part of the language yet.