SystemNavigation
SystemNavigation — Class-registry navigation queries ("who implements X", "where is X used").
Home for System Browser-style navigation queries that previously lived
on Beamtalk (BT-2189 / BT-2190). Matches the Pharo/Squeak/Cuis
convention: callers reach an instance via a constructor and dispatch
queries against it.
SystemNavigation default returns a navigation rooted at the full
loaded class registry. Future constructors (BT-2201) — e.g.
SystemNavigation over: aPackage, SystemNavigation forClasses: aList
— will hand back instances scoped to a narrower set, reusing the same
query protocol.
All query logic lives on instance methods. The class side is just a
constructor surface. This is deliberate: routing the iteration through
a class gen_server would self-deadlock when the walk reaches
SystemNavigation itself. Instance methods on a sealed Object value
type run as plain Erlang in the calling eval worker.
Examples
SystemNavigation default implementorsOf: #asString
SystemNavigation default sendersOf: #+
Methods
- class » default
- allClasses
- actorClasses
- dnuHandlers
- extendersOf: aClass
- extensionsBy: aPackage
- requireClassName: aClass selector: aSelector
- classesInPackage: aPackageName
- subclassesOf: aClass in: aPackageName
- requirePackage: aPackageName
- implementorsOf: aSelector
- messagesSentBy: aMethod
- sendersOf: aSelector
- senderClassFor: ownerTag classSide: isClassSide
- indexedSiteRecordsFrom: indexedRows into: result
- indexedSenderRecordsFrom: indexedRows into: result
- indexedSelectorsFrom: indexedRows into: result
- collectSendersFor: aSelector in: aScope into: result
- appendSendersIn: methodSource selector: aSelector class: aClass methodSelector: methodSel into: result
- announcementsSentBy: aClass
- announcementSitesSentBy: aClass
- collectAnnounceSitesIn: aScope into: result
- appendAnnounceSitesIn: methodSource class: aClass methodSelector: methodSel into: result
- isAnnouncementClass: aClass
- collectExtensionAnnounceSitesFor: aClass into: result
- extensionTargets: extClass names: targetNames
- referencesTo: aClass
- collectReferencesFor: targetName in: aScope into: result
- appendReferencesIn: methodSource targetName: targetName owner: ownerClass methodSelector: methodSel into: result
- fieldReadersOf: aSym in: aClass
- fieldWritersOf: aSym in: aClass
- collectFieldAccessesOf: aSym in: aClass readers: wantReaders
- collectFieldAccessesFor: aSym in: aScope readers: wantReaders into: result
- appendFieldAccessesIn: methodSource field: aSym owner: ownerClass methodSelector: methodSel readers: wantReaders into: result
- ffiSitesFor: aSpec
- parseFfiSpec: aSpec
- raiseFfiSpecError: aSpec
- collectFfiSitesFor: module function: function arity: arity in: aScope into: result
- appendFfiSitesIn: methodSource module: module function: function arity: arity owner: ownerClass methodSelector: methodSel into: result
- collectExtensionFfiSitesFor: module function: function arity: arity into: result
- methodsMatching: aRegex
- collectMethodsMatching: aRegex in: aScope into: result
- selectorsMatching: aPattern
- selectorsForClass: aClass
- extensionSelectorsForBothSides: aClass
- extensionSelectorsFor: classTag
- collectExtensionSendersFor: aSelector into: result
- collectExtensionReferencesFor: targetName into: result
- collectExtensionMethodsMatching: aRegex into: result
- unimplementedSelectors
- unionFallbackClasses: a with: b
- definedSetFromIndex: definedRows into: result
- dnuHandlerScopesFrom: definedRows fallback: fallbackScope into: result
- dnuScopeKeyForOwner: ownerTag classSide: isClassSide
- dnuScopeKeyFor: aScope
- groupIndexedSends: sendRows excluding: excluded dnuScopes: dnuScopes into: result
- runtimeProvidedSelectors
- collectAllSendsIn: aScope excluding: excluded into: result
- appendAllSendsIn: methodSource owner: ownerClass methodSelector: methodSel handlesDnu: handlesDnu excluding: excluded into: result
- recordSend: sentSel line: line recv: recv owner: ownerClass methodSelector: methodSel handlesDnu: handlesDnu excluding: excluded into: result
- collectExtensionAllSendsExcluding: excluded into: result
- resolveExtensionClass: classTag
- unusedSelectors
- sentSetFromIndex: indexedSent into: result
- runtimeCalledSelectors
- collectSentSelectorsIn: aScope into: result
- addSentSelectorsIn: methodSource into: result
- collectExtensionSentSelectorsInto: result
- collectUnusedFor: aScope owner: owner sent: sent excluded: excluded into: result
- recordUnused: methodSel scope: aScope owner: owner isTestCase: isTestCase sent: sent excluded: excluded into: result
- isTestRunnerSelector: aSelector
- isPrimitiveMethod: methodSel in: aScope
- xrefImplementorsOf: aSelector
- xrefSendersOf: aSelector
- xrefReferencesTo: aSymbol
- xrefDefinedSelectors
- xrefAllSends
- xrefAllSentSelectors
- findClass: aName
- allSendsIn: aSource
- sendersIn: aSource selector: aSelector
- referencesIn: aSource class: aName
- announceSitesIn: aSource
- fieldReadersIn: aSource field: aSym
- fieldWritersIn: aSource field: aSym
- ffiSitesIn: aSource module: aModule function: aFunction arity: anArity
- extensionOwnersOf: aName
- extensionPairsBy: anOwner
- allExtensionsWithSource
- extensionEntriesFor: aTag
- classObjectTag: aName
Class Methods
Return a navigation query rooted at the default scope (all loaded classes in the workspace's class registry).
Examples
SystemNavigation default implementorsOf: #asString
Instance Methods
Snapshot of the class registry visible to this navigation query.
Currently always returns the full registry. Future scoped instances
(BT-2201) will override this to filter by package or explicit class
list. Wrapping the FFI in a typed instance method lets downstream
block parameters infer Class / Behaviour instead of Dynamic.
Return every class that participates in the actor model — i.e. each class
whose superclassChain includes Actor.
Includes Actor itself (the abstract root); callers wanting only concrete
actors can filter it out. Iterates allClasses, so stdlib and
user-defined actor classes are both in scope. Returns [] if Actor is
not in the loaded image (defensive).
Sorted alphabetically by class name for a stable, snapshot-friendly order.
name is read directly on the Behaviour-typed elements (it lives on
Behaviour per BT-2232), so no dnu suppression is needed.
Examples
SystemNavigation default actorClasses // => [Actor, ClassBuilder, ...]
Return every class that overrides the does-not-understand handler — i.e.
each class that locally defines doesNotUnderstand:args: (Beamtalk's DNU
override selector — note the :args:, NOT the Smalltalk-classic
doesNotUnderstand:).
These are the escape-valve classes — proxies, builders, DSL frontends —
whose instances can absorb arbitrary message sends. Useful for seeing
which classes might respond to a selector that has no static implementor,
and as an input to the unimplementedSelectors lint, which excludes sends
to DNU-routed receivers.
Includes ProtoObject, the root definer of the handler (matching the
methods includes: #doesNotUnderstand:args: convention used elsewhere in
this class); callers wanting only genuine overrides can filter it out.
Iterates allClasses, so stdlib and user-defined classes are both in
scope. includesSelector: checks the local method dictionary, so
inherited (un-overridden) handlers are not reported. Sorted alphabetically
by class name for a stable, snapshot-friendly order.
Examples
SystemNavigation default dnuHandlers // => [ErlangModule, ProtoObject, ...]
Return the packages (ADR 0070) that contribute extension methods (ADR 0066)
to aClass — answers "who extends class X?".
Extension owners are package/library names, not classes (the registry
records the owning package — see beamtalk_extensions:register/4), so each
owner is returned as a Package object. This mirrors how the other
navigation queries return rich objects (classes as Behaviour) rather than
bare name atoms. Backed by the reverse index
(beamtalk_extensions:extenders_of/1, BT-2202), which is deduplicated and
sorted: a package contributing ten extensions to aClass appears once.
Owners that are not currently-loaded packages are skipped (Package named:
would otherwise raise). Returns [] (not an error) when nothing extends
aClass; raises a typed error if aClass is a Symbol/String rather than a
class object.
Examples
SystemNavigation default extendersOf: String // => [(Package named: "my_lib"), ...]
SystemNavigation default extendersOf: NeverExtended // => []
Return the extension methods (ADR 0066) that the package aPackage
(ADR 0070) contributes — answers "what does library Y add, and to which
classes?". The inverse of extendersOf:.
Each result is a Dictionary #{#class => ExtendedClass, #selector => Selector} — one per extension method, shaped like the sendersOf: /
referencesTo: records. The #class value is the extended class object
(a real class, resolved via resolveExtensionClass:), not a name symbol,
so callers can navigate from it. The argument is a Package because
extension owners are packages, not classes. Backed by
beamtalk_extensions:extensions_by/1 (BT-2202). Returns [] (not an error)
when aPackage contributes no extensions; raises a typed error if
aPackage is not a Package object.
Examples
SystemNavigation default extensionsBy: (Package named: "my_lib")
// => [#{#class => String, #selector => #asJson}, ...]
Validate that aClass is a class/metaclass object and return its name
Symbol, raising a typed error otherwise. aSelector names the calling
query for the message. Mirrors the argument guard in referencesTo:;
respondsTo: #subclasses is the reliable "is a class" test (see that
method for why isKindOf: Behaviour is not).
Return the class objects belonging to package aPackageName (ADR 0070).
Delegates to Package classes: (which returns class-name Symbols) and
resolves each to a class object via findClass:. The package argument may
be a Symbol or String. Raises a typed error if the package is not loaded
(so a typo surfaces rather than silently returning []); returns [] if
the package exists but is empty.
Examples
SystemNavigation default classesInPackage: #stdlib // => [Actor, Array, ...]
Return the subclasses of aClass that live in package aPackageName
(ADR 0070) — allSubclasses filtered by package.
The package argument may be a Symbol or String. Raises a typed error if
the package is not loaded; returns [] if no subclasses live in that
package. A class's package is read via Package packageNameFor: (the same
authority behind metaclass packageName).
Examples
SystemNavigation default subclassesOf: Number in: #stdlib // => [Float, Integer]
Normalise aPackageName (Symbol or String) to a package-name String,
raising the dedicated #package_not_found error (via Package named:) if
it is not a currently-loaded package. Lets the package queries distinguish
"package doesn't exist" (a typo) from "package has no matching classes" (a
legitimately empty result), and reuses the same error type callers already
get from Package named:.
Return all classes that locally define the given selector.
Searches both instance-side and class-side definitions:
instance-side hits appear as class objects (e.g. Integer),
class-side hits appear as metaclass objects (e.g. Counter class).
A class that defines the selector on both sides appears twice — once
as the class and once as the metaclass.
Does NOT walk inherited methods — only classes with their own definition of the selector are returned.
Matches Pharo's SystemNavigation >> #allImplementorsOf:.
Maintained index (ADR 0087, BT-2302)
The implementors are read from the runtime-maintained xref methods table
(beamtalk_xref), populated at class-load time by codegen's baked
register_class/0 payload — one methods row per {class, side, selector} definition. This is a sub-millisecond ETS read in place of the
former includesSelector: probe over every class in the scope.
Miss policy. Mirrors sendersOf::
- Indexed implementor rows whose class is no longer loaded are dropped silently (stale).
- A class the registry reports as loaded but that is absent from the index
falls back to the legacy
includesSelector:/class includesSelector:probe for that class only, and the runtime logs onexref_misswarning. The partition is computed runtime-side bybeamtalk_xref:implementors_of_bt/1.
Raises an error if the argument is not a Symbol.
Examples
SystemNavigation default implementorsOf: #asString // => [Integer, Float, String, ...]
SystemNavigation default implementorsOf: #new // => [..., Counter class, ...]
SystemNavigation default implementorsOf: #nonExistent // => []
Return the message sends that occur in the body of aMethod — the
outgoing-call set of a single method, the dual of sendersOf:.
Each result is a Dictionary with two keys:
#selector— the selector being sent (a Symbol)#line— the 1-based line number withinaMethod sourcewhere the send appears
Order matches the AST walk: outer sends come before sends in their receiver/argument subtrees (a stable pre-order). A method that sends the same selector multiple times produces one entry per occurrence.
Backs the callHierarchy/outgoingCalls LSP query (BT-2243). The runtime
path used by Backend::delegate_nav_query reads the method source via
aMethod source and asks the compiler for every send — the same
per-method AST walk that powers unimplementedSelectors (BT-2206),
inverted to one-method-many-sends instead of all-methods-one-selector.
Erlang FFI receivers (Erlang foo / (Erlang foo) bar:) are excluded:
those are Erlang function calls routed through the ErlangModule DNU
bridge, not Beamtalk message sends, so reporting them as outgoing calls
would let an Erlang function name shadow a Beamtalk selector in the
caller's call hierarchy. Mirrors the same exclusion in unusedSelectors.
aMethod must be a CompiledMethod (the value returned by
aClass >> #selector). Passing anything else raises a typed error.
A method whose source is nil (a primitive / intrinsic registered
without source text) returns [].
Examples
method := Counter >> #increment
SystemNavigation default messagesSentBy: method
// => [#{#selector => #+, #line => 2}, ...]
Return a collection of call sites that send the given selector.
Each result is a Dictionary with three keys:
#class— the class whose method contains the send (the class object for an instance-side hit, the metaclass object for a class-side hit)#selector— the selector of the method that contains the send#line— the 1-based line number within the method source where the send appears
A method that sends aSelector multiple times produces one record per
occurrence. Matches Pharo's SystemNavigation >> #allSendersOf:.
Maintained index (ADR 0087, BT-2299)
The class-body call sites are read from the runtime-maintained xref index
(beamtalk_xref), populated at class-load time by codegen's baked
register_class/0 payload. This is a sub-millisecond ETS read in place of
the former per-call walk over every method source — see
docs/development/benchmarks.md.
Miss policy. The index is authoritative for loaded classes, but a miss must never become a wrong answer:
- Rows whose owning class is no longer loaded are dropped silently (stale).
- A class the registry reports as loaded but that is absent from the index
falls back to the legacy source-scan (
collectSendersFor:in:into:) for that class only, and the runtime logs onexref_misswarning. The partition is computed runtime-side bybeamtalk_xref:senders_of_bt/1.
Extension methods (ADR 0066) are scanned when registered with source via
beamtalk_extensions:register/5 (BT-2196), unchanged by the migration —
extensions are not yet part of the index (lands in a later phase). The
#class for an extension hit is the extended target class (or the
metaclass object resolved from the #'Target class' marker for
Target class extensions); register/4 registrations without source
remain invisible to the source-text scan.
Raises an error if the argument is not a Symbol. Returns an empty collection if no senders are found (not an error).
Examples
SystemNavigation default sendersOf: #asString // => [#{#class => Counter, #selector => ..., #line => 7}, ...]
SystemNavigation default sendersOf: #neverCalledXyzzy // => []
Resolve an xref sender row's owning class (a class-name Symbol) and side
flag to the class object the result record exposes as #class.
Instance-side hits report the class object; class-side hits report the
metaclass object (<class> class), matching the legacy scan's cls /
cls class convention (BT-2195). Falls back to the raw tag if the name
resolves to nil (a registry entry with no live class behind it — should
not happen, but the scan must not crash). On that defensive path
resolveExtensionClass: returns the raw ownerTag Symbol; it is
preserved as-is even for class-side hits rather than coerced into
Symbol class.
Map the xref index bt_row()s returned by beamtalk_xref:senders_of_bt/1
or references_to_bt/1 onto #{#class, #selector, #line} result records,
appending to result.
Each row is a Dictionary with atom keys #owner (class-name Symbol),
#class_side (Boolean), #method (Symbol), and #line (positive
Integer). #class is resolved via senderClassFor:classSide: so the
record shape is byte-for-byte identical to the legacy source-scan path.
The bt_row() shape is shared by senders and references (both index a
site to the containing method's selector), so the same mapper backs both
sendersOf: and referencesTo: (BT-2302). The explicit
-> List(Dictionary) return anchors element-type inference for the
accumulator.
Backwards-compatible alias for indexedSiteRecordsFrom:into:, retained
because the sendersOf: migration (BT-2299) named the mapper for senders
before referencesTo: (BT-2302) generalised it. Delegates unchanged.
Map the indexed defined_selectors_bt rows to their #selector Symbols,
appending to result. Each row's at: #selector access is Dynamic, so
the explicit -> List(Symbol) return anchors element-type inference for
the selectorsMatching: fallback accumulator (mirrors
indexedSiteRecordsFrom:into:), keeping acc strongly typed rather than
Dynamic (BT-2302).
Append senders of aSelector found in any instance method of aScope
(a class) to result. Returns the (possibly extended) result list.
Internal helper for sendersOf:. Iterates aScope methods, fetches
each method's source via >>, and asks the compiler to find matching
MessageSend line numbers.
Guards the >> send with respondsTo: so a class for which >>
is not yet supported (e.g. metaclass-shaped registry entries) is
silently skipped rather than aborting the whole walk.
Append one record per matching MessageSend found in methodSource.
Internal helper for collectSendersFor:in:into:. Splits the per-method
FFI call out so the surrounding inject:into: body stays a single
expression, which keeps the type-inference behaviour predictable.
Return the distinct Announcement subclasses that aClass statically
emits — the publisher-side dual of AnnouncementNavigation (ADR 0093 §7,
BT-2475).
Where AnnouncementNavigation answers "who listens for which events"
from the live runtime ETS subscription table, this answers "which event
classes does aClass publish" by mining the announce: /
announceAndWait: / announceAndWait:timeout: call sites out of
aClass's own method bodies (instance side + class side + extension
methods that target aClass). The result is the distinct set of resolved
announcement classes, sorted by name for a stable snapshot; [] when
aClass emits nothing statically resolvable.
Advisory, not a complete contract
This is a static analysis of a dynamic language: it reports an emission
only when the event argument resolves to a concrete Announcement
subclass at the call site — a constructor call on a bare class reference
(announce: (PriceChanged newPrice: 42) or announce: PriceChanged new).
A Dynamic-typed or indirect argument (announce: someVar,
perform:-style dispatch) is unresolvable by construction and is
silently skipped — it is a documented limitation, never an error. Treat
the result as advisory discoverability, not a sound or exhaustive emission
contract.
Raises a typed error if aClass is a Symbol/String rather than a class
object (argument-validation parity with referencesTo:).
Examples
SystemNavigation default announcementsSentBy: PriceTracker // => [PriceChanged, ...]
SystemNavigation default announcementsSentBy: SilentClass // => []
Return a collection of the announcement emission sites within aClass —
the site-level form of announcementsSentBy: for IDE / event-graph use
(BT-2475).
Each result is a Dictionary with four keys, the sendersOf: shape plus the
resolved announcement class:
#class— the publisher class whose method contains the emission (the class object for an instance-side hit, the metaclass object for a class-side hit, or the resolved extension target for an extension hit)#selector— the selector of the method that contains the emission#line— the 1-based line number within the method source where theannounce:send appears#announcementClass— the resolvedAnnouncementsubclass being emitted
A method that emits the same announcement multiple times produces one
record per occurrence. Only sites whose event argument resolves to a
concrete Announcement subclass are reported; unresolvable
(Dynamic-typed / indirect) sites are skipped (see announcementsSentBy:
for the advisory-analysis discipline).
Scope mirrors sendersOf:'s source-scan: aClass's instance side and
class side, plus source-bearing extension methods (ADR 0066 / BT-2196) that
target aClass. Unlike sendersOf:, this query is not index-backed: the
xref index records selectors and lines but not argument types, so each
method source is re-parsed via the compiler to resolve the event argument.
Raises a typed error if aClass is a Symbol/String rather than a class
object. Returns an empty collection if no resolvable emissions are found.
Examples
SystemNavigation default announcementSitesSentBy: PriceTracker
// => [#{#class => PriceTracker, #selector => #notify, #line => 4,
// #announcementClass => PriceChanged}, ...]
Append announcement emission sites found in any method of aScope (a class
or metaclass) to result. Returns the (possibly extended) result list.
Internal helper for announcementSitesSentBy:. Iterates aScope methods,
fetches each method's source via >>, and asks the compiler for the
announce: sites in it (with their resolved argument classes). Mirrors
collectSendersFor:in:into:; the >> send is guarded with respondsTo:
so a scope for which >> is not yet supported (e.g. metaclass-shaped
registry entries) is silently skipped, and a nil source (a primitive
registered without source text) contributes nothing.
Append one record per resolvable announcement emission found in
methodSource.
Internal helper for collectAnnounceSitesIn:into:. Splits the per-method
FFI call out so the surrounding inject:into: body stays a single
expression — same shape as appendSendersIn:selector:class:methodSelector:into:.
The compiler hands back one {Line, ClassNameBinary} tuple per announce
site; the class-name binary is resolved to a live class via findClass:
(which answers nil for the empty-binary "unresolvable" marker). A site is
recorded only when its argument resolves to a class that is an
Announcement subclass — unresolvable arguments and non-announcement
receivers (a misclassified constructor send) are both skipped.
Whether aClass is a loaded Announcement subclass (self-inclusive of
Announcement itself) — the gate that keeps announcementsSentBy: from
reporting a non-announcement class that a heuristic constructor match
happened to resolve.
nil (an unresolvable argument) and any value that does not respond to
superclassChain (not a class object) answer false. Returns false
defensively if Announcement is not in the loaded image. Uses
superclassChain includes: exactly as actorClasses tests membership of
Actor.
Append announcement emission sites found in every source-bearing extension
method (ADR 0066 / BT-2196) that targets aClass to result. Returns the
(possibly extended) result.
Mirrors collectExtensionSendersFor:into:, but — because the query is
scoped to one publisher class rather than one selector — only extensions
whose resolved target is aClass (instance side) or aClass class
(class side) are scanned. Extension target identity is matched by name so
both an instance-side tag (#Foo) and a metaclass tag (#'Foo class')
are recognised. Extensions registered via register/4 (no source) are
invisible, as elsewhere.
Whether the resolved extension-target class extClass is aClass or
aClass class — i.e. the extension extends the publisher under query.
targetNames is the precomputed [aClass name, aClass class name] pair.
Matching by name handles both an instance-side tag (#Foo → Foo, name
#Foo) and a metaclass tag (#'Foo class' → Foo class, name
#'Foo class') uniformly. A resolveExtensionClass: result that is a raw
atom (the defensive miss path) does not respond to name and answers
false. The @expect dnu suppresses the static does-not-understand check:
extClass is typed Object (the navigation queries report class objects,
metaclass objects, or a raw atom under one umbrella type), and the
respondsTo: #name guard makes the name send safe at runtime.
Return a collection of references to the given class.
Each result is a Dictionary with three keys:
#class— the class whose method contains the reference (the class object for an instance-side hit, the metaclass object for a class-side hit)#selector— the selector of the method that contains the reference#line— the 1-based line number within the method source where the reference appears
A method that mentions aClass multiple times produces one record per
occurrence. Class names that appear in type annotations (parameter,
return, or generic parameters such as List(Counter)) are also matched.
Matches Pharo's SystemNavigation >> #allReferencesTo:.
Maintained index (ADR 0087, BT-2302)
The class-body reference sites are read from the runtime-maintained xref
index (beamtalk_xref), populated at class-load time by codegen's baked
register_class/0 payload (the same references channel that feeds the
methods / senders tables). This is a sub-millisecond ETS read in place of
the former per-call walk over every method source — same envelope as
sendersOf:, see docs/development/benchmarks.md.
Miss policy. Identical to sendersOf::
- Rows whose owning class is no longer loaded are dropped silently (stale).
- A class the registry reports as loaded but that is absent from the index
falls back to the legacy source-scan (
collectReferencesFor:in:into:) for that class only, and the runtime logs onexref_misswarning. The partition is computed runtime-side bybeamtalk_xref:references_to_bt/1.
Extension methods (ADR 0066) are scanned when registered with source via
beamtalk_extensions:register/5 (BT-2196), unchanged by the migration —
extensions are not yet part of the index. The #class for an
extension hit is the extended target class (or the metaclass atom for
Target class extensions); register/4 registrations without source
remain invisible.
Argument must be a class object (a Behaviour). Passing a Symbol or
String — the most common mistake, since sendersOf: takes a Symbol —
raises a typed error pointing the caller at the right argument shape.
Returns an empty collection if no references are found (not an error).
Examples
SystemNavigation default referencesTo: Integer // => [#{#class => Counter, #selector => ..., #line => 7}, ...]
SystemNavigation default referencesTo: NeverUsedClass // => []
Append references to a class (identified by targetName) found in any
instance method of aScope (a class) to result. Returns the (possibly
extended) result list.
Internal helper for referencesTo:. Iterates aScope methods, fetches
each method's source via >>, and asks the compiler to find matching
ClassReference line numbers. Mirrors collectSendersFor:in:into:.
Guards the >> send with respondsTo: so a class for which >>
is not yet supported (e.g. metaclass-shaped registry entries) is
silently skipped rather than aborting the whole walk.
Append one record per matching ClassReference found in methodSource.
Internal helper for collectReferencesFor:in:into:. Splits the
per-method FFI call out so the surrounding inject:into: body stays a
single expression — same shape as appendSendersIn:selector:class:methodSelector:into:.
Return a collection of methods that READ the named field.
Each result is a Dictionary with three keys:
#class— the class whose method reads the slot (the class object for an instance-side hit, the metaclass object for a class-side hit)#selector— the selector of the method that reads the slot#line— the 1-based line number within the method source where the read appears
A "read" is an access self.x (where x is aSym) anywhere EXCEPT the
target of an assignment. So self.x := self.x + 1 reads x (on the RHS)
and writes it (on the LHS); the bare assignment target self.x := 0
counts as a write only, never a read (Pharo precedent). A bare local
variable x is not an field and is ignored. A method that
reads aSym multiple times produces one record per occurrence.
Scope: walks aClass and every class in aClass allSubclasses (a
subclass inherits the slot, so methods it defines can touch it), and on
each of those walks both the instance side (cls) and the class side
(cls class, the metaclass) so class-side methods that touch a class-side
field (classState:) are covered too.
aSym must be a Symbol naming the slot; passing anything else raises a
typed error. The slot must be declared on aClass — if aSym is neither
in aClass allFieldNames (instance-side slots, inherited included) nor
found by the source scan (which catches class-side slots, not reflectable
today), a typed error is raised so a typo surfaces rather than silently
returning []. A declared-but-untouched instance slot returns [].
Examples
SystemNavigation default fieldReadersOf: #value in: Counter
// => [#{#class => Counter, #selector => getValue, #line => 5}, ...]
SystemNavigation default fieldReadersOf: #neverReadSlot in: Counter // => []
Return a collection of methods that WRITE (assign) the named instance variable.
Each result is a Dictionary with three keys:
#class— the class whose method writes the slot (the class object for an instance-side hit, the metaclass object for a class-side hit)#selector— the selector of the method that writes the slot#line— the 1-based line number within the method source where the write appears
A "write" is an assignment whose target is self.x (where x is
aSym). The mirror of fieldReadersOf:in:: self.x := self.x + 1
appears in BOTH result sets (write on the LHS, read on the RHS). A method
that writes aSym multiple times produces one record per occurrence.
Scope and validation are identical to fieldReadersOf:in:: walks
aClass plus aClass allSubclasses on both the instance and class sides,
requires aSym to be a declared Symbol slot, and returns [] for a
declared-but-never-written slot.
Examples
SystemNavigation default fieldWritersOf: #value in: Counter
// => [#{#class => Counter, #selector => increment, #line => 3}, ...]
SystemNavigation default fieldWritersOf: #neverWrittenSlot in: Counter // => []
Shared driver for fieldReadersOf:in: and fieldWritersOf:in:.
wantReaders selects which access kind to collect (reads when true,
writes when false). Validates the arguments, builds the scope (aClass
aClass allSubclasses), walks each scope on both the instance and class sides, then applies slot validation:aSymmust be a declared slot ofaClass— instance-side (allFieldNames) or class-side (allClassVarNames, BT-2238), inherited included. An undeclared name raises a typed error (typo guard); a real-but-untouched slot returns[].
Append field access records found in any method of aScope
(a class or metaclass) to result. Returns the (possibly extended) list.
Internal helper for collectFieldAccessesOf:in:readers:. Iterates
aScope methods, fetches each method's source via >>, and asks the
compiler to find matching read or write line numbers. Guards the >>
send with respondsTo: so a scope for which >> is not yet supported
(e.g. metaclass-shaped registry entries) is silently skipped rather than
aborting the whole walk — mirrors collectSendersFor:in:into:.
Append one record per matching field access found in
methodSource.
Internal helper for collectFieldAccessesFor:in:readers:into:. Splits
the per-method FFI call out so the surrounding inject:into: body stays a
single expression — same shape as appendSendersIn:selector:class:methodSelector:into:.
wantReaders picks the reader vs writer FFI entry point.
Return a collection of the Erlang FFI call sites that invoke the named Erlang function across the loaded class registry.
aSpec is a String naming the target function in "module:function" form
(e.g. "lists:reverse"), optionally arity-qualified as
"module:function/arity" (e.g. "lists:reverse/1"). When an arity is
given, only call sites with exactly that argument count match; without it,
the function matches at any arity. A spec that does not match the
module:function[/arity] shape raises a typed error (kind #user_error).
An FFI call site is a send routed through the Erlang bridge —
(Erlang lists) reverse: xs or Erlang lists reverse: xs. There is no
dedicated AST node for these; the compiler recognises the
Erlang <module> <function> send shape, recovers the module/function/arity
the same way codegen lowers the call, and matches them against the spec.
Each result is a Dictionary with three keys:
#class— the class whose method contains the call (the class object for an instance-side hit, the metaclass object for a class-side hit, or the resolved extension target for an extension hit)#selector— the selector of the method that contains the call#line— the 1-based line number within the method source where the call appears
A method that calls the function multiple times produces one record per
occurrence. Iterates the navigation scope (allClasses) and walks both
instance-side and class-side method dictionaries (BT-2195), plus
source-bearing extension methods (ADR 0066 / BT-2196), so stdlib and user
classes are covered on all three. Returns an empty collection if no call
sites are found (not an error).
Examples
SystemNavigation default ffiSitesFor: "lists:reverse"
// => [#{#class => SystemNavigation, #selector => ..., #line => 7}, ...]
SystemNavigation default ffiSitesFor: "lists:reverse/1" // arity-qualified
SystemNavigation default ffiSitesFor: "lists:noSuchFunctionXyzzy" // => []
Parse an FFI spec String into its components, raising on a malformed shape.
Returns a Dictionary #{#module => String, #function => String, #arity => Integer | #any}. The #arity value is the special Symbol
#any when the spec omits the /arity suffix (the conventional encoding
the underlying FFI accepts for "match any arity"), otherwise the parsed
non-negative Integer.
Accepts "module:function" and "module:function/arity". Raises a typed
error (kind #user_error) for any other shape: a missing/extra :, an
empty module or function, more than one /, or a non-numeric arity.
Raise the shared malformed-spec error for ffiSitesFor: / parseFfiSpec:.
Always raises (kind #user_error); the ^nil is unreachable and exists
only so the helper type-checks as returning a value when its result is
used in an expression position (e.g. inside ifTrue: of parseFfiSpec:).
Append FFI call sites of module:function (constrained to arity unless
it is #any) found in any method of aScope to result. Returns the
(possibly extended) result list.
Internal helper for ffiSitesFor:. Iterates aScope methods, fetches each
method's source via >>, and asks the compiler to find matching FFI call
line numbers. Mirrors collectSendersFor:in:into:; the >> send is guarded
with respondsTo: so a scope for which >> is not yet supported (e.g.
metaclass-shaped registry entries) is silently skipped.
Append one record per matching FFI call site found in methodSource.
Internal helper for collectFfiSitesFor:function:arity:in:into:. Splits the
per-method FFI call out so the surrounding inject:into: body stays a
single expression — same shape as appendSendersIn:selector:class:methodSelector:into:.
Append FFI call sites of module:function found in every extension
method source (ADR 0066 / BT-2196) to result. Returns the (possibly
extended) result. Mirrors collectExtensionSendersFor:into:, swapping the
per-method scan to the FFI-site query and threading the module/function/
arity target.
Return a collection of {class, selector} records for every method
whose source text matches aRegex.
Source-text grep across the whole loaded class registry — answers "show me every method whose body mentions X." Each result is a Dictionary with two keys:
#class— the class that defines the method (the class object for an instance-side hit, the metaclass object for a class-side hit)#selector— the selector of the matching method
Iterates the navigation scope (allClasses) and walks both
instance-side and class-side method dictionaries (BT-2195). Methods
whose source is missing (e.g. generated primitives, methods registered
without source text) are skipped, not raised. One match per method
suffices — methodsMatching: does not enumerate every match position
within a single method.
The argument must be a compiled Regex instance. Callers that have a
pattern string should compile it explicitly via Regex from: ... unwrap; passing a String raises a typed error. Matches Pharo's
SystemNavigation >> #allMethodsWithSourceString: in spirit, but
requires an explicit regex object rather than guessing literal vs
regex from the input.
Performance: O(N x source-length). Fine for on-demand IDE search; not for hot live-highlight paths. A maintained index is a follow-up optimisation (see ADR 0033 / BT-2201).
Extension methods (ADR 0066) are scanned when registered with source via
beamtalk_extensions:register/5 (BT-2196). The #class for an
extension hit is the extended target class (or the metaclass atom for
Target class extensions); register/4 registrations without source
remain invisible.
Examples
pat := (Regex from: "ifTrue:") unwrap
SystemNavigation default methodsMatching: pat
// => [#{#class => Counter, #selector => increment}, ...]
SystemNavigation default methodsMatching:
(Regex from: "xyzzyNeverAppearsInAnyMethodBody") unwrap
// => []
Append {class, selector} records for methods of aScope whose source
matches aRegex to result. Returns the (possibly extended) result.
Internal helper for methodsMatching:. Iterates aScope methods,
fetches each via >>, and tests aRegex against the source. Methods
with no source (nil) — or classes whose method lookup is unsupported
(e.g. metaclass-shaped registry entries; see sendersOf:) — are
skipped silently.
Return a sorted, deduplicated list of selectors whose name contains
aPattern (case-insensitive substring match).
Powers omni-bar / Cmd-T "find any selector" surfaces in an IDE. Collects instance-side selectors, class-side selectors, and extension-method selectors (ADR 0066), then filters by lowercase substring match.
Matching uses selectorString lowercase includesSubstring: aPattern lowercase, mirroring Pharo's SystemNavigation >> #allSelectorsContaining:.
Maintained index (ADR 0087, BT-2302)
The class-body selectors are read from the runtime-maintained xref methods
table (beamtalk_xref), populated at class-load time by codegen's baked
register_class/0 payload — one methods row per {class, side, selector} definition. This is a sub-millisecond ETS read in place of the
former methods / class methods probe over every class in the scope.
Miss policy. Mirrors sendersOf::
- Indexed selector rows whose class is no longer loaded are dropped silently (stale).
- A class the registry reports as loaded but that is absent from the index
falls back to the legacy
selectorsForClass:gather for that class only, and the runtime logs onexref_misswarning. The partition is computed runtime-side bybeamtalk_xref:defined_selectors_bt/0.
Extension-method selectors (ADR 0066) are not yet in the index, so they
are still gathered unconditionally via selectorsForClass: over the full
scope; the final unique sort absorbs the overlap with the indexed and
fallback selectors.
Raises an error if the argument is not a String, or if the pattern is empty (an empty pattern would match every selector and is almost certainly a programming error).
Returns [] when no selectors match.
Examples
SystemNavigation default selectorsMatching: "append"
// => [#appendBinary:contents:, ...]
SystemNavigation default selectorsMatching: "AppEnd"
// => [#appendBinary:contents:, ...] (case-insensitive)
SystemNavigation default selectorsMatching: "noSelectorContainsThisXyzzy"
// => []
Return every selector defined on the given class: instance-side, class-side, and extension-method selectors (ADR 0066) registered on either the instance tag or the metaclass tag.
Internal helper for selectorsMatching:. Splits the per-class gather
out so the surrounding inject:into: body stays a single expression,
mirroring the collectSendersFor:in:into: / sendersOf: pairing
above.
The respondsTo: #name guard (mirroring collectSendersFor:in:into:)
keeps a registry-shaped value that lacks a name from aborting the walk
— it is silently skipped for extension lookups instead. (Every real
Behaviour answers name; the guard is purely defensive.)
Return the extension-method selectors (ADR 0066) registered against the class on either the instance tag or the metaclass tag.
Split out of selectorsForClass: (BT-2302) so selectorsMatching: can
gather extension selectors over the full scope independently of the
indexed class-body selectors (extensions are not yet in the xref index).
The respondsTo: #name guard keeps a registry-shaped value that lacks a
name from aborting the walk — it contributes no extension selectors
instead. (Every real Behaviour answers name; the guard is defensive.)
Return the selectors of all extension methods (ADR 0066) registered against the given class tag.
Internal helper for selectorsForClass:. The extension registry
returns {Selector, Owner} 2-tuples; this strips the owner, keeping
only the selector atoms. Returns [] when the tag has no extensions.
BT-2254: Erlang beamtalk_extensions list: is specced as a list of
{atom(), atom()} tuples, so the per-entry entry at: 1 infers Symbol
directly — no @expect type suppression needed.
Append senders of aSelector found in every extension method source
(ADR 0066 / BT-2196) to result. Returns the (possibly extended) result.
Iterates the extension sources table — populated by
beamtalk_extensions:register/5 — and runs the same per-method scan as
collectSendersFor:in:into: on each registered body. Extensions
registered via register/4 (without source) are silently absent from
the source table and therefore from the scan; that is the documented
behaviour (see sendersOf: docstring).
Each match emits a Dictionary #{#class => Class, #selector => Selector, #line => Line} shaped identically to the class-side scan, so callers
cannot distinguish a hit on an extension from a hit on a class-body
method. The #class value is resolved from the extension's registry
tag via resolveExtensionClass: — a real class object when the tag
is a known instance-side class name (e.g. #Integer), or the raw
atom (e.g. #'String class') when no class lookup succeeds (e.g. for
metaclass-tagged class-side extensions).
Append references to targetName found in every extension method
source (ADR 0066 / BT-2196) to result. Returns the (possibly extended)
result. Mirrors collectExtensionSendersFor:into:, swapping the
per-method scan to findReferencesToIn:class:.
Append {class, selector} records for every extension method (ADR 0066
/ BT-2196) whose source matches aRegex to result. Returns the
(possibly extended) result. Mirrors collectMethodsMatching:in:into:
but iterates the extension sources table rather than a class's
instance method dictionary.
Return a List of every selector that is sent somewhere in the loaded class registry but defined nowhere — the classic Smalltalk typo-finder.
Each result is a Dictionary with two keys:
#selector— the unimplemented selector (a Symbol)#sites— a List of site Dictionaries, one per send. Each site has:#class— the class whose method contains the send (class object for an instance-side hit, metaclass object for a class-side hit, or the resolved extension target for an extension hit)#selector— the selector of the method that contains the send#line— the 1-based line number within that method's source
So a typo like self printOnStream: x (when printOnStream: is defined
nowhere) surfaces with a site pointing straight at the offending method,
letting an IDE jump the user to the mistake.
Index-backed design (ADR 0087, BT-2303)
Computes allSentSelectors − allDefinedSelectors, both sets read from the
runtime-maintained xref index (beamtalk_xref) rather than re-parsed:
- The defined set is the indexed
defined_selectors_btrows (instance- and class-side), unioned with the extension selectors (not yet indexed) and the documentedruntimeProvidedSelectors. - The sent sites come from
all_sends_bt— one indexed row per send, carrying the sent selector, the containing method's owner / side / selector / line, and the receiver tag. Sends whose selector is in the defined set are dropped; the rest are grouped by selector into the result, sites resolved to#{#class, #selector, #line}records.
Miss policy (mirrors sendersOf: / selectorsMatching:). The index
is authoritative for loaded classes, but a miss must never become a wrong
answer:
- Indexed rows whose owning class is no longer loaded are dropped silently (stale), runtime-side.
- A class the registry reports as loaded but that is absent from the index
falls back to the legacy source-scan (
collectAllSendsIn:excluding:into:for sends,selectorsForClass:for defines) for that class only, and the runtime logs onexref_misswarning. The partitions are computed runtime-side bybeamtalk_xref:defined_selectors_bt/0andbeamtalk_xref:all_sends_bt/0. - Extension selectors / sends (ADR 0066) are not yet in the index, so they are gathered unconditionally across the full scope.
Exclusions (and their limits)
doesNotUnderstand:args:— a class that defines the DNU overridedoesNotUnderstand:args:can legitimately receive arbitrary selectors viaself/super(the DNU handler interprets them dynamically), so sends toself/superinside such a class are not flagged. This is a coarse heuristic: it does not know the runtime receiver type, so it only suppressesself/supersends in the defining class, not sends to other instances of it. Proper receiver-type analysis is out of scope.perform:— the dynamic selector argument toperform:/perform:with:is a Symbol literal, never an AST send, so it is never collected in the first place (no special-casing needed).perform:itself is defined on Object, so it is in the defined set and never flagged.- Erlang FFI —
Erlang module …and(Erlang module) selector: …are Erlang function calls routed throughErlangModule'sdoesNotUnderstand:bridge, not Beamtalk message sends. The compiler tags such sends with receiver kind#erlang_ffi; they are dropped wholesale (otherwise every FFI module name and FFI function name would be a false positive). This is exact, not heuristic: the receiver is a staticErlang …chain in the AST. - Runtime-provided selectors — a small documented set of selectors
that are real and resolvable at runtime but are not surfaced through the
compile-time
methodsdictionary (e.g. codegen-inlined pseudo-selectors or primitives registered under a different name). SeeruntimeProvidedSelectors.
Performance
Two sub-millisecond ETS reads (defined_selectors_bt + all_sends_bt)
plus the unindexed extension scan, in place of the former per-call AST
walk over every method source (same migration as sendersOf:; see
docs/development/benchmarks.md).
Returns [] when the registry is clean (modulo the documented
exclusions). Source-less methods and register/4 extensions (no source)
contribute neither sends nor flags, exactly as in sendersOf:.
Examples
SystemNavigation default unimplementedSelectors
// => [#{#selector => #printOnStream:, #sites => [#{#class => Foo, #selector => bar, #line => 3}]}]
Union two fallback-class lists into a List of class-name Symbols, dropping
duplicates. Internal helper for the composite queries: the defined-set and
sent-set _bt reads each compute their own miss partition, so a class
missing from the index can appear in both — the source-scan fallback must
visit it exactly once.
Add each indexed defined_selectors_bt row's #selector Symbol to the Set
result. Returns the (possibly extended) Set. The explicit -> Set(Symbol)
return anchors element-type inference for the defined-set accumulator
(mirrors indexedSelectorsFrom:into:), keeping acc strongly typed rather
than Dynamic (BT-2303).
Build the set of scope keys (ownerName for an instance-side scope,
ownerName ++ " class" for a class-side scope) whose methods include
doesNotUnderstand:args: — the classes the coarse DNU heuristic must
suppress self/super sends in (see unimplementedSelectors). Returns the
(possibly extended) Set of String keys.
The handler set is read from the SAME indexed defined_selectors_bt rows
the defined set comes from (a row whose #selector is
doesNotUnderstand:args: marks its owner/class_side scope), then
extended with the loaded-but-unindexed fallback scopes probed directly via
methods includes: (mirroring the legacy per-scope check). Keying on
owner + side keeps the side-specific semantics of the source-scan path:
instance-side DNU suppresses instance-side sends, class-side DNU suppresses
class-side sends.
The DNU-scope key for an xref owner-tag / class-side pair: the owner name
String for an instance-side scope, suffixed with " class" for a
class-side scope. Matches dnuScopeKeyFor: so an indexed scope and a
Behaviour scope for the same class+side produce the same key.
The DNU-scope key for a Behaviour scope: its name String, which already
carries the " class" suffix for a metaclass (so Counter class name is
"Counter class"). The fallback-scope counterpart of
dnuScopeKeyForOwner:classSide:.
Group indexed send rows (beamtalk_xref:all_sends_bt/0) by their sent
selector into result (selector -> [sites]), dropping any send that is
excluded. Returns the (possibly extended) Dictionary.
Internal helper for unimplementedSelectors. Each row is a Dictionary
with atom keys #sent (the sent selector), #owner (class-name Symbol),
#class_side (Boolean), #method (the containing method's selector),
#line (positive Integer), and #recv (the receiver tag #self / #super
/ #erlang_ffi / #other). A send is dropped when its #sent selector is
in excluded, when #recv is #erlang_ffi, or when its scope handles DNU
and #recv is #self / #super — the same three exclusions recordSend:…
applies on the source-scan path.
Selectors that are genuinely resolvable at runtime but are NOT surfaced
through the compile-time methods dictionary, so unimplementedSelectors
would otherwise flag them as typos.
Keep this list small and documented — each entry must be a real framework
selector, never a way to silence an actual typo. Empirically derived by
running unimplementedSelectors against the live stdlib and confirming
each entry is inlined / registered under a different name.
(Currently empty: the stdlib comes back clean after the structural
doesNotUnderstand: and perform: exclusions. Entries are added here
with a comment explaining the runtime mechanism that provides them.)
Append unimplemented-send sites found in any method of aScope (a class
or metaclass) to the grouping Dictionary result, dropping any send whose
selector is in excluded. Returns the (possibly extended) Dictionary.
Internal helper for unimplementedSelectors. Mirrors
collectSendersFor:in:into: but threads a grouping Dictionary
(selector -> [sites]) instead of a flat List, and asks the compiler for
EVERY send (allSendsIn:) rather than one selector's senders.
Guards the >> send with respondsTo: so a scope for which >> is not
yet supported is skipped rather than aborting the whole walk.
Append one grouped site per unimplemented send found in methodSource.
Internal helper for collectAllSendsIn:excluding:into:. Splits the
per-method FFI call out so the surrounding inject:into: body stays a
single expression — same shape as
appendSendersIn:selector:class:methodSelector:into:.
For each send tuple {sentSel, line, recv} returned by allSendsIn:,
the send is recorded in the grouping Dictionary unless:
sentSelis inexcluded(already defined, or runtime-provided), or- the owner defines
doesNotUnderstand:and the receiver is#self/#super(the coarse DNU heuristic — seeunimplementedSelectors).
Add a single send site to the grouping Dictionary, applying the defined /
runtime-provided and doesNotUnderstand: exclusions. Returns the
(possibly extended) Dictionary unchanged when the send is excluded.
Internal helper for appendAllSendsIn:.... Splitting the per-send
decision out keeps the surrounding inject:into: body a single
expression.
Append unimplemented-send sites found in every extension method source
(ADR 0066 / BT-2196) to the grouping Dictionary result. Returns the
(possibly extended) Dictionary.
Mirrors collectExtensionSendersFor:into: but threads a grouping
Dictionary and asks the compiler for EVERY send via allSendsIn:. The
#class for a site is resolved from the extension's registry tag via
resolveExtensionClass:, matching the class-body scan.
The doesNotUnderstand: exclusion does not apply to extension sources:
handlesDnu is false because an extension target's DNU handler (if any)
is a property of the target class, which resolveExtensionClass: may
return as a raw atom (not a queryable scope). Erring toward reporting is
safe — extension bodies are rare and the user can add a documented
exclusion if a real false positive ever appears.
Resolve an extension registry class tag (an atom) to the class object
the navigation queries should report in #class.
Two tag shapes occur in the extension registry, both resolved by a single
findClass: send (BT-2223):
- Instance-side tags (
#Integer) resolve to the class object. - Class-side tags (
#'Integer class') resolve to the metaclass object — matching what BT-2195's class-side method walk reports for an ordinary class-method hit (a metaclass object, not a Symbol). This keepsrecord at: #classcomparable across class-body and extension hits.
The metaclass-tag decode lives runtime-side in beamtalk_interface's
handle_class_named/1, alongside the beamtalk_class_registry authority
that encodes the tag (class_object_tag/1). The stdlib no longer
reverse-engineers the " class" convention with string surgery.
Falls back to the raw tag only when the lookup misses (a tag with no registered class behind it — should not happen in practice, but the scan must not crash on a stray registry entry).
Internal helper for the three collectExtension* walks. Splits the
lookup out so the surrounding inject:into: bodies stay a single
expression each (matches the helper style elsewhere in this file).
Return type is intentionally untyped (Dynamic) — the result is a class object, a metaclass object, or (defensively) the raw atom, which have no shared supertype in the declared type system.
Return a List of dead-method candidates — selectors that are DEFINED on
some class but SENT nowhere in the loaded class registry. The conservative
inverse of unimplementedSelectors.
Each result is a Dictionary with two keys:
#class— the class object for an instance-side definition, or the metaclass object for a class-side definition (mirrors how the other navigation queries report#class)#selector— the defined-but-unsent selector (a Symbol)
A diagnostic, not a delete list
This is deliberately framed as "worth a look", NOT "definitely dead". A static source scan cannot see every way a method is reached, so several categories of genuinely-live methods are invisible to it. Rather than flag those as false positives, the query excludes the known-invisible categories up front (see Exclusions). What remains is a high-signal set of candidates for an IDE "unused methods" pane — each one still warrants human judgement before deletion.
Index-backed design (ADR 0087, BT-2303)
Computes allDefinedSelectors − allSentSelectors. The sent set is read
from the runtime-maintained xref index via
beamtalk_xref:all_sent_selectors_bt/0 (the distinct set of selectors sent
somewhere, Erlang-FFI sends already excluded runtime-side) rather than
re-parsed, unioned with the unindexed extension-body sends and the
loaded-but-unindexed fallback classes' source-scanned sends. The defined
candidates are built by walking cls methods (instance-side, owner cls)
and cls class methods (class-side, owner cls class) over the live class
registry — a reflective method-dictionary read, NOT a source re-parse — so
each candidate remembers its owning class object for the #class field.
A candidate survives into the result only if its selector is absent from
the sent set and survives every exclusion below.
Miss policy (mirrors sendersOf:). Indexed sent-selector rows whose
owning class is no longer loaded are dropped silently (stale), runtime-side;
a class the registry reports as loaded but absent from the index has its
sends source-scanned (collectSentSelectorsIn:into:) for that class only,
and the runtime logs one xref_miss warning. The partition is computed
runtime-side by beamtalk_xref:all_sent_selectors_bt/0.
Exclusions (and their limits)
- Erlang FFI sends are NOT counted as Beamtalk uses.
Erlang module …/(Erlang module) sel: …are Erlang function calls (tagged#erlang_ffiat the AST), so the "selectors" they carry are Erlang module/function names, not Beamtalk selectors. They are dropped from the sent set wholesale — counting them would let an Erlang function name that happens to match a Beamtalk selector spuriously mark that selector as used.#self/#super/#othersends ARE counted: a self-send offoomeansfoois used. - Runtime-called overrides (
runtimeCalledSelectors) — framework selectors the runtime invokes without a source-visible send (lifecycle hooks, the DNU handler, display/hash protocols). Overrides of these are never flagged. See that method for the list and the per-entry rationale. - TestCase test-runner methods — for
TestCaseand any class that inherits from it (anincludesBehaviour: TestCasecheck, self-inclusive), the methodssetUp/tearDownand everytest…method are invoked reflectively bybeamtalk_test_case, never via a source send, so they are excluded. The pattern exclusion is scoped to the TestCase hierarchy specifically, so a normal class with an unrelated method literally namedtest…is still eligible to be flagged. - Primitive / intrinsic methods — a method whose source is
nilor whose body contains@primitive/@intrinsicis implemented in (or dispatched from) the runtime and is reachable in ways the source scan cannot see, so it is excluded. This is a heuristic: it keys off the@primitive/@intrinsicannotation text in the method source rather than a structural AST flag. perform:is invisible. A selector reached only viaself perform: #foo/perform:with:is a Symbol literal, never an AST send, so it is never added to the sent set — such a method WILL appear as a false positive. This is an inherent limit of a static scan; no special casing is attempted.
Extension methods (ADR 0066) are out of scope on the defined side: unused-detection covers instance-side and class-side definitions only, extension methods are never flagged. Extension bodies ARE scanned for sends, so they still count toward marking ordinary methods as used.
Performance
One sub-millisecond ETS read (all_sent_selectors_bt) for the sent set
plus the reflective defined-candidate walk and the unindexed extension
scan, in place of the former per-call AST walk over every method source
(same migration as sendersOf:; see docs/development/benchmarks.md).
Returns [] when nothing qualifies. Note that a real stdlib has many
public selectors that are part of the API surface but sent nowhere in the
library itself, so the live result is expected to be non-empty.
Examples
SystemNavigation default unusedSelectors
// => [#{#class => Foo, #selector => #neverCalledHelper}, ...]
Add each indexed all_sent_selectors_bt selector Symbol to the Set
result. Returns the (possibly extended) Set. Each indexed element is
Dynamic (it comes through the FFI partition map), so the explicit
-> Set(Symbol) return anchors element-type inference for the sent-set
accumulator (mirrors definedSetFromIndex:into:), keeping acc strongly
typed rather than Dynamic (BT-2303).
Selectors the runtime / framework invokes WITHOUT a source-visible Beamtalk
send, so an override of one would otherwise look unused to unusedSelectors.
Keep this list small and documented — each entry must be a real selector
the runtime dispatches on its own; never a way to silence a genuinely dead
method. Each name below was verified against the live stdlib (e.g. the DNU
override is doesNotUnderstand:args:, NOT the Smalltalk-classic
doesNotUnderstand:; Beamtalk has no printOn: or = selectors).
initialize— Actor lifecycle hook called automatically after spawn (seeActor >> #initialize).terminate:— Actor lifecycle hook called by the runtime on shutdown (seeActor >> #terminate:).doesNotUnderstand:args:— the DNU handler, invoked by the message dispatch runtime when a selector is not understood (ProtoObject, Erlang, ErlangModule, TimeoutProxy define it).printString— invoked by the REPL / transcript display path to render a value; an override is reached through display, not a source send.displayString— invoked by user-facing display surfaces.inspect— invoked by the inspector surface (ADR 0095):anObject inspectopens anInspectorcursor, which the REPL/MCP surfaces render.hash— invoked by hashed collections (Set / Dictionary) when storing or comparing elements, not via a source-level send.
Add every selector sent in any method of aScope (a class or metaclass)
to the Set result, EXCLUDING Erlang-FFI sends. Returns the (possibly
extended) Set.
Internal helper for unusedSelectors. Mirrors collectSendersFor:in:into:
but accumulates into a Set and asks the compiler for EVERY send
(allSendsIn:) rather than one selector's senders. Splitting it out keeps
the surrounding inject:into: body a single expression.
Guards the >> send with respondsTo: so a scope for which >> is not yet
supported is skipped rather than aborting the whole walk, and guards a nil
method source (a sourceless method, e.g. a primitive) so
addSentSelectorsIn: always receives a String.
Add each non-FFI sent selector found in methodSource to the Set result.
Returns the (possibly extended) Set.
Internal helper for collectSentSelectorsIn:into:. Splits the per-method
FFI call out so the surrounding inject:into: body stays a single
expression. For each send tuple {selector, line, recv} from allSendsIn:,
the selector is added unless the receiver kind is #erlang_ffi (an Erlang
call, not a Beamtalk send — see unusedSelectors).
Add every non-FFI sent selector found in any extension method source
(ADR 0066 / BT-2196) to the Set result. Returns the (possibly extended)
Set.
Internal helper for unusedSelectors. Mirrors
collectExtensionSendersFor:into: but accumulates into a Set and asks the
compiler for EVERY send. Extension bodies count toward marking ordinary
methods as used even though extension methods themselves are not candidates
for the unused report.
Append a #{#class, #selector} record for every locally-defined selector
of aScope that is unused, recording owner as the reported #class.
Returns the (possibly extended) result List.
Internal helper for unusedSelectors. aScope is the scope to iterate
(cls for instance-side, cls class for class-side); owner is the class
object to report (the same value, but threaded separately to match the
#class reporting convention and keep the inject:into: body a single
expression). A selector is reported only when none of the exclusions in
unusedSelectors apply.
Whether the test-runner pattern applies is decided once per scope via
includesBehaviour: TestCase, so it is not recomputed per method. Using
includesBehaviour: (self-inclusive) rather than inheritsFrom: (strict)
matters: TestCase itself defines setUp / tearDown, which the runner
invokes reflectively just like on a subclass, so they must be excluded on
the base class too.
Add a single #{#class => owner, #selector => methodSel} record to
result unless methodSel is excluded. Returns result unchanged when
the selector is excluded.
Internal helper for collectUnusedFor:owner:sent:excluded:into:. Splitting
the per-selector decision out keeps the surrounding inject:into: body a
single expression. A selector is excluded when it is sent somewhere, is a
runtime-called override, is a TestCase test-runner method, or is a
primitive / intrinsic (see unusedSelectors).
Whether aSelector is invoked by the BUnit test runner rather than via a
source send: setUp, tearDown, or any selector whose name starts with
test. Only meaningful for selectors in the TestCase hierarchy — the caller
gates this behind includesBehaviour: TestCase.
Whether the method methodSel on aScope is a primitive / intrinsic, and
therefore reachable in ways the source scan cannot see. Treated as such
when the source is nil (no AST-visible body) or contains the
@primitive / @intrinsic annotation text. Heuristic — see
unusedSelectors.
Guards the >> send with respondsTo: (a scope without >> support is
treated as primitive — its body cannot be inspected so it is conservatively
excluded rather than flagged).
Indexed/fallback partition of implementor rows for aSelector (ADR 0087).
Indexed/fallback partition of sender call sites for aSelector (ADR 0087).
Indexed/fallback partition of reference sites for aSymbol (ADR 0087).
Indexed/fallback partition of defined-selector rows (ADR 0087).
Indexed/fallback partition of every send site (ADR 0087).
Indexed/fallback partition of the distinct sent-selector set (ADR 0087).
Resolve a class-name tag to the class/metaclass object, or nil.
Every message send (with line + receiver tag) in aSource.
Line numbers of aSelector sends within aSource.
Line numbers of references to class aName within aSource.
Announcement emission sites (line + resolved class name) in aSource.
Line numbers reading field aSym within aSource.
Line numbers writing field aSym within aSource.
Line numbers of FFI call sites matching aModule:aFunction/anArity
within aSource.
Package owners (atoms) of extension methods on class aName.
{extendedClass, selector} pairs contributed by owner anOwner.
Every registered extension {classTag, selector, source} entry.
INVARIANT: the third tuple element (entry at: 3, the extension source)
is always a String. The underlying FFI spec types it binary()
(String | Binary, BT-2254), but the runtime always delivers source text.
The List(Tuple) return type drops per-element precision, so entry at: 3
reads as Dynamic. To restore the static String check the inline FFI
gave (e.g. at the ffiSitesIn:… seam in collectExtensionFfiSitesFor:),
every consumer re-asserts the invariant by binding the source through a
typed local: extSource :: String := entry at: 3. If a non-String third
element ever appears, that typed binding fails loudly rather than silently.
{selector, owner} entries registered against class tag aTag.
The metaclass display tag (Foo -> Foo class) for aName.
Inherited Methods
From Object
Return the class of the receiver.
Examples
42 class // => Integer
"hello" class // => String
Test if the receiver is nil. Returns false for all objects except nil.
Examples
42 isNil // => false
nil isNil // => true
Test if the receiver is not nil. Returns true for all objects except nil.
Examples
42 notNil // => true
nil notNil // => false
If the receiver is nil, evaluate nilBlock. Otherwise return self.
Examples
42 ifNil: [0] // => 42
nil ifNil: [0] // => 0
If the receiver is not nil, evaluate notNilBlock with self.
Examples
42 ifNotNil: [:v | v + 1] // => 43
nil ifNotNil: [:v | v + 1] // => nil
If nil, evaluate nilBlock; otherwise evaluate notNilBlock with self.
Examples
42 ifNil: [0] ifNotNil: [:v | v + 1] // => 43
nil ifNil: [0] ifNotNil: [:v | v + 1] // => 0
If not nil, evaluate notNilBlock with self; otherwise evaluate nilBlock.
Examples
42 ifNotNil: [:v | v + 1] ifNil: [0] // => 43
nil ifNotNil: [:v | v + 1] ifNil: [0] // => 0
Return the developer-readable (Debug) string representation.
printString is the Debug protocol (ADR 0094): the self-describing,
structural form used by the REPL, logs, and by any other printString
that nests this object. It is the REPL default — evaluating an expression
shows its printString.
This default returns the bare class name (no a/an article — the
old "a ClassName" form was dropped in ADR 0094). Value overrides it
with the structural ClassName(field: value, ...) form, actors render as
Actor(ClassName, pid), supervisors as Supervisor(ClassName, pid) /
DynamicSupervisor(ClassName, pid), and primitive types (Integer, String,
List, …) override it with their own richer output. Authors rarely override
printString directly — the default is derived.
Examples
42 printString // => "42"
Return the user-facing (Display) string representation.
displayString is the Display protocol (ADR 0094): the human-facing
form. It is the hook the language pulls during string interpolation —
every {...} segment renders via the value's displayString. Developers
rarely call it directly; they override it when a value has a natural
human rendering (e.g. Money → $10.50, where printString would still
show the Debug form).
It defaults to printString, so most types need no override. String
and Symbol demonstrate the split: "hi" printString → "\"hi\""
(quoted, Debug) while "hi" displayString → "hi" (plain, Display);
likewise #foo drops its # prefix under displayString.
displayString is not part of the Printable protocol (deferred per
ADR 0094 §5).
Examples
42 displayString // => "42"
Open a navigable Inspector cursor on the receiver.
ADR 0095 Phase 3 (BT-2504). inspect is repurposed from -> String
(the ADR-0094 deferral) to the verb that produces an Inspector — a
live, immutable cursor for drilling into the object (Inspector on: self).
anObject inspect is the shorthand; Inspector on: anObject is the
explicit spelling. The cursor exposes fields/at:/path/refresh/
printString (an indented text tree) and asDictionaries (the MCP/browser
wire form); see Inspector.
This is a breaking change: code that used inspect for its old
String result must switch to printString (the structural Debug string,
ADR 0094) — a transitional lint flags inspect used directly in ++/
string position.
Examples
42 inspect kind // => #value
(Point x: 3 y: 4) inspect fields size // => 2
(Point x: 3 y: 4) printString // => "Point(x: 3, y: 4)" (the old inspect string)
Return the receiver itself. Useful for cascading side effects.
Examples
42 yourself // => 42
Return a hash value for the receiver.
Examples
42 hash
Test if the receiver responds to the given selector.
Examples
42 respondsTo: #abs // => true
Return the names of fields.
Examples
42 fieldNames // => #()
Return the value of the named field.
Examples
object fieldAt: #name
Set the value of the named field (returns new state).
Examples
object fieldAt: #name put: "Alice"
Send a unary message dynamically.
Examples
42 perform: #abs // => 42
Send a message dynamically with arguments.
Examples
3 perform: #max: withArguments: #(5) // => 5
Raise an error indicating this method must be overridden by a subclass.
Examples
self subclassResponsibility
Raise an error indicating this method has not yet been implemented.
Use this for work-in-progress stubs. Distinct from subclassResponsibility,
which signals an interface contract violation.
Examples
self notImplemented
Send aValue to the current transcript without a trailing newline.
Nil-safe: does nothing when no transcript is set (batch compile, tests).
Examples
42 show: "value: "
Send aValue to the current transcript followed by a newline.
Nil-safe: does nothing when no transcript is set (batch compile, tests).
Examples
42 showCr: "hello world"
Test if the receiver is an instance of aClass or any of its subclasses.
For class-object receivers, follows Smalltalk semantics: self class
is the metaclass, so the check walks the parallel metaclass hierarchy.
The parallel chain is grounded at ProtoObject class superclass == Class
(ADR 0036), so the metaclass tower merges into the instance-side
Class → Behaviour → Object → ProtoObject chain. As a result,
Integer isKindOf: Object and Integer isKindOf: Class both return true.
Examples
42 isKindOf: Integer // => true
42 isKindOf: Object // => true
#foo isKindOf: Symbol // => true
#foo isKindOf: String // => false
Integer isKindOf: Number // => false (metaclass chain, not instance chain)
Integer isKindOf: Number class // => true (Number class is in the parallel chain)
Integer isKindOf: Object // => true (grounded — Object is reachable via the metaclass tower)
Integer isKindOf: Class // => true (Integer class inherits from Class)
Raise an error with the given message.
Examples
self error: "something went wrong"
Delegate message dispatch to the backing Erlang module (ADR 0101, BT-2720).
This method is a sentinel — a plain Object has no backing Erlang module,
so calling delegate raises an Error at runtime. Stateless Objects
declared with native: have their self delegate method bodies rewritten
by the compiler's codegen phase to call the backing module directly, so the
sentinel is never reached on a native: class.
Unlike Actor's delegate (visible only to Actor subclasses), this
Object-base sentinel is visible to every class, so delegate is a
reserved selector on the Object protocol.
Examples
42 delegate // => ERROR: delegate called on a non-native Object
From ProtoObject
Test value equality (Erlang ==).
Examples
42 == 42 // => true
"abc" == "abc" // => true
Test value inequality (negation of ==).
Examples
1 /= 2 // => true
42 /= 42 // => false
Return the class of the receiver.
Examples
42 class // => Integer
"hello" class // => String
Handle messages the receiver does not understand. Override for custom dispatch.
Examples
42 unknownMessage // => ERROR: does_not_understand
Send a message dynamically with an arguments list.
Examples
42 perform: #abs withArguments: #() // => 42
Execute a class method in the caller's process, bypassing gen_server dispatch.
The caller takes responsibility for knowing the method does not mutate class state. Useful for long-running class methods that would otherwise block the class object's gen_server.
Limitations: only resolves methods defined directly on the target class
module (does not walk the superclass chain). Class variables and self
are not available to the method (nil and #{} are passed).
Examples
MyClass performLocally: #run:ctx: withArguments: #(input, ctx)
Send a message dynamically with an arguments list and explicit timeout.
The timeout (in milliseconds or #infinity) applies to the gen_server:call
when the receiver is an actor. For value types, timeout is ignored.
Examples
actor perform: #query withArguments: #(sql) timeout: 30000