Skip to content

Lowering support

The support tables the plan derivation reads: object shapes and bounds, family sentinels, argument constraints, and the wire forms they resolve to. The module is implementation and exports no public names; the lowering entry point is Lower.

lower

The support tables of the plan.

A program's plan is derived from its checked kernel module by quivers.transpile.plan; this module holds what the derivation reads beside the module: the shapes, cardinalities, and declared bounds of the source's objects, the family metadata's sentinel instances and the argument constraints and supports read off them, the wire forms an argument takes to satisfy a constraint, and the alphabet of a class-index draw. Renderers consume the plan and do not materialize PyTorch tensors.

The tables are implementation of the plan derivation and contribute no public names; Lower is the entry point.

RealBounds didactic-model

RealBounds(*, low: float | None = None, high: float | None = None)

Bases: Model

The {low=..., high=...} bounds a continuous object declares.

object Rate : Real 3 {low=0.0, high=1.0} names the box [0, 1]^3 rather than R^3, so a variable whose event axis or per-row value space is Rate lives in that box. Renderers turn the pair into the target's own bounded declaration (Stan's <lower=0, upper=1>, JAGS' T(0, 1), ...); a variant with neither bound set is the unbounded default.

is_bounded property

is_bounded: bool

True when the declaration constrains at least one side.

ObjectShape didactic-model

ObjectShape(*, extent: int | None = None, real_width: int | None = None, finite: bool = False, bounds: RealBounds = field(default_factory=RealBounds))

Bases: Model

The transpile-visible shape of one object declaration.

Three independent readings, one per position an object name can occupy (see the module docstring):

  • extent is the size the object contributes as an axis: the cardinality of a finite object (FinSet N, an enum set, a product of finite factors) and the total coordinate count of a continuous one (Real 28 28 is 784 coordinates wide).
  • real_width is set only for Real, the one constructor whose value is a plain real vector, and is what a program-domain factor or a morphism codomain reads to size its wire.
  • bounds carries the constructor's {low=..., high=...}.

finite separates the two readings of extent: a finite object has |A| elements, so its extent is an alphabet a class index can range over, while a continuous one has extent coordinates and names no alphabet at all.

pick_program

pick_program(module: Module) -> ProgramDecl

Pick the ProgramDecl the module's export designates.

When the module declares multiple programs, prefer one referenced by an export declaration; otherwise pick the last declared program.

An export naming a define binding rather than a program is not a lowering target: a define is a morphism-level composition (scan(cell) >> decoder), and the transpile boundary is the probabilistic program. A define a step references is unfolded by expand_composite_lets at the call site instead, so the recurrence a scan denotes reaches the IR through the program that samples it.

Source code in src/quivers/transpile/lower.py
def pick_program(module: Module) -> ProgramDecl:
    """Pick the `ProgramDecl` the module's `export` designates.

    When the module declares multiple programs, prefer one
    referenced by an `export` declaration; otherwise pick the
    last declared program.

    An `export` naming a `define` binding rather than a program
    is not a lowering target: a `define` is a morphism-level
    composition (`scan(cell) >> decoder`), and the transpile
    boundary is the probabilistic program. A `define` a step
    references is unfolded by
    [`expand_composite_lets`][quivers.dsl.composite_lets.expand_composite_lets]
    at the call site instead, so the recurrence a `scan` denotes
    reaches the IR through the program that samples it.
    """
    programs: list[ProgramDecl] = []
    exported_names: set[str] = set()
    for stmt in module.statements:
        if isinstance(stmt, ProgramDecl):
            programs.append(stmt)
        elif isinstance(stmt, ExportDecl) and isinstance(stmt.expr, ExprIdent):
            exported_names.add(stmt.expr.name)
    if not programs:
        raise UnsupportedConstruct(
            "qvr-lower",
            ["program:absent"],
        )
    return next(
        (p for p in programs if p.name in exported_names),
        programs[-1],
    )

exported_return_names

