Skip to content

Commit 83c33e1

Browse files
author
Yogthos
committed
Protocol dispatch through a deftype/reify's declared interfaces
value-host-tags reported only ("Object") for a reify and (tag "Object") for a bare deftype, so an extend-protocol filed under an interface name could never reach one — clojure.lang.IReduceInit, java.lang.Iterable, or an ancestor like Associative for a type declaring IPersistentVector. Both now report their declared interfaces plus the modeled ancestry the class graph already holds (register-inline-protocol! files each declared interface as a super of the type's tag), which is what instanceof answers on the JVM. A defrecord's extra declared interfaces join its automatic map set the same way. satisfies? agreed with the old dispatch, not with the JVM: it checked only the type's own registry and answered false where an interface extension applied. It now falls through to the same interface walk. The type's own tag is still tried first, so an extend-type on the type wins, and Object extensions still catch types that declare nothing.
1 parent a9416e4 commit 83c33e1

5 files changed

Lines changed: 144 additions & 40 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
(`kvreduce`, lowercase) before throwing. `sequence` still requires a seqable
3535
source, as on the JVM.
3636

37+
- **Protocol dispatch reaches a deftype or reify through its declared
38+
interfaces.** `value-host-tags` now reports a reify's declared interfaces
39+
(with their modeled ancestry) and a bare deftype's declared interfaces from
40+
the class graph, so an `extend-protocol` filed under an interface name —
41+
`clojure.lang.IReduceInit`, `java.lang.Iterable`, an ancestor like
42+
`Associative` for a type declaring `IPersistentVector` — dispatches on such
43+
a value the way `instanceof` answers on the JVM. A defrecord's EXTRA declared
44+
interfaces join its automatic map set the same way. `satisfies?` agrees: it
45+
falls through to the same interface walk when the type's own registry has no
46+
entry (it used to answer false while dispatch succeeded). An extension on
47+
the type's own tag still wins, and `Object` extensions still catch types
48+
declaring nothing.
49+
3750
- **A deftype/reify can name a protocol by its dotted class spelling.**
3851
`(deftype T [] my.ns.PThing (m [_] …))` resolves `my.ns.PThing` to the
3952
protocol like the JVM does; the methods used to file as interface methods

‎host/chez/protocols.ss‎

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,36 @@
263263
(find-method-any-protocol (jrec-tag v) "toString")
264264
(record-method-dispatch v "toString" jolt-nil))))
265265

