Class CallableIdentity
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 CoverageDisplayName.identityForSymbol, 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.
-
Method Summary
Modifier and TypeMethodDescriptionstatic StringdeclaringConstructOf(String callableIdentity) THE inverse offorSymbol(ISymbol): the declaring construct of a call-graph identity.static StringThe call-graph identity for a callable symbol.
-
Method Details
-
forSymbol
-
declaringConstructOf
THE inverse offorSymbol(ISymbol): the declaring construct of a call-graph identity.module::Aggregate.memberyieldsmodule::Aggregate; a module-level function or programmodule::nameyields itself. Use this to lift a callable-granular call-graph answer to construct granularity; do NOT hand-roll the split.It lives here, beside the code that mints the format, on purpose. Two byte-identical private copies had already appeared in
compiler-servicesextractors, and a parser sitting in a different module from the minter is precisely the drifting shortcut that breaks silently the day the format changes — nothing would fail to compile, the split would just quietly return the wrong construct.- Parameters:
callableIdentity- an identity as produced byforSymbol(ISymbol), or null- Returns:
- the declaring construct's fully-qualified name, or null when given null
-