feat(strictness): num/cons/realized?/symbol/keyword reject malformed args

- num throws on non-numbers (no longer coerces chars)
- cons throws when the second arg isn't seqable (a number/keyword/boolean/fn)
- realized? throws on non-IPending values (only delays/promises/lazy-seqs)
- symbol: 1-arg accepts string/symbol/keyword (->symbol), throws otherwise;
  2-arg requires string ns (nil ok) and string name
- keyword: (keyword nil) is nil, 1-arg accepts string/symbol/keyword, 2-arg
  requires string name and nil-or-string ns

keyword file clean; symbol 60/0/1; cons 18/1/1; realized? 25/1/0.
clojure-test-suite pass 3738->3781. spec: seq/strictness (13). jpm test green.
This commit is contained in:
Yogthos 2026-06-05 11:04:06 -04:00
parent 07d5b43fbb
commit fb36577ee7
3 changed files with 50 additions and 15 deletions

View file

@ -535,6 +535,9 @@
(cond (cond
(lazy-seq? coll) @[x (fn [] coll)] (lazy-seq? coll) @[x (fn [] coll)]
(or (nil? coll) (plist? coll) (array? coll) (tuple? coll)) (pl-cons x coll) (or (nil? coll) (plist? coll) (array? coll) (tuple? coll)) (pl-cons x coll)
# second arg must be seqable (a collection or string); reject scalars
(not (or (core-coll? coll) (string? coll)))
(error (string "Don't know how to create ISeq from: " (type coll)))
(pl-cons x (realize-for-iteration coll)))) (pl-cons x (realize-for-iteration coll))))
(defn core-seq [coll] (defn core-seq [coll]
@ -1772,7 +1775,7 @@
(def core-long (fn [x] (if (core-char? x) (x :ch) (math/trunc x)))) (def core-long (fn [x] (if (core-char? x) (x :ch) (math/trunc x))))
(def core-double (fn [x] (* 1.0 (if (core-char? x) (x :ch) x)))) (def core-double (fn [x] (* 1.0 (if (core-char? x) (x :ch) x))))
(def core-float core-double) (def core-float core-double)
(def core-num (fn [x] (if (core-char? x) (x :ch) x))) (def core-num (fn [x] (if (number? x) x (error (string "num requires a number, got " (type x))))))
(defn core-char [x] (defn core-char [x]
"(char code-or-char) -> a character value." "(char code-or-char) -> a character value."
(cond (cond
@ -2470,7 +2473,8 @@
(core-delay? x) (x :realized) (core-delay? x) (x :realized)
(lazy-seq? x) (truthy? (x :realized)) (lazy-seq? x) (truthy? (x :realized))
(and (table? x) (= :jolt/atom (x :jolt/type))) true (and (table? x) (= :jolt/atom (x :jolt/type))) true
false)) # Clojure's realized? is only defined on IPending; reject anything else.
(error (string "realized? not supported on " (type x)))))
# delay macro: (delay body...) -> (make-delay (fn* [] body...)) # delay macro: (delay body...) -> (make-delay (fn* [] body...))
(defn core-delay [& body] (defn core-delay [& body]
@ -2794,21 +2798,36 @@
nil)) nil))
(defn core-keyword (defn core-keyword
"(keyword name) or (keyword ns name). Namespaced keywords are `:ns/name`." "(keyword name) or (keyword ns name). Namespaced keywords are `:ns/name`.
[& args] (keyword nil) is nil; the 2-arg form requires string args (nil ns allowed)."
(case (length args)
1 (let [a (in args 0)] (if (keyword? a) a (keyword (core-name a))))
2 (keyword (string (in args 0) "/" (in args 1)))
(keyword ;args)))
(defn core-symbol
"(symbol name) or (symbol ns name) -> a jolt symbol struct."
[& args] [& args]
(case (length args) (case (length args)
1 (let [a (in args 0)] 1 (let [a (in args 0)]
(if (and (struct? a) (= :symbol (a :jolt/type))) a (cond
{:jolt/type :symbol :ns nil :name (if (keyword? a) (string a) (string a))})) (nil? a) nil
2 {:jolt/type :symbol :ns (in args 0) :name (in args 1)} (keyword? a) a
(or (string? a) (core-symbol? a)) (keyword (core-name a))
(error (string "keyword requires a string, symbol or keyword, got " (type a)))))
2 (let [ns (in args 0) nm (in args 1)]
(when (not (and (or (nil? ns) (string? ns)) (string? nm)))
(error "keyword ns and name must be strings"))
(keyword (if ns (string ns "/" nm) nm)))
(keyword ;args)))
(defn core-symbol
"(symbol name) or (symbol ns name) -> a jolt symbol struct. name/ns must be
strings (a single symbol arg is returned as-is)."
[& args]
(case (length args)
1 (let [a (in args 0)]
(cond
(core-symbol? a) a
(or (string? a) (keyword? a)) {:jolt/type :symbol :ns nil :name (core-name a)}
(error (string "symbol requires a string or symbol, got " (type a)))))
2 (let [ns (in args 0) nm (in args 1)]
(when (not (and (or (nil? ns) (string? ns)) (string? nm)))
(error "symbol ns and name must be strings"))
{:jolt/type :symbol :ns ns :name nm})
(error "symbol expects 1 or 2 args"))) (error "symbol expects 1 or 2 args")))
(defn core-split-at [n coll] (defn core-split-at [n coll]

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 3730) (def baseline-pass 3770)
# 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

@ -129,3 +129,19 @@
["conj map + map" "{:a 0, :b 1}" "(conj {:a 0} {:b 1})"] ["conj map + map" "{:a 0, :b 1}" "(conj {:a 0} {:b 1})"]
["conj map + pair" "{:a 0, :b 1}" "(conj {:a 0} [:b 1])"] ["conj map + pair" "{:a 0, :b 1}" "(conj {:a 0} [:b 1])"]
["conj map merge wins" "{:a 2}" "(conj {:a 0} {:a 1} {:a 2})"]) ["conj map merge wins" "{:a 2}" "(conj {:a 0} {:a 1} {:a 2})"])
# Strictness: these reject malformed arguments like Clojure.
(defspec "seq / strictness (throws like Clojure)"
["cons non-seqable num" :throws "(cons 1 42)"]
["cons non-seqable kw" :throws "(cons 1 :k)"]
["cons onto nil ok" "[1]" "(cons 1 nil)"]
["cons onto seq ok" "[0 1 2]" "(cons 0 [1 2])"]
["num non-number" :throws "(num \"x\")"]
["num ok" "5" "(num 5)"]
["realized? on number" :throws "(realized? 1)"]
["realized? on nil" :throws "(realized? nil)"]
["symbol from nil" :throws "(symbol nil)"]
["symbol bad 2-arg" :throws "(symbol :a \"b\")"]
["symbol from keyword" "\"x\"" "(name (symbol :x))"]
["keyword bad 2-arg" :throws "(keyword \"abc\" nil)"]
["keyword from symbol" "\"x\"" "(name (keyword 'x))"])