docs: bring EBNF grammar and project docs up to date

- grammar.ebnf: rewrite the number rule to cover the literal syntaxes the reader
  now accepts — 0x/0X hex, N (bigint) / M (bigdec) suffixes, ratios a/b, radixed
  integers (NrXXX, base 2..36), exponents, and the ##Inf/##-Inf/##NaN symbolic
  floats — noting Jolt reads them as plain Janet numbers.
- README: Numbers bullet notes the literal syntaxes read; conformance count.
- reader-syntax-spec: drop the stale 'ratio not supported' case; add coverage
  for hex-uppercase/N/M/ratio/radix/exponent/##Inf/##NaN.
- PLAN.md: refresh the stale Current State snapshot for the 3-layer test
  structure (spec/integration/unit), ~3,920 suite assertions, 218/218
  conformance, current source size.

jpm test green.
This commit is contained in:
Yogthos 2026-06-05 14:40:47 -04:00
parent ac33124ed4
commit 858c7fed14
4 changed files with 37 additions and 15 deletions

19
PLAN.md
View file

@ -11,16 +11,21 @@ Three layers:
## Current State ## Current State
Tests are organized in three layers (see the README "Test" section):
`test/spec/` (the black-box contract), `test/integration/` (cross-cutting and
external-suite regression batteries), and `test/unit/` (white-box component
tests). `jpm test` is green.
| Metric | Value | | Metric | Value |
|--------|-------| |--------|-------|
| Total tests | 317 | | Spec contract | ~1,000 cases across 24 `test/spec/` files |
| Passing | 317 | | Integration batteries | 12 (conformance, SCI bootstrap/runtime, jank, clojure-test-suite, compile-mode, API, systematic-coverage) |
| Failing | 0 | | Unit tests | 9 files (reader, evaluator, types, collections, regex, compiler) |
| CLJS ported test files | 16 (1/1a/1b/2/3/3b/4/5/6/7/8/9/10/test + test-sci-runtime + eval-test) | | Clojure conformance | 218/218 |
| Total assertions | 440 across 31 test files | | clojure-test-suite | ~3,920 assertions pass (remainder are documented platform/design divergences) |
| Source lines | ~5,800 (7 core .janet files) | | Source lines | ~8,600 (`src/jolt/*.janet`; core.janet ~4,000) |
| SCI source files loading | 9/9 | | SCI source files loading | 9/9 |
| New features | `eval` special form, `with-meta` core binding, `var-dynamic?` core binding, `load-string` API, `^:dynamic` def handler | | Reader literals | full numeric syntax (`N`/`M`/ratio/radix/exponent/`##Inf`); see `doc/grammar.ebnf` |
## Phase Plan ## Phase Plan

View file

@ -74,7 +74,7 @@ Janet's standard library is reachable through `jolt.interop` (and the `jolt.shel
Jolt targets Clojure semantics but runs on Janet, not the JVM. The notable divergences: Jolt targets Clojure semantics but runs on Janet, not the JVM. The notable divergences:
- **Host platform.** No JVM and no Java interop — `import`, `gen-class`, `proxy` of Java classes, and `java.*` are unavailable. `instance?` recognizes a small set of built-in types (`clojure.lang.Atom`, `Number`, `String`, …). - **Host platform.** No JVM and no Java interop — `import`, `gen-class`, `proxy` of Java classes, and `java.*` are unavailable. `instance?` recognizes a small set of built-in types (`clojure.lang.Atom`, `Number`, `String`, …).
- **Numbers.** Janet integers and doubles. `(/ 1 3)` is `0.3333…` and large products lose precision. No ratios or `BigDecimal` (`ratio?` is always false, `bigdec` falls back to a double); `bigint`/`biginteger` use Janet's 64-bit `int/s64`, not arbitrary precision. The auto-promoting `+'`/`-'`/`*'`/`inc'`/`dec'` are aliases for the plain ops, since Janet numbers don't overflow. `quot`/`rem`/`mod` follow Clojure's sign rules. The symbolic values `##Inf`/`##-Inf`/`##NaN` read, and `infinite?`/`NaN?` work. Janet represents an integer and an integer-valued double identically, so `1` and `1.0` are indistinguishable: `(float?/double? 1.0)` is `false` and `(int? 1.0)` is `true``float?`/`double?` are true only for values with a fractional part or `##Inf`/`##NaN`. - **Numbers.** Janet integers and doubles. `(/ 1 3)` is `0.3333…` and large products lose precision. No ratios or `BigDecimal` (`ratio?` is always false, `bigdec` falls back to a double); `bigint`/`biginteger` use Janet's 64-bit `int/s64`, not arbitrary precision. The reader still accepts Clojure's numeric literal syntaxes — the BigInt/BigDecimal suffixes (`42N`, `1.5M`), ratios (`1/2`), radixed integers (`2r1010`, `16rFF`), and exponents (`1e3`) — but reads them as plain Janet numbers (a ratio becomes its double quotient). The auto-promoting `+'`/`-'`/`*'`/`inc'`/`dec'` are aliases for the plain ops, since Janet numbers don't overflow. `quot`/`rem`/`mod` follow Clojure's sign rules. The symbolic values `##Inf`/`##-Inf`/`##NaN` read, and `infinite?`/`NaN?` work. Janet represents an integer and an integer-valued double identically, so `1` and `1.0` are indistinguishable: `(float?/double? 1.0)` is `false` and `(int? 1.0)` is `true``float?`/`double?` are true only for values with a fractional part or `##Inf`/`##NaN`.
- **Collections.** By default Jolt uses immutable persistent data structures: vectors are 32-way branching tries (structural-sharing persistent vectors with O(log₃₂ n) `conj`/`assoc`/`nth`), lists are persistent singly-linked cons cells (O(1) `conj`/`cons` prepend with structural sharing), and maps/sets are persistent hash structures. Value equality and sequence operations are Clojure-compatible, but hash-map/hash-set iteration order is unspecified and differs from Clojure — use `sorted-map`/`sorted-set` when order matters. - **Collections.** By default Jolt uses immutable persistent data structures: vectors are 32-way branching tries (structural-sharing persistent vectors with O(log₃₂ n) `conj`/`assoc`/`nth`), lists are persistent singly-linked cons cells (O(1) `conj`/`cons` prepend with structural sharing), and maps/sets are persistent hash structures. Value equality and sequence operations are Clojure-compatible, but hash-map/hash-set iteration order is unspecified and differs from Clojure — use `sorted-map`/`sorted-set` when order matters.
- **Mutable build mode.** Jolt can be compiled to use fast Janet-native *mutable* collections instead, via a build-time flag: `JOLT_MUTABLE=1 jpm build` (default `jpm build` is immutable). In mutable mode vectors and lists share one mutable array representation (so `conj` mutates in place and appends, and `vector?`/`list?` no longer distinguish them) — a performance/looseness trade-off. The default immutable build has full Clojure value semantics. - **Mutable build mode.** Jolt can be compiled to use fast Janet-native *mutable* collections instead, via a build-time flag: `JOLT_MUTABLE=1 jpm build` (default `jpm build` is immutable). In mutable mode vectors and lists share one mutable array representation (so `conj` mutates in place and appends, and `vector?`/`list?` no longer distinguish them) — a performance/looseness trade-off. The default immutable build has full Clojure value semantics.
- **Concurrency / STM.** Single-threaded. No refs, `dosync`, agents, or `send`; `locking` evaluates its body without real locking. Atoms, volatiles, and delays are supported. - **Concurrency / STM.** Single-threaded. No refs, `dosync`, agents, or `send`; `locking` evaluates its body without real locking. Atoms, volatiles, and delays are supported.

