Native record representation + inline nil?/some?

Records were a jrec holding an alist of (kw . val) conses: ~113B/node, built
fresh per construction, field reads a list scan. Replace that with a shared
per-type descriptor (tag + field keywords + an eq?-keyed keyword->index table)
plus a flat per-instance value vector and an extension map for any non-field
keys assoc'd on (jolt-nil when there are none). Construction now allocates one
vector instead of a cons chain and a field read is an index lookup. binary-trees
construction allocation drops 2.085GB -> 1.19GB.

That alone barely moved binary-trees wall-time: profiling showed the read loop,
not allocation, dominates, and the read loop's own allocation came from (nil? l)
lowering to (jolt-invoke (var-deref "clojure.core" "nil?") l), which conses its
args every call. Add nil?/some? to the backend native-op table so they inline to
jolt-nil?/jolt-some? (and drop the truthy wrapper, like the other predicates).
check-tree's read loop goes from 1.476GB allocated to zero; binary-trees 18.9x
-> 9.7x vs JVM. The remaining gap is the field-read dispatch chain (jolt-c3mw).

Two JVM divergences fixed along the way, both certified:
- dissoc of a declared field downgrades a record to a plain map (was kept as a
  record); an extension key still drops cleanly.
- map->R keeps extension keys (was dropping anything outside the declared basis).

16 new corpus rows pin assoc/dissoc/count/keys/seq/=/hash/extension-field
behavior against JVM Clojure.
This commit is contained in:
Yogthos 2026-06-26 01:38:59 -04:00
parent eacfa04e5b
commit 6d100a66b8
8 changed files with 1049 additions and 934 deletions

View file

@ -33,7 +33,7 @@
((pvec? x) (make-pvec (pvec-v x) (pvec-ent x)))
((pmap? x) (make-pmap (pmap-root x) (pmap-cnt x)))
((pset? x) (make-pset (pset-m x)))
((jrec? x) (make-jrec (jrec-tag x) (jrec-pairs x)))
((jrec? x) (make-jrec (jrec-desc x) (jrec-vec-copy (jrec-vals x)) (jrec-ext x)))
;; a reify shares its (read-only) method table + protos but gets a fresh
;; identity, so attaching meta leaves the original's meta untouched. Every
;; Clojure reify implements IObj.

View file

