Skip to content

Claim slice 6b: language-neutral qualified-call resolver, Elixir and Clojure #390

Open

Description

@tag-und-nacht

I'd like to claim slice 6b: the language-neutral qualified-call resolver from Adding a language: common stays common, following the maintainer's invitation on #349.

This is a separate follow-up PR after #349 lands.

Problem and intended behavior

#349 records the written qualifier of a Clojure call but does not resolve namespace aliases or imports. Given:

(ns my.app.core
  (:require [my.app.util :as u]))

(defn run [x] (u/f x))

and an indexed f definition in my.app.util, the call should resolve to that definition rather than to unrelated bare-name candidates.

Scope

Generalise src/elixir_resolve.h's module table into one shared resolver driven by per-language alias/import/require facts. Preserve Elixir as an existing user and add Clojure as the first new user. Avoid a parallel Clojure-only module resolver or language-specific fallback guard.

Pin the supported Clojure namespace/require forms with fixtures before widening them; only indexed, statically established targets can supply resolved edges. Ambiguous, external, or unsupported forms must retain honest unresolved/declined behavior and disclosures.

The maintainers' slice 6a stops the wrong edge for an unplaced qualifier; 6b adds the right edge for a proven module target. Coordinate landing order with 6a and flip #349's retained str/join known-gap arm when the shared refusal lands. Implementing 6a is not claimed here.

Acceptance

  • Red-first alias/require and qualified-call fixtures, including same-name decoys, missing modules, and ambiguous targets.
  • The example above reaches my.app.util's f through the shared resolver.
  • Elixir's existing output stays byte-identical on the regression fixtures and before/after corpus comparisons.
  • No unrelated language's resolution changes; cold/warm cache parity, deterministic XML, and correct extraction-version / mirror / hash updates.
  • Document the supported namespace forms and remaining resolution limits.

Related: #349, the slice 5a/8a continuation #389, and the CONTRIBUTING feature-family map. Macro expansion, dynamic invocation, and protocol dispatch are outside this slice.

Activity

  1. joyful-ii-V-I commented on Oct 14, 2026

    @joyful-ii-V-I
    Collaborator

    @tag-und-nacht thank you, and confirmed: slice 6b is yours. #349 laid the groundwork: Clojure writes a generic Reference::qualifier from the parser's namespace field, and the str/join gap is pinned as an arm, not guarded locally. Slice 6a (the refusal) is ours and not yet scheduled. 6b does not have to wait for it. Leave the str/join arm as it is until 6a flips it.

    Where to start. ElixirResolver (src/elixir_resolve.h) has a Module::name table, Elixir-only fact readers (ElixirCallable/ElixirDefault/ElixirImport bindings, importsInForce), and an implicit-module fallback (Kernel). It has four consumers, and all of them must keep their output: buildGraph (graph.h), editcheck.h (the foldArity keying), mcpverbs.h and verbs_navigate.h. One possible seam:

    struct QualifiedCallLang {                 // per-language DATA, one constexpr row per Lang
        Lang lang; bool foldsArity; std::string_view implicitModule;   // "Kernel", "clojure.core"
    };
    struct QualifiedCallTable {                // shared; ElixirResolver becomes its first user
        QualifiedCallTable( const IngestResult&, std::span<const QualifiedCallLang> );
        void resolve( const Reference&, std::vector<NodeId>& out ) const;   // empty = refuse, counted
    };

    Elixir expands aliases at ingest (aliasInForce, ingest_elixir.h), so the qualifier already holds a module by the time the resolver sees it. Doing the same for Clojure keeps the shared table free of Lang:: branches.

    Pitfalls

    • A definition's namespace comes from the file's parsed (ns …) form, never from its path. Check whether Clojure symbols carry it today. A per-file module fact may be the first thing you need.
    • Aliases and requires: :as, :refer [..], :refer :all, :use, and require/alias outside ns. Pin each one you support. Anything else stays unresolved, and is counted, not laddered.
    • Labels come from parser nodes only. Namespaced keywords (::u/k, :a.b/k) are not calls. Interop like Math/abs names a class, so it resolves to nothing.
    • The implicit clojure.core row overlaps a shared builtin-name guard we plan separately: an unqualified (get m :a) binds an in-repo get, and Dart, Lua, Ruby and GDScript do the same. Please don't add a Clojure-only builtin list.

    Evidence we'll ask for. Red-first arms in clojurecheck, plus elixirnamearitycheck, elixircheck, resolverhonestycheck, editcheckcheck, rustqualcheck and stdqualcheck green. Provide byte identity as in "Definition of done" item 2: the Elixir fixtures plus one real Elixir repository, covering --callers/--callees/--impact/--edit-check, plus one untouched-language repository, with the argv list and an empty diff. Update the family-6 map row and the "No language-neutral qualified-call resolver exists yet" sentence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions