Derive class identity from one hierarchy graph

instance?, extend-protocol dispatch, isa?/supers/ancestors, and the exception
hierarchy each read their own hand-kept table, and those tables had drifted:
(instance? clojure.lang.Associative [1 2]) was true but a protocol extended to
Associative wouldn't dispatch to a vector; keyword/IFn and seq/Seqable had the
same split; (isa? ExceptionInfo RuntimeException) was false and
(supers NumberFormatException) was empty.

Add one FQN -> direct-supers graph (class-hierarchy.ss) and derive the views
from it. value-host-tags builds on the graph closure so a vector reports
Associative/Indexed/ILookup/Counted/Seqable, a keyword reports IFn, a seq
reports Seqable/List/Counted, etc. instance? now tests membership in that same
list, so it can't disagree with dispatch. canonical-host-tag recognizes any
modeled class (was a separate literal set missing Seqable/ILookup/...).
class-direct-supers unions the graph edges and class-supers returns the
transitive closure, so the exception hierarchy answers isa?/supers/ancestors.

The graph is open: jolt.host/register-class-supers! lets a library graft its
own classes on and get every view for free.

Runtime only, no re-mint. make test green (0 new/stale divergences), +3
JVM-certified corpus rows.
This commit is contained in:
Yogthos 2026-07-01 15:38:04 -04:00
parent d7dad2b450
commit d4acd69a73
5 changed files with 251 additions and 18 deletions

View file

@ -767,6 +767,12 @@
(register-instance-check-arm!
(lambda (type-sym val)
(let ((iface (hsc-last-segment (symbol-t-name type-sym))))
;; the value's own class-graph tags (value-host-tags) are authoritative — the
;; SAME source protocol dispatch reads, so instance? and extend-protocol can't
;; disagree about the interfaces a builtin implements.
(if (let ((tags (value-host-tags val)))
(or (member (symbol-t-name type-sym) tags) (member iface tags)))
#t
(let ((hit (cond
((or (string=? iface "IObj") (string=? iface "IMeta")) (hsc-imeta? val))
((or (string=? iface "IMapEntry") (string=? iface "MapEntry")) (jolt-map-entry? val))
@ -827,7 +833,7 @@
((or (string=? iface "Reader") (string=? iface "BufferedReader"))
(reader-jhost? val))
(else 'none))))
(if (eq? hit 'none) 'pass (if hit #t #f))))))
(if (eq? hit 'none) 'pass (if hit #t #f)))))))
;; java.lang.Class value: (class x) / (.getClass x) return one. It renders like
;; the JVM — str/.toString -> "class <name>", pr -> "<name>", .getName -> "<name>"
@ -985,9 +991,20 @@
(define (str-has-dollar? s)
(let loop ((i 0)) (and (< i (string-length s)) (or (char=? (string-ref s i) #\$) (loop (+ i 1))))))
(define (class-direct-supers name)
(or (hashtable-ref class-supers-tbl name #f)
(and (str-has-dollar? name) '("clojure.lang.AFunction"))
'()))
;; union the modeled class graph (jch, direct edges) with any legacy table entry,
;; so isa?/supers/ancestors see the single hierarchy source plus anything not yet
;; migrated. The closure below traverses these to the full transitive set.
(let ((jch (jch-direct-supers name))
(old (hashtable-ref class-supers-tbl name #f)))
(cond ((and (pair? jch) old)
(let merge ((ss old) (acc jch))
(cond ((null? ss) acc)
((member (car ss) acc) (merge (cdr ss) acc))
(else (merge (cdr ss) (append acc (list (car ss))))))))
((pair? jch) jch)
(old old)
((str-has-dollar? name) '("clojure.lang.AFunction"))
(else '()))))
;; transitive closure of direct supers (set semantics via an accumulator list)
(define (class-ancestors-list name)
(let loop ((pending (class-direct-supers name)) (seen '()))
@ -1037,8 +1054,9 @@
(def-var! "jolt.host" "class-supers"
(lambda (x)
(let ((name (class-key x)))
(if (and name (hashtable-contains? class-supers-tbl name))
(list->cseq (hashtable-ref class-supers-tbl name '()))
(if name
(let ((as (class-ancestors-list name))) ; transitive, like the JVM
(if (null? as) jolt-nil (list->cseq as)))
jolt-nil))))
(def-var! "jolt.host" "class-ancestors"
(lambda (x)