exported_return_names(module: Module) -> tuple[str, ...]

The variable names the module's exported program returns.

A QVR program declared prog : A -> B denotes a Markov kernel from A to B, and its return clause names the components of the value in B that the kernel carries. Those names are what a faithful emit has to expose through the target's own return surface, so they are the contract the export-equivalence tier checks a backend against. An empty tuple means the program declares no return clause and denotes only its joint.

Source code in src/quivers/transpile/lower.py
def exported_return_names(module: Module) -> tuple[str, ...]:
    """The variable names the module's exported program returns.

    A QVR program declared `prog : A -> B` denotes a Markov kernel
    from `A` to `B`, and its `return` clause names the components of
    the value in `B` that the kernel carries. Those names are what a
    faithful emit has to expose through the target's own return
    surface, so they are the contract the export-equivalence tier
    checks a backend against. An empty tuple means the program
    declares no `return` clause and denotes only its joint.
    """
    return tuple(pick_program(module).return_vars)

object_shapes

object_shapes(module: Module) -> dict[str, ObjectShape]

Return name -> ObjectShape for every object declaration in module, in source order.

Declarations are read in order so a later one can size itself from an earlier name (object M : FinSet N5, object Grid : Real Rows Cols) exactly as the runtime's resolver does. A declaration whose value has no static size (a FreeResiduated universe, a free monoid, a residuated slash) contributes no entry, and every downstream caller treats an absent name the way it always has.

Source code in src/quivers/transpile/lower.py
def object_shapes(module: Module) -> dict[str, ObjectShape]:
    """Return name -> [`ObjectShape`][quivers.transpile.lower.ObjectShape]
    for every `object` declaration in `module`, in source order.

    Declarations are read in order so a later one can size itself
    from an earlier name (``object M : FinSet N5``,
    ``object Grid : Real Rows Cols``) exactly as the runtime's
    resolver does. A declaration whose value has no static size (a
    `FreeResiduated` universe, a free monoid, a residuated slash)
    contributes no entry, and every downstream caller treats an
    absent name the way it always has.
    """
    out: dict[str, ObjectShape] = {}
    for stmt in module.statements:
        if not isinstance(stmt, ObjectDecl):
            continue
        init = stmt.init
        shape: ObjectShape | None = None
        if isinstance(init, TypeEnumSet):
            shape = ObjectShape(
                extent=len(init.elements),
                finite=True,
            )
        elif isinstance(init, TypeFromExpr):
            shape = _object_expr_shape(init.expr, out, stmt.names)
        if shape is None:
            continue
        for name in stmt.names:
            out[name] = shape
    return out

continuous_object_widths

continuous_object_widths(module: Module) -> dict[str, int]

Return name -> width for every Real ... object declaration.

object State : Real 4 names a value in R^4: a program with that object in its domain is applied to a 4-wide vector, and a morphism with it as codomain writes one. object Img : Real 28 28 names a value in R^784, the runtime's own reading of a multi-argument Real, so the width is the product of the arguments rather than the first of them. object Step : FinSet 64 names an index axis instead, so it is absent here even though object_cardinalities records its 64. The other continuous constructors (Simplex, Sphere, Covariance, ...) carry a support a plain real vector does not describe, so they are absent as well and a caller that needs them has to read the constructor itself.

Source code in src/quivers/transpile/lower.py
def continuous_object_widths(module: Module) -> dict[str, int]:
    """Return name -> width for every ``Real ...`` object declaration.

    ``object State : Real 4`` names a value in R^4: a program with
    that object in its domain is applied to a 4-wide vector, and a
    morphism with it as codomain writes one. ``object Img : Real 28
    28`` names a value in R^784, the runtime's own reading of a
    multi-argument `Real`, so the width is the product of the
    arguments rather than the first of them. ``object Step : FinSet
    64`` names an index axis instead, so it is absent here even
    though
    [`object_cardinalities`][quivers.transpile.lower.object_cardinalities]
    records its 64. The other continuous constructors (``Simplex``,
    ``Sphere``, ``Covariance``, ...) carry a support a plain real
    vector does not describe, so they are absent as well and a caller
    that needs them has to read the constructor itself.
    """
    return {
        name: shape.real_width
        for name, shape in object_shapes(module).items()
        if shape.real_width is not None
    }

