spec/ebnf: macroexpand order, set!, letfn primitive, numeric tower

Bring the formal definition in line with this session's language work:
- grammar.ebnf: numbers are a real tower (exact integer / Ratio / double); the M
  suffix reads a real BigDecimal, N an exact integer (drop the stale Janet note).
- 02-reader S5: M is a real java.math.BigDecimal with scale-insensitive equality.
- 03-special-forms: document the read -> macroexpand -> analyze order (macros
  expand before special-form dispatch); special-form heads are not shadowable but
  macros are and value-position locals may be named like a special; set! on a var
  sets the innermost binding (else root); letfn is a primitive with letrec*
  semantics.
This commit is contained in:
Yogthos 2026-06-22 01:03:48 -04:00
parent 212cd0399a
commit b9ab750983
3 changed files with 25 additions and 10 deletions

View file

@ -17,7 +17,17 @@ syntax is specified here, their behavior is host-defined.
Special-form head symbols are not shadowable: a binding named `if` does not
change the meaning of `(if ...)` in operator position. ⚠ This matches the
reference; it differs from Scheme.
reference; it differs from Scheme. A local *may* legally be named like a special
form and used in value position (`(let [if 5] if)``5`); only operator
position is reserved. **Macros, unlike special forms, ARE shadowable** by a local
(`(let [when (fn ...)] (when 1 2))` calls the local).
A list form in operator position is resolved in this order (the canonical
read → **macroexpand** → analyze pipeline): a local binding shadows everything;
otherwise a macro head is expanded and the result re-analyzed; otherwise a
special-form head is parsed by rule; otherwise the form is a function
application. Macroexpansion therefore happens *before* special-form dispatch, so
a macro is never mistaken for a special form (and vice versa).
---
@ -135,10 +145,11 @@ S1S3, E1 → jolt `forms-spec` let group; clojure-test-suite
| `do` | empty `(do)` → nil; top-level `do` splices for compilation units (important and under-documented) |
| `fn*` | arities, variadic `&`, closure capture, self-name, simple-symbol params only, recur target |
| `loop*` | recur arity must match bindings; recur rebinds in place |
| `letfn` | mutually-recursive local fns (`letrec*` semantics — a fn body sees every binding, not only earlier ones). jolt treats `letfn` as a primitive special, not the reference's `letfn` macro → `letfn*` indirection; behavior is identical |
| `recur` | tail-position rule (normative definition of tail position needed), across `if`/`do`/`let*`/`try` interactions |
| `quote` | self-evaluation table: which literals are self-evaluating unquoted |
| `var` | `#'` reader sugar; resolution at compile time |
| `throw` | any value vs Throwable — host question; jolt/cljs allow data, reference requires Throwable → classification needed |
| `try/catch/finally` | catch dispatch order, `:default`-style catch-all is a dialect extension (⚠ divergence note), finally evaluation guarantees, value of try |
| `set!` | host-dependent (dynamic vars + host fields) |
| `set!` | `(set! *var* val)` sets the var's innermost thread binding, else its root, and returns val (implemented); a local (deftype mutable field) or host `(.-field obj)` target is host-dependent |
| `.` / `new` | syntax only; behavior host-defined |