From 03652dce5d43bba86fbd13675368a37028f50745 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 09:35:44 -0400 Subject: [PATCH] feat: ##Inf/##-Inf/##NaN literals, infinite?/NaN?, fix intval? for infinity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader now reads the symbolic float values ##Inf, ##-Inf and ##NaN. Added infinite? and NaN? predicates. Fixed intval? to exclude infinity (floor(inf)=inf but inf isn't integer-valued), so float?/double? are true for ##Inf and int?/pos-int?/nat-int?/neg-int? are false for it. This unblocked many number-test files whose forms previously failed to READ (##Inf/##NaN literals), so clojure-test-suite jumped from 2241 to 2539 assertions and pass 1719 -> 1955. Baseline raised to 1900. NaN_qmark now runs. float?/double? on integer-valued doubles (1.0) remain false: Janet represents an integer and an integer-valued double identically, so they're inherently indistinguishable — documented in the README Numbers section. spec: numbers/floats-&-symbolic-values (15 cases). jpm test green. Closes jolt-fy8 (fixable parts; int-vs-float ambiguity is a documented divergence). --- .beads/issues.jsonl | 4 ++-- README.md | 2 +- src/jolt/core.janet | 9 +++++++- src/jolt/reader.janet | 8 +++++++ .../integration/clojure-test-suite-test.janet | 2 +- test/spec/numbers-spec.janet | 21 +++++++++++++++++++ 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f1c5e0d..3f6f032 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -15,13 +15,13 @@ {"_type":"issue","id":"jolt-ytt","title":"HIGH: str semantics — (str nil)='nil' should be '', (str :b)='b' should be ':b'","status":"closed","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:56Z","created_by":"Yogthos","updated_at":"2026-06-04T18:33:45Z","closed_at":"2026-06-04T18:33:45Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-9sb","title":"HIGH: destructuring gaps (nested seq, :strs, nested map, fn-param)","status":"closed","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:55Z","created_by":"Yogthos","updated_at":"2026-06-04T18:31:24Z","closed_at":"2026-06-04T18:31:24Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-dxx","title":"clojure-test-suite: merge nil/empty + collection-key semantics","description":"merge.cljc has 18 fail / 11 err — highest in suite. Investigate nil/empty-map merge return values and collection-key behavior.","status":"closed","priority":3,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:17Z","created_by":"Yogthos","updated_at":"2026-06-05T13:22:29Z","started_at":"2026-06-05T13:18:50Z","closed_at":"2026-06-05T13:22:29Z","close_reason":"merge follows Clojure (when some identity) (reduce conj ...) semantics; nil/empty/conj-entry cases fixed","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"jolt-1uw","title":"clojure-test-suite: transient strictness gaps","description":"Transient ops (conj!/assoc!/disj!/pop!) don't throw on misuse (bad shapes, use-after-persistent!) where Clojure does. Surfaced by conj_bang/assoc_bang/pop_bang/persistent_bang/transient suite files (~50 fails). Lenient behavior; decide whether to enforce.","status":"in_progress","priority":3,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:16Z","created_by":"Yogthos","updated_at":"2026-06-05T13:22:51Z","started_at":"2026-06-05T13:22:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"jolt-1uw","title":"clojure-test-suite: transient strictness gaps","description":"Transient ops (conj!/assoc!/disj!/pop!) don't throw on misuse (bad shapes, use-after-persistent!) where Clojure does. Surfaced by conj_bang/assoc_bang/pop_bang/persistent_bang/transient suite files (~50 fails). Lenient behavior; decide whether to enforce.","status":"closed","priority":3,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:16Z","created_by":"Yogthos","updated_at":"2026-06-05T13:29:38Z","started_at":"2026-06-05T13:22:51Z","closed_at":"2026-06-05T13:29:38Z","close_reason":"Fixed real gaps: invokable transients, assoc! odd args, use-after-persistent! invalidation, pop!-empty. Bad-shape type strictness left as accepted lenient divergence.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-do7","title":"phm-typed maps as set elements / map keys hash by identity","description":"A map produced via hash-map/assoc (a Janet table/phm) used as a set element or map key hashes by identity, so #{{:a 1}} (struct elem) != #{(hash-map :a 1)} (phm elem) and such elements aren't retrievable by value. Map literals (structs) work. Needs value-based hashing for phm keys in PHM/PHS. Edge case; spec covers literal-map cases.","status":"closed","priority":3,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-05T04:07:50Z","created_by":"Yogthos","updated_at":"2026-06-05T05:02:01Z","closed_at":"2026-06-05T05:02:01Z","close_reason":"Fixed: PHM/PHS canonicalize collection keys for value-based hashing/comparison; map literals \u0026 assoc promote to phm on collection keys; frequencies/group-by use phm base; set = is value-based. Also fixed set literals not evaluating elements and the REPL phm printer. Spec cases added. (commit pushed next)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-7dy","title":"MED: compiler path lacks IFn dispatch (collections-as-fn) — only interpreter fixed","status":"closed","priority":3,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:10:00Z","created_by":"Yogthos","updated_at":"2026-06-04T19:56:36Z","closed_at":"2026-06-04T19:56:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-90c","title":"MED: filter/take-while not lazy — infinite-seq (take N (filter p (range))) loops","status":"closed","priority":3,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:10:00Z","created_by":"Yogthos","updated_at":"2026-06-04T18:56:26Z","closed_at":"2026-06-04T18:56:26Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-7n5","title":"MED: REPL display — def/defn should print #'ns/name; lazy-seq printer realizes only head ((0 \u003cfunction\u003e))","status":"closed","priority":3,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:59Z","created_by":"Yogthos","updated_at":"2026-06-04T18:55:11Z","closed_at":"2026-06-04T18:55:11Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-h0n","title":"MED: missing core fns — letfn fnil trampoline doseq cycle partition-all reductions dedupe keep-indexed map-indexed transduce reduce-kv assoc-in peek subvec read-string format","status":"closed","priority":3,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:58Z","created_by":"Yogthos","updated_at":"2026-06-04T18:57:26Z","closed_at":"2026-06-04T18:57:26Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"jolt-fy8","title":"clojure-test-suite: float?/double?/number-predicate divergences","description":"jolt doesn't distinguish float vs double (all Janet numbers); float_qmark/double_qmark and str formatting of numbers diverge from Clojure. Documented divergence; track for completeness.","status":"open","priority":4,"issue_type":"task","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:19Z","created_by":"Yogthos","updated_at":"2026-06-05T13:05:19Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"jolt-fy8","title":"clojure-test-suite: float?/double?/number-predicate divergences","description":"jolt doesn't distinguish float vs double (all Janet numbers); float_qmark/double_qmark and str formatting of numbers diverge from Clojure. Documented divergence; track for completeness.","status":"in_progress","priority":4,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:19Z","created_by":"Yogthos","updated_at":"2026-06-05T13:30:05Z","started_at":"2026-06-05T13:30:05Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-dd5","title":"throw of a caught ex-info rethrows wrapped; var-set on dynamic var in binding no-ops","description":"Edges found via spec: (1) (catch :default e (throw e)) re-wraps the ex-info so the outer catch's (ex-message e) is nil. (2) (var-set #'*v* x) inside (binding [*v* ...] ...) doesn't update the thread-binding. Both rare; core try/catch/ex-info and dynamic binding work.","status":"closed","priority":4,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-05T05:11:04Z","created_by":"Yogthos","updated_at":"2026-06-05T05:23:54Z","closed_at":"2026-06-05T05:23:54Z","close_reason":"Fixed: catch unwraps the :jolt/exception envelope so rethrow doesn't nest; var-set updates the current thread-binding frame (falls back to root). Spec cases restored.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-e4p","title":"regex: lookbehind, backreferences (\\1), named groups (?\u003cn\u003e) unsupported (rare); engine is parse-\u003ePEG with backtracking","status":"closed","priority":4,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-04T20:23:39Z","created_by":"Yogthos","updated_at":"2026-06-05T05:02:24Z","closed_at":"2026-06-05T05:02:24Z","close_reason":"Won't-fix: backreferences and lookbehind are not expressible in a PEG engine (jolt regex compiles to Janet PEG); named groups are rare. Documented as unsupported in README's Regex section. Reopen if a concrete need arises.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-cn4","title":"regex engine is a common subset (PEG-backed); lookaround/backrefs/named-groups unsupported","status":"closed","priority":4,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-04T19:27:50Z","created_by":"Yogthos","updated_at":"2026-06-04T20:23:37Z","closed_at":"2026-06-04T20:23:37Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/README.md b/README.md index 81df65b..0b8bbbe 100644 --- a/README.md +++ b/README.md @@ -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. +- **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`. - **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. diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 96816b0..1125497 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -3009,7 +3009,10 @@ # Additional clojure.core functions # ============================================================ -(defn- intval? [x] (and (number? x) (= x (math/floor x)))) +# Integer-valued: a finite number equal to its floor. Infinity floors to itself +# but is NOT integer-valued (so float?/double? are true for ##Inf, and int?/ +# pos-int?/… are false), and NaN is excluded by the equality check. +(defn- intval? [x] (and (number? x) (< (math/abs x) math/inf) (= x (math/floor x)))) # Forcing lazy seqs (defn core-doall [a & rest] @@ -3061,6 +3064,8 @@ (defn core-ratio? [x] false) (defn core-decimal? [x] false) (defn core-rational? [x] (intval? x)) +(defn core-infinite? [x] (and (number? x) (= (math/abs x) math/inf))) +(defn core-NaN? [x] (and (number? x) (not= x x))) (defn core-numerator [x] x) (defn core-denominator [x] 1) @@ -3339,6 +3344,8 @@ "ratio?" core-ratio? "decimal?" core-decimal? "rational?" core-rational? + "infinite?" core-infinite? + "NaN?" core-NaN? "numerator" core-numerator "denominator" core-denominator "list*" core-list* diff --git a/src/jolt/reader.janet b/src/jolt/reader.janet index 89803b5..145ac9a 100644 --- a/src/jolt/reader.janet +++ b/src/jolt/reader.janet @@ -385,6 +385,14 @@ [{:jolt/type :jolt/skip} new-pos]) (= c 39) (read-var-quote s pos) # #' (= c 34) (read-regex s pos) # #"regex + (= c 35) # ## symbolic value: ##Inf ##-Inf ##NaN + (let [end (read-symbol-name s (+ pos 2) (+ pos 2)) + name (string/slice s (+ pos 2) end)] + (cond + (= name "Inf") [math/inf end] + (= name "-Inf") [(- math/inf) end] + (= name "NaN") [math/nan end] + (error (string "Invalid symbolic value: ##" name)))) # unknown dispatch — tagged literal (let [end (read-symbol-name s pos pos) tag (string/slice s pos end) diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index aefe856..c23c5a0 100644 --- a/test/integration/clojure-test-suite-test.janet +++ b/test/integration/clojure-test-suite-test.janet @@ -18,7 +18,7 @@ # Baseline: assertions Jolt currently passes across the suite. Raise as Jolt # improves so a regression (previously-passing assertion breaking) is caught. -(def baseline-pass 1650) +(def baseline-pass 1900) # A file is "clean" when it ran with zero failures AND zero errors. (def baseline-clean-files 22) # Per-file wall-clock budget (seconds). Normal files finish in well under 1s; diff --git a/test/spec/numbers-spec.janet b/test/spec/numbers-spec.janet index 384cf3b..05d7bfa 100644 --- a/test/spec/numbers-spec.janet +++ b/test/spec/numbers-spec.janet @@ -51,6 +51,27 @@ ["nat-int? neg" "false" "(nat-int? -1)"] ["ratio? false" "false" "(ratio? 5)"]) +# Symbolic float values and float/double predicates. NOTE: Janet represents +# integers and integer-valued doubles identically, so (float? 1.0) is false +# (1.0 is indistinguishable from 1) — a documented divergence. Fractional and +# non-finite values ARE recognized as floats. +(defspec "numbers / floats & symbolic values" + ["read ##Inf" "true" "(= ##Inf (/ 1.0 0.0))"] + ["read ##-Inf" "true" "(< ##-Inf 0)"] + ["##NaN not= itself" "true" "(not (== ##NaN ##NaN))"] + ["float? fractional" "true" "(float? 1.5)"] + ["double? fractional" "true" "(double? 0.25)"] + ["float? integer" "false" "(float? 3)"] + ["float? ##Inf" "true" "(float? ##Inf)"] + ["double? ##NaN" "true" "(double? ##NaN)"] + ["infinite? ##Inf" "true" "(infinite? ##Inf)"] + ["infinite? ##-Inf" "true" "(infinite? ##-Inf)"] + ["infinite? finite" "false" "(infinite? 1.5)"] + ["NaN? ##NaN" "true" "(NaN? ##NaN)"] + ["NaN? number" "false" "(NaN? 1.0)"] + ["int? ##Inf false" "false" "(int? ##Inf)"] + ["pos-int? ##Inf" "false" "(pos-int? ##Inf)"]) + (defspec "numbers / bit-ops & math" ["bit-and" "4" "(bit-and 12 6)"] ["bit-or" "14" "(bit-or 12 6)"]