Class CallableIdentity

java.lang.Object
org.ek9lang.compiler.support.CallableIdentity
All Implemented Interfaces:
Function<ISymbol,String>

public final class CallableIdentity extends Object implements Function<ISymbol,String>
THE identity of a callable in the phase-5 call graph — module::Aggregate.member(T1,T2) for a method, module::Aggregate.member for any other aggregate member, and the plain fully-qualified name for everything else. Use this for any call-graph node key or call-edge target; do NOT key on ISymbol.getFullyQualifiedName().

Why this exists. getFullyQualifiedName() composes the module scope with the symbol's own name only (Symbol.getFullyQualifiedName), so for a method it yields module::check — dropping the enclosing type. That is not a display wart: it made the node key non-unique, so two same-named members on UNRELATED types collapsed into a single graph node. The surviving node took one type's symbol and the other type's callers, and everything downstream inherited it — dead-code detection silently missed the second member (it looked reachable because its namesake was), and reachability, DI ordering and interprocedural lock analysis all read the merged node. A module with five ? operators had one node for all five.

Deliberately NOT CoverageIdentity, which is the join key for coverage/variety/HEX reports and looks almost identical. It additionally maps any name beginning _ (plus _i_init/_c_init) to its enclosing scope name, which is right for the synthetic _call/_main it was written for but wrong here: a dynamic function is named _Function_<hash>, and rewriting it to its enclosing scope's name would collide it with the real callable declared there — trading one merge bug for another. Dynamic functions are already unique by hash, so they pass through untouched.

The two schemes still AGREE on the shape that matters for joining reports to the graph — an aggregate member is Aggregate.member in both — so a Ca row and a HEX row can finally be matched on identity rather than on a name produced for humans.

🔑 A parameterised type's members are attributed to the GENERIC (added 2026-09-13, D15). EK9 monomorphises, so Box of String and Box of Integer hold substituted COPIES of one source. The graph used to hold BOTH identities and join neither: members were ENUMERATED under the template (Box.label(Boolean)) while edges TARGETED the monomorphisation (_Box_33B3….label(Boolean)), which was never registered at all. So a call into a generic reached a node that did not exist, the template's node was never reached, and find_unused_code reported a USED generic's methods as dead — recommending you delete live code.

Normalising here rather than at the call sites is what makes it correct: enumeration and edges both come through this one method, so they cannot disagree. Note the signature must come from the TEMPLATE too — normalising only the owner would yield Box.Box(org.ek9.lang::String) against a template registered as Box.Box(Box::T), which is two keys again. The correspondence is GenericTemplateMember's, by declaration position.

What this deliberately merges. Every parameterisation of one source member becomes ONE node, so reachability, dead-code, lock and DI analyses now reason about the source a developer wrote rather than about N copies of it. That is the intent: the bodies are identical, so a property proved of one holds of all. Per-parameterisation questions are the monomorphisation layer's, not the call graph's.

🔑 The PARAMETER SIGNATURE was the unfinished half of that fix (added 2026-09-05). Adding the aggregate separated same-named members on unrelated types, but left every OVERLOAD of one name on ONE type sharing a node — and operators are the worst case, since _add(Integer) and _add(String) on a single type were indistinguishable. The consequences are the same ones the aggregate fix removed, one level down: dead-code scanning misses an unused overload because its namesake is reachable, and purity impact attributes one overload's callers to another.

⚠️ Parameter types are FULLY QUALIFIED, deliberately. Simple names would read better and would reintroduce exactly the bug being fixed the moment two same-named types from different modules appear as the arguments of two overloads. This is an IDENTITY, not a label — anything rendering it for a human should use MethodSymbol.getFriendlyName() instead.

⚠️ The RETURN type is deliberately absent: EK9 does not admit overloads differing only by return type, so it cannot discriminate, and including it would churn the key whenever a return is widened.

🔑 A program's synthesised _main IS the program (added 2026-09-14). Phase 1 (DefinitionListener.processProgramDeclaration) records the PROGRAM AGGREGATE against the declaration and _main against its operationDetails; the collector, the data-touch assessor and the lock listener all enter through the declaration, so the graph holds module::Main and never module::Main._main(). Anything arriving from INSIDE the body — a cursor's nearest declaration, a MutexKey body's enclosing function — holds _main, and keyed to a node that does not exist: every program-body cursor read as a call-graph leaf (call_graph_shape showed stdout.println with no outgoing edge). Normalised here, for the same reason as the generic case above: the two routes to one callable must mint one key.

The identity of an aggregate's property initialisers is InstanceInitIdentity; the inverse, a callable identity's declaring construct, is DeclaringConstructOf (no-statics lane S3, 2026-10-29: was forSymbol, forInstanceInit and declaringConstructOf).

  • Constructor Details

    • CallableIdentity

      public CallableIdentity()
  • Method Details

    • apply

      public String apply(ISymbol symbol)
      The call-graph identity for a callable symbol.
      Specified by:
      apply in interface Function<ISymbol,String>
      Parameters:
      symbol - the callable's resolved symbol; must not be null
      Returns:
      module::Aggregate.member for an aggregate member, else the symbol's own FQN