SystemNavigation

Inherits from Object
Sealed

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 Methods

default source

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

allClasses source

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.

actorClasses source

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, ...]
dnuHandlers source

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, ...]
extendersOf: aClass source

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  // => []
extensionsBy: aPackage source

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}, ...]
requireClassName: aClass selector: aSelector source

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).

classesInPackage: aPackageName source

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, ...]
subclassesOf: aClass in: aPackageName source

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]
requirePackage: aPackageName source

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:.

implementorsOf: aSelector source

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 // => []
sendersOf: aSelector source

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  // => []
collectSendersFor: aSelector in: aScope into: result source

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.

appendSendersIn: methodSource selector: aSelector class: aClass methodSelector: methodSel into: result source

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.

referencesTo: aClass source

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  // => []
collectReferencesFor: targetName in: aScope into: result source

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.

appendReferencesIn: methodSource targetName: targetName owner: ownerClass methodSelector: methodSel into: result source

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:.

fieldReadersOf: aSym in: aClass source

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  // => []
fieldWritersOf: aSym in: aClass source

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  // => []
collectFieldAccessesOf: aSym in: aClass readers: wantReaders source

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: aSym must be a declared slot of aClass — 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 [].
collectFieldAccessesFor: aSym in: aScope readers: wantReaders into: result source

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:.

appendFieldAccessesIn: methodSource field: aSym owner: ownerClass methodSelector: methodSel readers: wantReaders into: result source

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.

ffiSitesFor: aSpec source

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"  // => []
parseFfiSpec: aSpec source

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.

raiseFfiSpecError: aSpec source

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:).

collectFfiSitesFor: module function: function arity: arity in: aScope into: result source

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.

appendFfiSitesIn: methodSource module: module function: function arity: arity owner: ownerClass methodSelector: methodSel into: result source

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:.

collectExtensionFfiSitesFor: module function: function arity: arity into: result source

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.

methodsMatching: aRegex source

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
  // => []
collectMethodsMatching: aRegex in: aScope into: result source

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.

selectorsMatching: aPattern source

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"
  // => []
selectorsForClass: aClass source

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.)

extensionSelectorsFor: classTag source

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.

collectExtensionSendersFor: aSelector into: result source

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).

collectExtensionReferencesFor: targetName into: result source

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:.

collectExtensionMethodsMatching: aRegex into: result source

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.

unimplementedSelectors source

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 override doesNotUnderstand:args: can legitimately receive arbitrary selectors via self/super (the DNU handler interprets them dynamically), so sends to self/super inside such a class are not flagged. This is a coarse heuristic: it does not know the runtime receiver type, so it only suppresses self/super sends in the defining class, not sends to other instances of it. Proper receiver-type analysis is out of scope.
  • perform: — the dynamic selector argument to perform: / 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 FFIErlang module … and (Erlang module) selector: … are Erlang function calls routed through ErlangModule's doesNotUnderstand: 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 static Erlang … 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 methods dictionary (e.g. codegen-inlined pseudo-selectors or primitives registered under a different name). See runtimeProvidedSelectors.

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}]}]
runtimeProvidedSelectors source

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.)

collectAllSendsIn: aScope excluding: excluded into: result source

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.

appendAllSendsIn: methodSource owner: ownerClass methodSelector: methodSel handlesDnu: handlesDnu excluding: excluded into: result source

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:

  • sentSel is in excluded (already defined, or runtime-provided), or
  • the owner defines doesNotUnderstand: and the receiver is #self / #super (the coarse DNU heuristic — see unimplementedSelectors).
recordSend: sentSel line: line recv: recv owner: ownerClass methodSelector: methodSel handlesDnu: handlesDnu excluding: excluded into: result source

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.

collectExtensionAllSendsExcluding: excluded into: result source

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.

resolveExtensionClass: classTag source

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 keeps record at: #class comparable 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.

