Skip to content

Hierarchy dispatch follow-ups: method caching, stable ambiguity message, isa? duplication, methods key fidelity #360

Description

@csm

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions