core: migrate repeatedly to Clojure + fix char-not-callable / take count validation

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.
This commit is contained in:
Yogthos 2026-06-08 23:51:43 -04:00
parent cab1a75b75
commit 0fa55cd670
4 changed files with 19 additions and 18 deletions

View file

@ -85,10 +85,12 @@
(cstep 0))) (cstep 0)))
())) ()))
;; repeatedly stays in the Janet seed for now (core-repeatedly): the canonical CLJ ;; --- repeatedly --- ((f) throws on a non-fn; (take n …) throws on a non-number
;; version doesn't validate args, so (first (repeatedly non-fn)) / (repeatedly \a +) ;; count — both now enforced in the seed (jolt-call / core-take), so the canonical
;; don't throw like the stricter Janet version (repeatedly.cljc throw cases). ;; CLJ form matches the repeatedly.cljc exception cases.)
;; Ported separately once the non-fn / non-number-count throws are matched. (defn repeatedly
([f] (lazy-seq (cons (f) (repeatedly f))))
([n f] (take n (repeatedly f))))
;; --- repeat --- ;; --- repeat ---
(defn repeat (defn repeat

View file

@ -508,7 +508,10 @@
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f))) (if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f)))
(in f k) (in f k)
(error (string "Index " k " out of bounds for vector of length " (length f))))) (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)] (let [v (get f (get args 0) :jolt/not-found)]
(if (= v :jolt/not-found) (get args 1) v)) (if (= v :jolt/not-found) (get args 1) v))
(error (string "Cannot call " (type f) " as a function")))) (error (string "Cannot call " (type f) " as a function"))))
@ -1091,6 +1094,9 @@
(error "Wrong number of args passed to: reduce")))) (error "Wrong number of args passed to: reduce"))))
(defn core-take [n & rest] (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) (if (= 0 (length rest)) (td-take n)
(let [coll (in rest 0)] (let [coll (in rest 0)]
# Option A: lazy take (returns a seq, not a vector, even over a vector). # 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). # repeat / iterate now live in the Clojure lazy tier (core/40-lazy.clj).
(defn core-repeatedly # repeatedly now lives in the Clojure lazy tier (core/40-lazy.clj).
"(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)))
# ============================================================ # ============================================================
# Higher-order functions # Higher-order functions
@ -2852,7 +2850,6 @@
"sort-by" core-sort-by "sort-by" core-sort-by
"partition" core-partition "partition" core-partition
"range" core-range "range" core-range
"repeatedly" core-repeatedly
"identity" core-identity "identity" core-identity
"constantly" core-constantly "constantly" core-constantly
"complement" core-complement "complement" core-complement

View file

@ -116,7 +116,9 @@
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f))) (if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f)))
(in f k) (in f k)
(error (string "Index " k " out of bounds for vector of length " (length f))))) (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)] (let [v (get f (get args 0) :jolt/not-found)]
(if (= v :jolt/not-found) (get args 1) v)) (if (= v :jolt/not-found) (get args 1) v))
(and (table? f) (get f :jolt/deftype)) (and (table? f) (get f :jolt/deftype))

View file

@ -40,9 +40,9 @@
# many cross-dialect files). Stable across runs. # many cross-dialect files). Stable across runs.
# Raised 3981 -> 4004 migrating 7 lazy seq fns to the Clojure overlay (40-lazy # 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). # 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. # 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 # 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 # 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 # (JOLT_SUITE_TIMEOUT) so CI — whose runners are slower than a dev machine — can