object_bounds

object_bounds(module: Module) -> dict[str, RealBounds]

Return name -> RealBounds for every continuous object that declares {low=...} or {high=...}. Objects with neither bound are absent.

Source code in src/quivers/transpile/lower.py
def object_bounds(module: Module) -> dict[str, RealBounds]:
    """Return name -> [`RealBounds`][quivers.transpile.lower.RealBounds]
    for every continuous object that declares ``{low=...}`` or
    ``{high=...}``. Objects with neither bound are absent."""
    return {
        name: shape.bounds
        for name, shape in object_shapes(module).items()
        if shape.bounds.is_bounded
    }

object_factors

object_factors(expr: ObjectExpr) -> tuple[ObjectExpr, ...]

Flatten a product object expression into its factors, in source order. A non-product expression is its own single factor.

Source code in src/quivers/transpile/lower.py
def object_factors(expr: ObjectExpr) -> tuple[ObjectExpr, ...]:
    """Flatten a product object expression into its factors, in
    source order. A non-product expression is its own single
    factor."""
    if isinstance(expr, ObjectProduct):
        out: list[ObjectExpr] = []
        for comp in expr.components:
            out.extend(object_factors(comp))
        return tuple(out)
    return (expr,)

object_cardinalities

object_cardinalities(module: Module) -> dict[str, int]

Return name -> axis extent for every statically-sized object declaration.

The extent is what the object contributes in an axis position: the cardinality of a finite object (FinSet N, an enum set, a product of finite factors) and the coordinate count of a continuous one (Real 28 28 is 784 wide). An object whose value has no static size contributes no entry.

Source code in src/quivers/transpile/lower.py
def object_cardinalities(module: Module) -> dict[str, int]:
    """Return name -> axis extent for every statically-sized object
    declaration.

    The extent is what the object contributes in an *axis* position:
    the cardinality of a finite object (`FinSet N`, an enum set, a
    product of finite factors) and the coordinate count of a
    continuous one (`Real 28 28` is 784 wide). An object whose value
    has no static size contributes no entry.
    """
    return {
        name: shape.extent
        for name, shape in object_shapes(module).items()
        if shape.extent is not None
    }

axis_shape

axis_shape(expr: ObjectExpr, cards: dict[str, int]) -> int | None

Return the cardinality of an axis object expression.

A product axis (A * B) is the flattened cardinality of its factors, matching the runtime's ProductSet; a factor whose size cards does not record makes the whole product unsized.

Source code in src/quivers/transpile/lower.py
def axis_shape(expr: ObjectExpr, cards: dict[str, int]) -> int | None:
    """Return the cardinality of an axis object expression.

    A product axis (`A * B`) is the flattened cardinality of its
    factors, matching the runtime's `ProductSet`; a factor whose
    size `cards` does not record makes the whole product unsized.
    """
    if isinstance(expr, TypeName):
        if expr.name.isdigit():
            return int(expr.name)
        return cards.get(expr.name)
    if isinstance(expr, DiscreteConstructor) and expr.args:
        try:
            return int(expr.args[0])
        except ValueError:
            return cards.get(expr.args[0])
    if isinstance(expr, ObjectProduct):
        total = 1
        for factor in object_factors(expr):
            size = axis_shape(factor, cards)
            if size is None:
                return None
            total *= size
        return total
    return None

build_shape_table

build_shape_table(program: ProgramDecl, cards: dict[str, int]) -> dict[str, tuple[int, ...]]

Return name -> shape for every sample / observe / let binding in program.

