(* =========================================================================== Jolt reader grammar (EBNF) =========================================================================== This grammar specifies the surface syntax accepted by Jolt's reader (src/jolt/reader.janet) — the text that `read`/`parse-string`/`load-string` turn into data/forms. It is the syntactic half of Jolt's contract; the behavioural half lives in test/spec/. Where Jolt diverges from Clojure the difference is called out in a comment. Notation (ISO-ish EBNF): = definition | alternation , concatenation ( ) grouping [ x ] optional (0 or 1) { x } repetition (0 or more) "x" 'x' terminal (* *) comment ? x ? informal/char-class terminal A source file is a sequence of forms; evaluation is form-by-form. =========================================================================== *) file = { ws | form } ; (* -------------------------------------------------------------------------- Whitespace, separators and comments -------------------------------------------------------------------------- *) ws = { whitespace-char | comment | discard } ; whitespace-char = " " | "\t" | "\n" | "\r" | "," ; (* comma is whitespace *) comment = ";" , { ? any char except "\n" ? } , [ "\n" ] ; discard = "#_" , ws , form ; (* read then drop next form *) (* -------------------------------------------------------------------------- Forms -------------------------------------------------------------------------- *) form = scalar | collection | reader-macro | dispatch ; scalar = nil | boolean | number | string | character | keyword | symbol ; collection = list | vector | map ; (* -------------------------------------------------------------------------- Atoms / scalars -------------------------------------------------------------------------- *) nil = "nil" ; boolean = "true" | "false" ; (* Numbers. Jolt carries a real numeric tower (JVM parity): an integer literal reads as an exact integer (arbitrary precision), a ratio a/b as an exact Ratio, a decimal/exponent literal as a double. The BigDecimal suffix M reads as a real BigDecimal (unscaled x 10^-scale) — 1.5M, 0.0M, 3M; class is java.math.BigDecimal. The BigInt suffix N reads as an exact integer. Radixed integers are computed by base; the symbolic floats ##Inf/##-Inf/##NaN are also read. (No octal-with-leading-0 literal.) *) number = symbolic-value | [ sign ] , ( radix-int | ratio | hex-int | decimal ) ; sign = "+" | "-" ; integer = digit , { digit } ; hex-int = "0" , ( "x" | "X" ) , hex-digit , { hex-digit } , [ "N" ] ; radix-int = integer , ( "r" | "R" ) , alnum , { alnum } ; (* base 2..36: 2r1010, 16rFF, 36rZ *) ratio = integer , "/" , integer ; (* exact Ratio *) decimal = integer , [ "." , digit , { digit } ] , [ exponent ] , [ num-suffix ] ; exponent = ( "e" | "E" ) , [ sign ] , digit , { digit } ; num-suffix = "N" | "M" ; (* N = exact integer (BigInt); M = BigDecimal *) symbolic-value = "##Inf" | "##-Inf" | "##NaN" ; digit = "0".."9" ; hex-digit = digit | "a".."f" | "A".."F" ; alnum = digit | "a".."z" | "A".."Z" ; (* Strings: double-quoted with backslash escapes. *) string = '"' , { string-char } , '"' ; string-char = ? any char except '"' and "\" ? | escape ; escape = "\" , ( "n" | "t" | "r" | "\" | '"' | ? other ? ) ; (* Characters: \x, a named char, or \uNNNN / \oNNN. *) character = "\" , ( char-name | ? any single char ? ) ; char-name = "newline" | "space" | "tab" | "return" | "formfeed" | "backspace" | "nul" | "u" , hex-digit , hex-digit , hex-digit , hex-digit | "o" , octal-digit , { octal-digit } ; octal-digit = "0".."7" ; (* Keywords: :name, :ns/name, or auto-resolved ::name. *) keyword = "::" , sym-token (* auto-resolved to current ns *) | ":" , sym-token ; (* Symbols: a token of symbol-constituent chars not parsed as a number. A "/" separates an optional namespace from the name. *) symbol = sym-token ; sym-token = sym-start , { sym-constituent } ; sym-start = sym-constituent - ( digit ) ; (* not a leading digit *) sym-constituent = letter | digit | "*" | "+" | "!" | "_" | "-" | "?" | "." | "<" | ">" | "=" | "&" | "|" | "$" | "%" | "/" ; letter = "a".."z" | "A".."Z" ; (* -------------------------------------------------------------------------- Collections -------------------------------------------------------------------------- *) list = "(" , { ws | form } , ")" ; (* -> a Clojure list *) vector = "[" , { ws | form } , "]" ; (* -> a persistent vector *) map = "{" , { ws | map-entry } , "}" ; map-entry = form , ws , form ; (* an even number of forms *) (* -------------------------------------------------------------------------- Reader macros (prefix sugar). Each expands to a 2-element form (op operand), e.g. 'x -> (quote x), @a -> (clojure.core/deref a). -------------------------------------------------------------------------- *) reader-macro = quote | syntax-quote | unquote | unquote-splice | deref | metadata ; quote = "'" , form ; (* (quote form) *) syntax-quote = "`" , form ; (* (syntax-quote form) *) unquote-splice = "~@" , form ; (* (unquote-splicing form) *) unquote = "~" , form ; (* (unquote form) *) deref = "@" , form ; (* (clojure.core/deref form) — qualified, *) (* so it derefs even where deref is shadowed *) metadata = "^" , meta-form , ws , form ; (* attach metadata to form *) meta-form = map | keyword | symbol | string ; (* Normalized like Clojure: a symbol or string is a type hint -> {:tag ...}; a keyword -> {keyword true}; a map is used as-is. A keyword/symbol/string meta-form on a symbol rides ON the symbol (it stays a bare symbol, so a hint like ^String is transparent in params/lets/bodies). A MAP meta-form routes through a runtime (with-meta form ...) even on a symbol (jolt-8w2), so a name with ^{:map} metadata reads as a form, not a bare symbol — def/defn/defmacro/ns unwrap that to the bare name (and attach the metadata). *) (* -------------------------------------------------------------------------- Dispatch forms — introduced by "#". -------------------------------------------------------------------------- *) dispatch = set | anon-fn | var-quote | regex | reader-conditional | discard (* #_form — see Whitespace above *) | tagged-literal ; set = "#{" , { ws | form } , "}" ; (* a persistent set *) (* Anonymous function. %, %1.. are positional params; %& is the rest param. *) anon-fn = "#(" , { ws | form } , ")" ; (* -> (fn* [..] body) *) anon-arg = "%" | "%" , digit , { digit } | "%&" ; var-quote = "#'" , symbol ; (* (var symbol) *) (* Regex literal -> a Janet PEG-backed regex value. Supported: groups, greedy/lazy quantifiers, (?:..), lookahead (?=..)/(?!..), alternation, anchors ^ $ \b \B, classes, (?i). NOT: lookbehind, backreferences, named groups. *) regex = '#"' , { ? any char except '"' ? | "\" , ? any char ? } , '"' ; (* Reader conditional. Jolt reads as the :clj platform; #?@ splices a sequence. *) reader-conditional = "#?@" , "(" , { ws | feature , ws , form } , ")" | "#?" , "(" , { ws | feature , ws , form } , ")" ; feature = ":clj" | ":cljs" | ":default" | keyword ; (* Tagged literal: #tag form. Built-in tags include #inst and #uuid; others dispatch to a registered data-reader (else error). *) tagged-literal = "#" , symbol , ws , form ; (* ── Destructuring (semantics, not reader syntax) ──────────────────────────── The reader produces plain vectors and maps; the binding MACROS (let, fn, loop, doseq, for, defmacro params, …) interpret them as destructuring patterns. The PRIMITIVES they desugar to — fn*, let*, loop* — take plain symbols ONLY (a non-symbol binding is an error, as in Clojure: "fn params must be Symbols" / "Bad binding form, expected symbol"). The grammar of a pattern: binding = symbol | seq-binding | map-binding ; seq-binding = "[" , { binding } , [ "&" , binding ] , [ ":as" , symbol ] , "]" ; map-binding = "{" , { binding , form (* {local key} *) | ":keys" , "[" {symbol} "]" (* x or ns/x *) | ":strs" , "[" {symbol} "]" | ":syms" , "[" {symbol} "]" | ":or" , map (* defaults *) | ":as" , symbol } , "}" ; A symbol in :keys/:syms may be namespaced (x/y): the namespaced key is looked up but the local is the bare name (y). Destructuring a *sequence* with a map-binding treats it as keyword args (alternating k v, or a trailing map) — this is the fn/macro `[& {:keys [...]}]` form. *)