From 8a3019a64e6f1f0d3998f870f5a2ad2d86f6ccc1 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sun, 14 Jun 2026 17:45:30 -0400 Subject: [PATCH] Records are not vectors: vector?/sequential? false for shape-recs (jolt-14k) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under direct-linking a record is a Janet tuple (its shape-rec), and core-vector? just delegated to jvec? which is true for any tuple — so (vector? a-record) and (sequential? a-record) returned true. That broke map-destructuring of a record: the destructure coerce treats a sequential source as & {:keys} kwargs and does (apply hash-map x), so destructuring a record fed its entries to make-phm as a flat kv-list and corrupted. Surfaced as reitit's router crashing on a wildcard route ('expected integer key for tuple in range [0,5), got 5') whenever the new direct-link default was on; minimal repro is (let [{:keys [a]} (->R ...)] ...). Fix: core-vector? excludes shape-recs, matching Clojure (a record is not a vector or sequential). jvec? is unchanged for internal representation dispatch. Regression cases added to record-declared-shape-test. --- src/jolt/core.janet | 7 ++++++- test/integration/record-declared-shape-test.janet | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 71c3b26..fa78e65 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -204,7 +204,12 @@ (defn core-fn? [x] (or (function? x) (cfunction? x))) (defn core-keyword? [x] (keyword? x)) (defn core-symbol? [x] (and (struct? x) (= :symbol (x :jolt/type)))) -(defn core-vector? [x] (jvec? x)) +# A record shape-rec is a Janet tuple (jvec? true), but a record is NOT a vector +# in Clojure — `(vector? record)` is false, and so is `(sequential? record)`. +# Excluding it here keeps map-destructuring of a record off the `& {:keys}` kwargs +# coerce path (which does `(apply hash-map x)` for a sequential x). jvec? itself +# stays as-is for internal representation dispatch. +(defn core-vector? [x] (and (jvec? x) (not (shape-rec? x)))) # map? is STRICT: a plain struct map literal, a phm, a sorted map, or a record. # Tagged structs (symbols/chars/uuids — anything with :jolt/type) are VALUES, # not maps. (sorted-map? is defined later, so the table check is inlined.) diff --git a/test/integration/record-declared-shape-test.janet b/test/integration/record-declared-shape-test.janet index 5171f47..7d0cf30 100644 --- a/test/integration/record-declared-shape-test.janet +++ b/test/integration/record-declared-shape-test.janet @@ -29,7 +29,13 @@ ["record? true" "(do (defrecord Rd [x y]) (record? (->Rd 1 2)))"] ["record vs map not=" "(do (defrecord Re [x y]) (not (= (->Re 1 2) {:x 1 :y 2})))"] ["assoc keeps type" "(do (defrecord Rf [x y]) (record? (assoc (->Rf 1 2) :x 9)))"] - ["pr declared order" "(do (defrecord Rg [b a c]) (= \"#user.Rg{:b 10, :a 20, :c 30}\" (pr-str (->Rg 10 20 30))))"]]) + ["pr declared order" "(do (defrecord Rg [b a c]) (= \"#user.Rg{:b 10, :a 20, :c 30}\" (pr-str (->Rg 10 20 30))))"] + # a record shape-rec is a Janet tuple, but a record is NOT a vector/sequential + # in Clojure — else map-destructuring it takes the kwargs coerce path and + # corrupts (reitit router crash, jolt-14k). + ["vector? record false" "(do (defrecord Rh [x y]) (not (vector? (->Rh 1 2))))"] + ["sequential? record false" "(do (defrecord Ri [x y]) (not (sequential? (->Ri 1 2))))"] + ["destructure record :or" "(do (defrecord Rj [a b c d e]) (let [{:keys [a e] :or {a 0}} (->Rj 1 2 3 4 5)] (= 6 (+ a e))))"]]) (each [label prog] cases (check label (eval-string dl prog) true))