From 7900173d45093106ec84820b11fe137fe706084b Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 13 Jun 2026 19:44:17 -0400 Subject: [PATCH] type-aware record equality + assoc extension + record-shape test (jolt-t34 R3) Record shape-recs now compare type-aware like the table form: eq-map-pairs returns nil for a record so equality falls to deep=, which is type-aware via the per-type interned descriptor. A record equals only a same-type record, never a plain map (cross-type and record-vs-map were wrongly equal under JOLT_SHAPE). assoc of a new (undeclared) key keeps the record type and grows a slot, matching Clojure's record-extension semantics. Adds test/integration/record-shape-test.janet locking in the runtime record mechanism (tag, field access, virtual :jolt/deftype, type-preserving assoc, dissoc demotion, type-aware equality, #ns.Type{...} printing). --- src/jolt/core.janet | 6 ++- test/integration/record-shape-test.janet | 64 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 test/integration/record-shape-test.janet diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 9143d7d..71c3b26 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -326,7 +326,11 @@ "Return [k v] pairs for a map-like value (phm/sorted-map/struct/table), else nil." [x] (cond - (shape-rec? x) (map (fn [k] @[k (shape-get x k nil)]) (shape-keys x)) + # a record shape-rec returns nil so equality falls to deep=, which is + # type-aware (the descriptor is interned per type): a record equals only a + # same-type record, never a plain map — mirroring the :jolt/deftype table + # form below. A plain-map shape-rec compares by pairs. + (shape-rec? x) (if (record-tag x) nil (map (fn [k] @[k (shape-get x k nil)]) (shape-keys x))) (phm? x) (phm-entries x) # sorted-map equals any map with the same pairs (representation-agnostic, as # in Clojure); sorted-set is handled by the set branch of jolt-equal? diff --git a/test/integration/record-shape-test.janet b/test/integration/record-shape-test.janet new file mode 100644 index 0000000..d929556 --- /dev/null +++ b/test/integration/record-shape-test.janet @@ -0,0 +1,64 @@ +# Records as shape-recs (jolt-t34 R3). A user record (defrecord/deftype) under +# JOLT_SHAPE is a shape-rec whose descriptor ALSO carries :type (the type tag), +# laid out in DECLARED field order. These build records directly via the runtime +# (make-record) and assert that every map/record operation treats them the way +# Clojure does — and crucially that type identity is preserved (a record is not +# a plain map, and two records are equal only when their types match). +(use ../../src/jolt/types) +(use ../../src/jolt/core) + +(var fails 0) +(defn check [label got want] + (if (deep= got want) (print " ok " label) + (do (++ fails) (printf " FAIL %s: got %j want %j" label got want)))) + +# (defrecord Point [x y]) instance (->Point 1 2) +(def P (make-record "my.Point" [:x :y] [1 2])) + +# --- it IS a shape-rec, and reports its type --------------------------------- +(check "shape-rec?" (shape-rec? P) true) +(check "record-tag" (record-tag P) "my.Point") +(check "map?" (core-map? P) true) + +# --- field access in declared order ------------------------------------------ +(check "get x" (core-get P :x nil) 1) +(check "get y" (core-get P :y nil) 2) +(check "get miss default" (core-get P :z :d) :d) +(check "count" (core-count P) 2) +(check "contains?" (core-contains? P :x) true) + +# --- the virtual :jolt/deftype key keeps every (get obj :jolt/deftype) site +# (record?/dispatch) working without special-casing each one ------------------ +(check "virtual deftype" (core-get P :jolt/deftype nil) "my.Point") + +# --- assoc preserves the type: in place for a declared field, and grows a +# slot Clojure-style for a new key (the result is still a record) ------------- +(def P2 (core-assoc P :x 9)) +(check "assoc field tag" (record-tag P2) "my.Point") +(check "assoc field val" (core-get P2 :x nil) 9) +(check "assoc keeps other" (core-get P2 :y nil) 2) +(def P3 (core-assoc P :z 3)) +(check "assoc new tag" (record-tag P3) "my.Point") +(check "assoc new val" (core-get P3 :z nil) 3) +(check "assoc new keeps" (core-get P3 :x nil) 1) + +# --- dissoc of a declared field demotes to a plain map (Clojure semantics) ---- +(def D (core-dissoc P :x)) +(check "dissoc demotes" (record-tag D) nil) +(check "dissoc gone" (core-get D :x :gone) :gone) +(check "dissoc keeps" (core-get D :y nil) 2) + +# --- equality is TYPE-AWARE: same type + same fields equal; a different type +# or a plain map with the same fields is NOT equal ---------------------------- +(check "= same type" (jolt-equal? P (make-record "my.Point" [:x :y] [1 2])) true) +(check "not= diff field" (jolt-equal? P (make-record "my.Point" [:x :y] [1 9])) false) +(check "not= diff type" (jolt-equal? P (make-record "my.Other" [:x :y] [1 2])) false) +(check "not= record vs map" (jolt-equal? P {:x 1 :y 2}) false) +(check "not= map vs record" (jolt-equal? {:x 1 :y 2} P) false) + +# --- printing: Clojure record syntax #ns.Type{:k v, ...}, fields in order ----- +(check "pr record" (core-pr-str1 P) "#my.Point{:x 1, :y 2}") + +(if (> fails 0) + (error (string "record-shape: " fails " failing check(s)")) + (print "\nRecord shape passed!"))