Source code in src/quivers/transpile/lower.py
def build_shape_table(
    program: ProgramDecl, cards: dict[str, int]
) -> dict[str, tuple[int, ...]]:
    """Return name -> shape for every sample / observe / let binding
    in `program`."""
    out: dict[str, tuple[int, ...]] = {}
    for step in program.draws:
        if isinstance(step, SampleStep):
            shape = _step_shape(step.index, cards)
            for v in step.vars:
                out[v] = shape
        elif isinstance(step, ObserveStep):
            out[_observe_var(step)] = _step_shape(step.index, cards)
        elif isinstance(step, LetStep):
            out[step.name] = ()
        elif isinstance(step, MarginalizeStep):
            out[step.var] = _step_shape(step.index, cards)
            inner = build_shape_table(
                ProgramDecl(
                    name=program.name,
                    domain=program.domain,
                    codomain=program.codomain,
                    draws=step.scope,
                ),
                cards,
            )
            out.update(inner)
    return out

exogenous_data_inputs

exogenous_data_inputs(program: ProgramDecl, bound: set[str]) -> list[str]

Return the ordered list of exogenous identifier names referenced in program but not bound by any step.

Sources: free names in let / score expressions; bracket-indexed arg references (mu[cls] -> cls, mu); via= fibrations. Order is deterministic by traversal order.

Source code in src/quivers/transpile/lower.py
def exogenous_data_inputs(program: ProgramDecl, bound: set[str]) -> list[str]:
    """Return the ordered list of exogenous identifier names
    referenced in `program` but not bound by any step.

    Sources: free names in let / score expressions; bracket-indexed
    arg references (`mu[cls]` -> `cls`, `mu`); `via=` fibrations.
    Order is deterministic by traversal order.
    """
    out: list[str] = []
    seen: set[str] = set()

    def consider(name: str) -> None:
        if name in bound or name in seen:
            return
        seen.add(name)
        out.append(name)

    for step in program.draws:
        for n in _names_in_step(step):
            consider(n)
    return out

free_vars_in_let

free_vars_in_let(expr: LetExprNode) -> list[str]

Return the ordered list of free variable names in a let expression tree.

Source code in src/quivers/transpile/lower.py
def free_vars_in_let(expr: LetExprNode) -> list[str]:
    """Return the ordered list of free variable names in a let
    expression tree."""
    out: list[str] = []
    seen: set[str] = set()

    def visit(n: LetExprNode, bound: frozenset[str]) -> None:
        if isinstance(n, LetExprVar):
            if n.name in bound or n.name in seen:
                return
            seen.add(n.name)
            out.append(n.name)
            return
        if isinstance(n, LetExprLiteral):
            return
        if isinstance(n, LetExprString):
            return
        if isinstance(n, LetExprBinOp):
            visit(n.left, bound)
            visit(n.right, bound)
            return
        if isinstance(n, LetExprUnaryOp):
            visit(n.operand, bound)
            return
        if isinstance(n, LetExprCall):
            for a in n.args:
                visit(a, bound)
            return
        if isinstance(n, LetExprIndex):
            visit(n.array, bound)
            for i in n.indices:
                visit(i, bound)
            return
        if isinstance(n, LetExprList):
            for it in n.items:
                visit(it, bound)
            return
        if isinstance(n, LetExprLambda):
            visit(n.body, bound | frozenset({n.param}))
            return
        if isinstance(n, LetExprFactor):
            inner_bound = bound | frozenset(b.var for b in n.binders)
            if n.body is not None:
                visit(n.body, inner_bound)
            for case in n.cases:
                visit(case.value, inner_bound)
            return
        if isinstance(n, LetExprMethodCall):
            visit(n.receiver, bound)
            for a in n.args:
                visit(a, bound)
            return
        if isinstance(n, LetExprAffineMap):
            visit(n.weight, bound)
            for source in n.sources:
                visit(source.value, bound)
            visit(n.bias, bound)
            return

    visit(expr, frozenset())
    return out

