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

@ -50,11 +50,12 @@ collection = list | vector | map ;
nil = "nil" ;
boolean = "true" | "false" ;
(* Numbers. Jolt accepts Clojure's numeric literal syntaxes, but since Jolt
numbers are Janet ints/doubles it has no distinct bignum, ratio or
BigDecimal types: the BigInt suffix N and BigDecimal suffix M are read as the
plain number, a ratio a/b is read as its double quotient, and radixed
integers are computed by base. The symbolic floats ##Inf/##-Inf/##NaN are
(* 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 ) ;
@ -62,10 +63,10 @@ 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 ; (* read as a double quotient *)
ratio = integer , "/" , integer ; (* exact Ratio *)
decimal = integer , [ "." , digit , { digit } ] , [ exponent ] , [ num-suffix ] ;
exponent = ( "e" | "E" ) , [ sign ] , digit , { digit } ;
num-suffix = "N" | "M" ; (* BigInt / BigDecimal in Clojure; plain number in Jolt *)
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" ;

View file

@ -43,7 +43,10 @@ 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.
numeric type and MUST document the choice. The Chez host carries the full
tower: `N` reads as an exact integer (arbitrary precision) and `M` as a real
BigDecimal — `1.5M`, `0.0M`, `3M` — with value equality ignoring scale
(`1.0M = 1.00M`), `(class 1.5M)``java.math.BigDecimal`, and `decimal?` true.
### Symbols and keywords

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 |