266+
;; the interfaces every defrecord carries, spelled both ways (the record arm of
267+
;; value-host-tags below).
268+
(define jrec-record-iface-tags
269+
'("IRecord" "clojure.lang.IRecord" "IPersistentMap" "clojure.lang.IPersistentMap"
270+
"APersistentMap" "Associative" "ILookup" "Seqable" "Counted"
271+
"IPersistentCollection" "IObj" "IMeta" "Map" "java.util.Map"
272+
"Iterable" "java.lang.Iterable" "Object"))
273+
274+
;; a jch-tags list always ends with "Object"; the tail without it, for splicing
275+
;; ahead of another tag list.
276+
(define (jch-tags-sans-object ts)
277+
(if (null? (cdr ts)) '() (cons (car ts) (jch-tags-sans-object (cdr ts)))))
278+
279+
;; A reify's dispatch tags: each declared protocol/interface as its JVM interface
280+
;; name plus that interface's modeled ancestry, deduped, Object last — so an
281+
;; extend-protocol filed under an interface name (clojure.lang.IReduceInit,
282+
;; java.lang.Iterable, …) reaches a reify declaring it, exactly as instanceof
283+
;; answers on the JVM. The reify's own method table is consulted before these
284+
;; (protocol-resolve), so an inline impl still wins.
285+
(define (jreify-host-tags obj)
286+
(let loop ((ps (jreify-protos obj)) (acc '()))
287+
(if (null? ps)
288+
(reverse (cons "Object" acc))
289+
(let inner ((ts (jch-tags (proto-iface-name (car ps)))) (acc acc))
290+
(if (null? ts)
291+
(loop (cdr ps) acc)
292+
(inner (cdr ts)
293+
(let ((t (car ts)))
294+
(if (or (string=? t "Object") (member t acc)) acc (cons t acc)))))))))
295+
266296
;; host type-tag candidates for a non-record value (extend-protocol on builtins).
267297
(define (value-host-tags obj)
268298
;; numbers dispatch by actual type (a Double is NOT a Long): flonum -> Double,
@@ -364,15 +394,29 @@
364394
;; IWalkTerm to clojure.lang.IRecord, and walking a record value must hit
365395
;; that, not the Object default (which would recur forever). The record's
366396
;; own type is tried first (dispatch checks jrec-tag before these tags).
397+
;; a reify reports its declared interfaces and their ancestry — see
398+
;; jreify-host-tags above.
399+
((jreify? obj) (jreify-host-tags obj))
400+
;; A record that declares FURTHER interfaces beyond the automatic map set
401+
;; reports those too (register-inline-protocol! files them as the tag's
402+
;; supers in the class graph, so cddr of the cached jch-tags list — own
403+
;; tag and simple segment dropped — is exactly the declared ancestry).
404+
;; The common record declares nothing extra and keeps the shared list,
405+
;; costing this hot fallback path one cons, as before.
367406
((jrec-record? obj)
368-
(cons (jrec-tag obj)
369-
'("IRecord" "clojure.lang.IRecord" "IPersistentMap" "clojure.lang.IPersistentMap"
370-
"APersistentMap" "Associative" "ILookup" "Seqable" "Counted"
371-
"IPersistentCollection" "IObj" "IMeta" "Map" "java.util.Map"
372-
"Iterable" "java.lang.Iterable" "Object")))
373-
;; a bare deftype is opaque — its declared interfaces dispatch via the
374-
;; inline methods registered under its own tag (tried before these tags).
375-
((jrec? obj) (list (jrec-tag obj) "Object"))
407+
(let* ((tag (jrec-tag obj))
408+
(extra (cddr (jch-tags tag))))
409+
(cons tag (if (null? (cdr extra))
410+
jrec-record-iface-tags
411+
(append (jch-tags-sans-object extra) jrec-record-iface-tags)))))
412+
;; a bare deftype dispatches through its own tag first, then its declared
413+
;; interfaces and their modeled ancestry — an extend-protocol filed under
414+
;; clojure.lang.IReduceInit reaches a deftype declaring it, as instanceof
415+
;; does. The tag's own SIMPLE segment is dropped (cddr): extensions on a
416+
;; deftype name file under its ns-qualified tag, and a bare segment could
417+
;; collide with a host class of the same name. A type declaring no
418+
;; interfaces keeps (tag "Object").
419+
((jrec? obj) (let ((tag (jrec-tag obj))) (cons tag (cddr (jch-tags tag)))))
376420
;; a throwable reports its OWN class and that class's ancestry, so
377421
;; (extend-protocol P Throwable …) reaches an ex-info or a host-constructed
378422
;; RuntimeException. clojure.datafy extends Datafiable to Throwable exactly

‎host/chez/records-dispatch.ss‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -409,16 +409,23 @@
409409
(cond ((jclass? proto) (jclass-name proto))
410410
((jolt-nil? proto) "nil")
411411
(else (jolt-final-str proto))))))
412-
(cond
413-
((jrec? obj) (type-satisfies? (jrec-tag obj) pn-str))
414-
((jreify? obj)
415-
(and (memp (lambda (p) (or (string=? p pn-str) (proto-class-match? p pn-str)))
416-
(jreify-protos obj))
417-
#t))
418-
(else (let loop ((tags (value-host-tags obj)))
419-
(cond ((null? tags) #f)
420-
((type-satisfies? (car tags) pn-str) #t)
421-
(else (loop (cdr tags)))))))))
412+
(or
413+
;; direct: a record type's own registry, a reify's declared list.
414+
(cond
415+
((jrec? obj) (and (type-satisfies? (jrec-tag obj) pn-str) #t))
416+
((jreify? obj)
417+
(and (memp (lambda (p) (or (string=? p pn-str) (proto-class-match? p pn-str)))
418+
(jreify-protos obj))
419+
#t))
420+
(else #f))
421+
;; extended: the protocol may be extended to an interface or class the
422+
;; value reports — value-host-tags includes a deftype/reify's declared
423+
;; interfaces — the same walk dispatch takes. On the JVM one instanceof
424+
;; answers both the direct and the extended case.
425+
(let loop ((tags (value-host-tags obj)))
426+
(cond ((null? tags) #f)
427+
((type-satisfies? (car tags) pn-str) #t)
428+
(else (loop (cdr tags))))))))
422429
(define (last-dot s)
423430
(let loop ((i (- (string-length s) 1)))
424431
(cond ((< i 0) s) ((char=? (string-ref s i) #\.) (substring s (+ i 1) (string-length s))) (else (loop (- i 1))))))

‎host/gambit/records-gambit.ss‎

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,33 @@
14311431
(find-method-any-protocol (jrec-tag v) "toString")
14321432
(record-method-dispatch v "toString" jolt-nil))))
14331433

1434+
(define jrec-record-iface-tags
1435+
'("IRecord" "clojure.lang.IRecord" "IPersistentMap"
1436+
"clojure.lang.IPersistentMap" "APersistentMap" "Associative"
1437+
"ILookup" "Seqable" "Counted" "IPersistentCollection" "IObj"
1438+
"IMeta" "Map" "java.util.Map" "Iterable"
1439+
"java.lang.Iterable" "Object"))
1440+
1441+
(define (jch-tags-sans-object ts)
1442+
(if (null? (cdr ts))
1443+
'()
1444+
(cons (car ts) (jch-tags-sans-object (cdr ts)))))
1445+
1446+
(define (jreify-host-tags obj)
1447+
(let loop ((ps (jreify-protos obj)) (acc '()))
1448+
(if (null? ps)
1449+
(reverse (cons "Object" acc))
1450+
(let inner ((ts (jch-tags (proto-iface-name (car ps))))
1451+
(acc acc))
1452+
(if (null? ts)
1453+
(loop (cdr ps) acc)
1454+
(inner
1455+
(cdr ts)
1456+
(let ((t (car ts)))
1457+
(if (or (string=? t "Object") (member t acc))
1458+
acc
1459+
(cons t acc)))))))))
1460+
14341461
(define (value-host-tags obj)
14351462
(cond
14361463
((flonum? obj) '("Double" "Float" "Number" "Object"))
@@ -1492,15 +1519,19 @@
14921519
((jbigdec? obj) (jch-tags "java.math.BigDecimal"))
14931520
((procedure? obj) (jch-tags "clojure.lang.AFunction"))
14941521
((jolt-nil? obj) '("nil"))
1522+
((jreify? obj) (jreify-host-tags obj))
14951523
((jrec-record? obj)
1496-
(cons
1497-
(jrec-tag obj)
1498-
'("IRecord" "clojure.lang.IRecord" "IPersistentMap"
1499-
"clojure.lang.IPersistentMap" "APersistentMap" "Associative"
1500-
"ILookup" "Seqable" "Counted" "IPersistentCollection" "IObj"
1501-
"IMeta" "Map" "java.util.Map" "Iterable"
1502-
"java.lang.Iterable" "Object")))
1503-
((jrec? obj) (list (jrec-tag obj) "Object"))
1524+
(let* ((tag (jrec-tag obj)) (extra (cddr (jch-tags tag))))
1525+
(cons
1526+
tag
1527+
(if (null? (cdr extra))
1528+
jrec-record-iface-tags
1529+
(append
1530+
(jch-tags-sans-object extra)
1531+
jrec-record-iface-tags)))))
1532+
((jrec? obj)
1533+
(let ((tag (jrec-tag obj)))
1534+
(cons tag (cddr (jch-tags tag)))))
15041535
((jolt-ex-info-record? obj)
15051536
(jch-tags (jolt-ex-info-record-class-name obj)))
15061537
((jolt-atom? obj) (jch-tags "clojure.lang.Atom"))
@@ -2332,20 +2363,21 @@
23322363
((jclass? proto) (jclass-name proto))
23332364
((jolt-nil? proto) "nil")
23342365
(else (jolt-final-str proto))))))
2335-
(cond
2336-
((jrec? obj) (type-satisfies? (jrec-tag obj) pn-str))
2337-
((jreify? obj)
2338-
(and (memp
2339-
(lambda (p)
2340-
(or (string=? p pn-str) (proto-class-match? p pn-str)))
2341-
(jreify-protos obj))
2342-
#t))
2343-
(else
2344-
(let loop ((tags (value-host-tags obj)))
2345-
(cond
2346-
((null? tags) #f)
2347-
((type-satisfies? (car tags) pn-str) #t)
2348-
(else (loop (cdr tags)))))))))
2366+
(or (cond
2367+
((jrec? obj)
2368+
(and (type-satisfies? (jrec-tag obj) pn-str) #t))
2369+
((jreify? obj)
2370+
(and (memp
2371+
(lambda (p)
2372+
(or (string=? p pn-str) (proto-class-match? p pn-str)))
2373+
(jreify-protos obj))
2374+
#t))
2375+
(else #f))
2376+
(let loop ((tags (value-host-tags obj)))
2377+
(cond
2378+
((null? tags) #f)
2379+
((type-satisfies? (car tags) pn-str) #t)
2380+
(else (loop (cdr tags))))))))
23492381

23502382
(define (last-dot s)
23512383
(let loop ((i (- (string-length s) 1)))

‎test/chez/corpus.edn‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5239,4 +5239,12 @@
52395239
{:suite "maps / array-map 1.13" :label "into {} of 9 keyword pairs is a PersistentHashMap" :expected "true" :actual "(instance? clojure.lang.PersistentHashMap (into {} (map (fn [i] [(keyword (str \"k\" i)) i]) (range 9))))" :portability :common}
52405240
{:suite "maps / array-map 1.13" :label "transient assoc! promotes at capacity regardless of key type" :expected "[:k8 :k11 :k5 :k7 :k9 :k6 :k0 :k10 :k3 :k4 :k1 :k2]" :actual "(vec (keys (persistent! (reduce (fn [t i] (assoc! t (keyword (str \"k\" i)) i)) (transient {}) (range 12)))))" :portability :common}
52415241
{:suite "maps / array-map 1.13" :label "transient dissoc! moves the last entry into the removed slot" :expected "[:a :d :c]" :actual "(vec (keys (persistent! (dissoc! (transient (array-map :a 1 :b 2 :c 3 :d 4)) :b))))" :portability :common}
5242+
{:suite "protocols / interface tags" :label "extend-protocol under an interface reaches a reify declaring it" :expected ":ireduce" :actual "(do (defprotocol IftP (iftp [x])) (extend-protocol IftP clojure.lang.IReduceInit (iftp [x] :ireduce)) (iftp (reify clojure.lang.IReduceInit (reduce [_ f init] init))))" :portability :common}
5243+
{:suite "protocols / interface tags" :label "extend-protocol under an interface reaches a deftype declaring it" :expected ":ireduce" :actual "(do (defprotocol IftQ (iftq [x])) (extend-protocol IftQ clojure.lang.IReduceInit (iftq [x] :ireduce)) (deftype IftT1 [] clojure.lang.IReduceInit (reduce [_ f init] init)) (iftq (->IftT1)))" :portability :common}
5244+
{:suite "protocols / interface tags" :label "a declared interface's ANCESTORS dispatch too (IPersistentVector is an Associative)" :expected ":assoc" :actual "(do (defprotocol IftR (iftr [x])) (extend-protocol IftR clojure.lang.Associative (iftr [x] :assoc)) (deftype IftT2 [] clojure.lang.IPersistentVector) (iftr (->IftT2)))" :portability :common}
5245+
{:suite "protocols / interface tags" :label "an extension on the type's own tag beats an interface extension" :expected ":own" :actual "(do (defprotocol IftS (ifts [x])) (extend-protocol IftS clojure.lang.IReduceInit (ifts [x] :iface)) (deftype IftT3 [] clojure.lang.IReduceInit (reduce [_ f init] init)) (extend-type IftT3 IftS (ifts [x] :own)) (ifts (->IftT3)))" :portability :common}
5246+
{:suite "protocols / interface tags" :label "satisfies? sees an interface extension for reify, deftype, and not others" :expected "[true true false]" :actual "(do (defprotocol IftU (iftu [x])) (extend-protocol IftU clojure.lang.IReduceInit (iftu [x] :i)) (deftype IftT4 [] clojure.lang.IReduceInit (reduce [_ f init] init)) [(satisfies? IftU (reify clojure.lang.IReduceInit (reduce [_ f init] init))) (satisfies? IftU (->IftT4)) (satisfies? IftU 42)])" :portability :common}
5247+
{:suite "protocols / interface tags" :label "an Object extension still catches a reify declaring interfaces" :expected ":obj" :actual "(do (defprotocol IftV (iftv [x])) (extend-protocol IftV Object (iftv [x] :obj)) (iftv (reify clojure.lang.IReduceInit (reduce [_ f init] init))))" :portability :common}
5248+
{:suite "protocols / interface tags" :label "a defrecord's EXTRA declared interface dispatches alongside the map set" :expected ":rec" :actual "(do (defprotocol IftW (iftw [x])) (extend-protocol IftW clojure.lang.IReduceInit (iftw [x] :rec)) (defrecord IftR1 [] clojure.lang.IReduceInit (reduce [_ f init] init)) (iftw (->IftR1)))" :portability :common}
5249+
{:suite "protocols / interface tags" :label "IRecord extension: dispatch and satisfies? agree on a record" :expected "[:irec true]" :actual "(do (defprotocol IftX (iftx [x])) (extend-protocol IftX clojure.lang.IRecord (iftx [x] :irec)) (defrecord IftR2 [a]) [(iftx (->IftR2 1)) (satisfies? IftX (->IftR2 1))])" :portability :common}
52425250
]

0 commit comments

Comments
 (0)