unusedSelectors source

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_ffi at 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 / #other sends ARE counted: a self-send of foo means foo is 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 TestCase and any class that inherits from it (an includesBehaviour: TestCase check, self-inclusive), the methods setUp / tearDown and every test… method are invoked reflectively by beamtalk_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 named test… is still eligible to be flagged.
  • Primitive / intrinsic methods — a method whose source is nil or whose body contains @primitive / @intrinsic is 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 / @intrinsic annotation text in the method source rather than a structural AST flag.
  • perform: is invisible. A selector reached only via self 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}, ...]
runtimeCalledSelectors source

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 (see Actor >> #initialize).
  • terminate: — Actor lifecycle hook called by the runtime on shutdown (see Actor >> #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.
collectSentSelectorsIn: aScope into: result source

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.

addSentSelectorsIn: methodSource into: result source

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).

collectExtensionSentSelectorsInto: result source

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.

collectUnusedFor: aScope owner: owner sent: sent excluded: excluded into: result source

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.

recordUnused: methodSel scope: aScope owner: owner isTestCase: isTestCase sent: sent excluded: excluded into: result source

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).

isTestRunnerSelector: aSelector source

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.

isPrimitiveMethod: methodSel in: aScope source

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

class

Return the class of the receiver.

Examples

42 class              // => Integer
"hello" class         // => String
isNil

Test if the receiver is nil. Returns false for all objects except nil.

Examples

42 isNil              // => false
nil isNil             // => true
notNil

Test if the receiver is not nil. Returns true for all objects except nil.

Examples

42 notNil             // => true
nil notNil            // => false
ifNil: _nilBlock

If the receiver is nil, evaluate nilBlock. Otherwise return self.

Examples

42 ifNil: [0]         // => 42
nil ifNil: [0]        // => 0
ifNotNil: notNilBlock

If the receiver is not nil, evaluate notNilBlock with self.

Examples

42 ifNotNil: [:v | v + 1]   // => 43
nil ifNotNil: [:v | v + 1]  // => nil
ifNil: _nilBlock ifNotNil: notNilBlock

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
ifNotNil: notNilBlock ifNil: _nilBlock

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
printString

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"
displayString

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

Inspect the receiver.

Examples

42 inspect             // => "42"
yourself Sealed

Return the receiver itself. Useful for cascading side effects.

Examples

42 yourself            // => 42
hash

Return a hash value for the receiver.

Examples

42 hash
respondsTo: selector Sealed

Test if the receiver responds to the given selector.

Examples

42 respondsTo: #abs    // => true
fieldNames Sealed

Return the names of fields.

Examples

42 fieldNames             // => #()
fieldAt: name Sealed

Return the value of the named field.

Examples

object fieldAt: #name
fieldAt: name put: value Sealed

Set the value of the named field (returns new state).

Examples

object fieldAt: #name put: "Alice"
perform: selector Sealed

Send a unary message dynamically.

Examples

42 perform: #abs       // => 42
perform: selector withArguments: args Sealed

Send a message dynamically with arguments.

Examples

3 perform: #max: withArguments: #(5)   // => 5
subclassResponsibility

Raise an error indicating this method must be overridden by a subclass.

Examples

self subclassResponsibility
notImplemented

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
show: aValue

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: "
showCr: aValue

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"
isKindOf: aClass

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)
error: message

Raise an error with the given message.

Examples

self error: "something went wrong"

From ProtoObject

== other

Test value equality (Erlang ==).

Examples

42 == 42           // => true
"abc" == "abc"     // => true
/= other

Test value inequality (negation of ==).

Examples

1 /= 2             // => true
42 /= 42           // => false
class

Return the class of the receiver.

Examples

42 class            // => Integer
"hello" class       // => String
doesNotUnderstand: selector args: arguments

Handle messages the receiver does not understand. Override for custom dispatch.

Examples

42 unknownMessage   // => ERROR: does_not_understand
perform: selector withArguments: arguments

Send a message dynamically with an arguments list.

Examples

42 perform: #abs withArguments: #()   // => 42
performLocally: selector withArguments: arguments

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)
perform: selector withArguments: arguments timeout: timeoutMs

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