Skip to content

Step resolution

Resolving the distribution references of program steps.

Internal module

This module is an implementation detail of the compiler and its tooling. Its __all__ is empty, so none of the names below are part of the public API, and they may change without notice.

step_resolution

Resolve the distribution references of program steps.

A draw may name a family, a declared morphism, or a let alias. This module resolves those forms to canonical family names and arguments, for the QIEC elaboration and the transpilers alike. It also rejects consumed parameter-source morphisms because their learned weights are absent from the QVR wire representation.

StepResolutionError

StepResolutionError(target: str, kinds: list[str])

Bases: ValueError

A step's distribution reference could not be resolved.

PARAMETER DESCRIPTION
target

The consumer the resolution ran for, such as a transpile target.

TYPE: str

kinds

Structured identifiers of what could not be resolved.

TYPE: list[str]

Source code in src/quivers/dsl/step_resolution.py
def __init__(self, target: str, kinds: list[str]) -> None:
    super().__init__(f"{target}: " + "; ".join(kinds))
    self.target = target
    self.kinds = list(kinds)

ResolvedDist dataclass

ResolvedDist(family: str, args: tuple[str | float, ...], original_morphism_name: str, via: tuple[str, ...] = ())

The (family, args) pair a sample / observe step resolves to.

family is the canonical QVR family name, exactly as it would appear if the user had written sample x <- family(args). args is the tuple of positional arguments after resolution, held in the legacy wire form str | float for backends that have not migrated to the structural DrawArg representation.

The original_morphism_name and via fields are diagnostic: when a sample resolves through a morphism declaration, the transpiled output can reference both the underlying family and the original morphism name (some backends prefer one or the other).

param_source_kind

param_source_kind(decl: MorphismDecl, *, target: str) -> str | None

Return the parameter-source kind a morphism declaration names in its [param_source=...] option, or None when it carries no such option.

Two surface shapes name a kind. The bare identifier ([param_source=mlp]) parses to an OptionName whose value is the kind; the parameterised call ([param_source=mlp(16, 8)]) parses to an OptionCall whose func is the kind and whose args are the hidden widths. Both denote the same source architecture, so both resolve to the same kind string and hit the same transpile boundary.

Any other option shape (a bare flag, a number, a list) names no architecture, so there is nothing for the message to report and nothing a target could be told it is missing. That is a malformed declaration rather than an unsupported one, and it is rejected here by name instead of reaching a target.

Source code in src/quivers/dsl/step_resolution.py
def param_source_kind(decl: MorphismDecl, *, target: str) -> str | None:
    """Return the parameter-source kind a morphism declaration names
    in its ``[param_source=...]`` option, or ``None`` when it carries
    no such option.

    Two surface shapes name a kind. The bare identifier
    (``[param_source=mlp]``) parses to an
    [`OptionName`][quivers.dsl.ast_nodes._shared.OptionName] whose
    ``value`` is the kind; the parameterised call
    (``[param_source=mlp(16, 8)]``) parses to an
    [`OptionCall`][quivers.dsl.ast_nodes._shared.OptionCall] whose
    ``func`` is the kind and whose ``args`` are the hidden widths.
    Both denote the same source architecture, so both resolve to the
    same kind string and hit the same transpile boundary.

    Any other option shape (a bare flag, a number, a list) names no
    architecture, so there is nothing for the message to report and
    nothing a target could be told it is missing. That is a malformed
    declaration rather than an unsupported one, and it is rejected
    here by name instead of reaching a target.
    """
    for entry in decl.options:
        if entry.key != "param_source":
            continue
        value = entry.value
        if isinstance(value, OptionName):
            return value.value
        if isinstance(value, OptionCall):
            return value.func
        raise StepResolutionError(
            target,
            [
                f"param-source:unnamed:{decl.names[0]}",
            ],
        )
    return None

morphism_table

morphism_table(module: Module) -> dict[str, MorphismDecl]

Return name → MorphismDecl for every morphism declaration in module, without the parameter-source boundary check.

A plural-name declaration contributes one entry per name (each name is an independent morphism with the same signature and init). Duplicate names are an error.

PARAMETER DESCRIPTION
module

The parsed module.

TYPE: Module

RETURNS DESCRIPTION
dict[str, MorphismDecl]

The declaration each morphism name denotes.

RAISES DESCRIPTION
StepResolutionError

If a morphism name is declared twice.

