From 57b8ffcb9b6dcb2c09c42efd49ca3b57452181a5 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 6 Jun 2026 18:02:34 -0400 Subject: [PATCH] core: move frequencies/group-by/not-empty/filterv/max-key/min-key to Clojure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New collection tier (core/20-coll.clj): six pure, eager fns expressed as compositions of frozen primitives (reduce/assoc/get/conj/filter/vec). Six more core-* primitives + table entries gone from the Janet kernel. Two semantics to preserve, caught by the suite: - max-key/min-key use the canonical multi-arity form (strict on the first pair, <=/>= in the fold) to reproduce the JVM IEEE-754 NaN behavior; a single reduce got the NaN cases wrong. - frequencies/group-by base on (hash-map), not the {} literal: a struct map doesn't canonicalize collection keys across representations ({:a 1} literal vs (hash-map :a 1)), a PHM does — same reason the Janet impl used make-phm. Conformance 218/218 all modes; battery holds 3916. --- jolt-core/clojure/core/20-coll.clj | 53 ++++++++++++++++++++++++++++ src/jolt/api.janet | 3 +- src/jolt/core.janet | 55 +++--------------------------- 3 files changed, 60 insertions(+), 51 deletions(-) create mode 100644 jolt-core/clojure/core/20-coll.clj diff --git a/jolt-core/clojure/core/20-coll.clj b/jolt-core/clojure/core/20-coll.clj new file mode 100644 index 0000000..0132764 --- /dev/null +++ b/jolt-core/clojure/core/20-coll.clj @@ -0,0 +1,53 @@ +;; clojure.core — collection tier. Pure, eager fns expressed as compositions of +;; already-frozen core primitives (reduce/assoc/get/conj/filter/vec/count/>=). +;; No host internals, no laziness, no macros — so they compile cleanly and stay +;; redefinable. Loaded after the seq tier; self-hosted in compile mode. +;; +;; Same migration rule as the seq tier (see 10-seq.clj): not in core-renames, no +;; internal Janet callers, not used by the self-hosted compiler. + +;; Base is (hash-map), not the {} literal: a literal map is a struct that doesn't +;; canonicalize collection keys across representations (a {:a 1} literal vs +;; (hash-map :a 1) key), whereas a PHM does — so counting/grouping by collection +;; value needs the PHM base (the prior Janet impl used make-phm for this reason). +(defn frequencies [coll] + (reduce (fn [counts x] (assoc counts x (inc (get counts x 0)))) (hash-map) coll)) + +(defn group-by [f coll] + (reduce (fn [ret x] (let [k (f x)] (assoc ret k (conj (get ret k []) x)))) (hash-map) coll)) + +(defn not-empty [coll] + (if (or (nil? coll) (zero? (count coll))) nil coll)) + +(defn filterv [pred coll] + (vec (filter pred coll))) + +;; Greatest/least x by (k x). Canonical Clojure multi-arity: the first pair uses +;; strict < / > and the fold uses <= / >= — this exact ordering reproduces the +;; JVM IEEE-754 NaN behavior (e.g. (min-key identity 1 ##NaN) => ##NaN). > / < +;; throw on non-numbers, as Clojure does. +(defn max-key + ([k x] x) + ([k x y] (if (> (k x) (k y)) x y)) + ([k x y & more] + (let [kx (k x) ky (k y) + v (if (> kx ky) x y) + kv (if (> kx ky) kx ky)] + (loop [v v kv kv more more] + (if (seq more) + (let [w (first more) kw (k w)] + (if (>= kw kv) (recur w kw (next more)) (recur v kv (next more)))) + v))))) + +(defn min-key + ([k x] x) + ([k x y] (if (< (k x) (k y)) x y)) + ([k x y & more] + (let [kx (k x) ky (k y) + v (if (< kx ky) x y) + kv (if (< kx ky) kx ky)] + (loop [v v kv kv more more] + (if (seq more) + (let [w (first more) kw (k w)] + (if (<= kw kv) (recur w kw (next more)) (recur v kv (next more)))) + v))))) diff --git a/src/jolt/api.janet b/src/jolt/api.janet index bb6bc91..66d739a 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -38,7 +38,8 @@ # lazily on the first such form, sees the kernel tier already in place). (def- core-tiers [{:ns "clojure.core.00-kernel" :kernel true} - {:ns "clojure.core.10-seq" :kernel false}]) + {:ns "clojure.core.10-seq" :kernel false} + {:ns "clojure.core.20-coll" :kernel false}]) (defn- eval-overlay-source [ctx src] (var s src) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 9cb1a5a..e6ad3c3 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -1166,21 +1166,8 @@ (do (put seen x true) (array/push result x)))) (if (jvec? coll) (make-vec result) result))))) -(defn core-group-by [f coll] - (def f (as-fn f)) - # phm base so collection keys group by value - (var result (make-phm)) - (each x (realize-for-iteration coll) - (let [k (f x)] - (set result (phm-assoc result k (array/push (phm-get result k @[]) x))))) - result) - -(defn core-frequencies [coll] - # phm base so collection elements are counted by value - (var result (make-phm)) - (each x (realize-for-iteration coll) - (set result (phm-assoc result x (+ 1 (phm-get result x 0))))) - result) +# group-by / frequencies now live in the Clojure collection tier +# (core/20-coll.clj). (defn core-partition "(partition n coll) or (partition n step coll). Only complete partitions of @@ -3071,10 +3058,7 @@ (when (not (number? n)) (error "nthnext requires a numeric count")) (let [r (core-nthrest coll n)] (if (or (nil? r) (= 0 (length r))) nil r))) -(defn core-filterv [pred coll] - (def pred (as-fn pred)) - (let [r @[]] (each x (realize-for-iteration coll) (when (truthy? (pred x)) (array/push r x))) - (make-vec r))) +# filterv now lives in the Clojure collection tier (core/20-coll.clj). # mapv lives in the Clojure kernel tier — core/00-kernel.clj. @@ -3143,8 +3127,7 @@ (table? coll) @{} nil)) -(defn core-not-empty [coll] - (if (or (nil? coll) (= 0 (core-count coll))) nil coll)) +# not-empty now lives in the Clojure collection tier (core/20-coll.clj). # rseq is defined only on vectors and sorted collections (Reversible). (defn core-rseq [coll] @@ -3199,29 +3182,7 @@ # asymmetry reproduces the JVM's NaN-ordering behavior. Janet's < / > are used # directly (NaN comparisons are false, never throwing). # keys must be numbers (NaN allowed) — like Clojure, which compares them with . -(defn core-min-key [f & xs] - (def f (as-fn f)) - (when (= 0 (length xs)) (error "min-key requires at least one value")) - (if (= 1 (length xs)) (first xs) - (do (var v (in xs 0)) (var kv (need-num (f v) "min-key")) - (let [y (in xs 1) ky (need-num (f y) "min-key")] (when (not (< kv ky)) (set v y) (set kv ky))) - (var i 2) - (while (< i (length xs)) - (let [w (in xs i) kw (need-num (f w) "min-key")] (when (<= kw kv) (set v w) (set kv kw))) - (++ i)) - v))) - -(defn core-max-key [f & xs] - (def f (as-fn f)) - (when (= 0 (length xs)) (error "max-key requires at least one value")) - (if (= 1 (length xs)) (first xs) - (do (var v (in xs 0)) (var kv (need-num (f v) "max-key")) - (let [y (in xs 1) ky (need-num (f y) "max-key")] (when (not (> kv ky)) (set v y) (set kv ky))) - (var i 2) - (while (< i (length xs)) - (let [w (in xs i) kw (need-num (f w) "max-key")] (when (>= kw kv) (set v w) (set kv kw))) - (++ i)) - v))) +# min-key / max-key now live in the Clojure collection tier (core/20-coll.clj). (defn core-not-every? [pred coll] (def pred (as-fn pred)) @@ -3791,9 +3752,7 @@ "take-nth" core-take-nth "nthrest" core-nthrest "nthnext" core-nthnext - "filterv" core-filterv "empty" core-empty - "not-empty" core-not-empty "rseq" core-rseq "shuffle" core-shuffle "replace" core-replace @@ -3803,8 +3762,6 @@ "ifn?" core-ifn? "indexed?" core-indexed? "distinct?" core-distinct? - "min-key" core-min-key - "max-key" core-max-key "not-every?" core-not-every? "not-any?" core-not-any? "vary-meta" core-vary-meta @@ -3829,8 +3786,6 @@ "sort" core-sort "sort-by" core-sort-by "distinct" core-distinct - "group-by" core-group-by - "frequencies" core-frequencies "partition" core-partition "partition-by" core-partition-by "range" core-range