feat(strictness): numeric ops reject non-numbers/non-integers like Clojure

- zero?/pos?/neg? throw on non-numbers; odd?/even? throw on non-integers
  (nil, infinities, NaN, fractional) via need-num/need-int helpers
- comparisons < > <= >= throw on non-number args (1-arity stays true, no check)
- max/min throw on non-number args
- quot/rem/mod throw on zero divisor and non-finite operands

odd?/even?/lt/gt/lt_eq/gt_eq suite files now clean; pass 3691->3738.
Updated systematic-coverage-test (zero? nil now throws). spec:
numbers/strictness (15). jpm test green.
This commit is contained in:
Yogthos 2026-06-05 10:58:21 -04:00
parent 4238ec745b
commit 07d5b43fbb
5 changed files with 50 additions and 15 deletions

View file

@ -5,6 +5,7 @@
{"_type":"issue","id":"jolt-x8s","title":"CRITICAL: collections not callable as IFn (vector/map/set)","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:14Z","created_by":"Yogthos","updated_at":"2026-06-04T17:50:54Z","closed_at":"2026-06-04T17:50:54Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-x8s","title":"CRITICAL: collections not callable as IFn (vector/map/set)","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:14Z","created_by":"Yogthos","updated_at":"2026-06-04T17:50:54Z","closed_at":"2026-06-04T17:50:54Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-alz","title":"CRITICAL: self-referential lazy-seq/lazy-cat yields only literal prefix","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:13Z","created_by":"Yogthos","updated_at":"2026-06-04T18:09:15Z","closed_at":"2026-06-04T18:09:15Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-alz","title":"CRITICAL: self-referential lazy-seq/lazy-cat yields only literal prefix","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:13Z","created_by":"Yogthos","updated_at":"2026-06-04T18:09:15Z","closed_at":"2026-06-04T18:09:15Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-wec","title":"CRITICAL: multi-collection map returns unrealized thunk","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:12Z","created_by":"Yogthos","updated_at":"2026-06-04T18:06:02Z","closed_at":"2026-06-04T18:06:02Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-wec","title":"CRITICAL: multi-collection map returns unrealized thunk","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:12Z","created_by":"Yogthos","updated_at":"2026-06-04T18:06:02Z","closed_at":"2026-06-04T18:06:02Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-lko","title":"clojure-test-suite: strictness — throw on malformed calls like Clojure","description":"Make jolt throw where Clojure throws (bad-shape conj!/assoc!, arithmetic on non-numbers, out-of-range indices, etc.) to satisfy the suite's ~292 p/thrown? assertions. Targeted input validation across affected fns; guard against regressing jolt's spec/conformance.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T14:45:20Z","created_by":"Yogthos","updated_at":"2026-06-05T14:45:39Z","started_at":"2026-06-05T14:45:39Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-kxb","title":"clojure-test-suite: transducers don't short-circuit over infinite seqs (hangs)","description":"(into [] (take 5) (range)) and similar transducer-over-infinite forms hang jolt's eager evaluator (no reduced/early-termination). 7 suite files time out. Make transducing honor reduced.","status":"closed","priority":2,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:18Z","created_by":"Yogthos","updated_at":"2026-06-05T13:18:37Z","started_at":"2026-06-05T13:09:59Z","closed_at":"2026-06-05T13:18:37Z","close_reason":"reduce-with-reduced steps lazy seqs incrementally; transducers short-circuit over infinite seqs","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-kxb","title":"clojure-test-suite: transducers don't short-circuit over infinite seqs (hangs)","description":"(into [] (take 5) (range)) and similar transducer-over-infinite forms hang jolt's eager evaluator (no reduced/early-termination). 7 suite files time out. Make transducing honor reduced.","status":"closed","priority":2,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:18Z","created_by":"Yogthos","updated_at":"2026-06-05T13:18:37Z","started_at":"2026-06-05T13:09:59Z","closed_at":"2026-06-05T13:18:37Z","close_reason":"reduce-with-reduced steps lazy seqs incrementally; transducers short-circuit over infinite seqs","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-js6","title":"Port clojure-test-suite as integration battery","description":"Build a minimal clojure.test + portability shim so Jolt can load the external ~/src/clojure-test-suite (240 per-fn .cljc files in clojure.test format). Add a baseline-guarded integration runner following the jank-conformance pattern (skip if absent).","status":"closed","priority":2,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T12:22:01Z","created_by":"Yogthos","updated_at":"2026-06-05T13:05:04Z","started_at":"2026-06-05T12:22:14Z","closed_at":"2026-06-05T13:05:04Z","close_reason":"Ported clojure-test-suite as baseline-guarded integration battery; surfaced+fixed 2 evaluator macro bugs","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-js6","title":"Port clojure-test-suite as integration battery","description":"Build a minimal clojure.test + portability shim so Jolt can load the external ~/src/clojure-test-suite (240 per-fn .cljc files in clojure.test format). Add a baseline-guarded integration runner following the jank-conformance pattern (skip if absent).","status":"closed","priority":2,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T12:22:01Z","created_by":"Yogthos","updated_at":"2026-06-05T13:05:04Z","started_at":"2026-06-05T12:22:14Z","closed_at":"2026-06-05T13:05:04Z","close_reason":"Ported clojure-test-suite as baseline-guarded integration battery; surfaced+fixed 2 evaluator macro bugs","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-i3g","title":"Clear final 2 SCI bootstrap forms (Var/IVar collision + clojure.repl/template)","description":"namespaces.cljc forms 126/127 fail: (126) lang_stubs IVar collides with real vars.cljc IVar so unbind dispatch misses on sci.lang.Var; (127) clojure.repl + clojure.template namespaces missing. Fix: trim bootstrap stubs to let real modules own canonical protocols/types; port clojure.repl + clojure.template.","status":"closed","priority":2,"issue_type":"task","owner":"yogthos@gmail.com","created_at":"2026-06-05T03:21:40Z","created_by":"Yogthos","updated_at":"2026-06-05T03:35:43Z","closed_at":"2026-06-05T03:35:43Z","close_reason":"Done — SCI bootstrap now loads 422/422 with 0 failures. Fixed deftype protocol-method registration, with-meta on functions, select-keys over vectors, require :as+:refer, defmethod-on-fn; added clojure.set/join and host-module stubs. test-load-sci asserts 0 failures.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-i3g","title":"Clear final 2 SCI bootstrap forms (Var/IVar collision + clojure.repl/template)","description":"namespaces.cljc forms 126/127 fail: (126) lang_stubs IVar collides with real vars.cljc IVar so unbind dispatch misses on sci.lang.Var; (127) clojure.repl + clojure.template namespaces missing. Fix: trim bootstrap stubs to let real modules own canonical protocols/types; port clojure.repl + clojure.template.","status":"closed","priority":2,"issue_type":"task","owner":"yogthos@gmail.com","created_at":"2026-06-05T03:21:40Z","created_by":"Yogthos","updated_at":"2026-06-05T03:35:43Z","closed_at":"2026-06-05T03:35:43Z","close_reason":"Done — SCI bootstrap now loads 422/422 with 0 failures. Fixed deftype protocol-method registration, with-meta on functions, select-keys over vectors, require :as+:refer, defmethod-on-fn; added clojure.set/join and host-module stubs. test-load-sci asserts 0 failures.","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -122,11 +122,20 @@
(defn core-false? [x] (= false x)) (defn core-false? [x] (= false x))
(defn core-identical? [a b] (= a b)) (defn core-identical? [a b] (= a b))
(defn core-zero? [x] (and (number? x) (= x 0))) # Strictness helpers: like Clojure, numeric ops reject non-numbers, and the
(defn core-pos? [x] (and (number? x) (> x 0))) # integer ops (odd?/even?) reject non-integers (incl. infinities, NaN, fractions).
(defn core-neg? [x] (and (number? x) (< x 0))) (defn- finite-num? [x] (and (number? x) (= x x) (< (if (< x 0) (- x) x) math/inf)))
(defn core-even? [n] (= 0 (% n 2))) (defn- need-num [x op]
(defn core-odd? [n] (not= 0 (% n 2))) (if (number? x) x (error (string op " requires a number, got " (type x)))))
(defn- need-int [x op]
(if (and (number? x) (= x x) (< (if (< x 0) (- x) x) math/inf) (= x (math/floor x))) x
(error (string op " requires an integer"))))
(defn core-zero? [x] (= (need-num x "zero?") 0))
(defn core-pos? [x] (> (need-num x "pos?") 0))
(defn core-neg? [x] (< (need-num x "neg?") 0))
(defn core-even? [n] (= 0 (% (need-int n "even?") 2)))
(defn core-odd? [n] (not= 0 (% (need-int n "odd?") 2)))
(defn core-integer? [x] (and (number? x) (= x (math/floor x)))) (defn core-integer? [x] (and (number? x) (= x (math/floor x))))
(defn core-boolean? [x] (or (= x true) (= x false))) (defn core-boolean? [x] (or (= x true) (= x false)))
@ -173,14 +182,17 @@
(def core-dec dec) (def core-dec dec)
# Clojure integer division: quot truncates toward zero; rem matches the sign of # Clojure integer division: quot truncates toward zero; rem matches the sign of
# the dividend; mod matches the sign of the divisor (floored). # the dividend; mod matches the sign of the divisor (floored).
(def core-quot (fn [n d] (let [q (/ n d)] (if (< q 0) (math/ceil q) (math/floor q))))) (def core-quot (fn [n d]
(when (or (not (finite-num? n)) (not (finite-num? d))) (error "quot requires finite numbers"))
(when (= d 0) (error "Divide by zero"))
(let [q (/ n d)] (if (< q 0) (math/ceil q) (math/floor q)))))
(def core-rem (fn [n d] (- n (* (core-quot n d) d)))) (def core-rem (fn [n d] (- n (* (core-quot n d) d))))
(def core-mod (fn [n d] (def core-mod (fn [n d]
(let [m (core-rem n d)] (let [m (core-rem n d)]
(if (or (= m 0) (= (> n 0) (> d 0))) m (+ m d))))) (if (or (= m 0) (= (> n 0) (> d 0))) m (+ m d)))))
(defn core-max [& args] (apply max args)) (defn core-max [& args] (each x args (need-num x "max")) (apply max args))
(defn core-min [& args] (apply min args)) (defn core-min [& args] (each x args (need-num x "min")) (apply min args))
(defn core-abs [x] (if (neg? x) (- 0 x) x)) (defn core-abs [x] (if (neg? x) (- 0 x) x))
(defn core-rand [] (math/random)) (defn core-rand [] (math/random))
@ -268,16 +280,18 @@
(defn core-not= [& args] (not (apply core-= args))) (defn core-not= [& args] (not (apply core-= args)))
# Comparisons are variadic: (< a b c) means a < b < c. # Comparisons are variadic: (< a b c) means a < b < c.
(defn- chain-cmp [op xs] (defn- chain-cmp [op opname xs]
# 1-arity (e.g. (< x)) is true regardless of x and does no type check.
(when (>= (length xs) 2) (each x xs (need-num x opname)))
(var ok true) (var i 0) (var ok true) (var i 0)
(while (and ok (< i (dec (length xs)))) (while (and ok (< i (dec (length xs))))
(unless (op (in xs i) (in xs (+ i 1))) (set ok false)) (unless (op (in xs i) (in xs (+ i 1))) (set ok false))
(++ i)) (++ i))
ok) ok)
(defn core-< [& xs] (chain-cmp < xs)) (defn core-< [& xs] (chain-cmp < "<" xs))
(defn core-> [& xs] (chain-cmp > xs)) (defn core-> [& xs] (chain-cmp > ">" xs))
(defn core-<= [& xs] (chain-cmp <= xs)) (defn core-<= [& xs] (chain-cmp <= "<=" xs))
(defn core->= [& xs] (chain-cmp >= xs)) (defn core->= [& xs] (chain-cmp >= ">=" xs))
# ============================================================ # ============================================================
# Collections # Collections

View file

@ -18,7 +18,7 @@
# Baseline: assertions Jolt currently passes across the suite. Raise as Jolt # Baseline: assertions Jolt currently passes across the suite. Raise as Jolt
# improves so a regression (previously-passing assertion breaking) is caught. # improves so a regression (previously-passing assertion breaking) is caught.
(def baseline-pass 3690) (def baseline-pass 3730)
# A file is "clean" when it ran with zero failures AND zero errors. # A file is "clean" when it ran with zero failures AND zero errors.
(def baseline-clean-files 45) (def baseline-clean-files 45)
# Per-file wall-clock budget (seconds). Normal files finish in well under 1s; # Per-file wall-clock budget (seconds). Normal files finish in well under 1s;

View file

@ -59,7 +59,8 @@
(let [ctx (init)] (let [ctx (init)]
(assert (= true (ct-eval ctx "(zero? 0)")) "zero? 0") (assert (= true (ct-eval ctx "(zero? 0)")) "zero? 0")
(assert (= false (ct-eval ctx "(zero? 1)")) "zero? 1") (assert (= false (ct-eval ctx "(zero? 1)")) "zero? 1")
(assert (= false (ct-eval ctx "(zero? nil)")) "zero? nil") # zero?/pos?/neg? now reject non-numbers (Clojure-strict), like the JVM.
(assert (not ((protect (ct-eval ctx "(zero? nil)")) 0)) "zero? nil throws")
(assert (= true (ct-eval ctx "(pos? 1)")) "pos?") (assert (= true (ct-eval ctx "(pos? 1)")) "pos?")
(assert (= false (ct-eval ctx "(pos? 0)")) "pos? 0") (assert (= false (ct-eval ctx "(pos? 0)")) "pos? 0")
(assert (= false (ct-eval ctx "(pos? -1)")) "pos? -1") (assert (= false (ct-eval ctx "(pos? -1)")) "pos? -1")

View file

@ -90,6 +90,25 @@
["exponent" "1000.0" "1e3"] ["exponent" "1000.0" "1e3"]
["exponent neg" "0.015" "1.5e-2"]) ["exponent neg" "0.015" "1.5e-2"])
# Strictness: numeric ops reject non-numbers like Clojure; the integer
# predicates reject non-integers; quot/rem/mod reject zero/non-finite.
(defspec "numbers / strictness (throws like Clojure)"
["odd? nil" :throws "(odd? nil)"]
["odd? fractional" :throws "(odd? 1.5)"]
["even? inf" :throws "(even? ##Inf)"]
["zero? nil" :throws "(zero? nil)"]
["pos? false" :throws "(pos? false)"]
["neg? keyword" :throws "(neg? :a)"]
["< nil" :throws "(< nil 1)"]
["> with nil" :throws "(> 1 nil)"]
["max non-number" :throws "(max 1 nil)"]
["quot by zero" :throws "(quot 10 0)"]
["quot inf" :throws "(quot ##Inf 1)"]
["< arity-1 any" "true" "(< :anything)"]
["odd? ok" "true" "(odd? 3)"]
["< ok" "true" "(< 1 2 3)"]
["quot ok" "3" "(quot 10 3)"])
(defspec "numbers / printing of inf & nan" (defspec "numbers / printing of inf & nan"
["str Infinity" "\"Infinity\"" "(str ##Inf)"] ["str Infinity" "\"Infinity\"" "(str ##Inf)"]
["str -Infinity" "\"-Infinity\"" "(str ##-Inf)"] ["str -Infinity" "\"-Infinity\"" "(str ##-Inf)"]