Follow-ups identified while reviewing #346 (ad-hoc hierarchies + hierarchy dispatch). None of these block that PR — it is correct against clojure/core.clj and the vendored clojure.core-test.derive / .underive namespaces pass in both the interpreted (cljrs test --src-path) and AOT (cljrs compile --test) paths. These are the gaps left open.
1. No method cache — the miss path is the only path for hierarchy multimethods
hierarchy_method_key (crates/cljrs-runtime/src/interp/apply.rs) re-scans every entry in dispatch_vals on each call, running isa_in_global_hierarchy against each one, then applies an O(m²) domination filter over the matches — all while holding the mf.methods mutex.
Clojure caches the resolved method per dispatch value (method-cache) and invalidates the cache when the hierarchy changes. Here there is no cache, and a multimethod that dispatches through a hierarchy never hits the exact-key fast path, so this scan is the steady-state cost of every call, not a corner case.
Additional per-call overhead on that path:
isa_in_global_hierarchy does a fresh env.globals.lookup_var("clojure.core", "global-hierarchy") and allocates Keyword::simple("ancestors") on every invocation.
- For a vector dispatch value it repeats both per element, recursively.
Suggested shape: a HashMap<String, String> cache on MultiFn from dispatch key to resolved method key, plus a generation counter on the global hierarchy (bumped by derive / underive / prefer-method / remove-method / defmethod) to invalidate it.
2. Ambiguity error message is nondeterministic
The matches vector in hierarchy_method_key is built by iterating a HashMap, so the "Multiple methods ... match dispatch value" error lists the conflicting methods in arbitrary order. The same program prints
Multiple methods in multimethod 'g' match dispatch value :user/d: :user/c and :user/b, and neither is preferred
on one run and the two names swapped on the next. Sorting the keys before formatting would make the message stable, diffable, and assertable in tests.
Related nit in the same message: it says "and neither is preferred" but the match set can contain more than two methods.
3. isa? is implemented twice with nothing pinning the two together
There are now two independent implementations:
- the
clojure.core/isa? defn in bootstrap.cljrs
isa_in_global_hierarchy in crates/cljrs-runtime/src/interp/apply.rs, used by dispatch
The native copy is a reasonable call — calling back into the interpreter from apply_value while holding mf.methods would be a re-entrancy hazard — and the crate README does document the seam. But the two can silently drift the moment either side gains a feature (class-like tags, per-multimethod :hierarchy, etc.).
They also read the var differently today: the Rust side uses lookup_var(...) + root deref, while the Clojure side resolves global-hierarchy through the normal path (which consults the dynamic binding stack). Harmless right now since global-hierarchy is not ^:dynamic, but it is a second way for them to diverge.
A shared test that exercises both against the same hierarchy table would be cheap insurance.
4. methods returns string keys, not dispatch values
builtin_methods in crates/cljrs-runtime/src/builtins/builtins.rs keys its result map by the pr_str rendering of the dispatch value:
(derive ::a ::b)
(defmulti f identity)
(defmethod f ::b [_] :p)
(keys (methods f))
;; => (":user/b") ; a string; Clojure returns (:user/b)
#346 adds the dispatch_vals map, which holds exactly the original Values needed to fix this. Same applies to the keys of prefers.
5. Minor
derive docstring. It says "Both must be namespaced keywords or symbols", but that is only the 2-arity rule — the 3-arity accepts unqualified tags, and clojure.core-test.derive depends on that ((derive (make-hierarchy) :rect :shape)).
prefer-method does not reject conflicting preferences. Clojure throws when (prefers y x) already holds; here the second prefer-method is accepted and the resulting match set fails the domination check at dispatch time with the generic ambiguity error instead.
- Six new names are visible in
clojure.core. named?, hierarchy?, check-hierarchy!, check-tag!, extend-relation are declared defn-, but defn- privacy is not enforced, so all of them (plus global-hierarchy) are auto-referred into every namespace — (resolve 'named?) from a fresh user ns returns #'clojure.core/named?. Shadowing works (covered by core_shadows.rs), so this is cosmetic, but named? is a plausible user-defined name. Either enforce defn- or move the helpers somewhere less exposed.
Follow-ups identified while reviewing #346 (ad-hoc hierarchies + hierarchy dispatch). None of these block that PR — it is correct against
clojure/core.cljand the vendoredclojure.core-test.derive/.underivenamespaces pass in both the interpreted (cljrs test --src-path) and AOT (cljrs compile --test) paths. These are the gaps left open.1. No method cache — the miss path is the only path for hierarchy multimethods
hierarchy_method_key(crates/cljrs-runtime/src/interp/apply.rs) re-scans every entry indispatch_valson each call, runningisa_in_global_hierarchyagainst each one, then applies an O(m²) domination filter over the matches — all while holding themf.methodsmutex.Clojure caches the resolved method per dispatch value (
method-cache) and invalidates the cache when the hierarchy changes. Here there is no cache, and a multimethod that dispatches through a hierarchy never hits the exact-key fast path, so this scan is the steady-state cost of every call, not a corner case.Additional per-call overhead on that path:
isa_in_global_hierarchydoes a freshenv.globals.lookup_var("clojure.core", "global-hierarchy")and allocatesKeyword::simple("ancestors")on every invocation.Suggested shape: a
HashMap<String, String>cache onMultiFnfrom dispatch key to resolved method key, plus a generation counter on the global hierarchy (bumped byderive/underive/prefer-method/remove-method/defmethod) to invalidate it.2. Ambiguity error message is nondeterministic
The
matchesvector inhierarchy_method_keyis built by iterating aHashMap, so the "Multiple methods ... match dispatch value" error lists the conflicting methods in arbitrary order. The same program printson one run and the two names swapped on the next. Sorting the keys before formatting would make the message stable, diffable, and assertable in tests.
Related nit in the same message: it says "and neither is preferred" but the match set can contain more than two methods.
3.
isa?is implemented twice with nothing pinning the two togetherThere are now two independent implementations:
clojure.core/isa?defninbootstrap.cljrsisa_in_global_hierarchyincrates/cljrs-runtime/src/interp/apply.rs, used by dispatchThe native copy is a reasonable call — calling back into the interpreter from
apply_valuewhile holdingmf.methodswould be a re-entrancy hazard — and the crate README does document the seam. But the two can silently drift the moment either side gains a feature (class-like tags, per-multimethod:hierarchy, etc.).They also read the var differently today: the Rust side uses
lookup_var(...)+ root deref, while the Clojure side resolvesglobal-hierarchythrough the normal path (which consults the dynamic binding stack). Harmless right now sinceglobal-hierarchyis not^:dynamic, but it is a second way for them to diverge.A shared test that exercises both against the same hierarchy table would be cheap insurance.
4.
methodsreturns string keys, not dispatch valuesbuiltin_methodsincrates/cljrs-runtime/src/builtins/builtins.rskeys its result map by thepr_strrendering of the dispatch value:#346 adds the
dispatch_valsmap, which holds exactly the originalValues needed to fix this. Same applies to the keys ofprefers.5. Minor
derivedocstring. It says "Both must be namespaced keywords or symbols", but that is only the 2-arity rule — the 3-arity accepts unqualified tags, andclojure.core-test.derivedepends on that ((derive (make-hierarchy) :rect :shape)).prefer-methoddoes not reject conflicting preferences. Clojure throws when(prefers y x)already holds; here the secondprefer-methodis accepted and the resulting match set fails the domination check at dispatch time with the generic ambiguity error instead.clojure.core.named?,hierarchy?,check-hierarchy!,check-tag!,extend-relationare declareddefn-, butdefn-privacy is not enforced, so all of them (plusglobal-hierarchy) are auto-referred into every namespace —(resolve 'named?)from a freshuserns returns#'clojure.core/named?. Shadowing works (covered bycore_shadows.rs), so this is cosmetic, butnamed?is a plausible user-defined name. Either enforcedefn-or move the helpers somewhere less exposed.