Source code in src/quivers/dsl/step_resolution.py
def morphism_table(module: Module) -> dict[str, MorphismDecl]:
    """Return name → MorphismDecl for every morphism declaration in
    ``module``, without the parameter-source boundary check.

    A plural-name declaration contributes one entry per name (each
    name is an independent morphism with the same signature and
    init). Duplicate names are an error.

    Parameters
    ----------
    module
        The parsed module.

    Returns
    -------
    dict[str, MorphismDecl]
        The declaration each morphism name denotes.

    Raises
    ------
    StepResolutionError
        If a morphism name is declared twice.
    """
    out: dict[str, MorphismDecl] = {}
    for stmt in module.statements:
        if isinstance(stmt, MorphismDecl):
            for name in stmt.names:
                if name in out:
                    msg = (
                        f"duplicate morphism declaration {name!r}: "
                        f"first at line {out[name].line}, again at "
                        f"line {stmt.line}"
                    )
                    raise StepResolutionError("qvr-transpile", [msg])
                out[name] = stmt
    return out

build_morphism_table

build_morphism_table(module: Module) -> dict[str, MorphismDecl]

Return name → MorphismDecl for every morphism declaration in module, checked for the transpile boundary.

The table is morphism_table's; building it is also where a module is checked for parameter-source morphisms it consumes, in a site, value or composite position; see _reject_param_source_consumed. The table is the resolver's authority on what each name denotes, so it is the one place that sees every declaration at once, which is what deciding a module-wide boundary needs.

PARAMETER DESCRIPTION
module

The parsed module.

TYPE: Module

RETURNS DESCRIPTION
dict[str, MorphismDecl]

The declaration each morphism name denotes.

RAISES DESCRIPTION
StepResolutionError

If a morphism name is declared twice, or a parameter-source morphism is consumed.

Source code in src/quivers/dsl/step_resolution.py
def build_morphism_table(module: Module) -> dict[str, MorphismDecl]:
    """Return name → MorphismDecl for every morphism declaration in
    ``module``, checked for the transpile boundary.

    The table is [`morphism_table`][quivers.dsl.step_resolution.morphism_table]'s;
    building it is also where a module is checked for parameter-source
    morphisms it consumes, in a site, value or composite position; see
    ``_reject_param_source_consumed``. The table is the resolver's
    authority on what each name denotes, so it is the one place that
    sees every declaration at once, which is what deciding a
    module-wide boundary needs.

    Parameters
    ----------
    module
        The parsed module.

    Returns
    -------
    dict[str, MorphismDecl]
        The declaration each morphism name denotes.

    Raises
    ------
    StepResolutionError
        If a morphism name is declared twice, or a parameter-source
        morphism is consumed.
    """
    out = morphism_table(module)
    _reject_param_source_consumed(module, out)
    return out

build_let_table

build_let_table(module: Module) -> dict[str, Expr]

Return name → expr for every top-level let_decl.

Source code in src/quivers/dsl/step_resolution.py
def build_let_table(module: Module) -> dict[str, Expr]:
    """Return name → expr for every top-level ``let_decl``."""
    out: dict[str, Expr] = {}
    for stmt in module.statements:
        if isinstance(stmt, DefineDecl):
            out[stmt.name] = stmt.expr
    return out

resolve_step_dist

resolve_step_dist(morphism_name: str, raw_args: tuple[DrawArg, ...] | None, *, morphisms: dict[str, MorphismDecl], lets: dict[str, Expr], family_registry: frozenset[str], target: str, _seen: tuple[str, ...] = ()) -> ResolvedDist

Resolve a sample / observe step's morphism slot to a (family, args) pair.

PARAMETER DESCRIPTION
morphism_name

The string in SampleStep.morphism / ObserveStep.morphism. Either a family name, a declared morphism's name, or a let-binding name.

TYPE: str

raw_args

Positional arguments on the step (None is treated as empty). When the resolver unfolds a morphism with its own ~ Family(args) init clause, the step-supplied raw_args take precedence over the declaration's defaults; in current practice the step does not supply args when referring to a morphism (the morphism's args carry the parameter set).

TYPE: tuple[DrawArg, ...] | None

morphisms

Name → MorphismDecl table from build_morphism_table.

TYPE: dict[str, MorphismDecl]

lets

Name → Expr table from build_let_table.

TYPE: dict[str, Expr]

family_registry

Frozen set of canonical QVR family names. When morphism_name is in this set, the resolver returns immediately with the step's own args.

TYPE: frozenset[str]

target

Backend name (for error messages).

TYPE: str

_seen

Internal: the chain of names visited during resolution. The resolver detects cycles (let a = b; let b = a) by membership in this tuple.

TYPE: tuple[str, ...] DEFAULT: ()

