Grammatik
CASC ist durch eine PEG-Grammatik in Aether definiert, der Grammatiksprache von Ichor. Die Parser für Elixir und Praxis werden daraus erzeugt statt von Hand geschrieben; die Parser für Node.js und PHP übertragen sie Regel für Regel.
Pragmas und Tokens
Dies ist die vollständige Grammatik aus priv/grammar/casc.aether, wie sie mit Cooper 0.7.0 veröffentlicht ist (unverändert seit 0.6.0), nur ohne Kommentare. Namen in Großbuchstaben sind Tokens, längste Übereinstimmung zuerst, bei Gleichstand gewinnt die frühere Deklaration; Namen in Kleinbuchstaben sind Regeln, die per geordneter Auswahl über den Tokens arbeiten. @skip TRIVIA erlaubt Leerraum und Kommentare zwischen je zwei Elementen einer Regel.
@grammar "casc"
@root file
@skip TRIVIA
COMMENT := "#" !("@" | ALPHA | "*") (!"\n" .)*
TRIVIA := (SPACE | COMMENT)*
NIL_KW := "nil"
TRUE_KW := "true"
FALSE_KW := "false"
INF_KW := ("+" | "-")? "inf" !(ALNUM | "_" | "+" | "-" | "?" | "!")
IDENT := ("_" | ALPHA) (ALNUM | "_" | "+" | "-")* ("?" | "!")?
DATE := DIGIT{4} "-" DIGIT{2} "-" DIGIT{2}
TIME := DIGIT{2} ":" DIGIT{2} ":" DIGIT{2} ("." DIGIT+)?
DATETIME := DATE "T" TIME (("Z") | (("+" | "-") DIGIT{2} ":" DIGIT{2}))?
HEX_GROUP := HEX{1,4}
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 | "_")* ("." DIGIT (DIGIT | "_")*)? DURATION_UNIT
(DIGIT (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
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+
)
TRIPLE_STRING := "\"\"\"" (!"\"\"\"" .)* "\"\"\""
DQ_STRING := "\"" ("\\" . | !"\"" .)* "\""
SQ_STRING := "'" (!"'" .)* "'"
RESOLVER_REF_RAW := @native("Cooper.Native.ResolverRef", "scan")DIGIT, ALPHA, ALNUM, SPACE und HEX sind in jeder Aether-Grammatik vordefiniert. RESOLVER_REF_RAW ist das eine Token, das Elixir-Code statt eines Musters erkennt: Ein !{…}-Resolver-Verweis darf Klammern verschachteln, und das kann kein festes Token zählen.
Anweisungen
file := TRIVIA? version_header statement* TRIVIA?
version_header := "#" "@" kw:IDENT "="? version_number
version_head := FLOAT | INTEGER
version_number := version_head ("." n:INTEGER)*
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 | followed_delete
| sigil_kv_statement | delete_statement
followed_delete := delete_statement &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)?
iterable_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
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_STRINGWerte und Verweise
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
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)
| (":" signed_alt:signed_number) | (":" default:value)
signed_number := INTEGER | FLOAT | INF_KW
filters := ("|" filter)+
filter := name:IDENT (":" arg:filter_arg)?
filter_arg := DQ_STRING | SQ_STRING
list := "[" (element ","?)* "]"
tuple := "(" (element ","?)* ")"
element := block | value
atom_value := colon_atom | IDENT
colon_atom := ":" atom_word
atom_word := IDENT | NIL_KW | TRUE_KW | FALSE_KW | INF_KWInterpolation
Eine Zeichenkette in doppelten Anführungszeichen wird ein zweites Mal von einer eigenen Grammatik gelesen, priv/grammar/casc_interp.aether, die sie in Textstücke und eingebettete Verweise zerlegt. Jeder Verweis endet an seiner ersten schließenden Klammer.
@grammar "casc_interp"
@root text
@noskip
AT_REF_RAW := "@{" (!"}" .)* "}"
ENV_REF_RAW := "${" (!"}" .)* "}"
CONFIG_REF_RAW := "%{" (!"}" .)* "}"
RESOLVER_REF_RAW := @native("Cooper.Native.ResolverRef", "scan")
TAG_NAME := ALPHA (ALNUM | "_" | "+" | "-")* ("?" | "!")?
TAGGED_REF_RAW := "!" TAG_NAME "(" (!")" .)* ")"
BANG_TAG := "!" TAG_NAME "("
TEXT_CHUNK := (!("@{" | "${" | "%{" | "!{" | BANG_TAG) .)+
text := segment*
segment := AT_REF_RAW | ENV_REF_RAW | CONFIG_REF_RAW | RESOLVER_REF_RAW | TAGGED_REF_RAW | TEXT_CHUNKFehlerkategorien
Jeder Ladefehler gehört zu genau einer Kategorie, benannt nach der Stufe des Ladens, die das Dokument abgelehnt hat. Jede Implementierung meldet Fehler auf ihre Weise, muss die Kategorie aber angeben. Kategorien werden exakt verglichen; die Meldungstexte gehören nicht zur Spezifikation.
| Kategorie | Ausgelöst, wenn |
|---|---|
lexer | sich der Text nicht in Tokens zerlegen lässt: eine nicht geschlossene Zeichenkette, ein Zeichen, mit dem kein Token beginnt |
parser | die Tokens kein Dokument ergeben: eine fehlende oder fehlerhafte #@version-Zeile, eine Anweisung, für die die Grammatik keine Regel hat |
action | ein Literal wohlgeformt ist, aber nichts bedeutet: ein unmögliches Datum, eine Adresse außerhalb des Bereichs, eine fehlerhafte Zeitdauer oder Bytegröße |
import | ein Import nicht gelesen werden kann: keine Datei, ein Zyklus, ein nicht registriertes Schema |
merge | sich zwei Werte nicht zusammenführen lassen: eine Listenoperation auf etwas, das keine Liste ist |
loop | ein for nicht laufen kann: eine Quelle, die keine Liste ist, gebundene Listen ungleicher Länge, eine Schleife nur mit Index |
resolve | ein Verweis keine Antwort findet: ein undefinierter Name ohne Default, ein Zyklus, ein nicht registrierter oder fehlschlagender Resolver oder Tag, ein interpolierter Wert ohne Textform |
dotenv | sich eine für ${…} gelesene .env-Datei nicht parsen lässt |
Wo ein Dokument scheitert, sagt trotzdem jede Implementierung. Seit 0.7.0 meldet Cooper einen Parse-Fehler am weitesten Token, das der Parser erreicht hat, als unexpected X -- expected one of: …, und ein in der Stufe action abgelehntes Literal mit Zeile, Spalte und der Quellzeile darunter:
#@version = 1.0 name = "api" allow = [10.0.0.1, 300.0.0.1]
3:20: invalid IP address: "300.0.0.1" 3 | allow = [10.0.0.1, 300.0.0.1] | ^
Wo die Implementierungen abweichen
An zwei Stellen ist die Spezifikation den Implementierungen voraus. Bis beide sich treffen, sind dies die portablen Entscheidungen:
| Spezifikation | Derzeit | Schreibweise |
|---|---|---|
| Eigene Anweisungen gewinnen gegen Imports | Streng nach Position | Imports zuerst |
Ein \ am Zeilenende setzt einen nackten Wert fort | Nicht implementiert: ein Lexer-Fehler | Eine dreifach gequotete Zeichenkette |
Vier frühere Abweichungen gibt es seit Cooper 0.5.0 nicht mehr: Kommentare dürfen vor der Kopfzeile stehen, key "value" ohne = ist eine Zuweisung, Zeitdauern nehmen _ als Trenner, und ein interpolierter Schlüssel funktioniert auch außerhalb einer Schleife. Eine fünfte verschwand mit node-cooper 0.2.2: Ein zugeordnetes Modul liest sich in einer Zeichenkette jetzt auch in Node.js als das, worauf es abgebildet ist, wie in Elixir und PHP.
Das Mischen von Listen nach Schlüssel (~items[id] = […]) ist in der Spezifikation vorgemerkt und bewusst noch nicht Teil der Sprache.