feat: collection-as-IFn dispatch (vector/map/set/keyword); add Clojure conformance harness

This commit is contained in:
Yogthos 2026-06-04 13:50:54 -04:00
parent 1d47fc9ad2
commit accb7a2c2f
4 changed files with 145 additions and 14 deletions

View file

@ -0,0 +1 @@
{"id":"int-621ea914","kind":"field_change","created_at":"2026-06-04T17:50:54.012588Z","actor":"Yogthos","issue_id":"jolt-x8s","extra":{"field":"status","new_value":"closed","old_value":"open"}}

6
.beads/issues.jsonl Normal file
View file

@ -0,0 +1,6 @@
{"_type":"issue","id":"jolt-lp5","title":"CRITICAL: vec/into over lazy-seq leaks PV struct; into {} builds int keys","status":"open","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:15Z","created_by":"Yogthos","updated_at":"2026-06-04T17:46:15Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-nte","title":"Build phased Clojure conformance harness (extracted assertions)","status":"open","priority":1,"issue_type":"task","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:15Z","created_by":"Yogthos","updated_at":"2026-06-04T17:46:15Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-tws","title":"CRITICAL: iterate arity bug","status":"open","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:14Z","created_by":"Yogthos","updated_at":"2026-06-04T17:46:14Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-x8s","title":"CRITICAL: collections not callable as IFn (vector/map/set)","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:14Z","created_by":"Yogthos","updated_at":"2026-06-04T17:50:54Z","closed_at":"2026-06-04T17:50:54Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-alz","title":"CRITICAL: self-referential lazy-seq/lazy-cat yields only literal prefix","status":"open","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:13Z","created_by":"Yogthos","updated_at":"2026-06-04T17:46:13Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-wec","title":"CRITICAL: multi-collection map returns unrealized thunk","status":"open","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:12Z","created_by":"Yogthos","updated_at":"2026-06-04T17:46:12Z","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -28,6 +28,55 @@
(var eval-form nil)
(defn- coll-lookup
"Clojure `get` semantics over a jolt collection, used for collection-as-IFn."
[coll k default]
(cond
(phm? coll) (phm-get coll k default)
(set? coll) (if (phs-contains? coll k) k default)
(or (tuple? coll) (array? coll))
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length coll)))
(in coll k) default)
(or (struct? coll) (table? coll))
(let [v (get coll k :jolt/not-found)]
(if (= v :jolt/not-found) default v))
(nil? coll) default
default))
(defn jolt-invoke
"Apply f to already-evaluated args. Handles real functions and Clojure's
IFn collections: vectors (index lookup), maps/sets/keywords/symbols (get),
and deftype/record values implementing IFn. `args` is an array."
[ctx f args]
(cond
(function? f) (apply f args)
(keyword? f) (coll-lookup (get args 0) f (get args 1))
(and (struct? f) (= :symbol (f :jolt/type)))
(coll-lookup (get args 0) f (get args 1))
(phm? f) (phm-get f (get args 0) (get args 1))
(set? f) (if (phs-contains? f (get args 0)) (get args 0) (get args 1))
(or (tuple? f) (array? f))
(let [k (get args 0)]
(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)
(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))
(let [ifn-fn (find-protocol-method ctx (get f :jolt/deftype) "IFn" "-invoke")]
(if ifn-fn (apply ifn-fn f args)
(if (get f :jolt/protocol-methods)
(let [invoke-fn (get (f :jolt/protocol-methods) :-invoke)]
(if invoke-fn (apply invoke-fn f args)
(error (string "Cannot call " (type f) " as a function"))))
(error (string "Cannot call " (type f) " as a function")))))
(and (table? f) (get f :jolt/protocol-methods))
(let [invoke-fn (get (f :jolt/protocol-methods) :-invoke)]
(if invoke-fn (apply invoke-fn f args)
(error (string "Cannot call " (type f) " as a function"))))
(error (string "Cannot call " (type f) " as a function"))))
(defn- syntax-quote*
[ctx bindings form]
(cond
@ -893,22 +942,10 @@
(eval-form ctx bindings (apply macro-fn args)))
(let [f (eval-form ctx bindings first-form)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(apply f args)))))))
(jolt-invoke ctx f args)))))))
(let [f (eval-form ctx bindings first-form)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(if (function? f)
(apply f args)
(if (keyword? f)
(get (first args) f)
(if (and (table? f) (get f :jolt/deftype))
(let [ifn-fn (find-protocol-method ctx (get f :jolt/deftype) "IFn" "-invoke")]
(if ifn-fn (apply ifn-fn f args)
(if (get f :jolt/protocol-methods)
(let [invoke-fn (get (f :jolt/protocol-methods) :-invoke)]
(if invoke-fn (apply invoke-fn f args)
(error (string "Cannot call " (type f) " as a function"))))
(error (string "Cannot call " (type f) " as a function")))))
(error (string "Cannot call " (type f) " as a function")))))))))
(jolt-invoke ctx f args)))))
(set eval-form (fn [ctx bindings form]
(cond

87
test/conformance.janet Normal file
View file

@ -0,0 +1,87 @@
# Clojure conformance harness (phase 1: extracted assertion pairs).
#
# Each case is [name expected-clj actual-clj]. The harness evaluates the
# single Clojure program (= <expected> <actual>) inside a fresh jolt ctx
# and asserts it returns boolean true. Comparison therefore uses jolt's OWN
# `=`, which implements Clojure sequential/collection equality -- so results
# reflect real Clojure semantics rather than Janet-level identity.
#
# `actual` may be a multi-form body; wrap such cases in (do ...).
#
# Source of truth: ~/src/clojure/test/clojure/test_clojure/*.clj
# These pairs are hand-extracted from those files (and canonical idioms)
# until a minimal clojure.test lets us load the real files directly.
(use ../src/jolt/api)
(def cases
[
### ---- CRITICAL: lazy sequences ----
["self-ref lazy-cat fib"
"(quote (0 1 1 2 3 5 8 13 21 34))"
"(do (def fib-seq (lazy-cat [0 1] (map + (rest fib-seq) fib-seq))) (take 10 fib-seq))"]
["self-ref lazy-seq ones"
"(quote (1 1 1 1 1))"
"(do (def ones (lazy-seq (cons 1 ones))) (take 5 ones))"]
["self-ref lazy-seq nats"
"(quote (0 1 2 3 4))"
"(do (def nats (lazy-cat [0] (map inc nats))) (take 5 nats))"]
### ---- CRITICAL: multi-collection map ----
["map two colls" "(quote (11 22 33))" "(map + [1 2 3] [10 20 30])"]
["map three colls" "(quote (12 24 36))" "(map + [1 2 3] [10 20 30] [1 2 3])"]
["map uneven (shortest)" "(quote ([1 :a] [2 :b]))" "(map vector [1 2 3] [:a :b])"]
["map over range+vec" "(quote (1 3 5))" "(map + (range 3) [1 2 3])"]
["map fn list arg" "(quote (2 3 4))" "(map inc (list 1 2 3))"]
### ---- CRITICAL: iterate / infinite seqs ----
["iterate" "(quote (0 1 2 3 4))" "(take 5 (iterate inc 0))"]
["iterate double" "(quote (1 2 4 8 16))" "(take 5 (iterate (fn [x] (* 2 x)) 1))"]
["range over inf map" "(quote (1 2 3))" "(take 3 (map inc (range)))"]
["count of take" "100" "(count (take 100 (range)))"]
["last of take" "5" "(last (take 5 (iterate inc 1)))"]
### ---- CRITICAL: collections as IFn ----
["vector as fn" ":b" "([:a :b :c] 1)"]
["map as fn" "1" "({:a 1} :a)"]
["map as fn miss" "nil" "({:a 1} :z)"]
["map as fn default" "99" "({:a 1} :z 99)"]
["set as fn" "2" "(#{1 2 3} 2)"]
["set as fn miss" "nil" "(#{1 2 3} 9)"]
["keyword as fn" "1" "(:a {:a 1})"]
["map fn over coll" "(quote (1 3))" "(map {:a 1 :b 3} [:a :b])"]
### ---- CRITICAL: vec / into over lazy + maps ----
["vec of map-result" "[2 3 4]" "(vec (map inc [1 2 3]))"]
["vec of range" "[0 1 2 3 4]" "(vec (range 5))"]
["into vec" "[1 2 3 4 5 6]" "(into [1 2 3] [4 5 6])"]
["into vec from lazy" "[2 3 4]" "(into [] (map inc [1 2 3]))"]
["into map pairs" "{:a 1 :b 2}" "(into {} [[:a 1] [:b 2]])"]
["into map onto map" "{:a 1 :b 2 :c 3}" "(into {:a 1} [[:b 2] [:c 3]])"]
["into list" "(quote (3 2 1))" "(into (list) [1 2 3])"]
])
(var pass 0)
(def fails @[])
(each [name expected actual] cases
(def ctx (init))
(def prog (string "(= " expected " " actual ")"))
(def res (protect (eval-string ctx prog)))
(cond
(not= (res 0) true)
(array/push fails [name "ERROR" (string (res 1))])
(= (res 1) true)
(++ pass)
# not equal: re-eval actual alone to show what we got
(let [got (protect (eval-string (init) actual))]
(array/push fails [name "MISMATCH"
(string "want=" expected
" got=" (if (= (got 0) true) (string/format "%q" (got 1)) (string "ERR:" (got 1))))]))))
(printf "\n=== CONFORMANCE: %d/%d passed ===" pass (length cases))
(unless (empty? fails)
(print "\n--- Failures ---")
(each [name kind detail] fails
(printf "[%s] %s: %s" kind name detail)))
(print)
(when (pos? (length fails)) (os/exit 1))