Source code in src/quivers/dsl/step_resolution.py
def resolve_step_dist(
    morphism_name: str,
    raw_args: tuple[DrawArg, ...] | None,
    *,
    morphisms: dict[str, MorphismDecl],
    lets: dict[str, Expr],
    family_registry: frozenset[str],
    target: str,
    _seen: tuple[str, ...] = (),
) -> ResolvedDist:
    """Resolve a sample / observe step's morphism slot to a
    ``(family, args)`` pair.

    Parameters
    ----------
    morphism_name
        The string in ``SampleStep.morphism`` /
        ``ObserveStep.morphism``. Either a family name, a declared
        morphism's name, or a let-binding name.
    raw_args
        Positional arguments on the step (``None`` is treated as
        empty). When the resolver unfolds a morphism with its own
        ``~ Family(args)`` init clause, the step-supplied ``raw_args``
        take precedence over the declaration's defaults; in current
        practice the step does not supply args when referring to a
        morphism (the morphism's args carry the parameter set).
    morphisms
        Name → MorphismDecl table from
        [`build_morphism_table`][quivers.dsl.step_resolution.build_morphism_table].
    lets
        Name → Expr table from
        [`build_let_table`][quivers.dsl.step_resolution.build_let_table].
    family_registry
        Frozen set of canonical QVR family names. When
        ``morphism_name`` is in this set, the resolver returns
        immediately with the step's own args.
    target
        Backend name (for error messages).
    _seen
        Internal: the chain of names visited during resolution. The
        resolver detects cycles (``let a = b; let b = a``) by
        membership in this tuple.
    """
    if morphism_name in family_registry:
        wire: tuple[str | float, ...]
        if raw_args:
            wire = tuple(_draw_arg_to_wire(a) for a in raw_args)
        else:
            wire = _FAMILY_DEFAULT_ARGS.get(morphism_name, ())
        return ResolvedDist(
            family=morphism_name,
            args=wire,
            original_morphism_name=morphism_name,
        )

    if morphism_name in _seen:
        msg = (
            f"morphism / let cycle while resolving {morphism_name!r}; "
            f"chain: {' -> '.join((*_seen, morphism_name))}"
        )
        raise StepResolutionError(target, [msg])

    chain = (*_seen, morphism_name)

    if morphism_name in morphisms:
        decl = morphisms[morphism_name]
        param_source = param_source_kind(decl, target=target)
        if param_source is not None:
            msg = (
                f"morphism {morphism_name!r} draws its parameters from a "
                f"{param_source!r} network. The network's weights are "
                f"model-internal and appear in neither the wire form nor the "
                f"sample sites, so no backend can reconstruct the mean the "
                f"morphism computes. Express the network as explicit sampled "
                f"weights and a deterministic forward pass, or observe against "
                f"a closed-form family."
            )
            raise StepResolutionError(target, [f"param-source:{param_source}", msg])
        if decl.init_family is not None:
            return _from_init_family(
                morphism_name=morphism_name,
                init=decl.init_family,
                step_args=raw_args,
                chain=chain,
                morphism_options=decl.options,
            )
        if decl.init_expr is not None:
            # When the init expression is a bare family identifier
            # (`morphism foo : T -> T [scale=0.1] ~ Normal`) the
            # parser models it as an `init_expr=ExprIdent("Normal")`
            # rather than `init_family=Normal`. Route this case
            # through the same option-aware family-merge that
            # `_from_init_family` uses so the morphism's option
            # block (`[scale=0.1]`) populates the matching family
            # parameter slot.
            if (
                isinstance(decl.init_expr, ExprIdent)
                and decl.init_expr.name in family_registry
            ):
                return _from_init_family(
                    morphism_name=morphism_name,
                    init=MorphismInitFamily(
                        family=decl.init_expr.name,
                        args=(),
                    ),
                    step_args=raw_args,
                    chain=chain,
                    morphism_options=decl.options,
                )
            return _resolve_expr(
                morphism_name=morphism_name,
                expr=decl.init_expr,
                raw_args=raw_args,
                morphisms=morphisms,
                lets=lets,
                family_registry=family_registry,
                target=target,
                chain=chain,
            )
        if _role_of(decl) == "embed":
            msg = (
                f"morphism {morphism_name!r} is an embedding, which places a "
                f"Gaussian kernel at each element's learned centre; the "
                f"centres and scales are model-internal tables that appear "
                f"in neither the wire form nor the sample sites, so no "
                f"backend can reconstruct the kernel."
            )
            raise StepResolutionError(target, [f"embed:{morphism_name}", msg])
        msg = (
            f"morphism {morphism_name!r} has neither `~ Family(...)` "
            f"nor `~ <expr>` init clause; transpile cannot derive a "
            f"distribution"
        )
        raise StepResolutionError(target, [msg])

    if morphism_name in lets:
        return _resolve_expr(
            morphism_name=morphism_name,
            expr=lets[morphism_name],
            raw_args=raw_args,
            morphisms=morphisms,
            lets=lets,
            family_registry=family_registry,
            target=target,
            chain=chain,
        )

    msg = (
        f"sample / observe step references {morphism_name!r} which "
        f"is neither a family in the registry, a declared morphism, "
        f"nor a let-bound name"
    )
    raise StepResolutionError(target, [f"family:{morphism_name}", msg])