free_names_in_arg

free_names_in_arg(arg: IRArg) -> list[str]

Return the ordered list of free names in an IR arg tree.

Source code in src/quivers/transpile/lower.py
def free_names_in_arg(arg: IRArg) -> list[str]:
    """Return the ordered list of free names in an IR arg tree."""
    out: list[str] = []
    seen: set[str] = set()

    def visit(a: IRArg) -> None:
        if isinstance(a, IRArgRef):
            if a.name not in seen:
                seen.add(a.name)
                out.append(a.name)
            for i in a.indices:
                visit(i)
            return
        if isinstance(a, IRArgBroadcast):
            visit(a.value)
            return
        if isinstance(a, IRArgList):
            for e in a.elements:
                visit(e)
            return
        if isinstance(a, IRArgMatrix):
            for row in a.rows:
                visit(row)
            return
        if isinstance(a, IRArgFamilyRef):
            if a.name not in seen:
                seen.add(a.name)
                out.append(a.name)
            return
        if isinstance(a, IRArgKernel):
            if a.x_name not in seen:
                seen.add(a.x_name)
                out.append(a.x_name)
            return
        if isinstance(a, IRArgNumber):
            return

    visit(arg)
    return out

arg_ref_shape

arg_ref_shape(arg: IRArg, shape_table: dict[str, tuple[int, ...]]) -> tuple[int, ...]

Return the broadcast-evaluated shape of an IR arg referenced through shape_table.

Source code in src/quivers/transpile/lower.py
def arg_ref_shape(
    arg: IRArg, shape_table: dict[str, tuple[int, ...]]
) -> tuple[int, ...]:
    """Return the broadcast-evaluated shape of an IR arg referenced
    through `shape_table`."""
    if isinstance(arg, IRArgRef):
        base = shape_table.get(arg.name, ())
        # Indexing peels off one dim per index expression.
        peeled = max(0, len(base) - len(arg.indices))
        return base[:peeled]
    if isinstance(arg, IRArgList):
        return (len(arg.elements),)
    if isinstance(arg, IRArgMatrix):
        return (len(arg.rows), len(arg.rows[0].elements) if arg.rows else 0)
    if isinstance(arg, IRArgBroadcast):
        return arg.target_shape
    return ()

lower_factors

lower_factors(program: ProgramDecl, cards: dict[str, int]) -> dict[str, tuple[int, ...]]

Return name -> factor shape for every plated binding in program. Mirror of build_shape_table intended for renderers that need the factor shape (event_dims plus batch_dims) of every name.

Source code in src/quivers/transpile/lower.py
def lower_factors(
    program: ProgramDecl, cards: dict[str, int]
) -> dict[str, tuple[int, ...]]:
    """Return name -> factor shape for every plated binding in
    `program`. Mirror of [`build_shape_table`][quivers.transpile.lower.build_shape_table]
    intended for renderers that need the factor shape (event_dims
    plus batch_dims) of every name."""
    return build_shape_table(program, cards)

inline_list_lets

inline_list_lets(program: ProgramDecl) -> dict[str, tuple[str, ...]]

Return name -> tuple of element names for every let-step whose RHS is a LetExprList of bare-variable references.

Source code in src/quivers/transpile/lower.py
def inline_list_lets(
    program: ProgramDecl,
) -> dict[str, tuple[str, ...]]:
    """Return name -> tuple of element names for every let-step
    whose RHS is a [`LetExprList`][quivers.dsl.ast_nodes.let_expressions.LetExprList]
    of bare-variable references."""
    out: dict[str, tuple[str, ...]] = {}
    for step in program.draws:
        if not isinstance(step, LetStep):
            continue
        if isinstance(step.value, LetExprList):
            names: list[str] = []
            ok = True
            for it in step.value.items:
                if not isinstance(it, LetExprVar):
                    ok = False
                    break
                names.append(it.name)
            if ok:
                out[step.name] = tuple(names)
    return out