View file

@ -50,16 +50,26 @@ collection = list | vector | map ;
nil = "nil" ; nil = "nil" ;
boolean = "true" | "false" ; boolean = "true" | "false" ;
(* Numbers: decimal integer, hexadecimal integer, or decimal float, each with (* Numbers. Jolt accepts Clojure's numeric literal syntaxes, but since Jolt
an optional leading sign. No ratios, radix (NrXXX), BigInt (N) or BigDecimal numbers are Janet ints/doubles it has no distinct bignum, ratio or
(M) suffixes, octal, or exponents Jolt numbers are Janet ints/doubles. *) BigDecimal types: the BigInt suffix N and BigDecimal suffix M are read as the
number = [ sign ] , ( hex-int | float | integer ) ; 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
also read. (No octal-with-leading-0 literal.) *)
number = symbolic-value
| [ sign ] , ( radix-int | ratio | hex-int | decimal ) ;
sign = "+" | "-" ; sign = "+" | "-" ;
integer = digit , { digit } ; integer = digit , { digit } ;
hex-int = "0x" , hex-digit , { hex-digit } ; hex-int = "0" , ( "x" | "X" ) , hex-digit , { hex-digit } , [ "N" ] ;
float = digit , { digit } , "." , digit , { digit } ; radix-int = integer , ( "r" | "R" ) , alnum , { alnum } ; (* base 2..36: 2r1010, 16rFF, 36rZ *)
ratio = integer , "/" , integer ; (* read as a double quotient *)
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 *)
symbolic-value = "##Inf" | "##-Inf" | "##NaN" ;
digit = "0".."9" ; digit = "0".."9" ;
hex-digit = digit | "a".."f" | "A".."F" ; hex-digit = digit | "a".."f" | "A".."F" ;
alnum = digit | "a".."z" | "A".."Z" ;
(* Strings: double-quoted with backslash escapes. *) (* Strings: double-quoted with backslash escapes. *)
string = '"' , { string-char } , '"' ; string = '"' , { string-char } , '"' ;

View file

@ -12,8 +12,15 @@
["namespaced keyword" "true" "(= :a/b :a/b)"] ["namespaced keyword" "true" "(= :a/b :a/b)"]
["char" "\\a" "\\a"] ["char" "\\a" "\\a"]
["char newline" "true" "(= \\newline (first \"\\n\"))"] ["char newline" "true" "(= \\newline (first \"\\n\"))"]
["ratio not supported but reads ints" "3" "3"]
["hex literal" "255" "0xff"] ["hex literal" "255" "0xff"]
["hex uppercase" "31" "0X1F"]
["bigint suffix N" "42" "42N"]
["bigdec suffix M" "1.5" "1.5M"]
["ratio -> double" "0.75" "3/4"]
["radix integer" "255" "16rFF"]
["exponent" "1500.0" "1.5e3"]
["symbolic Infinity" "true" "(infinite? ##Inf)"]
["symbolic NaN" "true" "(NaN? ##NaN)"]
["symbol via quote" "'foo" "'foo"]) ["symbol via quote" "'foo" "'foo"])
(defspec "reader / collection literals" (defspec "reader / collection literals"