Skip to content

Scope inspection

scope

Scope-path resolution for compiled QVR modules.

Every meta-command in the REPL that takes a binding name (:info, :type, :doc, :browse, :where, ...) routes through the resolver in this module. The grammar of a scope path is::

path     ::= name ('::' name)*
name     ::= a valid Python-style identifier

The leftmost segment is looked up against the module's top-level declarations (objects / spaces / morphisms / rules / programs / deductions / signatures / encoders / decoders / losses / bundles / contractions / categories / compositions). Each subsequent segment is looked up against the previous binding's scope view, a Mapping[str, ScopedRef] derived from its AST shape:

  • A program lda(α, β) : ... exposes its typed parameters and every step (sample / observe / let / marginalize / score / return) by the names they bind.
  • A marginalize z : T <- F [over=G] exposes its inner body steps the same way, so lda::z::w works the same as lda::w would at top level.
  • A deduction CCG exposes rules and atoms as sub-bindings; each rule under rules exposes premises and conclusion for further drill-down.
  • A signature LF exposes sorts / constructors / binders / vertex_kinds / edge_kinds as sub-maps.
  • encoder / decoder / bundle / inline-bodied composition declarations expose the named entries from their bodies (op_rules, member rule names, tensor_op / join / unit / zero).

Container kinds that do not introduce names (rule premise patterns, let value expressions, leaf morphisms) return an empty scope; the resolver stops walking at them.

The resolver is purely structural. It does not recompile the program; it reads the already-elaborated AST nodes from the Compiler instance the REPL hands it. Consumers that need runtime values (a sample site's actual tensor, a deduction rule's weight) should look those up via the runtime accessors after locating the binding.

ScopeKind module-attribute

ScopeKind = Literal['object', 'space', 'morphism', 'rule', 'program', 'deduction', 'signature', 'encoder', 'decoder', 'loss', 'bundle', 'contraction', 'composition', 'category', 'param', 'sample-site', 'observe-site', 'let-site', 'marginalize-site', 'score-site', 'return-site', 'atom', 'deduction-rule', 'lexicon-entry', 'sort', 'constructor', 'binder', 'vertex-kind', 'edge-kind', 'op-rule', 'init-rule', 'message-rule', 'update-rule', 'var-init', 'decoder-head', 'composition-entry', 'bundle-member']

The kind tag carried by every ScopedRef, used for pretty-printing, click-target classification, and the :browse section grouping.

TOP_LEVEL_KINDS module-attribute

TOP_LEVEL_KINDS: tuple[ScopeKind, ...] = ('object', 'space', 'morphism', 'rule', 'program', 'deduction', 'signature', 'encoder', 'decoder', 'loss', 'bundle', 'contraction', 'composition', 'category')

The top-level container kinds, those reachable directly from the compiler's public accessors, in the order ReplSession.browse() walks them for output.

SCOPE_SEPARATOR module-attribute

SCOPE_SEPARATOR = '::'

The separator between the segments of a scoped path such as "prog::x".

ScopedRef didactic-model

ScopedRef(*, name: str, kind: ScopeKind, path: str, parent_kind: ScopeKind | None = None, node: object = field(opaque=True))

Bases: Model

One resolved binding.

ATTRIBUTE DESCRIPTION
name

Last path segment. "lda" for lda, "w" for lda::z::w.

TYPE: str

kind

Categorical tag indicating which declaration / step kind this ref points at.

TYPE: ScopeKind

path

The full ::-separated path from the module root.

TYPE: str

parent_kind

None for a top-level binding; otherwise the enclosing container's kind (useful for context-aware rendering: a sample-site belongs to a program or a marginalize-site).

TYPE: ScopeKind | None

scope_children

scope_children(ref: ScopedRef) -> Mapping[str, ScopedRef]

Return the named children of ref's scope, or an empty mapping if ref.kind introduces no further bindings.

Idempotent + pure; callers can cache. Reads only the AST fields exposed on each declaration shape, so adding a new declaration kind requires registering one entry in _SCOPE_DISPATCH.

PARAMETER DESCRIPTION
ref

The binding whose scope is listed.

TYPE: ScopedRef

RETURNS DESCRIPTION
Mapping[str, ScopedRef]

The child bindings, keyed by name.

Source code in src/quivers/analysis/scope.py
def scope_children(ref: ScopedRef) -> Mapping[str, ScopedRef]:
    """Return the named children of ``ref``'s scope, or an empty
    mapping if ``ref.kind`` introduces no further bindings.

    Idempotent + pure; callers can cache. Reads only the AST
    fields exposed on each declaration shape, so adding a new
    declaration kind requires registering one entry in
    ``_SCOPE_DISPATCH``.

    Parameters
    ----------
    ref : ScopedRef
        The binding whose scope is listed.

    Returns
    -------
    Mapping[str, ScopedRef]
        The child bindings, keyed by name.
    """
    builder = _SCOPE_DISPATCH.get(ref.kind)
    if builder is None:
        return {}
    return builder(ref)  # type: ignore[operator]