@ -1,9 +1,13 @@
;; records + protocols — the deftype/defrecord + defprotocol/extend-type
;; subsystem.
;;
;; A record is a `jrec`: a type tag ("ns.Name") + an alist of (kw . val) in
;; declared field order. It is map?/coll?, equal to another jrec of the same tag
;; with equal fields (never equal to a plain map), and prints as #ns.Name{...}.
;; A record is a `jrec`: a shared per-type descriptor + a flat vector of field
;; values in declared order, plus an extension map for any non-field keys assoc'd
;; on (jolt-nil when there are none — the common case). This lays fields out like a
;; native struct: construction allocates one vector, not a chain of cons cells, and
;; a field read is an index lookup, not a list scan. It is map?/coll?, equal to
;; another jrec of the same type with equal fields (never equal to a plain map),
;; and prints as #ns.Name{...}.
;; The collection dispatchers (jolt-get/count/keys/vals/seq/assoc/contains?/=/
;; hash/conj + the printers) are set!-extended with a jrec arm that delegates to
;; the original — the transients.ss pattern — so all record logic lives here and
@ -13,69 +17,116 @@
;; Loaded after collections/seq/values/converters/printing/transients/multimethods
;; (the dispatchers it wraps + chez-current-ns).
(define-record-type jrec (fields tag pairs) (nongenerative chez-jrec-v1))
;; The per-type descriptor: built once at deftype/defrecord definition and shared
;; by every instance. Holds the tag, the field keywords in declared order, and an
;; eq?-keyed keyword->index table (field keys are interned, so identity lookup).
(define-record-type (jrdesc make-jrdesc-rec jrdesc?)
(fields tag fkeys index) (nongenerative chez-jrdesc-v1))
(define (make-jrdesc tag fkey-list)
(let ((index (make-eq-hashtable)))
(let loop ((ks fkey-list) (i 0))
(unless (null? ks) (hashtable-set! index (car ks) i) (loop (cdr ks) (+ i 1))))
(make-jrdesc-rec tag (list->vector fkey-list) index)))
;; An instance: the shared descriptor, the field-value vector, and an extension
;; map (jolt-nil unless non-field keys have been assoc'd on).
(define-record-type (jrec make-jrec jrec?) (fields desc vals ext) (nongenerative chez-jrec-v2))
(define (jrec-tag r) (jrdesc-tag (jrec-desc r)))
(define jolt-deftype-kw (keyword "jolt" "deftype"))
;; unique present-vs-absent sentinel for extension-map lookups (so a present nil
;; in the extension map is distinguished from a genuine miss).
(define jrec-absent (list 'jrec-absent))
;; Field keys are interned keywords, so a keyword query compares by eq? (the hot
;; (:field rec) / (get rec :field) case); any other key type falls back to jolt=2.
(define (jrec-key=? field-key k)
(if (keyword? k) (eq? field-key k) (jolt=2 field-key k)))
;; index of a declared field key, or #f (only an interned keyword can be one).
(define (jrec-field-index r k) (hashtable-ref (jrdesc-index (jrec-desc r)) k #f))
;; a vector-copy that doesn't depend on the optional rnrs vector-copy being present.
(define (jrec-vec-copy v)
(let* ((n (vector-length v)) (out (make-vector n)))
(let loop ((i 0)) (when (< i n) (vector-set! out i (vector-ref v i)) (loop (+ i 1))))
out))
;; extension-map entries as an (k . v) alist in iteration order.
(define (jrec-ext-pairs ext)
(let loop ((s (jolt-seq ext)) (acc '()))
(if (jolt-nil? s) (reverse acc)
(let ((e (seq-first s)))
(loop (jolt-seq (seq-more s)) (cons (cons (jolt-nth e 0) (jolt-nth e 1)) acc))))))
;; lookup with default d: a declared field reads index+vector-ref (a present nil
;; returns nil), then the extension map, then d.
(define (jrec-lookup r k d)
(if (eq? k jolt-deftype-kw)
(jrec-tag r)
(let loop ((ps (jrec-pairs r)))
(cond ((null? ps) d)
((jrec-key=? (caar ps) k) (cdar ps))
(else (loop (cdr ps)))))))
(let ((i (jrec-field-index r k)))
(if i (vector-ref (jrec-vals r) i)
(let ((ext (jrec-ext r)))
(if (jolt-nil? ext) d
(let ((v (jolt-get ext k jrec-absent)))
(if (eq? v jrec-absent) d v))))))))
(define (jrec-has? r k)
(let loop ((ps (jrec-pairs r)))
(cond ((null? ps) #f) ((jrec-key=? (caar ps) k) #t) (else (loop (cdr ps))))))
;; The get path: one scan (vs jrec-has? + jrec-lookup = two); a deftype's ILookup
;; valAt runs only when the field is genuinely missing.
(and (not (eq? k jolt-deftype-kw))
(or (and (jrec-field-index r k) #t)
(let ((ext (jrec-ext r)))
(and (not (jolt-nil? ext))
(not (eq? jrec-absent (jolt-get ext k jrec-absent))))))))
;; The get path: like jrec-lookup, but a deftype's ILookup valAt runs when a key
;; is genuinely missing from both the fields and the extension map.
(define (jrec-ref coll k d)
(if (eq? k jolt-deftype-kw)
(jrec-tag coll)
(let loop ((ps (jrec-pairs coll)))
(cond ((null? ps)
(cond ((find-method-any-protocol (jrec-tag coll) "valAt")
=> (lambda (m) (jolt-invoke m coll k d)))
(else d)))
((jrec-key=? (caar ps) k) (cdar ps))
(else (loop (cdr ps)))))))
;; mutate a deftype's mutable field in place: the pairs are runtime cons cells,
;; so set-cdr! updates the field. (set! field v) inside a method
;; lowers to this; returns v, as set! does.
(let ((i (jrec-field-index coll k)))
(if i (vector-ref (jrec-vals coll) i)
(let* ((ext (jrec-ext coll))
(v (if (jolt-nil? ext) jrec-absent (jolt-get ext k jrec-absent))))
(if (eq? v jrec-absent)
(cond ((find-method-any-protocol (jrec-tag coll) "valAt")
=> (lambda (m) (jolt-invoke m coll k d)))
(else d))
v))))))
;; mutate a deftype's mutable field in place: the value vector is mutable, so
;; vector-set! updates the field. (set! field v) inside a method lowers to this;
;; returns v, as set! does.
(define (jolt-set-field! inst k v)
(if (jrec? inst)
(let loop ((ps (jrec-pairs inst)))
(cond ((null? ps) (error #f "set! of an unknown field" k))
((jolt=2 (caar ps) k) (set-cdr! (car ps) v) v)
(else (loop (cdr ps)))))
(let ((i (jrec-field-index inst k)))
(if i (begin (vector-set! (jrec-vals inst) i v) v)
(error #f "set! of an unknown field" k)))
(error #f "set! of a field on a non-record" inst)))
(define (jrec-replace pairs k v) ; replace existing field (keep order) or append
(let loop ((ps pairs) (acc '()) (hit #f))
(cond ((null? ps) (reverse (if hit acc (cons (cons k v) acc))))
((jolt=2 (caar ps) k) (loop (cdr ps) (cons (cons k v) acc) #t))
(else (loop (cdr ps) (cons (car ps) acc) hit)))))
(define (jrec-ext=? ea eb)
(cond ((and (jolt-nil? ea) (jolt-nil? eb)) #t)
((or (jolt-nil? ea) (jolt-nil? eb)) #f)
(else (jolt=2 ea eb))))
(define (jrec=? a b)
(and (string=? (jrec-tag a) (jrec-tag b))
(= (length (jrec-pairs a)) (length (jrec-pairs b)))
(let loop ((ps (jrec-pairs a)))
(or (null? ps)
(and (jrec-has? b (caar ps))
(jolt=2 (cdar ps) (jrec-lookup b (caar ps) jolt-nil))
(loop (cdr ps)))))))
(= (vector-length (jrec-vals a)) (vector-length (jrec-vals b)))
(let ((va (jrec-vals a)) (vb (jrec-vals b)) (n (vector-length (jrec-vals a))))
(let loop ((i 0))
(or (= i n)
(and (jolt=2 (vector-ref va i) (vector-ref vb i)) (loop (+ i 1))))))
(jrec-ext=? (jrec-ext a) (jrec-ext b))))
(define (jrec-hash r)
(fold-left (lambda (acc p) (+ acc (jolt-hash (car p)) (jolt-hash (cdr p))))
(string-hash (jrec-tag r)) (jrec-pairs r)))
(let* ((fkeys (jrdesc-fkeys (jrec-desc r))) (vals (jrec-vals r)) (n (vector-length vals))
(base (let loop ((i 0) (acc (string-hash (jrec-tag r))))
(if (= i n) acc
(loop (+ i 1) (+ acc (jolt-hash (vector-ref fkeys i))
(jolt-hash (vector-ref vals i))))))))
(let ((ext (jrec-ext r)))
(if (jolt-nil? ext) base (+ base (jolt-hash ext))))))
(define (jrec-pr r) ; #ns.Name{:k v, :k v}
(string-append "#" (jrec-tag r) "{"
(let loop ((ps (jrec-pairs r)) (first #t) (acc ""))
(if (null? ps) acc
(loop (cdr ps) #f
(string-append acc (if first "" ", ")
(jolt-pr-readable (caar ps)) " " (jolt-pr-readable (cdar ps))))))
"}"))
(let ((fkeys (jrdesc-fkeys (jrec-desc r))) (vals (jrec-vals r)))
(string-append "#" (jrec-tag r) "{"
(let ((n (vector-length vals)))
(let loop ((i 0) (first #t) (acc ""))
(if (= i n)
(let ((ext (jrec-ext r)))
(if (jolt-nil? ext) acc
(let eloop ((es (jrec-ext-pairs ext)) (first first) (acc acc))
(if (null? es) acc
(eloop (cdr es) #f
(string-append acc (if first "" ", ")
(jolt-pr-readable (caar es)) " " (jolt-pr-readable (cdar es))))))))
(loop (+ i 1) #f
(string-append acc (if first "" ", ")
(jolt-pr-readable (vector-ref fkeys i)) " " (jolt-pr-readable (vector-ref vals i)))))))
"}")))
;; ---- extend the collection dispatchers with a jrec arm ----------------------
;; equality for a jrec: a deftype implementing IPersistentCollection/equiv (e.g.
@ -105,7 +156,8 @@
(define %r-jolt-count jolt-count)
(set! jolt-count (lambda (coll)
(cond ((jrec-cl coll "count") => (lambda (m) (jolt-invoke m coll)))
((jrec? coll) (length (jrec-pairs coll)))
((jrec? coll) (+ (vector-length (jrec-vals coll))
(let ((ext (jrec-ext coll))) (if (jolt-nil? ext) 0 (%r-jolt-count ext)))))
(else (%r-jolt-count coll)))))
;; contains?: a deftype implementing Associative/containsKey (e.g. core.cache's
;; caches) answers through that; a plain defrecord checks its fields.
@ -114,21 +166,48 @@
(cond ((jrec-cl coll "containsKey") => (lambda (m) (if (jolt-truthy? (jolt-invoke m coll k)) #t #f)))
((jrec? coll) (jrec-has? coll k))
(else (%r-jolt-contains? coll k)))))
;; assoc: replacing a declared field copies the value vector; any other key grows
;; the extension map (the value vector is shared — fields are immutable).
(define %r-jolt-assoc1 jolt-assoc1)
(set! jolt-assoc1 (lambda (coll k v)
(cond ((jrec-cl coll "assoc") => (lambda (m) (jolt-invoke m coll k v)))
((jrec? coll) (make-jrec (jrec-tag coll) (jrec-replace (jrec-pairs coll) k v)))
((jrec? coll)
(let ((i (and (keyword? k) (jrec-field-index coll k))))
(if i
(let ((nv (jrec-vec-copy (jrec-vals coll))))
(vector-set! nv i v)
(make-jrec (jrec-desc coll) nv (jrec-ext coll)))
(let ((ext (jrec-ext coll)))
(make-jrec (jrec-desc coll) (jrec-vals coll)
(%r-jolt-assoc1 (if (jolt-nil? ext) empty-pmap ext) k v))))))
(else (%r-jolt-assoc1 coll k v)))))
;; dissoc: a deftype implementing IPersistentMap/without answers through it; a
;; plain defrecord drops the field pair.
;; dissoc: a deftype implementing IPersistentMap/without answers through it.
;; Removing a declared field downgrades a plain record to a map (JVM parity); an
;; extension key drops from the ext map (normalized back to jolt-nil when empty).
(define (jrec->map-without r drop-k)
(let* ((fkeys (jrdesc-fkeys (jrec-desc r))) (vals (jrec-vals r)) (n (vector-length vals)))
(let loop ((i 0) (m empty-pmap))
(if (= i n)
(let ((ext (jrec-ext r)))
(if (jolt-nil? ext) m
(fold-left (lambda (mm p) (%r-jolt-assoc1 mm (car p) (cdr p))) m (jrec-ext-pairs ext))))
(let ((fk (vector-ref fkeys i)))
(loop (+ i 1) (if (eq? fk drop-k) m (%r-jolt-assoc1 m fk (vector-ref vals i)))))))))
(define %r-jolt-dissoc jolt-dissoc)
(define (jrec-dissoc1 coll k)
(if (not (jrec? coll))
(%r-jolt-dissoc coll k) ; an earlier declared-field dissoc downgraded it
(let ((i (and (keyword? k) (jrec-field-index coll k))))
(if i (jrec->map-without coll k)
(let ((ext (jrec-ext coll)))
(if (jolt-nil? ext) coll
(let ((ne (%r-jolt-dissoc ext k)))
(make-jrec (jrec-desc coll) (jrec-vals coll)
(if (= 0 (%r-jolt-count ne)) jolt-nil ne)))))))))
(set! jolt-dissoc (lambda (coll . ks)
(cond ((jrec-cl coll "without")
=> (lambda (m) (fold-left (lambda (c k) (jolt-invoke m c k)) coll ks)))
((jrec? coll)
(fold-left (lambda (c k) (make-jrec (jrec-tag c)
(filter (lambda (p) (not (jolt=2 (car p) k))) (jrec-pairs c))))
coll ks))
((jrec? coll) (fold-left jrec-dissoc1 coll ks))
(else (apply %r-jolt-dissoc coll ks)))))
;; keys/vals over a jrec read its entry seq (jolt-seq is method-first, so a
;; map-like deftype delegates to its Seqable; a defrecord's seq is its fields, so
@ -141,10 +220,20 @@
(set! jolt-keys (lambda (m) (if (jrec? m) (jrec-seq-col m 0) (%r-jolt-keys m))))
(define %r-jolt-vals jolt-vals)
(set! jolt-vals (lambda (m) (if (jrec? m) (jrec-seq-col m 1) (%r-jolt-vals m))))
;; a record's seq is its field map-entries in declared order, then any extensions.
(define (jrec-entry-list r)
(let* ((fkeys (jrdesc-fkeys (jrec-desc r))) (vals (jrec-vals r)) (n (vector-length vals)))
(let loop ((i 0) (acc '()))
(if (= i n)
(let ((ext (jrec-ext r)))
(append (reverse acc)
(if (jolt-nil? ext) '()
(map (lambda (p) (make-map-entry (car p) (cdr p))) (jrec-ext-pairs ext)))))
(loop (+ i 1) (cons (make-map-entry (vector-ref fkeys i) (vector-ref vals i)) acc))))))
(define %r-jolt-seq jolt-seq)
(set! jolt-seq (lambda (x)
(cond ((jrec-cl x "seq") => (lambda (m) (jolt-seq (jolt-invoke m x))))
((jrec? x) (list->cseq (map (lambda (p) (make-map-entry (car p) (cdr p))) (jrec-pairs x))))
((jrec? x) (list->cseq (jrec-entry-list x)))
(else (%r-jolt-seq x)))))
(define %r-jolt-conj1 jolt-conj1)
(set! jolt-conj1 (lambda (coll x)
@ -265,11 +354,15 @@
(define (make-deftype-ctor name-sym field-kws . _ignored)
(let* ((tag (string-append (chez-current-ns) "." (symbol-t-name name-sym)))
(kws (seq->list field-kws))
(desc (make-jrdesc tag kws))
(nf (length kws))
(ctor (lambda args
(make-jrec tag (let loop ((ks kws) (as args) (acc '()))
(if (null? ks) (reverse acc)
(loop (cdr ks) (if (null? as) '() (cdr as))
(cons (cons (car ks) (if (null? as) jolt-nil (car as))) acc))))))))
;; fill the value vector from the positional args, padding missing
;; trailing fields with nil and ignoring any extras.
(let ((v (make-vector nf jolt-nil)))
(let loop ((as args) (i 0))
(if (or (null? as) (= i nf)) (make-jrec desc v jolt-nil)
(begin (vector-set! v i (car as)) (loop (cdr as) (+ i 1)))))))))
;; Register the ctor globally by simple class name (like StringBuilder) so
;; (Name. …) interop resolves ns-agnostically: a deftype used across files works
;; even when the runtime current ns is the caller's, not the defining ns

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -16,6 +16,7 @@
(define-record-type jolt-nil-t (fields) (nongenerative jolt-nil-v1))
(define jolt-nil (make-jolt-nil-t))
(define (jolt-nil? x) (jolt-nil-t? x))
(define (jolt-some? x) (not (jolt-nil-t? x)))
;; --- truthiness: only nil and false are falsey -------------------------------
(define (jolt-truthy? x) (not (or (jolt-nil? x) (eq? x #f))))

View file

@ -598,7 +598,12 @@
;; deftype already defines ->name (= the ctor); no (name. …) interop needed,
;; so defrecord compiles too. map->name builds via that ctor.
(deftype ~name-sym ~fields)
(def ~mapf (fn* [~m] (~arrow ~@(map (fn [f] `(get ~m ~(keyword (name f)))) fields))))
;; build via the positional ctor for declared fields, then carry any
;; remaining keys as extension fields (JVM keeps them on the record).
(def ~mapf (fn* [~m]
(reduce-kv assoc
(~arrow ~@(map (fn [f] `(get ~m ~(keyword (name f)))) fields))
(dissoc ~m ~@(map (fn [f] (keyword (name f))) fields)))))
~@(mapcat (fn [g]
(let [proto (first g)
names (distinct (map (fn [spec] (name (first spec))) (rest g)))]

View file

@ -34,7 +34,7 @@
"range" "jolt-range" "take" "jolt-take" "drop" "jolt-drop"
"keys" "jolt-keys" "vals" "jolt-vals"
"even?" "jolt-even?" "odd?" "jolt-odd?" "pos?" "jolt-pos?" "neg?" "jolt-neg?"
"zero?" "jolt-zero?" "identity" "jolt-identity"
"zero?" "jolt-zero?" "identity" "jolt-identity" "nil?" "jolt-nil?" "some?" "jolt-some?"
"ex-info" "jolt-ex-info"})
;; Value-position resolution for a clojure.core ref passed AS A VALUE (to map /
@ -56,7 +56,7 @@
"first" #(= % 1) "rest" #(= % 1) "next" #(= % 1) "seq" #(= % 1)
"reverse" #(= % 1) "last" #(= % 1) "keys" #(= % 1) "vals" #(= % 1)
"even?" #(= % 1) "odd?" #(= % 1) "pos?" #(= % 1) "neg?" #(= % 1)
"zero?" #(= % 1) "identity" #(= % 1)
"zero?" #(= % 1) "identity" #(= % 1) "nil?" #(= % 1) "some?" #(= % 1)
"cons" #(= % 2) "filter" #(= % 2) "remove" #(= % 2) "into" #(= % 2)
"take" #(= % 2) "drop" #(= % 2) "map" #(>= % 2) "apply" #(>= % 2)
"reduce" #(or (= % 2) (= % 3)) "range" #(and (>= % 0) (<= % 3))
@ -76,7 +76,7 @@
(def ^:private bool-returning-ops
#{"<" "<=" ">" ">=" "jolt=" "jolt-not"
"jolt-even?" "jolt-odd?" "jolt-pos?" "jolt-neg?"
"jolt-zero?" "jolt-empty?" "jolt-contains?"})
"jolt-zero?" "jolt-empty?" "jolt-contains?" "jolt-nil?" "jolt-some?"})
;; Numeric-specialized op strings. jolt.passes.numeric tags an arithmetic invoke
;; :num-kind :double|:long when every operand is that kind; these are the Chez

View file

@ -3190,4 +3190,20 @@
{:suite "regex / literal value" :label "re-matches on a literal" :expected "\"aaa\"" :actual "(re-matches #\"a+\" \"aaa\")"}
{:suite "regex / literal value" :label "quoted regex round-trips" :expected "\"#\\\"a.c\\\"\"" :actual "(pr-str (quote #\"a.c\"))"}
{:suite "regex / literal value" :label "extend-protocol to Pattern dispatches" :expected "[:pattern :other]" :actual "(do (defprotocol Tg (tg [_])) (extend-protocol Tg java.util.regex.Pattern (tg [_] :pattern) Object (tg [_] :other)) [(tg #\"x\") (tg 1)])"}
{:suite "records / representation" :label "assoc a declared field keeps the record" :expected "[true 9 2]" :actual "(do (defrecord R [a b]) (let [x (assoc (->R 1 2) :a 9)] [(record? x) (:a x) (:b x)]))"}
{:suite "records / representation" :label "assoc an extension field keeps the record" :expected "[true 3 1]" :actual "(do (defrecord R [a b]) (let [x (assoc (->R 1 2) :c 3)] [(record? x) (:c x) (:a x)]))"}
{:suite "records / representation" :label "dissoc an extension field keeps the record" :expected "[true nil 1]" :actual "(do (defrecord R [a b]) (let [x (-> (->R 1 2) (assoc :c 3) (dissoc :c))] [(record? x) (:c x) (:a x)]))"}
{:suite "records / representation" :label "dissoc a declared field downgrades to a map" :expected "[false true 2 false]" :actual "(do (defrecord R [a b]) (let [m (dissoc (->R 1 2) :a)] [(record? m) (map? m) (:b m) (contains? m :a)]))"}
{:suite "records / representation" :label "count includes extension fields" :expected "[2 3]" :actual "(do (defrecord R [a b]) [(count (->R 1 2)) (count (assoc (->R 1 2) :c 3))])"}
{:suite "records / representation" :label "keys are declared then extension order" :expected "[[:a :b] [:a :b :c]]" :actual "(do (defrecord R [a b]) [(vec (keys (->R 1 2))) (vec (keys (assoc (->R 1 2) :c 3)))])"}
{:suite "records / representation" :label "a nil field value is present, not absent" :expected "[nil true :d]" :actual "(do (defrecord R [a b]) (let [n (->R nil 2)] [(:a n) (contains? n :a) (get (->R 1 2) :zzz :d)]))"}
{:suite "records / representation" :label "seq yields field map-entries" :expected "[[:a 1] [:b 2]]" :actual "(do (defrecord R [a b]) (vec (map (fn [e] [(key e) (val e)]) (->R 1 2))))"}
{:suite "records / representation" :label "conj a map-entry vector assocs it" :expected "3" :actual "(do (defrecord R [a b]) (:c (conj (->R 1 2) [:c 3])))"}
{:suite "records / representation" :label "= after assoc of an unchanged field" :expected "true" :actual "(do (defrecord R [a b]) (= (assoc (->R 1 2) :a 1) (->R 1 2)))"}
{:suite "records / representation" :label "= compares extension fields" :expected "[true false]" :actual "(do (defrecord R [a b]) [(= (assoc (->R 1 2) :c 9) (assoc (->R 1 2) :c 9)) (= (assoc (->R 1 2) :c 9) (->R 1 2))])"}
{:suite "records / representation" :label "assoc then dissoc an extension field = original" :expected "true" :actual "(do (defrecord R [a b]) (= (-> (->R 1 2) (assoc :c 3) (dissoc :c)) (->R 1 2)))"}
{:suite "records / representation" :label "map-> keeps extension keys" :expected "[true 3 [:a :b :c]]" :actual "(do (defrecord R [a b]) (let [m (map->R {:a 1 :b 2 :c 3})] [(record? m) (:c m) (vec (keys m))]))"}
{:suite "records / representation" :label "record as a map key" :expected ":v" :actual "(do (defrecord R [a]) (get {(->R 1) :v} (->R 1)))"}
{:suite "records / representation" :label "records dedup in a set by value" :expected "2" :actual "(do (defrecord R [a]) (count (into #{} [(->R 1) (->R 1) (->R 2)])))"}
{:suite "records / representation" :label "nested record field reads" :expected "[2 1]" :actual "(do (defrecord N [l r v]) (let [t (->N (->N nil nil 1) nil 2)] [(:v t) (:v (:l t))]))"}
]