jolt/docs/spec/02-reader.md
Yogthos 5f59c02b69 feat: expand type-hint lookup specialization (^Record, get-form, checked mode, docs)
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).
2026-06-12 20:20:25 -04:00

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) and M (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. ::kw MUST resolve to the current namespace at read time (::k in ns user reads as :user/k); ::alias/k resolves 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, let bindings, def names, 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 ^Name where Name is a defrecord/deftype. The assertion is the programmer's (an inaccurate hint yields a wrong lookup, like a wrong Clojure ^String); JOLT_CHECK_HINTS=1 turns a violated hint into an error at no cost to unchecked builds. See RFC 0004.
  • S13a. #'ns/sym MUST 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, %n positional, %& the rest parameter. Arity = highest %n mentioned (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 #_

  • #_form reads and discards the next form entirely (it is never evaluated).
  • Discards compose: #_ #_ a b discards 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. :default matches 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) is 5 everywhere) — not by key priority. Implementations SHOULD provide a per-loading-context compatibility override for foreign-dialect libraries. (jolt: #{:jolt :default}, opt-in via reader-features-set!/JOLT_FEATURES; decision + A/B data in RFC 0002 — inheriting :clj cost 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 resolves tag in 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 (jank fail-unsupported-tag).
  • Built-in tags every implementation MUST provide: #uuid "…" → a UUID value (§9 parse-uuid semantics — 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.core MUST be qualified to clojure.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 (all sym# in the same template denote the same generated symbol; distinct templates generate distinct symbols).
  • S23. ~form inserts the value of form; ~@form splices a sequential value; ~'sym is 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).