Builds on the ^:struct keyword-lookup hint: - ^TypeName for records. A tag naming a defrecord/deftype now resolves to the struct fast path: record instances are tables tagged :jolt/deftype (not :jolt/type), so a raw keyword get is correct for them. A new host contract fn record-type? detects a record by its ->Name constructor; a non-record tag (^String, ^long, ...) is ignored, as before. - (get m :k) and (get m :k default) now get the same inlined keyword lookup as (:k m): the representation guard fast path when unhinted, and the bare get when the subject is ^:struct/^Record. A variable/number/string key still falls through to core-get. The two call shapes share one emitter (emit-kw-lookup). - JOLT_CHECK_HINTS=1 turns a violated hint into a clear runtime error (naming the local and key) by keeping the guard and throwing on the tagged arm. It is off by default with zero cost to normal builds (a hinted lookup still emits a bare get), and is part of the image-cache fingerprint. This is the answer to "a lying hint is silent": opt into checking during development. - Docs: RFC 0004 records the design, soundness contract, and measurements; the reader spec gains S12b (hints are semantically transparent; jolt recognizes ^:struct and ^Record as lookup-optimization assertions). There is no Clojure keyword equivalent for "plain map / fast keyword access" (Clojure hints are class names), so ^:struct stays a jolt-specific flag, analogous to ^:dynamic. Verified: conformance 335/335 in all three modes and the full jpm test pass; a seeded ray-tracer render is byte-identical hinted vs unhinted; the struct-hint test covers record hints, the get-form, inline propagation, and the checked-mode error. Full render with hints holds at 13.3s -> 10.9s (1.22x).
9.5 KiB
§2 The Reader (Lexical Syntax)
Status: token grammar drafted; reader-macro catalog complete with
normative entries; #inst and literal-collapse divergences resolved.
Conformance: jolt reader-forms-spec + reader-syntax-spec (granularity
model: jank's per-construct corpus, 62 files under
test/jank/{reader-macro,syntax-quote} — adapted rows cited per entry).
The reader maps a stream of characters to forms (data). Reading is
independent of evaluation: every form the reader produces is a value of the
language (§4), and read-string exposes the reader as a function. Evaluation
of forms is §1's concern; only quote-family reader macros reference it here.
2.1 Tokens
Whitespace is space, tab, newline, return, and comma (, is whitespace —
S1). A ; begins a comment to end of line (S2). Tokens:
form := literal | symbol | keyword | list | vector | map | set
| reader-macro-form
list := '(' form* ')'
vector := '[' form* ']'
map := '{' (form form)* '}'
literal := nil | boolean | number | string | character
nil := 'nil' boolean := 'true' | 'false'
- S3. A map literal MUST contain an even number of forms; duplicate keys MUST be an error at read time.
- S4. A set literal (
#{…}, §2.3) with duplicate elements MUST be an error at read time.
Numbers
integer := ['+'|'-'] (digits | '0' [xX] hexdigits | '0' octdigits | radixR digits)
float := ['+'|'-'] digits '.' digits? exponent? | ['+'|'-'] digits exponent
ratio := ['+'|'-'] digits '/' digits ; host-numeric-tower (§4 note)
exponent := [eE] ['+'|'-'] digits
- S5. Trailing
N(BigInt) andM(BigDecimal) suffixes are part of the grammar; their value semantics are the §4 numeric-tower question. Implementations without those towers SHOULD read them as the nearest numeric type and MUST document the choice.
Symbols and keywords
symbol := name | ns '/' name ; '/' alone names the division fn
keyword := ':' name | ':' ns '/' name | '::' name | '::' alias '/' name
- S6. Symbol constituent characters: alphanumerics and
* + ! - _ ' ? < > = . $ & %(with%and&further constrained inside#()); a symbol MUST NOT begin with a digit;.and/have positional restrictions. - S7.
::kwMUST resolve to the current namespace at read time (::kin nsuserreads as:user/k);::alias/kresolves the alias or MUST be a read error if the alias does not exist.
Strings and characters
- S8. Strings are
"…"with escapes\" \\ \n \t \r \b \f \uNNNN \oNNN. - S9. Character literals:
\c, the named set\newline \space \tab \return \backspace \formfeed, unicode\uNNNN, octal\oNNN.
Conformance (2.1): jolt reader-syntax-spec "dispatch & sugar";
clojure-test-suite reader files; jank form/* literal dirs. S3/S4 duplicate
checks → UNVERIFIED (rows to add).
2.2 Quote-family reader macros
| Sugar | Reads as | |
|---|---|---|
'form |
(quote form) |
S10 |
@form |
(deref form) |
S11 |
^meta form |
form with metadata attached (see below) | S12 |
#'sym |
(var sym) |
S13 |
`form |
syntax-quote (§2.4) | |
~form, ~@form |
unquote / unquote-splicing — only within syntax-quote (S14: MUST error outside) |
- S12a.
^:kw form≡^{:kw true} form;^Sym form≡^{:tag Sym} form;^"str"≡^{:tag "str"} form. Multiple^stack, rightmost innermost, merged left-over-right. - S12b. Type hints are semantically transparent: a hint MUST NOT change a
program's result. Hints parse in every position they do in Clojure (params,
letbindings,defnames, return position, arbitrary forms) and are otherwise inert. As a non-normative optimization, jolt recognizes two hints on a local as an assertion that a constant-keyword lookup may skip its runtime representation guard:^:struct(a plain struct/record map) and^NamewhereNameis adefrecord/deftype. The assertion is the programmer's (an inaccurate hint yields a wrong lookup, like a wrong Clojure^String);JOLT_CHECK_HINTS=1turns a violated hint into an error at no cost to unchecked builds. See RFC 0004. - S13a.
#'ns/symMUST denote the same var as(var ns/sym):(= (var clojure.core/str) #'clojure.core/str)is true.
Conformance: jolt reader-forms-spec "var-quote #'", "metadata ^",
"syntax-quote"; jank var-quote/pass-qualified.jank, metadata/*.
2.3 Dispatch (#) reader macros
| Form | Meaning | Entry |
|---|---|---|
#{…} |
set literal | S4 above |
#"…" |
regex literal — reads to a regex value; escaping is regex-level, not string-level (single \d) |
S15 |
#(…) |
anonymous fn | S16 below |
#_form |
discard | S17 below |
#?(…) / #?@(…) |
reader conditional (+splicing) | S18 below |
##Inf ##-Inf ##NaN |
symbolic floats | S19 |
#tag form |
tagged literal | S20 below |
#! … |
shebang comment line (implementations SHOULD accept) |
S16 — anonymous function #(…)
#(body)reads as(fn [args…] (body))with parameters derived from the%-symbols appearing in body:%≡%1,%npositional,%&the rest parameter. Arity = highest%nmentioned (plus rest if%&).#()literals MUST NOT nest.
(#(+ %1 %2) 1 2) ;=> 3
(apply #(apply + %&) [1 2 3]) ;=> 6
(map #(* % %) [1 2]) ;=> (1 4)
S17 — discard #_
#_formreads and discards the next form entirely (it is never evaluated).- Discards compose:
#_ #_ a bdiscards two following forms. #_inside collection literals removes the element:[1 #_2 3]⇒[1 3].
S18 — reader conditionals
#?(:feat₁ f₁ :feat₂ f₂ …)reads as the form of the first feature key the platform satisfies, else nothing.:defaultmatches any platform.#?@(…)splices a sequential form into the surrounding context.- Feature keys are implementation-defined; each implementation MUST document
its feature set, and SHOULD follow the portable convention *own dialect key
:default*. Matching MUST be by clause order — the first clause whose key the platform satisfies wins (#?(:default 5 :clj 6)is5everywhere) — not by key priority. Implementations SHOULD provide a per-loading-context compatibility override for foreign-dialect libraries. (jolt:#{:jolt :default}, opt-in viareader-features-set!/JOLT_FEATURES; decision + A/B data in RFC 0002 — inheriting:cljcost 146 suite assertions and 38 errors.)
- Reader conditionals MUST be an error outside
.cljc-style reading unless the implementation documents otherwise.
S19 — symbolic values
##Inf, ##-Inf, ##NaN read as the IEEE-754 values. (= ##NaN ##NaN) is
false; (NaN? ##NaN) is true.
S20 — tagged literals
#tag form: the reader resolvestagin the data-reader table and MUST apply the reader function to the read form, yielding its result as the read value. An unknown tag MUST be a read error (jankfail-unsupported-tag).- Built-in tags every implementation MUST provide:
#uuid "…"→ a UUID value (§9parse-uuidsemantics — round-trips through printing), and#inst "…"→ an instant value: RFC3339 with partial-timestamp defaults (#inst "2020"≡#inst "2020-01-01T00:00:00.000-00:00"), equality by instant (offset-normalized),inst?/inst-ms(epoch milliseconds), printed canonically as#inst "yyyy-MM-ddThh:mm:ss.fff-00:00"and round-tripping. A malformed timestamp MUST be an error.
Conformance (2.3): jolt reader-forms-spec "#() (% %N %&)" + new rows
(symbolic values, stacked discard, conditionals); uuid-spec reader-literal
group; jank reader-macro/{function,regex,uuid,symbolic-value}/*,
fail-unsupported-tag.jank.
2.4 Syntax-quote
Syntax-quote (`) is read-level template construction with namespace
resolution:
- S21. Inside syntax-quote, an unqualified symbol that resolves in
clojure.coreMUST be qualified toclojure.core/sym; a symbol resolving through a namespace alias MUST be qualified to the aliased namespace; an unresolved symbol MUST be qualified to the current namespace. Special-form names stay bare. - S22.
sym#generates a fresh symbol, stable within one syntax-quote template (allsym#in the same template denote the same generated symbol; distinct templates generate distinct symbols). - S23.
~forminserts the value ofform;~@formsplices a sequential value;~'symis the idiom for an intentionally-unqualified symbol. - S24. Syntax-quote distributes through collection literals (vectors, maps, sets) — qualification and unquoting apply inside them.
- S25. A syntax-quoted self-evaluating literal is the literal, collapsed at
read time — so nested/adjacent backticks over literals are inert:
(= "meow" ```"meow")is true. General nested syntax-quote over symbols and collections expands recursively (quasiquote semantics) — that general case remains UNVERIFIED pending dedicated conformance rows.
Conformance: jolt reader-forms-spec "syntax-quote" (gensym, unquote,
splice) + conformance "syntax-quote fully-qualifies"; jank
syntax-quote/{pass-gensym,pass-namespace-resolution,pass-resolve-alias, unquote,unquote-splice}/*. S25 → UNVERIFIED.
2.5 What the reader is not
The reader performs no macroexpansion and no evaluation (tagged-literal
reader functions are the deliberate exception, S20). Forms read identically
whether or not they will be evaluated; read-string of any printable value
v followed by evaluation yields a value equal to v for the
self-evaluating types (§4 print/read round-trip contract).