split_path

split_path(path: str) -> list[str]

Split "a::b::c" into ["a", "b", "c"]. Leading or trailing separators yield empty segments, which the resolver rejects.

PARAMETER DESCRIPTION
path

A SCOPE_SEPARATOR-separated path.

TYPE: str

RETURNS DESCRIPTION
list[str]

The path's segments, in order.

Source code in src/quivers/analysis/scope.py
def split_path(path: str) -> list[str]:
    """Split ``"a::b::c"`` into ``["a", "b", "c"]``. Leading or
    trailing separators yield empty segments, which the resolver
    rejects.

    Parameters
    ----------
    path : str
        A `SCOPE_SEPARATOR`-separated path.

    Returns
    -------
    list[str]
        The path's segments, in order.
    """
    return path.split(SCOPE_SEPARATOR)

resolve_scoped_path

resolve_scoped_path(compiler, path: str) -> ScopedRef | None

Resolve a ::-separated path against compiler's elaborated module. Returns None if any segment fails to resolve.

PARAMETER DESCRIPTION
compiler

A quivers.dsl.compiler.Compiler instance whose compile_env() has run.

path

A non-empty ::-separated string.

TYPE: str

RETURNS DESCRIPTION
ScopedRef | None

The terminal binding, or None if any segment doesn't exist in its parent's scope.

Source code in src/quivers/analysis/scope.py
def resolve_scoped_path(compiler, path: str) -> ScopedRef | None:  # type: ignore[no-untyped-def]
    """Resolve a ``::``-separated path against ``compiler``'s
    elaborated module. Returns ``None`` if any segment fails to
    resolve.

    Parameters
    ----------
    compiler
        A ``quivers.dsl.compiler.Compiler`` instance whose
        ``compile_env()`` has run.
    path
        A non-empty ``::``-separated string.

    Returns
    -------
    ScopedRef | None
        The terminal binding, or ``None`` if any segment doesn't
        exist in its parent's scope.
    """
    segments = split_path(path)
    if not segments or any(not s for s in segments):
        return None
    current = _top_level_lookup(compiler, segments[0])
    if current is None:
        return None
    for seg in segments[1:]:
        children = scope_children(current)
        if seg not in children:
            return None
        current = children[seg]
    return current

find_all_references

find_all_references(compiler, name: str) -> list[ScopedRef]

Find every binding in compiler's module whose final path segment equals name.

Returns refs sorted by depth (top-level first) then by path lexicographic order. Used by :where NAME to surface every scope a bare name appears in.

PARAMETER DESCRIPTION
compiler

A quivers.dsl.Compiler whose module has been compiled.

TYPE: Compiler

name

The bare name to search for.

TYPE: str

RETURNS DESCRIPTION
list[ScopedRef]

Every binding whose final path segment is name.

Source code in src/quivers/analysis/scope.py
def find_all_references(compiler, name: str) -> list[ScopedRef]:  # type: ignore[no-untyped-def]
    """Find every binding in ``compiler``'s module whose final
    path segment equals ``name``.

    Returns refs sorted by depth (top-level first) then by path
    lexicographic order. Used by ``:where NAME`` to surface every
    scope a bare name appears in.

    Parameters
    ----------
    compiler : Compiler
        A `quivers.dsl.Compiler` whose module has been compiled.
    name : str
        The bare name to search for.

    Returns
    -------
    list[ScopedRef]
        Every binding whose final path segment is ``name``.
    """
    hits: list[ScopedRef] = []
    # Walk every top-level binding and recursively visit scopes.
    # Bounded by the AST size; QVR programs are small.
    seen: set[str] = set()
    for attr, kind in (
        ("objects", "object"),
        ("spaces", "space"),
        ("morphisms", "morphism"),
        ("rules", "rule"),
        ("programs", "program"),
        ("deductions", "deduction"),
        ("signatures", "signature"),
        ("encoders", "encoder"),
        ("decoders", "decoder"),
        ("losses", "loss"),
        ("bundles", "bundle"),
        ("contractions", "contraction"),
    ):
        mapping = getattr(compiler, attr, None) or {}
        for n, node in mapping.items():
            top = ScopedRef(name=n, kind=kind, path=n, parent_kind=None, node=node)
            _collect_named(top, name, hits, seen)
    hits.sort(key=lambda r: (r.path.count(SCOPE_SEPARATOR), r.path))
    return hits