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
- sendersOf: aSelector
- collectSendersFor: aSelector in: aScope into: result
- appendSendersIn: methodSource selector: aSelector class: aClass methodSelector: methodSel into: result
- 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
- extensionSelectorsFor: classTag
- collectExtensionSendersFor: aSelector into: result
- collectExtensionReferencesFor: targetName into: result
- collectExtensionMethodsMatching: aRegex into: result
- unimplementedSelectors
- 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
- 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
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. Iterates the navigation scope so stdlib and user classes are both in the search set.
Matches Pharo's SystemNavigation >> #allImplementorsOf:.
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 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. Iterates the navigation scope and walks both instance-side
and class-side method dictionaries (BT-2195), so stdlib and user
classes are both covered on both sides. Matches Pharo's
SystemNavigation >> #allSendersOf:.
Naive implementation: parses every method's source on every call. Fine
for an on-demand IDE pane (a few seconds on a 200-class workspace) but
not for hot live-highlight paths. A maintained selector -> [sites]
index is a follow-up optimization.
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 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 // => []
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 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. Iterates the navigation scope and walks both instance-side
and class-side method dictionaries (BT-2195), so stdlib and user
classes are both covered on both sides. Class names that appear in
type annotations (parameter, return, or generic parameters such as
List(Counter)) are also matched. Matches Pharo's
SystemNavigation >> #allReferencesTo:.
Naive implementation: parses every method's source on every call. Same
performance envelope as sendersOf:. A maintained
class -> [sites] index is a follow-up optimization.
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.
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. Pure registry walk — no AST parsing required. For each class in the navigation scope, collects instance-side selectors, class-side selectors, and extension-method selectors (ADR 0066) registered on either side, then filters by lowercase substring match.
Matching uses selectorString lowercase includesSubstring: aPattern lowercase, mirroring Pharo's SystemNavigation >> #allSelectorsContaining:.
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 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.
Single-pass design
Computes allSentSelectors − allDefinedSelectors. The defined set is the
union of selectorsForClass: over every class in the navigation scope
(instance-side, class-side, and extension selectors). The sent set is
gathered by asking the compiler, once per method source, for EVERY send
via (Erlang beamtalk_interface) allSendsIn: — a single AST walk per
method rather than one walk per candidate selector. Sends whose selector
is already in the defined set are dropped; the rest are grouped by
selector into the result.
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
Naive: parses every method's source on every call (same envelope as
sendersOf: — a few seconds on a 200-class workspace). Fine for an
on-demand IDE lint; a maintained send/define index is a follow-up.
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}]}]
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.
Single-pass design
Computes allDefinedSelectors − allSentSelectors. The sent set is gathered
by asking the compiler, once per method source, for EVERY send via
(Erlang beamtalk_interface) allSendsIn: — a single AST walk per method
rather than one walk per candidate selector, exactly as
unimplementedSelectors does. The defined candidates are built by walking
cls methods (instance-side, owner cls) and cls class methods
(class-side, owner cls class) directly, because each candidate must
remember its owning class — the flattened selectorsForClass: discards
that. A candidate survives into the result only if its selector is absent
from the sent set and survives every exclusion below.
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
Naive: parses every method's source on every call (same envelope as
sendersOf: / unimplementedSelectors — a few seconds on a 200-class
workspace). Fine for an on-demand IDE lint; a maintained send/define index
is a follow-up.
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}, ...]
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.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).
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 a developer-readable string representation.
Default implementation returns "a ClassName". Subclasses such as
Integer, String, and List override this to return richer output.
Examples
42 printString // => "42"
Return a user-facing string representation for display purposes.
Default implementation delegates to printString. Subclasses such as
String and Symbol override this to return a more readable form without
developer annotations (e.g. no surrounding quotes or # prefix).
Examples
42 displayString // => "42"
Inspect the receiver.
Examples
42 inspect // => "42"
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"
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