jolt/docs/grammar.ebnf
Yogthos 33eff7c7d8 Clean up codebase: rename stdlib layer, strip porting residue, fix tooling
Rename src/jolt -> stdlib (the runtime-loaded layer; jolt-core stays the
seed-baked layer) and update the loader / emit-image / doc paths. Drop dead
code: the spike/ experiments, the duplicate clojuredocs-export.edn (json moves
to tools/), the Janet-era jolt.http binding, and the orphaned
persistent_vector.clj whose ns/path didn't even match.

Strip porting residue from comments and docstrings across host/chez, jolt-core,
stdlib, tests, and docs: internal issue ids, "Phase N" markers, and the "vs
Janet" historical exposition, leaving present-tense descriptions and the real
JVM-Clojure semantic contrasts. Same pass over the corpus suite labels. The seed
is unchanged (docstrings/comments aren't emitted), so the self-host fixpoint and
corpus are untouched.

Port tools/spec_coverage.py off the dead janet probe to bin/joltc and regenerate
coverage.md; drop the dead :host/janet rule from certify.clj and regenerate the
conformance profile. Add docs/host-interop.md (the JVM shims and how to register
your own host class from a library) and a writing-style note in CLAUDE.md.

Stabilize the four racy concurrency corpus cases (future-cancel and agent
send/send-off): give the future a sleeping body and the agent a slow action, so
cancel reliably catches an in-flight future and deref reliably reads the
pre-update snapshot. They certify deterministically now, so drop their :flaky
allowlist entries and the orphaned legend.
2026-06-22 22:18:00 -04:00

191 lines
9.7 KiB
EBNF

(* ===========================================================================
Jolt reader grammar (EBNF)
===========================================================================
This grammar specifies the surface syntax accepted by Jolt's reader
(host/chez/reader.ss, with the portable half in jolt-core/jolt/reader.clj)
the text that `read`/`read-string`/`load-string` turn into data/forms. It is
the syntactic half of Jolt's contract; the behavioural half lives in the
conformance corpus (test/chez/corpus.edn, see docs/spec/02-reader.md). 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, 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 -> an irregex-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. *)