From 0fa55cd670932628383b1ecfd95b7caf34ca0303 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Mon, 8 Jun 2026 23:51:43 -0400 Subject: [PATCH] core: migrate repeatedly to Clojure + fix char-not-callable / take count validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the deferred repeatedly port (jolt-8qx). The blockers were two jolt leniencies vs Clojure, now fixed (and correct beyond repeatedly): - A char (a :jolt/type-tagged struct) fell into the struct-as-map branch of both jolt-call (compile path) and the interpreter's apply dispatch, so (\a) returned nil instead of throwing. Now only an UNtagged struct (a map literal) — or a record — is callable as a key lookup; tagged structs fall through to "Cannot call … as a function". Symbols are still handled (keyword-style get). - core-take didn't validate its count, letting Janet's >= silently compare an int to a char/string. It now rejects a non-number n like Clojure. With those, the canonical CLJ repeatedly matches: (first (repeatedly non-fn)) and (repeatedly non-number f) throw. Moved repeatedly to core/40-lazy.clj; removed core-repeatedly + its binding from the seed. These correctness fixes help broadly: repeatedly.cljc goes clean (19/10 -> 29/0), and the suite rises 4004 -> 4034 pass / 66 -> 67 clean. Baseline raised. Gate: conformance 262x3, lazy-infinite 44/44, clojure-test-suite 4034/67, fixpoint, self-host, sci, fallback-zero, specs+unit green. --- jolt-core/clojure/core/40-lazy.clj | 10 ++++++---- src/jolt/core.janet | 19 ++++++++----------- src/jolt/evaluator.janet | 4 +++- .../integration/clojure-test-suite-test.janet | 4 ++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/jolt-core/clojure/core/40-lazy.clj b/jolt-core/clojure/core/40-lazy.clj index 3f0b24a..f715491 100644 --- a/jolt-core/clojure/core/40-lazy.clj +++ b/jolt-core/clojure/core/40-lazy.clj @@ -85,10 +85,12 @@ (cstep 0))) ())) -;; repeatedly stays in the Janet seed for now (core-repeatedly): the canonical CLJ -;; version doesn't validate args, so (first (repeatedly non-fn)) / (repeatedly \a +) -;; don't throw like the stricter Janet version (repeatedly.cljc throw cases). -;; Ported separately once the non-fn / non-number-count throws are matched. +;; --- repeatedly --- ((f) throws on a non-fn; (take n …) throws on a non-number +;; count — both now enforced in the seed (jolt-call / core-take), so the canonical +;; CLJ form matches the repeatedly.cljc exception cases.) +(defn repeatedly + ([f] (lazy-seq (cons (f) (repeatedly f)))) + ([n f] (take n (repeatedly f)))) ;; --- repeat --- (defn repeat diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 309d30f..821ebea 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -508,7 +508,10 @@ (if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f))) (in f k) (error (string "Index " k " out of bounds for vector of length " (length f))))) - (or (struct? f) (and (table? f) (get f :jolt/deftype))) + # Map literal (struct with no :jolt/type marker) or a record: callable as a + # key lookup. A TAGGED struct (char/etc.) is NOT a fn — symbols are handled + # above; everything else with a :jolt/type falls through to the error. + (or (and (struct? f) (nil? (get f :jolt/type))) (and (table? f) (get f :jolt/deftype))) (let [v (get f (get args 0) :jolt/not-found)] (if (= v :jolt/not-found) (get args 1) v)) (error (string "Cannot call " (type f) " as a function")))) @@ -1091,6 +1094,9 @@ (error "Wrong number of args passed to: reduce")))) (defn core-take [n & rest] + # n is a count — reject non-numbers (e.g. a char/string) like Clojure, rather + # than letting Janet's >= silently compare mixed types. + (unless (number? n) (error (string "take: n must be a number, got " (type n)))) (if (= 0 (length rest)) (td-take n) (let [coll (in rest 0)] # Option A: lazy take (returns a seq, not a vector, even over a vector). @@ -1401,15 +1407,7 @@ # repeat / iterate now live in the Clojure lazy tier (core/40-lazy.clj). -(defn core-repeatedly - "(repeatedly f) -> infinite lazy seq of (f) calls; (repeatedly n f) -> n calls." - [a & rest] - (if (= 0 (length rest)) - (do (defn rstep [] (fn [] @[(a) (rstep)])) (make-lazy-seq (rstep))) - (let [n a f (in rest 0)] - (var result @[]) (var i 0) - (while (< i n) (array/push result (f)) (++ i)) - result))) +# repeatedly now lives in the Clojure lazy tier (core/40-lazy.clj). # ============================================================ # Higher-order functions @@ -2852,7 +2850,6 @@ "sort-by" core-sort-by "partition" core-partition "range" core-range - "repeatedly" core-repeatedly "identity" core-identity "constantly" core-constantly "complement" core-complement diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 4c8bcc9..1fa689e 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -116,7 +116,9 @@ (if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f))) (in f k) (error (string "Index " k " out of bounds for vector of length " (length f))))) - (struct? f) + # Map literal only (struct with no :jolt/type). A tagged struct (char/etc.) + # is not callable — symbols are handled above; chars fall through to the error. + (and (struct? f) (nil? (get f :jolt/type))) (let [v (get f (get args 0) :jolt/not-found)] (if (= v :jolt/not-found) (get args 1) v)) (and (table? f) (get f :jolt/deftype)) diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index fc687b1..872fe25 100644 --- a/test/integration/clojure-test-suite-test.janet +++ b/test/integration/clojure-test-suite-test.janet @@ -40,9 +40,9 @@ # many cross-dialect files). Stable across runs. # Raised 3981 -> 4004 migrating 7 lazy seq fns to the Clojure overlay (40-lazy # tier): the canonical CLJ versions add coverage (e.g. distinct value-equality). -(def baseline-pass 4004) +(def baseline-pass 4034) # A file is "clean" when it ran with zero failures AND zero errors. -(def baseline-clean-files 66) +(def baseline-clean-files 67) # Per-file wall-clock budget (seconds). Normal files finish in well under 1s, so # this normally only fires on genuinely-infinite-sequence hangs. It's an env var # (JOLT_SUITE_TIMEOUT) so CI — whose runners are slower than a dev machine — can