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
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 |
|--------|-------|
| Total tests | 317 |
| Passing | 317 |
| Failing | 0 |
| CLJS ported test files | 16 (1/1a/1b/2/3/3b/4/5/6/7/8/9/10/test + test-sci-runtime + eval-test) |
| Total assertions | 440 across 31 test files |
| Source lines | ~5,800 (7 core .janet files) |
| Spec contract | ~1,000 cases across 24 `test/spec/` files |
| Integration batteries | 12 (conformance, SCI bootstrap/runtime, jank, clojure-test-suite, compile-mode, API, systematic-coverage) |
| Unit tests | 9 files (reader, evaluator, types, collections, regex, compiler) |
| Clojure conformance | 218/218 |
| clojure-test-suite | ~3,920 assertions pass (remainder are documented platform/design divergences) |
| Source lines | ~8,600 (`src/jolt/*.janet`; core.janet ~4,000) |
| 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

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:
- **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.
- **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.

View file

@ -50,16 +50,26 @@ collection = list | vector | map ;
nil = "nil" ;
boolean = "true" | "false" ;
(* Numbers: decimal integer, hexadecimal integer, or decimal float, each with
an optional leading sign. No ratios, radix (NrXXX), BigInt (N) or BigDecimal
(M) suffixes, octal, or exponents Jolt numbers are Janet ints/doubles. *)
number = [ sign ] , ( hex-int | float | integer ) ;
(* 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
also read. (No octal-with-leading-0 literal.) *)
number = symbolic-value
| [ sign ] , ( radix-int | ratio | hex-int | decimal ) ;
sign = "+" | "-" ;
integer = digit , { digit } ;
hex-int = "0x" , hex-digit , { hex-digit } ;
float = digit , { digit } , "." , 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 *)
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" ;
hex-digit = digit | "a".."f" | "A".."F" ;
alnum = digit | "a".."z" | "A".."Z" ;
(* Strings: double-quoted with backslash escapes. *)
string = '"' , { string-char } , '"' ;

View file

@ -12,8 +12,15 @@
["namespaced keyword" "true" "(= :a/b :a/b)"]
["char" "\\a" "\\a"]
["char newline" "true" "(= \\newline (first \"\\n\"))"]
["ratio not supported but reads ints" "3" "3"]
["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"])
(defspec "reader / collection literals"