From fb36577ee7e2bad7ed29891fdbe2d7d68a8e44f6 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 11:04:06 -0400 Subject: [PATCH] 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. --- src/jolt/core.janet | 47 +++++++++++++------ .../integration/clojure-test-suite-test.janet | 2 +- test/spec/sequences-spec.janet | 16 +++++++ 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index ca6fab6..43c8451 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -535,6 +535,9 @@ (cond (lazy-seq? coll) @[x (fn [] 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)))) (defn core-seq [coll] @@ -1772,7 +1775,7 @@ (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-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] "(char code-or-char) -> a character value." (cond @@ -2470,7 +2473,8 @@ (core-delay? x) (x :realized) (lazy-seq? x) (truthy? (x :realized)) (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...)) (defn core-delay [& body] @@ -2794,21 +2798,36 @@ nil)) (defn core-keyword - "(keyword name) or (keyword ns name). Namespaced keywords are `:ns/name`." - [& args] - (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." + "(keyword name) or (keyword ns name). Namespaced keywords are `:ns/name`. + (keyword nil) is nil; the 2-arg form requires string args (nil ns allowed)." [& args] (case (length args) 1 (let [a (in args 0)] - (if (and (struct? a) (= :symbol (a :jolt/type))) a - {:jolt/type :symbol :ns nil :name (if (keyword? a) (string a) (string a))})) - 2 {:jolt/type :symbol :ns (in args 0) :name (in args 1)} + (cond + (nil? a) nil + (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"))) (defn core-split-at [n coll] diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index 711a7ac..2335ae8 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 3730) +(def baseline-pass 3770) # A file is "clean" when it ran with zero failures AND zero errors. (def baseline-clean-files 45) # Per-file wall-clock budget (seconds). Normal files finish in well under 1s; diff --git a/test/spec/sequences-spec.janet b/test/spec/sequences-spec.janet index 4b5ff82..b82f2ba 100644 --- a/test/spec/sequences-spec.janet +++ b/test/spec/sequences-spec.janet @@ -129,3 +129,19 @@ ["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 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))"])