From accb7a2c2f7c2d249b71b482330dc1cec473659b Mon Sep 17 00:00:00 2001 From: Yogthos Date: Thu, 4 Jun 2026 13:50:54 -0400 Subject: [PATCH] feat: collection-as-IFn dispatch (vector/map/set/keyword); add Clojure conformance harness --- .beads/interactions.jsonl | 1 + .beads/issues.jsonl | 6 +++ src/jolt/evaluator.janet | 65 ++++++++++++++++++++++------- test/conformance.janet | 87 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 .beads/issues.jsonl create mode 100644 test/conformance.janet diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index e69de29..81c8ea3 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -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"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 0000000..58b749b --- /dev/null +++ b/.beads/issues.jsonl @@ -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} diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index a70d7e4..8db631c 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -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 diff --git a/test/conformance.janet b/test/conformance.janet new file mode 100644 index 0000000..4ff0e22 --- /dev/null +++ b/test/conformance.janet @@ -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 (= ) 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))