Skip to content

Renderers

The per-backend renderers consuming the IR.

renderers

Per-backend renderers consuming the transpile IR.

Each backend implements the Renderer protocol: one public render(ir: IRProgram) -> panproto.Schema method plus four dispatch points (declare, sample, marginalize, broadcast) that receive the call's RenderContext. The IR-walk dispatch, index-substitution helpers, the atom enumeration a marginalize block is scored under, and the structural invariants (assert_no_dangling_refs, assert_no_dropped_param_map, assert_no_lists) live on RendererBase, which every in-tree renderer subclasses.

BlockKind

BlockKind = Literal['data', 'parameters', 'transformed_parameters', 'model', 'generated_quantities', 'function_body']

Where a declaration lands in the target's program structure.

Each backend interprets the kind per its own program layout: Stan has actual blocks, while NumPyro's block is the function body.

SchemaFragment

SchemaFragment = str

One panproto schema fragment.

Either an opaque vertex id, or the empty string when the dispatch point emits nothing of its own.

IRArgTransform didactic-model

IRArgTransform(*, inner: IRArg, transform: Literal['inv_square', 'inv', 'neg', 'log', 'exp', 'one_minus', 'pow_neg'], operand: IRArg | None = None, kind: Literal['transform'] = 'transform')

Bases: IRArg

A renderer-applied transform on an IR arg.

The renderer constructs this during emission when an arg_aliases rename targets a parameterisation that requires arithmetic conversion (e.g. BUGS tau = 1/(scale*scale) for Normal). Per-renderer _ALIAS_TRANSFORMS tables key the transform on the renamed target name; RendererBase provides the constructor.

Most transforms act on inner alone (inv, neg, one_minus = 1 - inner, ...). The two-operand pow_neg additionally reads operand, emitting pow(inner, -operand): the JAGS / BUGS Weibull rate parameterisation needs the concentration argument as the exponent, so the reorder helper threads it in as operand.

IRArgTransform is a renderer-side IR extension. Lower never constructs it.

PARAMETER DESCRIPTION
inner

The argument transformed.

TYPE: IRArg

transform

The transform applied.

TYPE: Literal['inv_square', 'inv', 'neg', 'log', 'exp', 'one_minus', 'pow_neg']

operand

The second operand of pow_neg; None otherwise.

TYPE: IRArg | None DEFAULT: None

kind

The discriminator; always "transform".

TYPE: Literal['transform'] DEFAULT: 'transform'

IRMarginalAtom didactic-model

IRMarginalAtom(*, value: IRArgNumber, weight_family: str, weight_args: tuple[IRArg, ...], weight_arg_names: tuple[str, ...], scope: tuple[IRNode, ...])

Bases: Model

One atom of a marginalized latent's finite support.

RendererBase.marginal_atoms returns one of these per support point of an IRMarginalize. Together they carry the whole integrated density: writing L_a for the log-density the target accumulates while emitting scope, and w_a for the log-density of weight_family(weight_args) at value, the block contributes

logsumexp_a (w_a + L_a)

to the program's log-density, elementwise over whatever rows the scope's own plates carry. No latent variable is declared: the atoms replace it.

weight_family is not always the latent's own family. The Bernoulli relaxations are integrated over the atoms 0 and 1 with discrete Bernoulli weights, matching what the QVR compiler enumerates, so weight_family reads "Bernoulli" and weight_args carries only the probability argument.

IRMarginalAtom is a renderer-side IR extension. Lower never constructs it.

PARAMETER DESCRIPTION
value

The support point.

TYPE: IRArgNumber

weight_family

The family whose log-density at value is the atom's log-prior weight.

TYPE: str

weight_args

The arguments weight_family is evaluated with.

TYPE: tuple[IRArg, ...]

weight_arg_names

The parameter name of each of weight_args.

TYPE: tuple[str, ...]

scope

The block's body with the latent replaced by value.

TYPE: tuple[IRNode, ...]

RenderContext

RenderContext(sb: SchemaBuilder, morphisms: dict[str, MorphismDecl], defines: dict[str, Expr], fresh_counter: int = 0, cards: dict[str, int] | None = None)

The mutable state of one render call.

A renderer creates one per call and threads it through the IR-walk dispatch and its helpers as the first positional argument. It holds the panproto schema builder the target program is assembled in, the resolved morphism and define tables, a counter for fresh vertex names, and the cardinality table. Unlike the IR it is mutable and does not round-trip.

PARAMETER DESCRIPTION
sb

The schema builder the renderer emits into.

TYPE: SchemaBuilder

morphisms

The module's morphism declarations, by name.

TYPE: dict[str, MorphismDecl]

defines

The module's define expressions, by name.

TYPE: dict[str, Expr]

fresh_counter

The next fresh-name ordinal.

TYPE: int DEFAULT: 0

cards

The cardinality of each finite object, by name; empty when not supplied.

TYPE: dict[str, int] | None DEFAULT: None

ATTRIBUTE DESCRIPTION
sb

The schema builder the renderer emits into.

morphisms

The module's morphism declarations, by name.

defines

The module's define expressions, by name.

fresh_counter

The next fresh-name ordinal; helpers increment it as they mint names.

cards

The cardinality of each finite object, by name.

TYPE: dict[str, int]

Source code in src/quivers/transpile/renderers/_base.py
def __init__(
    self,
    sb: panproto.SchemaBuilder,
    morphisms: dict[str, MorphismDecl],
    defines: dict[str, Expr],
    fresh_counter: int = 0,
    cards: dict[str, int] | None = None,
) -> None:
    self.sb = sb
    self.morphisms = morphisms
    self.defines = defines
    self.fresh_counter = fresh_counter
    self.cards: dict[str, int] = {} if cards is None else cards

Renderer

Bases: Protocol

The protocol every backend renderer satisfies.

transpile calls render on a lowered IRProgram; the four dispatch points are what the shared IR walk of RendererBase calls per node.

render abstractmethod

render(ir: IRProgram) -> Schema

Render a program to the target's panproto schema.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def render(self, ir: IRProgram) -> panproto.Schema:
    """Render a program to the target's panproto schema.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    ...

declare abstractmethod

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

Emit the declaration of a named variable in block.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """Emit the declaration of a named variable in ``block``.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    ...

sample abstractmethod

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Emit the ~ statement of a sample or observe step.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The site's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The site's support.

TYPE: ConstraintSpec

plate

The site's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Emit the ``~`` statement of a sample or observe step.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The site's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The site's support.
    plate
        The site's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    ...

marginalize abstractmethod

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Emit the scope integrating a discrete latent out.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Emit the scope integrating a discrete latent out.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    ...

broadcast abstractmethod

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit the target's broadcast of value to target_shape.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit the target's broadcast of ``value`` to ``target_shape``.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    ...

RendererBase

Bases: ABC

Shared IR-walk dispatch and helpers for backend renderers.

Subclasses implement target_protocol, declare, sample, marginalize, broadcast, and may override render to wrap the inherited walk with per-backend pre / post processing (block emission, function header, etc.).

target_protocol abstractmethod

target_protocol() -> Protocol

Return the panproto protocol for the renderer's target language.

Each renderer builds its schema from this protocol's schema() builder.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol for the renderer's target language.

    Each renderer builds its schema from this protocol's
    `schema()` builder.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """

declare abstractmethod

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

Emit the declaration of a named variable in block.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """Emit the declaration of a named variable in ``block``.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """

sample abstractmethod

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Emit the ~ statement of a sample or observe step.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The site's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The site's support.

TYPE: ConstraintSpec

plate

The site's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Emit the ``~`` statement of a sample or observe step.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The site's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The site's support.
    plate
        The site's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """

marginalize abstractmethod

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Emit the scope integrating a discrete latent out.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Emit the scope integrating a discrete latent out.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """

broadcast abstractmethod

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit the target's broadcast of value to target_shape.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/_base.py
@abc.abstractmethod
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit the target's broadcast of ``value`` to ``target_shape``.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """

render

render(ir: IRProgram) -> Schema

Walk the IR and route each node to the right dispatch.

The default walk dispatches on the IRNode discriminator:

node dispatch
IRDataInput declare(name, ..., block="data")
IRSample (latent) declare + sample(observed=False)
IRObserve declare + sample(observed=True)
IRDeterministic declare + sample(observed=True) (deterministic)
IRScore renderer-specific (target +=)
IRMarginalize marginalize(node)
IRReturn _emit_return(names)

Subclasses may override render to wrap the walk with their own block prologue / epilogue.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/_base.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Walk the IR and route each node to the right dispatch.

    The default walk dispatches on the IRNode discriminator:

    | node | dispatch |
    | --- | --- |
    | `IRDataInput` | `declare(name, ..., block="data")` |
    | `IRSample` (latent) | `declare + sample(observed=False)` |
    | `IRObserve` | `declare + sample(observed=True)` |
    | `IRDeterministic` | `declare + sample(observed=True)` (deterministic) |
    | `IRScore` | renderer-specific (`target +=`) |
    | `IRMarginalize` | `marginalize(node)` |
    | `IRReturn` | `_emit_return(names)` |

    Subclasses may override `render` to wrap the walk with
    their own block prologue / epilogue.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_lists(ir)
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    ctx = RenderContext(sb=sb, morphisms={}, defines={})
    self._walk(ctx, ir)
    return sb.build()

index_for

index_for(ctx: RenderContext, plate: Plate) -> tuple[str, ...]

Return loop-index names for the plate's batch_dims.

Returns one name per batch_dim, generated via ctx.fresh_counter. Renderers pair these with each batch dim's size to emit nested for (m_i in 1:B_i) loops.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

plate

The plate whose batch dimensions are looped over.

TYPE: Plate

RETURNS DESCRIPTION
tuple[str, ...]

One index name per batch dimension, outermost first.

Source code in src/quivers/transpile/renderers/_base.py
def index_for(self, ctx: RenderContext, plate: Plate) -> tuple[str, ...]:
    """Return loop-index names for the plate's batch_dims.

    Returns one name per batch_dim, generated via
    `ctx.fresh_counter`. Renderers pair these with each batch
    dim's size to emit nested `for (m_i in 1:B_i)` loops.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    plate
        The plate whose batch dimensions are looped over.

    Returns
    -------
    tuple[str, ...]
        One index name per batch dimension, outermost first.
    """
    del ctx
    return tuple(f"m_{dim.name}" for dim in plate.batch_dims)

substitute_indices

substitute_indices(arg: IRArg, names: tuple[str, ...]) -> IRArg

Apply the loop-index names from names to the arg's bracket indices.

Used by sample / observe rendering to thread the surrounding plate's loop variables through an IRArgRef's index list so the emitted call form is name[m_0, m_1, ...].

PARAMETER DESCRIPTION
arg

The argument whose index list is rewritten.

TYPE: IRArg

names

The surrounding plate's loop-index names.

TYPE: tuple[str, ...]

RETURNS DESCRIPTION
IRArg

The argument with its nested indices substituted; any other argument unchanged.

Source code in src/quivers/transpile/renderers/_base.py
def substitute_indices(self, arg: IRArg, names: tuple[str, ...]) -> IRArg:
    """Apply the loop-index names from `names` to the arg's
    bracket indices.

    Used by sample / observe rendering to thread the
    surrounding plate's loop variables through an
    [`IRArgRef`][quivers.transpile.ir.IRArgRef]'s index list so
    the emitted call form is `name[m_0, m_1, ...]`.

    Parameters
    ----------
    arg
        The argument whose index list is rewritten.
    names
        The surrounding plate's loop-index names.

    Returns
    -------
    IRArg
        The argument with its nested indices substituted; any other
        argument unchanged.
    """
    if isinstance(arg, IRArgRef) and arg.indices:
        new_indices = tuple(
            self.substitute_indices(idx, names) for idx in arg.indices
        )
        return IRArgRef(name=arg.name, indices=new_indices)
    return arg

marginal_atoms

marginal_atoms(node: IRMarginalize, *, support_size: int | None = None) -> tuple[IRMarginalAtom, ...]

Expand an IRMarginalize node into weighted atoms.

Each returned IRMarginalAtom pins the latent within a copy of node.scope. The renderer combines each atom's weight with its scope log density and reduces the result with node.reduction.

support_size gives the class count for a "class_index" atom set; binary atom sets ignore it.

PARAMETER DESCRIPTION
node

The marginalize block.

TYPE: IRMarginalize

support_size

The class count of a "class_index" atom set, when the family does not fix it.

TYPE: int | None DEFAULT: None

RETURNS DESCRIPTION
tuple[IRMarginalAtom, ...]

One atom per support point, in support order.

RAISES DESCRIPTION
UnsupportedConstruct

If the family is unknown, has no supported marginal, or its class count is unresolved.

Source code in src/quivers/transpile/renderers/_base.py
def marginal_atoms(
    self,
    node: IRMarginalize,
    *,
    support_size: int | None = None,
) -> tuple[IRMarginalAtom, ...]:
    """Expand an `IRMarginalize` node into weighted atoms.

    Each returned `IRMarginalAtom` pins the latent within a copy of
    `node.scope`. The renderer combines each atom's weight with its scope
    log density and reduces the result with `node.reduction`.

    `support_size` gives the class count for a ``"class_index"`` atom set;
    binary atom sets ignore it.

    Parameters
    ----------
    node
        The marginalize block.
    support_size
        The class count of a ``"class_index"`` atom set, when the family
        does not fix it.

    Returns
    -------
    tuple[IRMarginalAtom, ...]
        One atom per support point, in support order.

    Raises
    ------
    UnsupportedConstruct
        If the family is unknown, has no supported marginal, or its
        class count is unresolved.
    """
    meta = FAMILY_META.get(node.family)
    if meta is None:
        raise UnsupportedConstruct(
            f"qvr-{self.target}",
            [f"family:unknown:{node.family}"],
        )
    support = marginalize_support(meta)
    if support is None:
        raise UnsupportedConstruct(
            f"qvr-{self.target}",
            [f"marginalize:non-finite-support:{node.family}"],
        )
    size = support.size if support.size is not None else support_size
    if size is None:
        raise UnsupportedConstruct(
            f"qvr-{self.target}",
            [f"marginalize:unknown-cardinality:{node.family}"],
        )
    if support.atoms == "binary":
        weight_args: tuple[IRArg, ...] = (
            self._marginal_weight_arg(node, support.weight_arg),
        )
        weight_arg_names: tuple[str, ...] = (support.weight_arg,)
    else:
        weight_args = node.args
        weight_arg_names = node.arg_names
    return tuple(
        IRMarginalAtom(
            value=IRArgNumber(value=float(k)),
            weight_family=support.weight_family,
            weight_args=weight_args,
            weight_arg_names=weight_arg_names,
            scope=substitute_latent(
                node.scope, node.latent, IRArgNumber(value=float(k))
            ),
        )
        for k in range(size)
    )

BUGSRenderer

Bases: RendererBase

Render an IRProgram to BUGS.

Implements the four RendererBase dispatch points (declare, sample, marginalize, broadcast) plus target_protocol and overrides render to install the model { ... } block prologue and to wire the BUGS-specific declaration-plate table that drives slice-emission for vector arg positions.

target_protocol

target_protocol() -> Protocol

Return the panproto protocol for the BUGS tree-sitter grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/bugs.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol for the BUGS tree-sitter grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("bugs")

render

render(ir: IRProgram) -> Schema

Emit the BUGS source schema for ir.

Wraps the IR walk in a source_file -> model_block shell so the emitted bytes are a complete BUGS file. The model_block's ptrace-0 = Tmodel constraint forces the emitter to render the optional model keyword.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/bugs.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Emit the BUGS source schema for `ir`.

    Wraps the IR walk in a `source_file -> model_block` shell so
    the emitted bytes are a complete BUGS file. The model_block's
    `ptrace-0 = Tmodel` constraint forces the emitter to render
    the optional `model` keyword.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dangling_refs(ir)
    assert_no_dropped_param_map(ir, self.target)
    self._reject_list_args(ir)
    # BUGS / JAGS have no scalar-to-vector broadcast: an
    # IRDeterministic with empty plate whose expression references
    # a plate-less free data input that ends up consumed inside a
    # non-empty-plate observe must be lifted into the consumer's
    # loop, with the references re-indexed by the loop variable.
    ir = push_scalar_dets_into_loops(ir)
    proto = self.target_protocol()
    sb = proto.schema()
    ctx = _BugsCtx(sb=sb, morphisms={}, defines={})
    self._cards = dict(ir.cards)
    # Populate decl_plates for every IRDataInput and IRSample /
    # IRObserve / IRDeterministic / IRMarginalize-latent.
    self._populate_decl_plates(ir, ctx)
    src_id = self._fresh(ctx, "src")
    sb.vertex(src_id, "source_file")
    mb_id = self._fresh(ctx, "mb")
    sb.vertex(mb_id, "model_block")
    sb.constraint(mb_id, "ptrace-0", "Tmodel")
    sb.edge(src_id, mb_id, "model_block")
    ctx.block_id = mb_id
    for node in ir.body:
        self._dispatch_bugs_node(ctx, node)
    render_computations_static(sb, ir, target=self.target, destination=mb_id)
    return sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

No-op: BUGS has no declaration syntax distinct from ~ / data.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/bugs.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """No-op: BUGS has no declaration syntax distinct from `~` / data.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Emit a ~ statement with surrounding for loops.

BUGS treats observed and latent draws identically at the syntactic level: the caller's data list pins the value of observed variables and the same ~ line carries the likelihood factor.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/bugs.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Emit a `~` statement with surrounding `for` loops.

    BUGS treats observed and latent draws identically at the
    syntactic level: the caller's data list pins the value of
    observed variables and the same `~` line carries the
    likelihood factor.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    if not isinstance(ctx, _BugsCtx):
        raise UnsupportedConstruct(
            f"qvr-{self.target}",
            ["ctx:wrong-type"],
        )
    node = IRSample(
        name=name,
        family=family,
        args=args,
        arg_names=arg_names,
        constraint=constraint,
        plate=plate,
    )
    self._emit_sample_node(ctx, node, loop_suffix="", observed=observed)
    return ""

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Emit an IRMarginalize as the collapsed dcat row its atoms sum to when the scope is a categorical mixture, and refuse it otherwise.

QVR's marginalize denotes the integral over the latent of the measure the scope carries. A program that declares the latent instead denotes a measure on the product of the latent's support with the scope's, which differs from the integral by an amount that moves with the data, so it is no rendering of the block. BUGS has no statement that adds a free log-density term to the joint, which is what a general logsumexp reduction would need, so every scope the collapse does not write is refused.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/bugs.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Emit an [`IRMarginalize`][quivers.transpile.ir.IRMarginalize]
    as the collapsed `dcat` row its atoms sum to when the scope is
    a categorical mixture, and refuse it otherwise.

    QVR's `marginalize` denotes the integral over the latent of
    the measure the scope carries. A program that declares the
    latent instead denotes a measure on the product of the
    latent's support with the scope's, which differs from the
    integral by an amount that moves with the data, so it is no
    rendering of the block. BUGS has no statement that adds a
    free log-density term to the joint, which is what a general
    `logsumexp` reduction would need, so every scope the collapse
    does not write is refused.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    refuse_ungrouped_row_marginalize("qvr-bugs", node)
    if not isinstance(ctx, _BugsCtx):
        raise UnsupportedConstruct(
            f"qvr-{self.target}",
            ["ctx:wrong-type"],
        )
    self._emit_marginalize_node(ctx, node)
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Render a scalar-to-vector broadcast as a filled helper array.

BUGS has neither scalar-to-vector coercion nor a repetition function, so the renderer emits an indexed fill loop and references its resulting slice. Helpers are reused for identical scalar and extent pairs.

Only plate-free scalars can be hoisted outside enclosing plate loops. Values that depend on loop indices and broadcasts to ranks other than one raise UnsupportedConstruct.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/bugs.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Render a scalar-to-vector broadcast as a filled helper array.

    BUGS has neither scalar-to-vector coercion nor a repetition function, so
    the renderer emits an indexed fill loop and references its resulting
    slice. Helpers are reused for identical scalar and extent pairs.

    Only plate-free scalars can be hoisted outside enclosing plate loops.
    Values that depend on loop indices and broadcasts to ranks other than
    one raise `UnsupportedConstruct`.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    bctx = _as_bugs_ctx(ctx)
    if len(target_shape) != 1:
        raise UnsupportedConstruct(
            f"qvr-{self.target}",
            [
                f"arg:broadcast:rank-{len(target_shape)}: a BUGS "
                "fill loop builds a 1-D vector only"
            ],
        )
    size = int(target_shape[0])
    key = self._broadcast_key(bctx, value, size)
    name = bctx.broadcast_helpers.get(key)
    if name is None:
        name = self._fresh_broadcast_name(bctx)
        bctx.broadcast_helpers[key] = name
        self._emit_broadcast_fill(bctx, name, value, size)
    return self._emit_indexed_name(bctx, name, (), (str(size),))

ChurchRenderer

Bases: RendererBase

Render an IRProgram to a Scheme schema in the Church probabilistic-programming idiom.

Subclass of RendererBase: overrides render to wrap the IR walk in a top-level (define (model <inputs...>) <body...>) form, then dispatches each IRNode through declare / sample / marginalize / broadcast.

target_protocol

target_protocol() -> Protocol

Return the panproto protocol of the target grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/church.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol of the target grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("scheme")

render

render(ir: IRProgram) -> Schema

Render a program to the target's panproto schema.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/church.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Render a program to the target's panproto schema.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dropped_param_map(ir, self.target)
    # Snapshot `IRProgram.cards` for the renderer-local cards map
    # threaded through every `_LetExprCtx`; the Scheme let-expr
    # helper consults `ctx.cards` when a factor binder's index
    # references a named QVR object whose cardinality must be
    # resolved at unroll time.
    self._cards: dict[str, int] = dict(ir.cards)
    # Name -> binding plate for every input, sample, observe,
    # deterministic, and marginalize latent. Consulted by the
    # per-row indexing pass so a batched reference emits a
    # `(list-ref name m_<axis>)` access aligned with the
    # surrounding plate.
    self._binding_plates: dict[str, Plate] = {}
    self._input_names: set[str] = {inp.name for inp in ir.inputs}
    for inp in ir.inputs:
        self._binding_plates[inp.name] = inp.plate
    self._collect_binding_plates(ir.body)
    # The active marginalize group plate's batch axes (empty
    # outside a marginalize scope). Refs bound on this plate that
    # are read from an observe on a different plate thread through
    # the observe's `via` fibration.
    self._group_plate_axes: tuple[str, ...] = ()
    # The active observe's `via` fibration name (None otherwise).
    self._current_via: str | None = None
    self._module = ir.module
    # Whether the model body has bound its native operation table.
    self._operations_bound = False
    # The names checked value arguments have bound in the model body.
    self._value_bindings: set[str] = set()
    proto = self.target_protocol()
    sb = proto.schema()
    ctx = RenderContext(sb=sb, morphisms={}, defines={}, cards=self._cards)
    prog_id = _v(ctx, "prog", "program")
    # Every emitted program uses `sample` / `observe` / `factor`
    # and one distribution constructor per family, all defined in
    # the self-contained Church runtime at
    # [`runtime_church.scm`][quivers.transpile.runtime_church].
    # Graft the whole runtime subtree above the model `(define
    # ...)` form so every referenced identifier resolves through
    # Scheme's top-level binding lookup.
    _graft_runtime_church_helper(ctx, prog_id)

    # `(model <param1> <param2> ...)` -- function signature.
    signature_children: list[str] = [_sym(ctx, "model")]
    for inp in ir.inputs:
        signature_children.append(_sym(ctx, inp.name))
    signature = _list(ctx, tuple(signature_children))

    # Body forms: one or more forms per IR node, plus a trailing
    # return symbol. Most nodes emit one form; `IRMarginalize`
    # expands inline to the latent sample plus each scope step,
    # yielding several sibling forms at the model-body level.
    body_forms: list[str] = []
    for node in ir.body:
        body_forms.extend(self._render_body_forms(ctx, node))
    # Trailing return: Church returns the last expression of the
    # body. `_emit_return` appends one bare symbol per returned
    # name.
    return_form = self._return_form(ctx, ir.body)
    if return_form is not None:
        body_forms.append(return_form)
    if not body_forms:
        # Chez rejects an empty procedure body. QIEC-only and
        # declaration-only modules retain the existing `model` ABI with a
        # harmless scalar result before their named QIEC entry points.
        body_forms.append(_num(ctx, 0.0))

    top_define = _list(ctx, (_sym(ctx, "define"), signature, *body_forms))
    _e(ctx, prog_id, top_define)
    render_computations_dynamic(sb, ir, target=self.target, root=prog_id)
    return sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

Church has no separate declaration block.

Sample / observe bind their result names directly via (define name (map ...)); data inputs land in the model signature in render. This dispatch is a no-op for every block.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/church.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """Church has no separate declaration block.

    Sample / observe bind their result names directly via
    ``(define name (map ...))``; data inputs land in the model
    signature in `render`. This dispatch is a no-op for every
    block.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool, via: str | None = None) -> SchemaFragment

Emit the per-batch-axis sample / observe form.

For a non-observed step with N batch dims:

(define <name> (map (lambda (m_<a0>) (map (lambda (m_<a1>) (sample <dist>)) (iota <B1>))) (iota <B0>)))

For an observed step: a for-each over the observation plate, with (observe <dist> (list-ref <obs> m)) inside each iteration.

Half-support families (HalfNormal, HalfCauchy) inject a loc=0 argument and wrap the base distribution in the runtime half fold; references bound on an aligned plate are indexed by the per-row loop variable; and a scalar family stamped with event axes (via over=) wraps an inner map over the event dims so each draw is the declared vector.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

via

The fibration an observe gathers its rows through, or None.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/church.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
    via: str | None = None,
) -> SchemaFragment:
    """Emit the per-batch-axis sample / observe form.

    For a non-observed step with N batch dims:

    ``(define <name>
        (map (lambda (m_<a0>)
               (map (lambda (m_<a1>) (sample <dist>))
                    (iota <B1>)))
             (iota <B0>)))``

    For an observed step: a ``for-each`` over the observation
    plate, with ``(observe <dist> (list-ref <obs> m))`` inside
    each iteration.

    Half-support families (`HalfNormal`, `HalfCauchy`) inject a
    ``loc=0`` argument and wrap the base distribution in the
    runtime ``half`` fold; references bound on an aligned plate
    are indexed by the per-row loop variable; and a scalar family
    stamped with event axes (via ``over=``) wraps an inner map
    over the event dims so each draw is the declared vector.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.
    via
        The fibration an observe gathers its rows through, or ``None``.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del arg_names
    dist_form = self._dist_form(ctx, family, args, plate, via)
    if observed:
        return _statement(ctx, self._wrap_observe(ctx, name, dist_form, plate))
    return self._wrap_sample_define(ctx, name, dist_form, plate, constraint)

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Lower IRMarginalize to the forms that integrate its latent out.

The top-level render consumes the multi-form expansion via _render_marginalize_forms so the latent sample and scope observes sit as siblings of the surrounding (define (model ...) ...) body. When marginalize is invoked as a single dispatch point (per the Renderer protocol contract), the forms collapse into a (begin ...) block.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/church.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Lower [`IRMarginalize`][quivers.transpile.ir.IRMarginalize]
    to the forms that integrate its latent out.

    The top-level
    [`render`][quivers.transpile.renderers.church.ChurchRenderer.render]
    consumes the multi-form expansion via
    ``_render_marginalize_forms`` so the latent sample and scope
    observes sit as siblings of the surrounding ``(define (model
    ...) ...)`` body. When `marginalize` is invoked as a single
    dispatch point (per the Renderer protocol contract), the
    forms collapse into a ``(begin ...)`` block.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    forms = self._render_marginalize_forms(ctx, node)
    if not forms:
        return ""
    if len(forms) == 1:
        return forms[0]
    return _list(ctx, (_sym(ctx, "begin"), *forms))

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

(make-list K <value>) for 1-D broadcasts.

Higher-rank broadcasts raise UnsupportedConstruct with arg:broadcast-rank-2+ (Church has no canonical matrix form, so a rank-2 broadcast has no idiomatic rendering).

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/church.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """`(make-list K <value>)` for 1-D broadcasts.

    Higher-rank broadcasts raise
    `UnsupportedConstruct`
    with ``arg:broadcast-rank-2+`` (Church has no canonical
    matrix form, so a rank-2 broadcast has no idiomatic
    rendering).

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    if len(target_shape) != 1:
        raise UnsupportedConstruct(
            _TARGET,
            [f"arg:broadcast-rank-{len(target_shape)}"],
        )
    (k,) = target_shape
    value_form = self._render_arg(ctx, value)
    return _list(
        ctx,
        (_sym(ctx, "make-list"), _num(ctx, float(k)), value_form),
    )

Edward2Renderer

Bases: RendererBase

Render an IRProgram to TFP Edward2 Python source.

Output shape (single function, all inputs defaulted to None):

def model(alpha=None, beta=None, word_idx=None, w=None):
    theta = edward2.Dirichlet(tf.fill([3], alpha),
                              sample_shape=[20], name="theta")
    phi = edward2.Dirichlet(tf.fill([200], beta),
                            sample_shape=[3], name="phi")
    z = edward2.Categorical(probs=theta,
                            sample_shape=[20], name="z")
    w = edward2.Categorical(probs=phi[z[word_idx]], name="w")
    return theta

The walk overrides RendererBase.render because Edward2's program shape (single function, no block structure) does not map onto the inherited block-by-block default walk.

target_protocol

target_protocol() -> Protocol

Return the panproto protocol of the target grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/edward2.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol of the target grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("python")

render

render(ir: IRProgram) -> Schema

Render a program to the target's panproto schema.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/edward2.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Render a program to the target's panproto schema.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dangling_refs(ir)
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    py = PyCtx(
        sb,
        cards=dict(ir.cards),
        target="edward2",
        name_event_rank=name_event_rank_map(ir),
        gather_symbol=("tf", "gather"),
        factor_towers=factor_tower_names(ir),
        name_plates=name_plate_map(ir),
    )
    ctx = _Edward2Ctx(sb=sb, morphisms={}, defines={}, module=ir.module)

    sb.vertex("mod", "module")
    # A marginalize block reduces to a `MixtureSameFamily` whose
    # two component arguments are bare TFP distributions rather
    # than Edward2 random variables (an Edward2 constructor would
    # register a second, unobserved site on the trace). TFP is a
    # hard dependency of Edward2, so the emitted module imports it
    # directly when a marginalize is present. A score or a call
    # needs the factor helper, a TFP distribution.
    factors = _ir_has_factor(ir.body)
    if (
        _ir_has_marginalize(ir.body)
        or ir_uses_family(ir.body, "MixtureNormal")
        or qiec_families_used(ir)
        or factors
    ):
        self._emit_tfp_import(py)
    if factors:
        graft_python_statements(
            sb, _FACTOR_HELPER.read_text(), "mod", "qiec_factor"
        )
    body_vid = py.v(py.fresh("body"), "block")
    if not ir.body:
        noop = py.v(py.fresh("pass"), "pass_statement")
        py.e(body_vid, noop, "child_of")
    param_names = tuple(inp.name for inp in ir.inputs)
    fn = function_def(
        py, name="model", default_params=param_names, body_vid=body_vid
    )
    sb.edge("mod", fn, "child_of")

    # Track input constraint info so the renderer can decide whether
    # to wrap a scalar `IRArgRef` into a `tf.fill(...)` broadcast.
    input_specs: dict[str, ConstraintSpec] = {
        inp.name: inp.constraint for inp in ir.inputs
    }
    bindings: dict[str, _Binding] = {
        inp.name: _Binding(constraint=inp.constraint, plate=inp.plate)
        for inp in ir.inputs
    }

    # TensorFlow's ops are dtype-strict: an integer-valued tensor
    # supplied for a `Real`-declared covariate makes every mixed
    # expression it reaches (`20.0 * (t - tau)`) raise rather than
    # promote, the way JAX / PyTorch / PyTensor would. Casting each
    # real-declared input once at the top of the body makes the
    # emitted model self-contained whatever dtype the caller
    # supplies, mirroring the integer-index coercion the PyTorch
    # renderer emits for the mirror-image case.
    for name in _real_input_names(ir):
        py.e(
            body_vid,
            assignment(
                py,
                lhs_name=name,
                rhs=_cast_float32(py, identifier(py, name)),
            ),
            "child_of",
        )

    for node in ir.body:
        self._emit_node(py, ctx, body_vid, node, input_specs, bindings)

    render_computations_dynamic(sb, ir, target=self.target, root="mod")
    return sb.build()

emit_bytes

emit_bytes(ir: IRProgram) -> bytes

Convenience: render ir and run emit_pretty to bytes.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
bytes

The target program's source.

Source code in src/quivers/transpile/renderers/edward2.py
def emit_bytes(self, ir: IRProgram) -> bytes:
    """Convenience: render `ir` and run `emit_pretty` to bytes.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    bytes
        The target program's source.
    """
    schema = self.render(ir)
    return EmitPretty("python")(schema)

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

No-op: Edward2 has no separate declarations.

Data inputs are emitted as function parameters by render; sample / observe declarations are subsumed into the edward2.<Family>(...) call assigned to the bound name.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/edward2.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """No-op: Edward2 has no separate declarations.

    Data inputs are emitted as function parameters by ``render``;
    sample / observe declarations are subsumed into the
    ``edward2.<Family>(...)`` call assigned to the bound name.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Build the edward2.<Family>(...) call vertex.

The IR-walk in render calls this from a fresh PyCtx wrapping ctx.sb. Edward2 has no native obs= keyword; observations are surfaced by carrying them as default-None function parameters and conditioning via the caller's interceptor mechanism, so observed does not change the emitted call.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/edward2.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Build the ``edward2.<Family>(...)`` call vertex.

    The IR-walk in ``render`` calls this from a fresh `PyCtx`
    wrapping ``ctx.sb``. Edward2 has no native ``obs=`` keyword;
    observations are surfaced by carrying them as default-None
    function parameters and conditioning via the caller's
    interceptor mechanism, so ``observed`` does not change the
    emitted call.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del constraint, observed
    py = PyCtx(ctx.sb, target="edward2")
    return self._dist_call(
        py,
        name=name,
        family=family,
        args=args,
        arg_names=arg_names,
        plate=plate,
        input_specs={},
        bindings={},
        observed_name=None,
    )

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

No-op at the protocol dispatch point: the renderer drives the enumeration through _emit_marginalize, which needs the binding tables the top-level walk threads.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/edward2.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """No-op at the protocol dispatch point: the renderer drives
    the enumeration through `_emit_marginalize`, which needs the
    binding tables the top-level walk threads.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, node
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit a tf.fill([...], <value>) call.

1D targets render as tf.fill([K], <value>); 2D as tf.fill([R, C], <value>). The shape literal is a plain Python list (Edward2 / TF accept it).

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/edward2.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit a ``tf.fill([...], <value>)`` call.

    1D targets render as ``tf.fill([K], <value>)``; 2D as
    ``tf.fill([R, C], <value>)``. The shape literal is a plain
    Python list (Edward2 / TF accept it).

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    py = PyCtx(ctx.sb, target="edward2")
    return self._broadcast(py, value, target_shape, {}, {})

GenRenderer

Bases: RendererBase

Render an IR program as a Gen.jl @gen function definition.

Inherits the IR-walk dispatch from RendererBase. Overrides the per-node dispatch in render to collect declarations plus statements into the @gen function envelope; implements declare, sample, marginalize, broadcast per the Gen.jl idiom described in the module docstring.

target_protocol

target_protocol() -> Protocol

Return the panproto protocol of the target grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/gen.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol of the target grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("julia")

render

render(ir: IRProgram) -> Schema

Render a program to the target's panproto schema.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/gen.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Render a program to the target's panproto schema.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dangling_refs(ir)
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    gx = _GenCtx(sb=sb, cards={}, morphisms={})

    _harvest_cards(ir, gx)
    gx.shapes = JuliaShapes(
        name_event_rank=name_event_rank_map(ir),
        name_array_rank=name_array_rank_map(ir),
        nested_names=nested_tower_names(ir),
    )

    # Sort inputs alphabetically so the emitted `model(...)` signature
    # matches the probe driver's positional-arg convention (the probe
    # iterates the per-point `data` dict in sorted-key order).
    for inp in sorted(ir.inputs, key=lambda i: i.name):
        gx.params.append(inp.name)
        gx.inputs_by_name[inp.name] = inp
        gx.decl_axes[inp.name] = inp.plate.batch_dims

    # `RenderContext` is the inherited carrier; we re-use it for the
    # required dispatch signatures but the per-render scratch
    # lives on `_GenCtx`.
    ctx = RenderContext(sb=sb, morphisms={}, defines={})
    blk = gx.v("block", "body")
    gx.body = blk
    self._gx = gx
    self._module = ir.module
    self._ir = ir
    try:
        for node in ir.body:
            self._emit_node(ctx, node)
    finally:
        del self._gx
        del self._module
        del self._ir

    if gx.return_names:
        ret = gx.v("return_statement", "ret")
        if len(gx.return_names) == 1:
            gx.e(ret, _ident(gx, gx.return_names[0]))
        else:
            tup = gx.v("tuple_expression", "rtup")
            for n in gx.return_names:
                gx.e(tup, _ident(gx, n))
            gx.e(ret, tup)
        gx.e(blk, ret)

    # Probe drivers eval the source and look up `Main.model`; the
    # function is always named `model` so the harness has a
    # canonical entry point regardless of the QVR module name.
    fn = _function_def(
        gx,
        name="model",
        params=tuple(gx.params),
        body_vid=blk,
    )
    mc = _macro_call_body(gx, "gen", fn)
    src = gx.v("source_file", "src")
    # Gen.jl has no built-in `truncated_normal`. When the IR samples
    # or observes from `TruncatedNormal`, graft the helper from
    # [`runtime_gen.jl`][quivers.transpile.runtime_gen] onto the
    # module above the `@gen function model` so the model body can
    # call `truncated_normal(loc, scale, low, high)`. The graft
    # carries its own `using Gen` / `using Distributions`
    # statements; subsequent `@gen` macrocalls see the imported
    # names through normal Julia name lookup.
    if (
        any(_ir_uses_family(ir.body, f) for f in _GEN_RUNTIME_HELPER_FAMILIES)
        or qiec_helper_families_used(ir, self.target)
        or _ir_has_factor(ir.body)
    ):
        _graft_runtime_gen_helper(gx, src)
    gx.e(src, mc)
    render_computations_dynamic(sb, ir, target=self.target, root=src)
    return sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

Emit the declaration of a named variable in block.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/gen.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """Emit the declaration of a named variable in ``block``.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Emit the ~ statement of a sample or observe step.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/gen.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Emit the ``~`` statement of a sample or observe step.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del (
        ctx,
        name,
        family,
        args,
        arg_names,
        constraint,
        plate,
        observed,
    )
    return ""

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Emit the scope integrating a discrete latent out.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/gen.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Emit the scope integrating a discrete latent out.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    self._emit_marginalize(ctx, node)
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit the target's broadcast of value to target_shape.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/gen.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit the target's broadcast of ``value`` to ``target_shape``.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx
    gx = self._gx
    value_vid = _render_arg(gx, value, arg_ctx=_ArgCtx())
    return _broadcast_to_shape(gx, value_vid, target_shape)

JAGSRenderer

Bases: RendererBase

Render an IRProgram as a JAGS model source.

Subclasses RendererBase: overrides render to wrap the IR walk in a single top-level model { ... } block. The four dispatch points (declare, sample, marginalize, broadcast) follow the JAGS surface conventions described in section 5.1 of the design spec.

target_protocol

target_protocol() -> Protocol

Use the auto-derived jags tree-sitter protocol.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/jags.py
def target_protocol(self) -> panproto.Protocol:
    """Use the auto-derived ``jags`` tree-sitter protocol.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("jags")

render

render(ir: IRProgram) -> Schema

Walk the IR and emit a complete JAGS module.

Overrides the base IR walk because JAGS programs need a model { ... } wrapper around every body statement; the wrapper is a single model_block vertex whose children are the statements emitted by _dispatch_jags_node.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/jags.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Walk the IR and emit a complete JAGS module.

    Overrides the base IR walk because JAGS programs need a
    ``model { ... }`` wrapper around every body statement; the
    wrapper is a single ``model_block`` vertex whose children are
    the statements emitted by `_dispatch_jags_node`.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dropped_param_map(ir, self.target)
    # JAGS has no scalar-to-vector broadcast; lift empty-plate
    # IRDeterministic nodes whose expressions reference plate-less
    # free data inputs into the plate of the first downstream
    # consumer, then re-index those references at emit time.
    ir = push_scalar_dets_into_loops(ir)
    proto = self.target_protocol()
    sb = proto.schema()
    jctx = _JAGSCtx(sb=sb, morphisms={}, lets={})
    jctx.scalar_refs, jctx.bound_refs = _classify_bindings(ir)
    self._cards = dict(ir.cards)
    # Cache decl_plates so the deterministic emitter can re-index
    # let-expression refs by their declared batch_dims.
    jctx.decl_plates = build_decl_plates(ir)

    _vertex(jctx, "src", "source_file")
    # A site whose family JAGS cannot name, every `marginalize`
    # block, and every `score` step scores through the zeros trick,
    # whose constant-zero carrier is a node JAGS has to see as
    # data. The language's `data { ... }` transformation block
    # binds exactly such nodes from inside the model source, so the
    # emit declares one when, and only when, a site needs it.
    if (
        _ir_has_marginalize(ir.body)
        or _ir_has_score(ir.body)
        or any(ir_uses_family(ir.body, family) for family in _ZEROS_TRICK_FAMILIES)
    ):
        jctx.sb.constraint("src", "ptrace-0", "Cdata_block")
        jctx.sb.constraint("src", "ptrace-1", "Cmodel_block")
        jctx.sb.constraint("src", "chose-alt-child-kinds", "data_block model_block")
        db = _fresh(jctx, "db", "data_block")
        jctx.sb.edge("src", db, "child_of")
        jctx.data_block = db
    else:
        jctx.sb.constraint("src", "ptrace-0", "Cmodel_block")
        jctx.sb.constraint("src", "chose-alt-child-kinds", "model_block")

    mb = _fresh(jctx, "mb", "model_block")
    jctx.sb.edge("src", mb, "child_of")
    jctx.current_block = mb
    jctx.model_block = mb

    for node in ir.body:
        self._dispatch_jags_node(jctx, node)

    self._finalise_model_block(jctx)
    self._finalise_data_block(jctx)
    render_computations_static(sb, ir, target=self.target, destination=mb)
    return sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

JAGS variables are declared implicitly by their first ~ or <- binding; data inputs ride on the host's external .data file. This dispatch is a no-op for every block.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/jags.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """JAGS variables are declared implicitly by their first ``~``
    or ``<-`` binding; data inputs ride on the host's external
    ``.data`` file. This dispatch is a no-op for every block.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Emit a per-batch-axis for (m_<axis> in 1:N_<axis>) { <lhs> ~ d<family>(args) } form.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/jags.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Emit a per-batch-axis ``for (m_<axis> in 1:N_<axis>) { <lhs>
    ~ d<family>(args) }`` form.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del constraint
    jctx = _as_jags_ctx(ctx)
    return self._emit_sample(
        jctx,
        name=name,
        family=family,
        args=args,
        arg_names=arg_names,
        plate=plate,
        observed=observed,
    )

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Lower an IRMarginalize to the weighted sum over its latent's atoms.

QVR's marginalize denotes the integral over the latent of the measure the scope carries, so the emitted program has to score that integral and declare no latent site: a program that draws the latent instead denotes a measure on the product of the latent's support with the scope's, which differs from the integral by an amount that moves with the data.

JAGS has no reduction over a parameterised family, but it has every piece the integral needs. The support is finite, so the sum

p(y_n) = sum_a w_a * f_a(y_n)

is an ordinary arithmetic expression once the atom count is known, and the zeros trick adds its logarithm to the joint. _emit_marginal_reduction writes it.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/jags.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Lower an [`IRMarginalize`][quivers.transpile.ir.IRMarginalize]
    to the weighted sum over its latent's atoms.

    QVR's `marginalize` denotes the *integral* over the latent of
    the measure the scope carries, so the emitted program has to
    score that integral and declare no latent site: a program that
    draws the latent instead denotes a measure on the product of
    the latent's support with the scope's, which differs from the
    integral by an amount that moves with the data.

    JAGS has no reduction over a parameterised family, but it has
    every piece the integral needs. The support is finite, so the
    sum

        p(y_n) = sum_a w_a * f_a(y_n)

    is an ordinary arithmetic expression once the atom count is
    known, and the zeros trick adds its logarithm to the joint.
    `_emit_marginal_reduction`
    writes it.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    refuse_ungrouped_row_marginalize("qvr-jags", node)
    jctx = _as_jags_ctx(ctx)
    self._emit_marginal_reduction(jctx, node)
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Render a scalar-to-vector broadcast as JAGS' rep builtin.

JAGS has no scalar-to-vector broadcast operator, but its base function library provides rep(x, times), which returns a length-times vector filled with x. A scalar concentration over a K-atom Dirichlet event axis thus emits rep(<scalar>, K): a valid vector parent whose repeated entries reproduce the symmetric-Dirichlet measure the QVR [over=K] clause denotes.

Only a scalar reference or numeric literal can be repeated; a rank other than one has no rep form and raises rather than emitting an invalid parent.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/jags.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Render a scalar-to-vector broadcast as JAGS' ``rep`` builtin.

    JAGS has no scalar-to-vector broadcast operator, but its base
    function library provides ``rep(x, times)``, which returns a
    length-``times`` vector filled with ``x``. A scalar
    concentration over a ``K``-atom Dirichlet event axis thus
    emits ``rep(<scalar>, K)``: a valid vector parent whose repeated
    entries reproduce the symmetric-Dirichlet measure the QVR
    ``[over=K]`` clause denotes.

    Only a scalar reference or numeric literal can be repeated; a
    rank other than one has no ``rep`` form and raises rather than
    emitting an invalid parent.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    jctx = _as_jags_ctx(ctx)
    if len(target_shape) != 1:
        raise UnsupportedConstruct(
            f"qvr-{_BACKEND}",
            [
                f"arg:broadcast:rank-{len(target_shape)}: JAGS `rep` "
                "builds a 1-D vector only"
            ],
        )
    if not isinstance(value, (IRArgRef, IRArgNumber)):
        raise UnsupportedConstruct(
            f"qvr-{_BACKEND}",
            [
                f"arg:broadcast:value:{type(value).__name__}: only a "
                "scalar reference or numeric literal can be repeated"
            ],
        )
    return self._rep_call(jctx, value, target_shape[0])

NumPyroRenderer

Bases: RendererBase

Render an IRProgram as NumPyro Python source.

Subclasses RendererBase and supplies the four required dispatch points (declare, sample, marginalize, broadcast) plus a render override that wraps the IR walk with the def model(...): ... function header.

target_protocol

target_protocol() -> Protocol

Use the auto-derived Python tree-sitter protocol.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/numpyro.py
def target_protocol(self) -> panproto.Protocol:
    """Use the auto-derived Python tree-sitter protocol.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("python")

render

render(ir: IRProgram) -> Schema

Walk the IR and emit a complete NumPyro module.

Overrides the base IR walk because NumPyro programs need a function header that lists every IRDataInput in the signature (observed values get a =None default).

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/numpyro.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Walk the IR and emit a complete NumPyro module.

    Overrides the base IR walk because NumPyro programs need a
    function header that lists every
    [`IRDataInput`][quivers.transpile.ir.IRDataInput] in the
    signature (observed values get a ``=None`` default).

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    py = PyCtx(
        sb,
        cards=dict(ir.cards),
        target="numpyro",
        name_event_rank=name_event_rank_map(ir),
        factor_towers=factor_tower_names(ir),
        name_plates=name_plate_map(ir),
    )
    scalar_refs, bound_refs = _classify_bindings(ir)
    ctx = _NumPyroCtx(
        sb=sb,
        morphisms={},
        lets={},
        py=py,
        observed_names=self._collect_observed(ir),
        scalar_refs=scalar_refs,
        bound_refs=bound_refs,
        module=ir.module,
    )

    py.v("mod", "module")
    body = py.v(py.fresh("body"), "block")
    params = self._function_params(ir)
    func = self._build_function(py, body, params)
    if not ir.body:
        noop = py.v(py.fresh("pass"), "pass_statement")
        py.e(body, noop, "child_of")

    # Dispatch the body first so any ``LetExprCall`` records the
    # imports its symbol needs (jax.scipy.special / jax.nn), then emit
    # the import block, graft any runtime-helper classes the body
    # uses, and wire the function last so a reader sees imports, then
    # helper classes, then ``def model``.
    self._dispatch_body(ctx, body, ir.body)
    used_helpers = _ir_helper_classes_used(ir.body) | qiec_helper_roots(
        ir, _BACKEND
    )
    for cls_name in used_helpers:
        extra_import = _NUMPYRO_HELPER_IMPORTS.get(cls_name)
        if extra_import is not None:
            py.required_imports.add(extra_import)
    self._emit_imports(ctx)
    for cls_name in sorted(used_helpers):
        _emit_runtime_helper(py, cls_name)
    py.e("mod", func, "child_of")

    render_computations_dynamic(sb, ir, target=self.target, root="mod")
    return sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

No-op outside "function_body".

NumPyro declarations ARE sample calls; the function-header parameter list is built up front by render from IRDataInput entries. Function-body declarations land at the sample dispatch point when the IR walker reaches the binding step.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/numpyro.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """No-op outside ``"function_body"``.

    NumPyro declarations ARE sample calls; the function-header
    parameter list is built up front by `render` from
    [`IRDataInput`][quivers.transpile.ir.IRDataInput] entries.
    Function-body declarations land at the
    [`sample`][quivers.transpile.renderers.numpyro.NumPyroRenderer.sample]
    dispatch point when the IR walker reaches the binding step.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Emit a numpyro.sample call inside the surrounding plate stack.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/numpyro.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Emit a ``numpyro.sample`` call inside the surrounding plate
    stack.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del constraint
    npctx = _as_numpyro_ctx(ctx)
    return self._render_sample(
        npctx,
        name=name,
        family=family,
        args=args,
        arg_names=arg_names,
        plate=plate,
        observed=observed,
    )

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Integrate an IRMarginalize latent out, adding the reduced density to the model's log-density with numpyro.factor.

The block emits one scored copy of the scope per atom of the latent's finite support, then reduces across atoms:

gated_rate = 0.0 * rate
__marg_z_0 = numpyro.distributions.Poisson(gated_rate).log_prob(y)
gated_rate = 1.0 * rate
__marg_z_1 = numpyro.distributions.Poisson(gated_rate).log_prob(y)
__marg_z_w = jnp.stack([jnp.log1p(-pi_z), jnp.log(pi_z)], axis=-1)
__marg_z = jnp.stack([__marg_z_0, __marg_z_1], axis=-1)
numpyro.factor(
    "z", jnp.sum(jsp.logsumexp(__marg_z_w + __marg_z, axis=-1))
)

Each atom's deterministic bindings are re-emitted under their own names immediately before that atom is scored, so the scoring call reads the values that atom pins. No site is declared for the latent: the atoms replace it, and the emitted program denotes the same measure the QVR reference integrates.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/numpyro.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Integrate an [`IRMarginalize`][quivers.transpile.ir.IRMarginalize]
    latent out, adding the reduced density to the model's
    log-density with ``numpyro.factor``.

    The block emits one scored copy of the scope per atom of the
    latent's finite support, then reduces across atoms:

    ```python
    gated_rate = 0.0 * rate
    __marg_z_0 = numpyro.distributions.Poisson(gated_rate).log_prob(y)
    gated_rate = 1.0 * rate
    __marg_z_1 = numpyro.distributions.Poisson(gated_rate).log_prob(y)
    __marg_z_w = jnp.stack([jnp.log1p(-pi_z), jnp.log(pi_z)], axis=-1)
    __marg_z = jnp.stack([__marg_z_0, __marg_z_1], axis=-1)
    numpyro.factor(
        "z", jnp.sum(jsp.logsumexp(__marg_z_w + __marg_z, axis=-1))
    )
    ```

    Each atom's deterministic bindings are re-emitted under their
    own names immediately before that atom is scored, so the
    scoring call reads the values that atom pins. No site is
    declared for the latent: the atoms replace it, and the emitted
    program denotes the same measure the QVR reference integrates.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    npctx = _as_numpyro_ctx(ctx)
    body_vid = npctx.current_body
    if body_vid is None:
        raise UnsupportedConstruct(
            "qvr-numpyro",
            ["marginalize:no-enclosing-body"],
        )
    py = npctx.py
    raw = marginalize_body(node.scope, latent=node.latent, target=self.target)
    atoms = self.marginal_atoms(
        node,
        support_size=marginal_support_size(node, name_plates=py.name_plates),
    )
    prefix = f"__marg_{node.latent}"
    term_names: list[str] = []
    for position, atom in enumerate(atoms):
        scored = marginalize_body(
            atom.scope, latent=node.latent, target=self.target
        )
        for det in scored.deterministics:
            py.e(
                body_vid,
                self._deterministic_statement(npctx, det),
                "child_of",
            )
        dist_call = self._distribution_call(
            npctx,
            scored.observe.family,
            scored.observe.args,
            scored.observe.arg_names,
            scored.observe.plate,
        )
        dist_call = self._wrap_event_dims(
            npctx,
            dist_call,
            scored.observe.family,
            scored.observe.plate.event_dims,
        )
        term = f"{prefix}_{position}"
        py.e(
            body_vid,
            self._assignment_statement(
                py,
                term,
                _python_method_call(
                    py,
                    dist_call,
                    "log_prob",
                    (identifier(py, scored.observe.name),),
                ),
            ),
            "child_of",
        )
        term_names.append(term)
    weight_vid = self._marginal_log_weights(npctx, node, raw, atoms)
    py.e(
        body_vid,
        self._assignment_statement(
            py,
            f"{prefix}_w",
            weight_vid,
        ),
        "child_of",
    )
    py.e(
        body_vid,
        self._assignment_statement(
            py,
            prefix,
            self._stack_last_axis(
                py,
                tuple(identifier(py, name) for name in term_names),
            ),
        ),
        "child_of",
    )
    py.required_imports.add(_MARGINALIZE_IMPORT)
    # A grouped block keys its accumulator by group: the rows the
    # fibration sends to one group are summed before the reduction.
    fibration = marginalize_fibration(
        node,
        raw.observe,
        atoms[0].weight_args,
        atoms[0].weight_arg_names,
        name_plates=py.name_plates,
        target=self.target,
    )
    if fibration is not None:
        via, group = fibration
        extent = (
            str(group.size) if isinstance(group, DimStatic) else group.size_name
        )
        graft_python_statements(
            py.builder,
            f"{prefix} = jnp.zeros(({extent}, {prefix}.shape[-1]), "
            f"dtype={prefix}.dtype).at[{via}].add({prefix})\n",
            body_vid,
            f"marg_{node.latent}_group",
        )
    # An ungrouped block shares one latent across the body's rows,
    # so their per-class log-likelihoods are accumulated before the
    # reduction rather than each row reducing on its own.
    atoms = identifier(py, prefix)
    for _ in range(marginalize_row_rank(node)):
        atoms = call(
            py,
            attribute(py, ("jnp", "sum")),
            positional=(atoms,),
            keyword=(("axis", self._zero(py)),),
        )
    reduced = call(
        py,
        attribute(py, ("jsp", "logsumexp")),
        positional=(
            _python_binary_op(
                py,
                "+",
                identifier(py, f"{prefix}_w"),
                atoms,
            ),
        ),
        keyword=(("axis", self._minus_one(py)),),
    )
    total = call(
        py,
        attribute(py, ("jnp", "sum")),
        positional=(reduced,),
    )
    py.e(
        body_vid,
        self._expression_statement(
            py,
            call(
                py,
                attribute(py, ("numpyro", "factor")),
                positional=(string_literal(py, node.latent), total),
            ),
        ),
        "child_of",
    )
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit jnp.full((K,), value) for 1D / jnp.full((R, C), value) for 2D.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/numpyro.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit ``jnp.full((K,), value)`` for 1D / ``jnp.full((R, C),
    value)`` for 2D.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    npctx = _as_numpyro_ctx(ctx)
    py = npctx.py
    shape_tuple = self._render_shape_tuple(py, target_shape)
    # The fill value types the tensor: an integer literal here
    # builds an integer concentration / rate / scale, which the
    # real-valued family rejects.
    value_vid = (
        float_literal(py, value.value)
        if isinstance(value, IRArgNumber)
        else self._render_arg(npctx, value)
    )
    return call(
        py,
        attribute(py, ("jnp", "full")),
        positional=(shape_tuple, value_vid),
    )

PyMCRenderer

Bases: RendererBase

Render an IRProgram to a PyMC build_model function under the python tree-sitter grammar.

The output has the shape:

def build_model(<inputs>=None):
    with pymc.Model(coords={...}) as model:
        theta = pymc.Dirichlet("theta", a=..., dims=(...))
        phi = pymc.Dirichlet("phi", a=..., dims=(...))
        pymc.Categorical("w", p=phi[...], observed=w, dims=(...))
    return model

target_protocol

target_protocol() -> Protocol

Return the panproto protocol of the target grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/pymc.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol of the target grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("python")

render

render(ir: IRProgram) -> Schema

Render an IRProgram to a PyMC schema. Subclasses normally call render_with_tables when morphism / let tables are available.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/pymc.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Render an [`IRProgram`][quivers.transpile.ir.IRProgram] to a
    PyMC schema. Subclasses normally call
    [`render_with_tables`][quivers.transpile.renderers.pymc.PyMCRenderer.render_with_tables]
    when morphism / let tables are available.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    return self.render_with_tables(ir, morphisms={}, lets={})

render_with_tables

render_with_tables(ir: IRProgram, *, morphisms: dict[str, MorphismDecl], lets: dict[str, Expr]) -> Schema

Same as render but pre-populates ctx.morphisms / ctx.defines so IRArgFamilyRef rendering can read the referenced morphism's init_family clause.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

morphisms

The module's morphism declarations, by name.

TYPE: dict[str, MorphismDecl]

lets

The module's define expressions, by name.

TYPE: dict[str, Expr]

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/pymc.py
def render_with_tables(
    self,
    ir: IRProgram,
    *,
    morphisms: dict[str, MorphismDecl],
    lets: dict[str, Expr],
) -> panproto.Schema:
    """Same as `render` but pre-populates `ctx.morphisms` / `ctx.defines`
    so [`IRArgFamilyRef`][quivers.transpile.ir.IRArgFamilyRef]
    rendering can read the referenced morphism's `init_family`
    clause.

    Parameters
    ----------
    ir
        The lowered program.
    morphisms
        The module's morphism declarations, by name.
    lets
        The module's define expressions, by name.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dangling_refs(ir)
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    ctx = RenderContext(sb=sb, morphisms=dict(morphisms), defines=dict(lets))
    py = PyCtx(
        sb,
        cards=dict(ir.cards),
        target="pymc",
        name_event_rank=name_event_rank_map(ir),
        factor_towers=factor_tower_names(ir),
        name_plates=name_plate_map(ir),
    )
    bag = _PyMCCtx(ctx=ctx, py=py, ir=ir)

    # `module` root.
    py.v("mod", "module")

    # Function body block.
    fn_body = py.v(py.fresh("fbody"), "block")

    # `with pymc.Model(coords={...}) as model:` header.
    coords_dict = self._build_coords_dict(py, ir)
    model_call = call(
        py,
        attribute(py, ("pymc", "Model")),
        keyword=(("coords", coords_dict),),
    )
    with_body = py.v(py.fresh("wbody"), "block")
    ws = with_statement(
        py, expression=model_call, alias="model", body_vid=with_body
    )
    py.e(fn_body, ws, "child_of")

    # Walk the body, dispatching each node into the with-body block.
    bag.with_body = with_body
    if not ir.body:
        noop = py.v(py.fresh("pass"), "pass_statement")
        py.e(with_body, noop, "child_of")
    for node in ir.body:
        self._dispatch_pymc(bag, node)

    # Graft runtime helpers for families PyMC does not ship, once,
    # as top-level definitions preceding `build_model`.
    if any(
        _ir_uses_family(ir.body, family) for family in _PYMC_RUNTIME_HELPER_FAMILIES
    ) or qiec_helper_families_used(ir, self.target):
        _graft_runtime_pymc_helpers(py)

    # Trailing `return model` inside the function body (outside
    # the with-block). PyMC's idiom is for `build_model` to return
    # the constructed model object.
    ret = py.v(py.fresh("ret"), "return_statement")
    py.e(ret, identifier(py, "model"), "child_of")
    py.e(fn_body, ret, "child_of")

    # `def build_model(<data_inputs>=None): <fn_body>`.
    param_names = tuple(inp.name for inp in ir.inputs)
    fn = function_def(
        py,
        name="build_model",
        default_params=param_names,
        body_vid=fn_body,
    )
    py.e("mod", fn, "child_of")

    render_computations_dynamic(sb, ir, target=self.target, root="mod")
    return sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

PyMC declarations ARE the constructor calls (emitted in sample). declare is thus a no-op; the caller's emit handles both declaration and assignment in one node.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/pymc.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """PyMC declarations ARE the constructor calls (emitted in
    `sample`). `declare` is thus a no-op; the caller's emit
    handles both declaration and assignment in one node.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

No-op at the protocol dispatch point: the renderer drives the enumeration through _emit_marginalize, which needs the PyMC-specific with-body context.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/pymc.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """No-op at the protocol dispatch point: the renderer drives
    the enumeration through `_emit_marginalize`, which needs the
    PyMC-specific with-body context.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, node
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit np.full((K,), <value>) for 1D, np.full((R, C), <value>) for 2D target shapes.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/pymc.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit `np.full((K,), <value>)` for 1D, `np.full((R, C),
    <value>)` for 2D target shapes.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx
    msg = (
        "PyMCRenderer.broadcast: standalone broadcast emission "
        "is internal; use `_render_arg` on an `IRArgBroadcast` "
        "instead."
    )
    raise RuntimeError(msg)

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Renderer protocol entry: PyMC's emit is structurally joined with declare (the constructor call IS the declaration). Not called by the PyMC walker (which calls _emit_sample directly); kept here so external callers using the abstract protocol can drive the renderer.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/pymc.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """`Renderer` protocol entry: PyMC's emit is structurally
    joined with declare (the constructor call IS the declaration).
    Not called by the PyMC walker (which calls `_emit_sample`
    directly); kept here so external callers using the abstract
    protocol can drive the renderer.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, family, args, arg_names, constraint, plate, observed
    msg = (
        "PyMCRenderer.sample is not called directly; PyMC dispatch "
        "goes through `_emit_sample` to thread the per-walk bag."
    )
    raise RuntimeError(msg)

PyroRenderer

Bases: RendererBase

Render an IRProgram to a Python source panproto.Schema under the Pyro PPL idiom.

target_protocol

target_protocol() -> Protocol

Return the panproto protocol of the target grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/pyro.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol of the target grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("python")

render

render(ir: IRProgram) -> Schema

Walk the IR and emit a Pyro def model(...): ... function.

Overrides the inherited walk so the function header (with <observed>=None default parameters drawn from the IR's IRObserve nodes) and the body block are constructed up front; the inherited dispatch then routes each node into the body.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/pyro.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Walk the IR and emit a Pyro `def model(...): ...` function.

    Overrides the inherited walk so the function header (with
    `<observed>=None` default parameters drawn from the IR's
    `IRObserve` nodes) and the body block are constructed up
    front; the inherited dispatch then routes each node into
    the body.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    ctx = RenderContext(sb=sb, morphisms={}, defines={})
    pctx = _PyroCtx(
        sb=sb,
        cards=dict(ir.cards),
        name_event_rank=name_event_rank_map(ir),
        factor_towers=factor_tower_names(ir),
        name_plates=name_plate_map(ir),
    )

    # Resolve module-level morphism / let tables for IRArgFamilyRef
    # lookup. The IR carries family names directly for atomic
    # families; wrappers like `Truncated(base, ...)` carry the
    # `base` reference via IRArgFamilyRef and the renderer looks
    # the wrapped family up via the resolved init_family.
    # (The IR program does not carry the module; renderers either
    # receive an externally-built morphism table or rebuild from
    # the IR. We rebuild from `IRSample`s the IR exposes; opaque
    # wrappers requiring deeper resolution surface via
    # IRArgFamilyRef and we error if the wrapped family cannot be
    # resolved at emit time.)
    pctx.morphisms = ctx.morphisms

    # Gather observed names so the function signature carries
    # `<obs>=None` defaults.
    observed_names = _observed_names(ir.body)
    # Function header: positional params are every IRDataInput
    # that is not observed; default params are the observed ones.
    param_names = tuple(
        inp.name for inp in ir.inputs if inp.name not in observed_names
    )
    default_params = tuple(
        inp.name for inp in ir.inputs if inp.name in observed_names
    )

    pctx.v("mod", "module")
    # Pyro lacks several QVR families as built-ins (`TruncatedNormal`,
    # `LogitNormal`, `HalfStudentT`, `MatrixNormal`,
    # `InverseWishart`); their runtime helpers live at
    # [`quivers.transpile.runtime_pyro`][quivers.transpile.runtime_pyro].
    # When the IR samples or observes from such a family, graft the
    # parsed helper `class` subtree onto the module above `model`
    # so a reader sees the helper classes first (the natural Python
    # idiom: define classes before consumers).
    for helper_name in sorted(
        _ir_helper_classes_used(ir.body) | qiec_helper_roots(ir, _TARGET)
    ):
        _emit_runtime_helper(pctx, helper_name)
    body = pctx.v(pctx.fresh("body"), "block")
    func = _function_def_split(
        pctx,
        name="model",
        positional=param_names,
        defaults=default_params,
        body_vid=body,
    )
    pctx.e("mod", func, "child_of")

    pctx.body = body
    pctx.observed = frozenset(observed_names)
    pctx.module = ir.module
    if not ir.body:
        noop = pctx.v(pctx.fresh("pass"), "pass_statement")
        pctx.e(body, noop, "child_of")

    # torch's advanced indexing requires integer index tensors, so
    # every host-integer input the program subscripts with is
    # coerced to `long` at the top of the body. QVR declares these
    # gather covariates as integers; the coercion makes the model
    # self-contained regardless of the caller's supplied dtype.
    for idx_name in sorted(_integer_index_input_names(ir)):
        coerce = assignment(
            pctx,
            lhs_name=idx_name,
            rhs=_python_method_call(
                pctx,
                identifier(pctx, idx_name),
                "long",
                (),
            ),
        )
        pctx.e(pctx.body, coerce, "child_of")

    # Hoist each axis that backs more than one sample / observe site
    # to a single reused `pyro.plate(...)` object.
    for dim in _shared_plate_dims(ir.body):
        axis = str(dim.name)
        var_name = f"{axis}_plate"
        pctx.shared_plate_vars[axis] = var_name
        pctx.e(
            pctx.body,
            assignment(
                pctx,
                lhs_name=var_name,
                rhs=self._plate_object_call(pctx, dim),
            ),
            "child_of",
        )

    for node in ir.body:
        self._dispatch_pyro_node(pctx, ctx, node)

    render_computations_dynamic(sb, ir, target=self.target, root="mod")
    return sb.build()

declare

declare(ctx: RenderContext, name: str, constraint, plate: Plate, *, block: BlockKind) -> SchemaFragment

No-op outside "function_body": Pyro picks data inputs up in the model function signature, not via a declaration block.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/pyro.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """No-op outside `"function_body"`: Pyro picks data inputs up
    in the model function signature, not via a declaration block.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint, plate: Plate, observed: bool) -> SchemaFragment

Build the pyro.sample(...) call; the caller threads the return vertex into the enclosing with / block.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/pyro.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Build the `pyro.sample(...)` call; the caller threads the
    return vertex into the enclosing `with` / block.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del constraint, plate
    return self._build_sample_call(
        ctx,
        name=name,
        family=family,
        args=args,
        arg_names=arg_names,
        observed=observed,
    )

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Emit nothing: Pyro integrates latents from its own walk.

render routes each marginalize block through the Pyro walk, which carries the function-body state this dispatch point does not receive, so the shared dispatch point contributes no fragment of its own.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The empty fragment.

Source code in src/quivers/transpile/renderers/pyro.py
def marginalize(self, ctx: RenderContext, node: IRMarginalize) -> SchemaFragment:
    """Emit nothing: Pyro integrates latents from its own walk.

    [`render`][quivers.transpile.renderers.PyroRenderer.render] routes
    each marginalize block through the Pyro walk, which carries the
    function-body state this dispatch point does not receive, so the
    shared dispatch point contributes no fragment of its own.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The empty fragment.
    """
    del ctx, node
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit torch.full((K,), <value>) for rank-1, torch.full( (R, C), <value>) for rank-2.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/pyro.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit `torch.full((K,), <value>)` for rank-1, `torch.full(
    (R, C), <value>)` for rank-2.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    pctx = _PyroCtx(sb=ctx.sb)
    return self._broadcast(pctx, value, target_shape)

StanRenderer

StanRenderer(*, source_module: Module | None = None)

Bases: RendererBase

Render an IRProgram to a Stan panproto.Schema.

Subclasses RendererBase and overrides the four dispatch points (declare, sample, marginalize, broadcast) plus the two list / matrix arg helpers per the spec.

Initialise the renderer.

PARAMETER DESCRIPTION
source_module

Optional original Module the IR came from. Carries MorphismDecl / DefineDecl entries the renderer reads when resolving IRArgFamilyRef args (Truncated(base, ...) style wrappers). When omitted, the renderer raises on IRArgFamilyRef rather than guess.

TYPE: Module | None DEFAULT: None

Source code in src/quivers/transpile/renderers/stan.py
def __init__(self, *, source_module: Module | None = None) -> None:
    """Initialise the renderer.

    Parameters
    ----------
    source_module
        Optional original [`Module`][quivers.dsl.ast_nodes.Module]
        the IR came from. Carries `MorphismDecl` / `DefineDecl`
        entries the renderer reads when resolving
        [`IRArgFamilyRef`][quivers.transpile.ir.IRArgFamilyRef] args
        (`Truncated(base, ...)` style wrappers). When omitted, the
        renderer raises on `IRArgFamilyRef` rather than guess.
    """
    self._source_module = source_module
    # Block roots are populated lazily during the IR walk.
    self._blocks: dict[BlockKind, str] = {}
    # Marginalize state for nested arg-substitution.
    self._marginalize_stack: tuple[Plate, ...] = ()
    # `lps_<latent>` accumulators per marginalize call site,
    # used to thread per-group log-sums through scope observes.
    self._marginalize_var: str | None = None
    self._marginalize_family: str | None = None
    self._marginalize_atom: float | None = None
    self._marginalize_latent_card: int | None = None
    self._marginalize_group_idx: tuple[str, ...] = ()
    # True while the active marginalize keys its accumulator by
    # the observation rather than by the grouping plate.
    self._marginalize_per_row: bool = False
    # Scope-local let bindings inside the current marginalize
    # block; the per-k observe expands these inline since the
    # let target is never declared as a Stan parameter.
    self._marginalize_let_subs: dict[str, LetExprNode] = {}
    # Bookkeeping for redeclaration avoidance.
    self._declared: dict[BlockKind, set[str]] = {}
    # Fresh counter is renderer-internal; the base's RenderContext
    # counter remains untouched so per-walk node IDs stay stable.
    self._fresh_n = 0
    # Per-render lookup caches; `render()` clears them on every
    # entry so repeated render calls on the same renderer produce
    # identical schemas.
    self._simplex_cards_state: dict[str, int] = {}
    self._declared_shapes_state: dict[str, tuple[Constraint, Plate]] = {}
    # name -> K for chain variables whose downstream consumer
    # binds the value to a slot whose constraint has
    # `event_dim >= 1` (e.g. Categorical's `probs`). The
    # declaration path promotes the scalar real type to
    # `vector[K]`, and the deterministic / sample emit paths
    # wrap any scalar RHS in `rep_vector(rhs, K)`. See
    # `_compute_vector_promotions`.
    self._vector_promotions_state: dict[str, int] = {}
    self._class_index_widths_state: dict[str, int] = {}

target_protocol

target_protocol() -> Protocol

Return the panproto protocol of the target grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/stan.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol of the target grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("stan")

render

render(ir: IRProgram) -> Schema

Walk the IR and emit a Stan schema.

Override of the base render so the program-level layout (the program vertex with five child blocks) is built once per call.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/stan.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Walk the IR and emit a Stan schema.

    Override of the base `render` so the program-level layout
    (the `program` vertex with five child blocks) is built once
    per call.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dangling_refs(ir)
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    morphisms, lets = self._resolve_morphisms_and_lets()
    ctx = RenderContext(sb=sb, morphisms=morphisms, defines=lets)
    # Reset per-render state.
    self._blocks = {}
    self._declared = {
        "data": set(),
        "parameters": set(),
        "transformed_parameters": set(),
        "model": set(),
        "generated_quantities": set(),
        "function_body": set(),
    }
    self._marginalize_stack = ()
    self._marginalize_var = None
    self._marginalize_family = None
    self._marginalize_atom = None
    self._marginalize_latent_card = None
    self._marginalize_group_idx = ()
    self._marginalize_per_row = False
    self._fresh_n = 0
    self._cards = dict(ir.cards)
    # Reset the per-render lookup caches so repeated render
    # calls on the same renderer produce identical schemas.
    self._simplex_cards_state.clear()
    self._declared_shapes_state.clear()
    self._vector_promotions_state.clear()
    self._vector_promotions_state.update(self._compute_vector_promotions(ir))
    self._class_index_widths_state.clear()
    self._class_index_widths_state.update(self._compute_class_index_widths(ir))
    # A needed computation the static subset lacks is refused before
    # the body is walked, so its call site is never rendered.
    if has_runtime_computations(ir):
        refuse_static_gaps(ir, self.target)
    # Program root.
    ctx.sb.vertex("prog", "program")
    if carried_computations(ir, self.target):
        self._ensure_block(ctx, "function_body")
    # Stan ships `normal`, `beta`, `gamma`, ... as built-in
    # densities but lacks `kumaraswamy`. When the IR samples or
    # observes from a family whose Stan emit relies on a user-
    # defined `<family>_lpdf` / `<family>_rng`, graft the
    # hand-written helper at
    # [`runtime_stan_functions.stan`][quivers.transpile.runtime_stan_functions]
    # into the program above the data block so the sampling
    # statement `y ~ kumaraswamy(a, b);` resolves through Stan's
    # `<family>_lpdf` lookup convention.
    if any(_ir_uses_family(ir.body, f) for f in _STAN_RUNTIME_HELPER_FAMILIES):
        _graft_runtime_stan_helper(ctx.sb, self, "prog")
    # Pre-create blocks in canonical Stan order so emit_pretty
    # honours the `data? transformed_data? parameters?
    # transformed_parameters? model? generated_quantities?`
    # grammar production. Without this the IR-walk's lazy
    # `_ensure_block` connects blocks in IR-encounter order
    # (`data` -> `parameters` -> `model` -> `transformed_parameters`),
    # which puts `transformed_parameters` past `model` and
    # makes the pretty-printer drop it.
    for kind in self._needed_blocks(ir):
        self._ensure_block(ctx, kind)
    # IRDataInput entries land in `data`.
    for inp in ir.inputs:
        self.declare(
            ctx,
            inp.name,
            inp.constraint,
            inp.plate,
            block="data",
        )
    # Walk the body.
    for node in ir.body:
        self._dispatch_node(ctx, node)
    if has_runtime_computations(ir):
        render_computations_static(
            ctx.sb,
            ir,
            target=self.target,
            destination=self._blocks.get("function_body", "prog"),
        )
    return ctx.sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

Emit a declaration for name in block.

Dispatches purely on the support predicates of §2.2 of the spec, threaded through ConstraintSpec.to_constraint. The table is the §5 Stan table.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/stan.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """Emit a declaration for `name` in `block`.

    Dispatches purely on the support predicates of §2.2 of the
    spec, threaded through
    [`ConstraintSpec.to_constraint`][quivers.transpile.ir.ConstraintSpec.to_constraint].
    The table is the §5 Stan table.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    if name in self._declared[block]:
        return ""
    self._declared[block].add(name)
    sup = constraint.to_constraint()
    # Track for later GQ aliasing emission.
    self._declared_shapes[name] = (sup, plate)
    parent = self._ensure_block(ctx, block)
    decl = self._fresh(ctx, "decl")
    ctx.sb.vertex(decl, "top_var_decl_no_assign")
    ctx.sb.edge(parent, decl, "child_of")
    # arr_dims when there are batch dims.
    if plate.batch_dims:
        arr = self._fresh(ctx, "arr")
        ctx.sb.vertex(arr, "arr_dims")
        for dim in plate.batch_dims:
            size_vid = self._dim_size_vertex(ctx, dim)
            ctx.sb.edge(arr, size_vid, "child_of")
        ctx.sb.edge(decl, arr, "child_of")
    # top_var_type, then the appropriate inner type per predicate.
    tvt = self._fresh(ctx, "tvt")
    ctx.sb.vertex(tvt, "top_var_type")
    promoted_k = self._vector_promotions.get(name)
    class_width = self._class_index_widths.get(name)
    if promoted_k is not None:
        self._emit_vector_type_of_size(ctx, tvt, promoted_k)
    elif class_width is not None:
        # A class-index outcome is a subscript into its family's
        # alphabet, so Stan reads it on `1:K`. The IR carries the
        # sentinel-derived `[0, 1]` interval every integer support
        # starts from, which would declare a 200-word vocabulary
        # as a bit.
        self._emit_int_type(ctx, tvt, lower=1, upper=class_width)
    else:
        self._emit_type(ctx, tvt, sup, plate.event_dims)
    ctx.sb.edge(decl, tvt, "child_of")
    # name field.
    nm = self._fresh(ctx, "ident")
    ctx.sb.vertex(nm, "identifier")
    ctx.sb.constraint(nm, "literal-value", name)
    ctx.sb.edge(decl, nm, "name")
    return decl

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Emit a target += <family>_lpdf(...) statement for a sample / observe step.

Stan's ~ sampling notation drops every term that does not depend on a parameter, so model.log_prob(jacobian=False) on a ~-built program omits data-only contributions and the omitted amount moves with the data. The explicit target += <family>_lpdf(<lhs> | <args>) form keeps every normalizing constant, which is what the constant-spread equivalence contract requires: the emitted program computes the joint exactly, not up to a data-dependent offset.

Wraps the increment in nested for (m_<axis> in 1:<size>) loops for each batch dim. Indexes the LHS and every arg whose ref-name sits on the plate.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/stan.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Emit a `target += <family>_lpdf(...)` statement for a
    sample / observe step.

    Stan's `~` sampling notation drops every term that does not
    depend on a parameter, so `model.log_prob(jacobian=False)`
    on a `~`-built program omits data-only contributions and the
    omitted amount moves with the data. The explicit
    `target += <family>_lpdf(<lhs> | <args>)` form keeps every
    normalizing constant, which is what the constant-spread
    equivalence contract requires: the emitted program computes
    the joint exactly, not up to a data-dependent offset.

    Wraps the increment in nested `for (m_<axis> in 1:<size>)`
    loops for each batch dim. Indexes the LHS and every arg whose
    ref-name sits on the plate.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del observed  # The sample emission shape does not differ.
    del constraint  # The constraint shaped the declaration; the
    # log-density suffix comes from the Stan family name.
    # Resolve the per-target family name.
    meta = FAMILY_META.get(family)
    if meta is None:
        raise UnsupportedConstruct(
            "qvr-stan",
            [f"family:unknown:{family}"],
        )
    stan_name = meta.target_names.get("stan")
    if stan_name is None:
        raise UnsupportedConstruct(
            "qvr-stan",
            [f"family:no-stan-target:{family}"],
        )
    if family == "MixtureNormal":
        return self._emit_mixture_normal(
            ctx, name=name, args=args, arg_names=arg_names, plate=plate
        )
    if family == "Categorical" and arg_names == ("logits",):
        stan_name = "categorical_logit"
    density_name = self._log_density_name(family, stan_name)
    del arg_names  # Stan is positional; arg_names are unused.
    parent = self._ensure_block(ctx, "model")
    # Build loop indices for the batch dims.
    loop_names = self.index_for(ctx, plate)
    # The innermost block we'll attach the target statement to.
    innermost_parent = self._wrap_in_for_loops(
        ctx, parent, plate.batch_dims, loop_names
    )
    # Args + optional truncation correction.
    # TruncatedNormal: split (loc, scale, low, high) -> two args
    # to `normal_lpdf()` plus an explicit log-mass correction.
    if family == "TruncatedNormal":
        if len(args) != 4:
            raise UnsupportedConstruct(
                "qvr-stan",
                [
                    f"family:TruncatedNormal: expected 4 args "
                    f"(loc, scale, low, high), got {len(args)}"
                ],
            )
        family_args = args[:2]
        truncation_args: tuple[IRArg, ...] | None = args[2:]
    else:
        family_args = args
        truncation_args = None
    injected = self._inject_stan_specific_args(family, family_args)
    rewritten = self._broadcast_scalar_refs(injected, meta, plate)
    # `<density_name>(<lhs> | <args>)`.
    de = self._fresh(ctx, "de")
    ctx.sb.vertex(de, "distr_expression")
    dn = self._fresh(ctx, "dnm")
    ctx.sb.vertex(dn, "identifier")
    ctx.sb.constraint(dn, "literal-value", density_name)
    ctx.sb.edge(de, dn, "name")
    dal = self._fresh(ctx, "dal")
    ctx.sb.vertex(dal, "distr_argument_list")
    # Variate: name indexed by loop vars.
    ctx.sb.edge(dal, self._build_lhs(ctx, name, loop_names), "child_of")
    if family == "NegativeBinomial":
        self._emit_neg_binomial_2_args(ctx, dal, rewritten, plate, loop_names)
    else:
        for arg in rewritten:
            substituted = self._substitute_for_loops(arg, plate, loop_names)
            arg_vid = self._render_arg(ctx, substituted)
            ctx.sb.edge(dal, arg_vid, "child_of")
    ctx.sb.edge(de, dal, "child_of")
    if truncation_args is None:
        rhs = de
    else:
        rhs = self._apply_truncation_correction(
            ctx,
            de,
            stan_name,
            rewritten,
            truncation_args,
            plate,
            loop_names,
        )
    ts = self._fresh(ctx, "sts")
    ctx.sb.vertex(ts, "target_statement")
    ctx.sb.edge(ts, rhs, "child_of")
    ctx.sb.edge(innermost_parent, ts, "child_of")
    return ts

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Emit a marginalized latent.

Families with declared finite marginal support are enumerated with a log_sum_exp reduction. This includes relaxation families whose marginal heads declare discrete atoms. Families without finite support become constrained Stan parameters: the renderer adds their prior density and emits the scope with that parameter in context. Stan then samples the resulting joint model.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/stan.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Emit a marginalized latent.

    Families with declared finite marginal support are enumerated with a
    `log_sum_exp` reduction. This includes relaxation families whose
    marginal heads declare discrete atoms. Families without finite support
    become constrained Stan parameters: the renderer adds their prior
    density and emits the scope with that parameter in context. Stan then
    samples the resulting joint model.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    meta = FAMILY_META.get(node.family)
    if meta is None:
        raise UnsupportedConstruct(
            "qvr-stan",
            [f"family:unknown:{node.family}"],
        )
    latent_sup = node.constraint.to_constraint()
    # `finite_enumerable_at_call_site` answers the plate question,
    # whether the marginalize index sizes the family's own
    # support; `marginalize_support` answers the integration one.
    # A relaxation family replicates along its index and is still
    # integrable over its two atoms, so the latter is what decides
    # between enumerating and declaring a live latent.
    integrable = marginalize_support(meta) is not None
    if not integrable and _is_continuous_support(latent_sup):
        return self._marginalize_continuous(ctx, node, meta)
    if not integrable:
        raise UnsupportedConstruct(
            "qvr-stan",
            [f"marginalize:non-finite-support:{node.family}"],
        )
    stan_name = meta.target_names.get("stan")
    if stan_name is None:
        raise UnsupportedConstruct(
            "qvr-stan",
            [f"family:no-stan-target:{node.family}"],
        )
    if node.family == "Categorical" and node.arg_names == ("logits",):
        stan_name = "categorical_logit"
    # Eligibility checks satisfied; compute the latent cardinality.
    latent_card = self._latent_cardinality(meta, node, ctx)
    if latent_card is None:
        raise UnsupportedConstruct(
            "qvr-stan",
            [
                f"marginalize:unknown-cardinality:{node.family}: "
                f"cannot determine support size at compile time"
            ],
        )
    parent = self._ensure_block(ctx, "model")
    # Open a fresh `{ ... }` block scope for the lps_<latent> array.
    outer_block = self._fresh(ctx, "mbs")
    ctx.sb.vertex(outer_block, "block_statement")
    ctx.sb.edge(parent, outer_block, "child_of")
    # Choose what the accumulator is keyed by. A per-row prior --
    # the probability argument is itself indexed by the grouping
    # plate, so each observation reads its own group's row --
    # replicates the latent per observation, and the enumeration
    # runs once per observation. A global prior instead shares one
    # latent across each group's rows, so the rows' contributions
    # scatter into the group before the reduction. This is the
    # same discriminator the QVR compiler applies.
    per_row = self._prior_is_group_plated(node)
    if per_row:
        acc_dims = self._marginalize_row_dims(node)
        acc_loop_names = self._observe_scope_loop_names(acc_dims)
    else:
        acc_dims = node.plate.batch_dims
        acc_loop_names = self._marginalize_loop_names(acc_dims)
    # 1. Declare `array[acc_dims] vector[K] lps_<latent>;`.
    lps_name = f"lps_{node.latent}"
    self._declare_lps_array(ctx, outer_block, lps_name, acc_dims, latent_card)
    # 2. Seed each accumulator row with the latent log-pmf:
    #    for each accumulator index, for k in 1:K,
    #    lps[..., k] = lpmf(k | args).
    self._emit_lps_init(
        ctx,
        outer_block,
        lps_name,
        acc_dims,
        acc_loop_names,
        latent_card,
        stan_name,
        node.args,
        meta,
        self._prior_index_args(node, acc_loop_names, per_row),
    )
    # 3. Walk the scope body: each scope IRObserve emits an
    #    inner-loop that accumulates per-k contributions into
    #    `lps[<accumulator index>, k]`.
    prev_marg_var = self._marginalize_var
    prev_marg_family = self._marginalize_family
    prev_marg_card = self._marginalize_latent_card
    prev_group_idx = self._marginalize_group_idx
    prev_per_row = self._marginalize_per_row
    prev_stack = self._marginalize_stack
    self._marginalize_var = lps_name
    self._marginalize_family = node.family
    self._marginalize_latent_card = latent_card
    self._marginalize_group_idx = acc_loop_names
    self._marginalize_per_row = per_row
    self._marginalize_stack = (*self._marginalize_stack, node.plate)
    try:
        for scope_node in node.scope:
            self._dispatch_marginalize_scope(ctx, outer_block, scope_node, node)
    finally:
        self._marginalize_var = prev_marg_var
        self._marginalize_family = prev_marg_family
        self._marginalize_latent_card = prev_marg_card
        self._marginalize_group_idx = prev_group_idx
        self._marginalize_per_row = prev_per_row
        self._marginalize_stack = prev_stack
    # 4. Accumulate the per-row log-sums into `target`.
    self._emit_lps_accumulate(
        ctx,
        outer_block,
        lps_name,
        acc_dims,
        acc_loop_names,
    )
    return outer_block

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit a Stan broadcast call: rep_vector(<value>, K) for 1D, rep_matrix(<value>, R, C) for 2D.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/stan.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit a Stan broadcast call:
    `rep_vector(<value>, K)` for 1D, `rep_matrix(<value>, R, C)`
    for 2D.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    if len(target_shape) == 0:
        return self._render_arg(ctx, value)
    if len(target_shape) == 1:
        fn_name = "rep_vector"
    elif len(target_shape) == 2:
        fn_name = "rep_matrix"
    else:
        raise UnsupportedConstruct(
            "qvr-stan",
            [
                f"broadcast:rank:{len(target_shape)}: Stan "
                f"broadcast supports up to rank 2"
            ],
        )
    fn = self._fresh(ctx, "bfn")
    ctx.sb.vertex(fn, "function_expression")
    fnid = self._fresh(ctx, "bfnid")
    ctx.sb.vertex(fnid, "identifier")
    ctx.sb.constraint(fnid, "literal-value", fn_name)
    ctx.sb.edge(fn, fnid, "name")
    al = self._fresh(ctx, "bal")
    ctx.sb.vertex(al, "argument_list")
    val_vid = self._render_arg(ctx, value)
    ctx.sb.edge(al, val_vid, "child_of")
    for size in target_shape:
        ctx.sb.edge(al, self._int_literal(ctx, size), "child_of")
    ctx.sb.edge(fn, al, "child_of")
    return fn

TuringRenderer

TuringRenderer(*, morphisms: dict | None = None, lets: dict | None = None)

Bases: RendererBase

Render an IRProgram as a Turing.jl model.

Overrides render to wrap the IR-walk with the @model function model(...) ... end prologue / epilogue Turing.jl requires. The per-node dispatches (declare, sample, marginalize, broadcast) accumulate Julia schema vertices into a single function body block.

Construct a renderer.

morphisms and lets are the resolved tables from the surface module; the renderer consults morphisms when an IRArgFamilyRef needs the referenced morphism's ~ Family(...) init clause. Both default to empty so callers driving a synthetic IRProgram (tests) can instantiate the renderer with no surface context.

PARAMETER DESCRIPTION
morphisms

The module's morphism declarations, by name.

TYPE: dict | None DEFAULT: None

lets

The module's define expressions, by name.

TYPE: dict | None DEFAULT: None

Source code in src/quivers/transpile/renderers/turing.py
def __init__(
    self,
    *,
    morphisms: dict | None = None,
    lets: dict | None = None,
) -> None:
    """Construct a renderer.

    `morphisms` and `lets` are the resolved tables from the surface
    module; the renderer consults `morphisms` when an
    [`IRArgFamilyRef`][quivers.transpile.ir.IRArgFamilyRef] needs
    the referenced morphism's `~ Family(...)` init clause. Both
    default to empty so callers driving a synthetic
    [`IRProgram`][quivers.transpile.ir.IRProgram] (tests) can
    instantiate the renderer with no surface context.

    Parameters
    ----------
    morphisms
        The module's morphism declarations, by name.
    lets
        The module's define expressions, by name.
    """
    self._morphisms: dict = morphisms or {}
    self._lets: dict = lets or {}

target_protocol

target_protocol() -> Protocol

Return the panproto protocol of the target grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/turing.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol of the target grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("julia")

render

render(ir: IRProgram) -> Schema

Render a program to the target's panproto schema.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/turing.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Render a program to the target's panproto schema.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    counter: list[int] = [0]
    # Source file shell.
    source = _vertex(sb, counter, "source_file")
    body = _vertex(sb, counter, "block")

    # The Turing.jl function signature carries every IRDataInput
    # in alphabetical name order. The in-container probe (which
    # has no access to the IR's lowering order) passes observed
    # values as positional args in `sort(collect(keys(data)))`
    # order; sorting here keeps the two sides in lockstep so
    # callers never have to know the original IR order.
    params = tuple(sorted(inp.name for inp in ir.inputs))

    # Walk the body. Each node emits a `~` / `=` / `return`
    # statement into `body`.
    ctx = _TuringCtx(
        sb=sb,
        morphisms=self._morphisms,
        lets=self._lets,
        counter=counter,
        cards=dict(ir.cards),
        body=body,
        input_plates={inp.name: inp.plate for inp in ir.inputs},
        sample_plates={},
        batch_shaped_names=set(),
        shapes=JuliaShapes(
            name_event_rank=name_event_rank_map(ir),
            name_array_rank=name_array_rank_map(ir),
            nested_names=nested_tower_names(ir),
        ),
        module=ir.module,
    )
    # Pre-populate the sample-plate table by walking the body so
    # observe / marginalize bodies can detect index-dependent args
    # against the originating sample's plate.
    _seed_sample_plates(ctx, ir.body)
    # Pre-populate the batch-shaped-names table: every
    # IRDataInput / IRSample with non-empty batch_dims, plus every
    # IRDeterministic whose RHS transitively references one such
    # name. Drives the broadcast-dot fallback for observes whose
    # `loc=` etc. is a let-bound vector (e.g. `mu = a + b *
    # x_design` in bayes_linear_regression).
    _seed_batch_shaped(ctx, ir.body)
    for node in ir.body:
        self._dispatch(ctx, node)

    fn = _function_def(sb, counter, name="model", params=params, body_vid=body)
    macro = _macro_call(sb, counter, "model", fn)
    # Turing.jl + Distributions.jl ship a large catalogue of
    # distributions but lack `ContinuousBernoulli` and an RBF
    # kernel builder. When the IR samples or observes through
    # either, graft the helper at
    # [`runtime_turing.jl`][quivers.transpile.runtime_turing] onto
    # the source above the `@model function model` macrocall so the
    # body's `~ ContinuousBernoulli(...)` / `_qvr_rbf_kernel(...)`
    # call sites resolve through normal Julia name lookup.
    if any(
        _ir_uses_family(ir.body, f) for f in _TURING_RUNTIME_HELPER_FAMILIES
    ) or qiec_helper_families_used(ir, self.target):
        _graft_runtime_turing_helper(sb, counter, source)
    sb.edge(source, macro, "child_of")
    render_computations_dynamic(sb, ir, target=self.target, root=source)
    return sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

No-op: in Turing.jl, declarations are the ~ statements themselves. The function signature is populated from the IRDataInput list in render.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/turing.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """No-op: in Turing.jl, declarations are the `~` statements
    themselves. The function signature is populated from the
    IRDataInput list in [`render`][quivers.transpile.renderers.TuringRenderer.render].

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, name, constraint, plate, block
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool, *, via: str | None = None) -> SchemaFragment

Emit one ~ / .~ statement for a sample or observe step.

Plate handling: * batch_dims=() and no via: plain name ~ Family(args). * batch_dims=(B,) and no arg references a batch-plated name via index: name ~ filldist(Family(args), B). * Otherwise (index-dependent args or a via fibration): broadcast-dot form name .~ Family.(idx_args), or the arraydist([Family(args[i]) for i in 1:B]) fallback when no via is present.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

via

The fibration an observe gathers its rows through, or None.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/turing.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
    *,
    via: str | None = None,
) -> SchemaFragment:
    """Emit one `~` / `.~` statement for a sample or observe step.

    Plate handling:
    * `batch_dims=()` and no `via`: plain `name ~ Family(args)`.
    * `batch_dims=(B,)` and no arg references a batch-plated name
      via index: `name ~ filldist(Family(args), B)`.
    * Otherwise (index-dependent args or a `via` fibration):
      broadcast-dot form `name .~ Family.(idx_args)`, or the
      `arraydist([Family(args[i]) for i in 1:B])` fallback when no
      via is present.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.
    via
        The fibration an observe gathers its rows through, or ``None``.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del constraint  # output support already encoded in the family choice
    assert isinstance(ctx, _TuringCtx)
    ctx.sample_plates[name] = plate
    sb, counter = ctx.sb, ctx.counter

    meta = _meta_or_raise(family)
    target_dist = meta.target_names.get("turing")
    if target_dist is None:
        raise UnsupportedConstruct(
            "qvr-turing", [f"family:{family}: no Turing.jl mapping"]
        )

    if family == "MixtureNormal":
        return self._emit_mixture_normal(
            ctx,
            name=name,
            target_dist=target_dist,
            args=args,
            arg_names=arg_names,
            plate=plate,
        )

    # Split the site's event dims into the trailing ones the
    # family produces natively (`Normal` none, `Dirichlet` one,
    # `LKJCholesky` two) and the leading residual the source
    # declared with `[over=<Axis>]`. The residual is iid
    # replication, so it joins the batch dims in the `filldist`
    # wrapper; without it a `Normal` site declared `over=LatentDim`
    # renders rank-1 where the reference measure is rank-2.
    own_start = len(plate.event_dims) - meta.event_rank
    residual_event = plate.event_dims[:own_start]
    own_event = plate.event_dims[own_start:]

    # Detect index-dependent arg shapes against the surrounding
    # observe/sample's batch axes. The presence of `via` is the
    # strongest signal: it always rewrites the indexing to thread
    # through the fibration variable.
    index_dep = via is not None or _args_have_batch_index(
        args,
        ctx.sample_plates,
        plate,
        ctx.batch_shaped_names,
    )

    if own_event and (plate.batch_dims or residual_event):
        # A batched draw from a family with its own event shape.
        # `filldist(Dirichlet(...), 8)` would produce a 16-by-8
        # matrix: every Turing product of a multivariate family
        # is event-major, where the QVR site is batch-major
        # (`array[8] simplex[16]` in Stan, `dims=("State","Obs")`
        # in PyMC). The pre-allocated container plus a per-row `~`
        # is the Turing idiom that lands the axes in the declared
        # order, and DynamicPPL resolves each `name[i, :]` varname
        # against the whole-array entry the caller conditions on.
        return self._emit_row_plate(
            ctx,
            name=name,
            target_dist=target_dist,
            args=args,
            arg_names=arg_names,
            family=family,
            meta=meta,
            plate=plate,
            own_event=own_event,
            residual_event=residual_event,
            index_dep=index_dep,
        )

    if index_dep and residual_event:
        raise UnsupportedConstruct(
            "qvr-turing",
            [
                f"sample:event-axis-lift:{name}: the site declares "
                f"the residual event "
                f"{'axis' if len(residual_event) == 1 else 'axes'} "
                f"{[d.name for d in residual_event]!r} and its "
                f"arguments index the surrounding batch axis. The "
                f"broadcast-dot and `arraydist` forms both fix the "
                f"drawn value at the family's own rank, so the "
                f"residual axis has no Turing.jl spelling on "
                f"either path"
            ],
        )

    lhs = _identifier(sb, counter, name)

    if index_dep and observed and via is not None:
        # Index-dependent observe with a `via` fibration: emit the
        # broadcast-dot form `name .~ Family.(rewritten_args)`.
        rhs_args = tuple(
            _arg_to_julia(ctx, a, via=via, family=family) for a in args
        )
        rhs = _dist_expr(
            sb,
            counter,
            target_dist,
            rhs_args,
            family,
            dotted=True,
            event_dims=own_event,
        )
        stmt = _tilde(sb, counter, lhs, rhs, broadcast=True)
        sb.edge(ctx.body, stmt, "child_of")
        return ""

    if index_dep and observed and via is None:
        # Index-dependent observe whose arg(s) reference a
        # batch-shaped name (e.g. a let-bound vector) directly,
        # without a `via` fibration. The plain
        # `filldist(Family(args), B)` form requires the args to
        # be scalar; wrap the per-element distribution array in
        # `product_distribution(Family.(args))` and emit the
        # scalar tilde. DynamicPPL no longer accepts `.~` over
        # an array of distributions; `product_distribution` is
        # the supported replacement.
        rhs_args = tuple(_arg_to_julia(ctx, a, family=family) for a in args)
        elemwise = _dist_expr(
            sb,
            counter,
            target_dist,
            rhs_args,
            family,
            dotted=True,
            event_dims=own_event,
        )
        rhs = _call(
            sb,
            counter,
            _identifier(sb, counter, "product_distribution"),
            (elemwise,),
        )
        stmt = _tilde(sb, counter, lhs, rhs)
        sb.edge(ctx.body, stmt, "child_of")
        return ""

    if index_dep and not observed:
        # Index-dependent latent: fall back to `arraydist([Family(
        # args[i]) for i in 1:B])`. Pick the first batch dim as
        # the comprehension axis.
        if not plate.batch_dims:
            # No batch axis to iterate; treat as a scalar call.
            rhs = self._family_call(
                ctx, target_dist, args, family, plate.event_dims
            )
        else:
            rhs = self._arraydist_call(
                ctx, target_dist, args, plate.batch_dims[0], family
            )
        stmt = _tilde(sb, counter, lhs, rhs)
        sb.edge(ctx.body, stmt, "child_of")
        return ""

    # Plain / batch-wrapped form. Apply filldist over the batch
    # dims plus any residual event dims the family does not itself
    # produce. Pre-process args: a scalar IRArgRef against a
    # vector-shaped arg constraint (e.g. Dirichlet
    # `concentration`) is wrapped in `fill(<ref>, <event_size>)` so
    # the emitted call matches the Turing.jl idiom of a
    # fully-shaped vector for the event-dim arg.
    promoted = tuple(
        _promote_scalar_ref(a, name, plate, meta)
        for name, a in zip(arg_names, args, strict=False)
    )
    dist = self._family_call(ctx, target_dist, promoted, family, own_event)
    fill_dims = (*plate.batch_dims, *residual_event)
    if fill_dims:
        size_vids = tuple(_dim_to_size(sb, counter, dim) for dim in fill_dims)
        dist = _call(
            sb,
            counter,
            _identifier(sb, counter, "filldist"),
            (dist, *size_vids),
        )
    stmt = _tilde(sb, counter, lhs, dist)
    sb.edge(ctx.body, stmt, "child_of")
    return ""

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Integrate an IRMarginalize latent out, adding the reduced density to the model's log-joint with Turing.@addlogprob!.

One scored copy of the scope per atom of the latent's finite support, then an elementwise reduction across atoms:

__marg_state_0 = logpdf.(Categorical(eachrow(emission_rows)[1]), obs)
...
__marg_state_w = log.(initial_row)
__marg_state_t_0 = __marg_state_w[1] .+ __marg_state_0
...
__marg_state_m = max.(__marg_state_t_0, ..., __marg_state_t_7)
__marg_state = __marg_state_m .+ log.(
  exp.(__marg_state_t_0 .- __marg_state_m) + ...
)
@addlogprob! sum(__marg_state)

The max-shifted form is the numerically stable logsumexp written in Base, so the emitted model needs no import beyond what using Turing already brings into scope. No site is declared for the latent: the atoms replace it, and the emitted program denotes the same measure the QVR reference integrates.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/turing.py
def marginalize(self, ctx: RenderContext, node: IRMarginalize) -> SchemaFragment:
    """Integrate an [`IRMarginalize`][quivers.transpile.ir.IRMarginalize]
    latent out, adding the reduced density to the model's
    log-joint with `Turing.@addlogprob!`.

    One scored copy of the scope per atom of the latent's finite
    support, then an elementwise reduction across atoms:

    ```julia
    __marg_state_0 = logpdf.(Categorical(eachrow(emission_rows)[1]), obs)
    ...
    __marg_state_w = log.(initial_row)
    __marg_state_t_0 = __marg_state_w[1] .+ __marg_state_0
    ...
    __marg_state_m = max.(__marg_state_t_0, ..., __marg_state_t_7)
    __marg_state = __marg_state_m .+ log.(
      exp.(__marg_state_t_0 .- __marg_state_m) + ...
    )
    @addlogprob! sum(__marg_state)
    ```

    The max-shifted form is the numerically stable `logsumexp`
    written in `Base`, so the emitted model needs no import beyond
    what `using Turing` already brings into scope. No site is
    declared for the latent: the atoms replace it, and the emitted
    program denotes the same measure the QVR reference integrates.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    refuse_ungrouped_row_marginalize(f"qvr-{self.target}", node)
    assert isinstance(ctx, _TuringCtx)
    sb, counter = ctx.sb, ctx.counter
    plates = {**ctx.input_plates, **ctx.sample_plates}
    atoms = self.marginal_atoms(
        node,
        support_size=marginal_support_size(node, name_plates=plates),
    )
    raw = marginalize_body(node.scope, latent=node.latent, target=self.target)
    prefix = f"__marg_{node.latent}"
    term_names: list[str] = []
    for position, atom in enumerate(atoms):
        scored = marginalize_body(
            rebase_index_literals(atom.scope),
            latent=node.latent,
            target=self.target,
        )
        for det in scored.deterministics:
            self._emit_deterministic(ctx, det)
        term = f"{prefix}_{position}"
        self._emit_assignment(
            ctx, term, self._atom_log_density(ctx, scored.observe)
        )
        term_names.append(term)
    # A grouped block keys its accumulator by group: the rows the
    # fibration sends to one group are summed before the reduction.
    fibration = marginalize_fibration(
        node,
        raw.observe,
        atoms[0].weight_args,
        atoms[0].weight_arg_names,
        name_plates=plates,
        target=self.target,
    )
    if fibration is not None:
        via, group = fibration
        for term in term_names:
            self._emit_assignment(
                ctx, term, self._group_sums(ctx, term, via, group)
            )
    weight_names = self._emit_atom_weights(ctx, node, raw, atoms, prefix, plates)
    shifted: list[str] = []
    for position, (weight, term) in enumerate(
        zip(weight_names, term_names, strict=True)
    ):
        name = f"{prefix}_t_{position}"
        self._emit_assignment(
            ctx,
            name,
            _dotted_binary(
                sb,
                counter,
                _identifier(sb, counter, weight),
                "+",
                _identifier(sb, counter, term),
            ),
        )
        shifted.append(name)
    max_name = f"{prefix}_m"
    self._emit_assignment(
        ctx,
        max_name,
        _broadcast_call(
            sb,
            counter,
            _identifier(sb, counter, "max"),
            tuple(_identifier(sb, counter, n) for n in shifted),
        ),
    )
    exp_terms = tuple(
        _broadcast_call(
            sb,
            counter,
            _identifier(sb, counter, "exp"),
            (
                _dotted_binary(
                    sb,
                    counter,
                    _identifier(sb, counter, n),
                    "-",
                    _identifier(sb, counter, max_name),
                ),
            ),
        )
        for n in shifted
    )
    total = exp_terms[0]
    for extra in exp_terms[1:]:
        total = _dotted_binary(sb, counter, total, "+", extra)
    reduced = _dotted_binary(
        sb,
        counter,
        _identifier(sb, counter, max_name),
        "+",
        _broadcast_call(sb, counter, _identifier(sb, counter, "log"), (total,)),
    )
    self._emit_assignment(ctx, prefix, reduced)
    summed = _call(
        sb,
        counter,
        _identifier(sb, counter, "sum"),
        (_identifier(sb, counter, prefix),),
    )
    self._add_log_weight(ctx, summed, prefix)
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit the target's broadcast of value to target_shape.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/turing.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit the target's broadcast of ``value`` to ``target_shape``.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    assert isinstance(ctx, _TuringCtx)
    sb, counter = ctx.sb, ctx.counter
    value_vid = _arg_to_julia(ctx, value)
    shape_args = tuple(_integer(sb, counter, s) for s in target_shape)
    return _call(
        sb,
        counter,
        _identifier(sb, counter, "fill"),
        (value_vid, *shape_args),
    )

WebPPLRenderer

WebPPLRenderer(*, source_module: Module | None = None)

Bases: RendererBase

Render an IRProgram to a WebPPL panproto.Schema.

Subclasses RendererBase and overrides the four dispatch points (declare, sample, marginalize, broadcast) plus the two list / matrix arg helpers per the spec.

Initialise the renderer.

PARAMETER DESCRIPTION
source_module

Optional original Module the IR came from. Carries MorphismDecl / DefineDecl entries the renderer reads when resolving IRArgFamilyRef args. When omitted, the renderer raises on IRArgFamilyRef rather than guess.

TYPE: Module | None DEFAULT: None

Source code in src/quivers/transpile/renderers/webppl.py
def __init__(self, *, source_module: Module | None = None) -> None:
    """Initialise the renderer.

    Parameters
    ----------
    source_module
        Optional original [`Module`][quivers.dsl.ast_nodes.Module]
        the IR came from. Carries `MorphismDecl` / `DefineDecl`
        entries the renderer reads when resolving
        [`IRArgFamilyRef`][quivers.transpile.ir.IRArgFamilyRef]
        args. When omitted, the renderer raises on
        `IRArgFamilyRef` rather than guess.
    """
    self._source_module = source_module
    # Per-render scratch state. Reset at the top of every
    # `render` call so a single renderer instance can serve
    # multiple programs back-to-back.
    self._fresh_n = 0
    # The body block id under the top-level function expression.
    self._body_vid: str = ""
    # Per-render map from a sample / observe / input name to its
    # binding plate. Powers the index-dependent / broadcast
    # decisions on later sample steps.
    self._binding_plates: dict[str, Plate] = {}
    # Per-render map from a sample / observe / input name to its
    # support constraint. Powers broadcast detection (a scalar
    # ref into a vector-arg slot needs `repeat` wrapping).
    self._binding_supports: dict[str, ConstraintSpec] = {}
    # `via` fibrations seen on the active observe scope; used by
    # ref substitution to thread the per-observe loop var through
    # references whose binding plate matches the fibration's
    # group axis.
    self._observe_via: str | None = None
    # The per-observe loop variable name, when an observe's
    # mapIndexed body is being emitted; None outside that scope.
    self._observe_loop_var: str | None = None
    # The active marginalize / group plate batch_dim names. When
    # an observe inside a marginalize scope renders, refs to
    # names whose binding plate aligns with this tuple get
    # `via[loop_var]` indexed through the fibration; refs to
    # other plates pass through unchanged.
    self._group_plate_axes: tuple[str, ...] = ()
    # Static-cardinality table snapshotted from `IRProgram.cards`
    # at the top of every `render` call. The let-expression shim
    # reads this when unrolling `LetExprFactor` binders whose
    # axis size needs to be known at render time.
    self._cards: dict[str, int] = {}
    # Counter for fresh loop-variable names generated by the
    # deterministic-binding lift (`__i_<n>`); kept independent of
    # the schema-vertex counter so the names stay short and
    # legible in the rendered source.
    self._lift_n = 0
    # The first observe step's plate seen during the walk, used
    # as the fallback shape for lifted deterministic bindings
    # whose data-input references the IR records as scalar but
    # which are array-shaped at runtime.
    self._observe_array_fallback: Plate | None = None
    # Names registered as IRDataInputs at render start; used by
    # ref-substitution to discriminate function parameters from
    # in-scope sample bindings.
    self._function_parameters_state: set[str] = set()
    # Names the emit binds to JS arrays (plate carries batch / event
    # dims); drives the vectorised-arithmetic rewrite of
    # deterministic bindings.
    self._array_names: frozenset[str] = frozenset()
    # Per-name event rank (`len(plate.event_dims)`) read from the
    # IR. A reducing primitive applied to a positive-rank argument
    # collapses the innermost axis through a `_qvr_<f>_last`
    # runtime helper; WebPPL's own `sum` flattens every axis.
    self._name_event_rank: dict[str, int] = {}

target_protocol

target_protocol() -> Protocol

Return the panproto protocol of the target grammar.

RETURNS DESCRIPTION
Protocol

The protocol of the target grammar.

Source code in src/quivers/transpile/renderers/webppl.py
def target_protocol(self) -> panproto.Protocol:
    """Return the panproto protocol of the target grammar.

    Returns
    -------
    panproto.Protocol
        The protocol of the target grammar.
    """
    return target_protocol("javascript")

render

render(ir: IRProgram) -> Schema

Walk the IR and emit a WebPPL schema.

Override of the base render so the program-level shape (a single var model = function(<inputs>) { ... }; declaration) is built once per call.

PARAMETER DESCRIPTION
ir

The lowered program.

TYPE: IRProgram

RETURNS DESCRIPTION
Schema

The target program, in the target grammar's theory.

Source code in src/quivers/transpile/renderers/webppl.py
def render(self, ir: IRProgram) -> panproto.Schema:
    """Walk the IR and emit a WebPPL schema.

    Override of the base `render` so the program-level shape
    (a single `var model = function(<inputs>) { ... };`
    declaration) is built once per call.

    Parameters
    ----------
    ir
        The lowered program.

    Returns
    -------
    panproto.Schema
        The target program, in the target grammar's theory.
    """
    assert_no_dangling_refs(ir)
    assert_no_dropped_param_map(ir, self.target)
    proto = self.target_protocol()
    sb = proto.schema()
    morphisms, lets = self._resolve_morphisms_and_lets()
    ctx = RenderContext(sb=sb, morphisms=morphisms, defines=lets)
    # Reset per-render state.
    self._fresh_n = 0
    self._binding_plates = {}
    self._binding_supports = {}
    self._observe_via = None
    self._observe_loop_var = None
    self._lift_n = 0
    # Snapshot the IR's static-cardinality table so the let-expr
    # shim can resolve factor binders.
    self._cards = dict(ir.cards)
    # Choose the first observe plate as the lift-fallback plate;
    # lifted deterministic bindings whose data inputs the IR
    # records as scalar still flow through this plate's `mapIndexed`
    # so the per-element computation runs over the right array.
    self._observe_array_fallback = self._first_observe_plate(ir)
    self._array_names = _static_array_names(ir)
    # Per-name event rank, so a reduction over an event axis
    # (`sum(z_row * w_row)`) collapses only the innermost axis
    # instead of flattening the whole nested array to one scalar.
    self._name_event_rank = name_event_rank_map(ir)
    # Program root: a single var-decl wrapping a function expression.
    ctx.sb.vertex("prog", "program")
    # WebPPL's `dists` module ships `Gaussian`, `Beta`, `Categorical`,
    # etc. as built-in distributions but lacks `Logistic`,
    # `BetaBinomial`, `HalfStudentT`, `Kumaraswamy`, `LKJCholesky`,
    # and `ContinuousBernoulli`. When the IR samples or observes
    # from any of these, graft the helper at
    # [`runtime_webppl.js`][quivers.transpile.runtime_webppl] onto
    # the source above the `var model = function (...) {...};`
    # declaration so the body's `sample(<Family>({...}))` call
    # sites resolve through normal JS name lookup.
    if (
        any(_ir_uses_family(ir.body, f) for f in _WEBPPL_RUNTIME_HELPER_FAMILIES)
        or _ir_emits_qvr_bcast(ir, self._array_names)
        or _ir_uses_webppl_math(ir.body)
        or _ir_reduces_event_axis(ir.body, self._name_event_rank)
        or _ir_has_marginalize(ir.body)
        or _ir_has_affine_map(ir.body)
        # A `Categorical` call site reads its support from
        # `_qvr_support`, which lives in the same helper block
        # although the family itself is scored through `.score`
        # rather than through the helper path.
        or _ir_uses_family(ir.body, "Categorical")
        or qiec_helper_families_used(ir, self.target)
    ):
        _graft_runtime_webppl_helper(ctx.sb, self, "prog")
    var_decl = self._fresh(ctx, "vd")
    ctx.sb.vertex(var_decl, "variable_declaration")
    declarator = self._fresh(ctx, "dr")
    ctx.sb.vertex(declarator, "variable_declarator")
    model_name = self._ident(ctx, "model")
    ctx.sb.edge(declarator, model_name, "name")
    fn = self._fresh(ctx, "fn")
    ctx.sb.vertex(fn, "function_expression")
    params = self._fresh(ctx, "ps")
    ctx.sb.vertex(params, "formal_parameters")
    self._function_parameters.clear()
    for inp in ir.inputs:
        ctx.sb.edge(params, self._ident(ctx, inp.name), "child_of")
        self._binding_plates[inp.name] = inp.plate
        self._binding_supports[inp.name] = inp.constraint
        self._function_parameters.add(inp.name)
    body = self._fresh(ctx, "body")
    ctx.sb.vertex(body, "statement_block")
    ctx.sb.edge(fn, params, "parameters")
    ctx.sb.edge(fn, body, "body")
    ctx.sb.edge(declarator, fn, "value")
    ctx.sb.edge(var_decl, declarator, "child_of")
    ctx.sb.edge("prog", var_decl, "child_of")
    self._body_vid = body
    self._module = ir.module
    self._operations_bound: set[str] = set()
    # Walk the body. Inputs were registered above; declare is a
    # no-op for WebPPL, so we dispatch only the body nodes.
    for node in ir.body:
        self._dispatch_node(ctx, node)
    render_computations_dynamic(ctx.sb, ir, target=self.target, root="prog")
    return ctx.sb.build()

declare

declare(ctx: RenderContext, name: str, constraint: ConstraintSpec, plate: Plate, *, block: BlockKind) -> SchemaFragment

No-op: WebPPL's var <name> = sample(...); binding is emitted by sample directly. The declaration table threads the name's binding plate / support into the renderer's bookkeeping so later sample steps can index into the name correctly.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

block

Where the declaration lands in the target program.

TYPE: BlockKind

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/webppl.py
def declare(
    self,
    ctx: RenderContext,
    name: str,
    constraint: ConstraintSpec,
    plate: Plate,
    *,
    block: BlockKind,
) -> SchemaFragment:
    """No-op: WebPPL's `var <name> = sample(...);` binding is
    emitted by `sample` directly. The declaration table threads
    the name's binding plate / support into the renderer's
    bookkeeping so later sample steps can index into the name
    correctly.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    block
        Where the declaration lands in the target program.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del ctx, block
    self._binding_plates[name] = plate
    self._binding_supports[name] = constraint
    return ""

sample

sample(ctx: RenderContext, name: str, family: str, args: tuple[IRArg, ...], arg_names: tuple[str, ...], constraint: ConstraintSpec, plate: Plate, observed: bool) -> SchemaFragment

Emit a sample / observe statement for a sample / observe step.

Wraps the call in repeat(B, function() { ... }) per batch dim when no arg uses the per-element index; uses mapIndexed(function(m, _) { ... }, repeat(B, function() { return 0; })) when an arg refers to a name whose binding plate aligns with the surrounding plate's batch dims.

For an observed step the emit shape is mapIndexed(function(n, <obs>_n) { observe(<dist>, <obs>_n); }, <obs>); when batched; the unbatched case emits a bare observe(<dist>, <obs>); statement.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

name

The variable's name.

TYPE: str

family

The distribution family's QVR name.

TYPE: str

args

The family's arguments, in family order.

TYPE: tuple[IRArg, ...]

arg_names

The parameter name of each argument.

TYPE: tuple[str, ...]

constraint

The variable's support.

TYPE: ConstraintSpec

plate

The variable's event and batch dimensions.

TYPE: Plate

observed

Whether the site is conditioned on data.

TYPE: bool

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/webppl.py
def sample(
    self,
    ctx: RenderContext,
    name: str,
    family: str,
    args: tuple[IRArg, ...],
    arg_names: tuple[str, ...],
    constraint: ConstraintSpec,
    plate: Plate,
    observed: bool,
) -> SchemaFragment:
    """Emit a `sample` / `observe` statement for a sample / observe
    step.

    Wraps the call in `repeat(B, function() { ... })` per batch
    dim when no arg uses the per-element index; uses
    `mapIndexed(function(m, _) { ... }, repeat(B, function() {
    return 0; }))` when an arg refers to a name whose binding
    plate aligns with the surrounding plate's batch dims.

    For an observed step the emit shape is
    `mapIndexed(function(n, <obs>_n) { observe(<dist>, <obs>_n);
    }, <obs>);` when batched; the unbatched case emits a bare
    `observe(<dist>, <obs>);` statement.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    name
        The variable's name.
    family
        The distribution family's QVR name.
    args
        The family's arguments, in family order.
    arg_names
        The parameter name of each argument.
    constraint
        The variable's support.
    plate
        The variable's event and batch dimensions.
    observed
        Whether the site is conditioned on data.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    del constraint  # Constraint shaped the (no-op) declaration.
    meta = FAMILY_META.get(family)
    if meta is None:
        raise UnsupportedConstruct(
            "qvr-webppl",
            [f"family:unknown:{family}"],
        )
    webppl_name = meta.target_names.get("webppl")
    if webppl_name is None:
        raise UnsupportedConstruct(
            "qvr-webppl",
            [f"family:no-webppl-target:{family}"],
        )
    if family == "MixtureNormal":
        return self._emit_mixture_normal(
            ctx, name, args, arg_names, plate, observed=observed
        )
    injected_args, injected_arg_names = _inject_webppl_specific_args(
        family, args, arg_names
    )
    if observed:
        return self._emit_observe(
            ctx,
            name,
            meta,
            webppl_name,
            injected_args,
            injected_arg_names,
            plate,
        )
    return self._emit_sample(
        ctx,
        name,
        meta,
        webppl_name,
        injected_args,
        injected_arg_names,
        plate,
    )

marginalize

marginalize(ctx: RenderContext, node: IRMarginalize) -> SchemaFragment

Integrate an IRMarginalize latent out, adding the reduced density to the program's log-density with WebPPL's factor.

One scored copy of the scope per atom of the latent's finite support, then a reduction across atoms:

var __marg_z_atom_0 = function () {
  var gated_rate = _qvr_bcast("*", 0, rate);
  return mapIndexed(function (n, y_n) {
    return (Poisson({mu:gated_rate[n]})).score(y_n);
  }, y);
};
var __marg_z_0 = __marg_z_atom_0();
...
var __marg_z_t_0 = _qvr_bcast("+", __marg_z_w_0, __marg_z_0);
...
var __marg_z = _qvr_logsumexp([__marg_z_t_0, __marg_z_t_1]);
factor(_qvr_total(__marg_z));

Each atom's deterministic bindings live inside that atom's own function, which is what gives them a scope: WebPPL's single-assignment subset rejects a second var gated_rate in the same block. No site is declared for the latent: the atoms replace it, and the emitted program denotes the same measure the QVR reference integrates.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

node

The marginalize block.

TYPE: IRMarginalize

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/webppl.py
def marginalize(
    self,
    ctx: RenderContext,
    node: IRMarginalize,
) -> SchemaFragment:
    """Integrate an
    [`IRMarginalize`][quivers.transpile.ir.IRMarginalize] latent
    out, adding the reduced density to the program's log-density
    with WebPPL's `factor`.

    One scored copy of the scope per atom of the latent's finite
    support, then a reduction across atoms:

    ```javascript
    var __marg_z_atom_0 = function () {
      var gated_rate = _qvr_bcast("*", 0, rate);
      return mapIndexed(function (n, y_n) {
        return (Poisson({mu:gated_rate[n]})).score(y_n);
      }, y);
    };
    var __marg_z_0 = __marg_z_atom_0();
    ...
    var __marg_z_t_0 = _qvr_bcast("+", __marg_z_w_0, __marg_z_0);
    ...
    var __marg_z = _qvr_logsumexp([__marg_z_t_0, __marg_z_t_1]);
    factor(_qvr_total(__marg_z));
    ```

    Each atom's deterministic bindings live inside that atom's own
    function, which is what gives them a scope: WebPPL's
    single-assignment subset rejects a second `var gated_rate` in
    the same block. No site is declared for the latent: the atoms
    replace it, and the emitted program denotes the same measure
    the QVR reference integrates.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    node
        The marginalize block.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    plates = dict(self._binding_plates)
    atoms = self.marginal_atoms(
        node,
        support_size=marginal_support_size(node, name_plates=plates),
    )
    raw = marginalize_body(node.scope, latent=node.latent, target=self.target)
    prefix = f"__marg_{node.latent}"
    prev_group = self._group_plate_axes
    self._group_plate_axes = tuple(str(d.name) for d in node.plate.batch_dims)
    try:
        term_names = tuple(
            self._emit_atom_scope(ctx, node, atom, prefix, position)
            for position, atom in enumerate(atoms)
        )
        weight_names = self._emit_atom_weights(
            ctx, node, raw, atoms, prefix, plates
        )
    finally:
        self._group_plate_axes = prev_group
    # A grouped block keys its accumulator by group: the rows the
    # fibration sends to one group are summed before the reduction.
    fibration = marginalize_fibration(
        node,
        raw.observe,
        atoms[0].weight_args,
        atoms[0].weight_arg_names,
        name_plates=plates,
        target=self.target,
    )
    # An ungrouped block shares one latent across the body's rows,
    # so each class's per-row log-likelihoods are accumulated to a
    # scalar before the weight is added and the reduction runs
    # over the classes. Reducing the rows elementwise instead
    # would give every row its own draw.
    accumulate_rows = marginalize_row_rank(node) > 0
    shifted: list[str] = []
    for position, (weight, term) in enumerate(
        zip(weight_names, term_names, strict=True)
    ):
        name = f"{prefix}_t_{position}"
        term_vid = self._ident(ctx, term)
        if fibration is not None:
            via, group = fibration
            term_vid = self._call(
                ctx,
                self._ident(ctx, "_qvr_group_sums"),
                (
                    term_vid,
                    self._ident(ctx, via),
                    self._dim_size_value(ctx, group),
                ),
            )
        if accumulate_rows:
            term_vid = self._call(
                ctx,
                self._ident(ctx, "_qvr_total"),
                (term_vid,),
            )
        self._emit_var_decl(
            ctx,
            self._body_vid,
            name,
            self._call(
                ctx,
                self._ident(ctx, "_qvr_bcast"),
                (
                    self._string_literal(ctx, "+"),
                    self._ident(ctx, weight),
                    term_vid,
                ),
            ),
        )
        shifted.append(name)
    terms_array = self._fresh(ctx, "arr")
    ctx.sb.vertex(terms_array, "array")
    for name in shifted:
        ctx.sb.edge(terms_array, self._ident(ctx, name), "child_of")
    self._emit_var_decl(
        ctx,
        self._body_vid,
        prefix,
        self._call(ctx, self._ident(ctx, "_qvr_logsumexp"), (terms_array,)),
    )
    self._emit_expression_statement(
        ctx,
        self._body_vid,
        self._call(
            ctx,
            self._ident(ctx, "factor"),
            (
                self._call(
                    ctx,
                    self._ident(ctx, "_qvr_total"),
                    (self._ident(ctx, prefix),),
                ),
            ),
        ),
    )
    return ""

broadcast

broadcast(ctx: RenderContext, value: IRArg, target_shape: tuple[int, ...]) -> SchemaFragment

Emit a WebPPL broadcast call: repeat(K, function() { return <value>; }) for 1D, nested for higher rank.

Rank-0 returns the value unchanged. The renderer raises with a precise kind tag for rank > 2 because the canonical WebPPL Dirichlet / MVN call shapes only exercise rank-1 and rank-2 broadcast positions.

PARAMETER DESCRIPTION
ctx

The render call's mutable state.

TYPE: RenderContext

value

The argument broadcast.

TYPE: IRArg

target_shape

The shape broadcast to.

TYPE: tuple[int, ...]

RETURNS DESCRIPTION
SchemaFragment

The emitted vertex id, or "" when nothing is emitted.

Source code in src/quivers/transpile/renderers/webppl.py
def broadcast(
    self,
    ctx: RenderContext,
    value: IRArg,
    target_shape: tuple[int, ...],
) -> SchemaFragment:
    """Emit a WebPPL broadcast call:
    `repeat(K, function() { return <value>; })` for 1D, nested
    for higher rank.

    Rank-0 returns the value unchanged. The renderer raises with
    a precise kind tag for rank > 2 because the canonical
    WebPPL Dirichlet / MVN call shapes only exercise rank-1 and
    rank-2 broadcast positions.

    Parameters
    ----------
    ctx
        The render call's mutable state.
    value
        The argument broadcast.
    target_shape
        The shape broadcast to.

    Returns
    -------
    SchemaFragment
        The emitted vertex id, or ``""`` when nothing is emitted.
    """
    if len(target_shape) == 0:
        return self._render_arg(ctx, value)
    if len(target_shape) > 2:
        raise UnsupportedConstruct(
            "qvr-webppl",
            [
                f"broadcast:rank:{len(target_shape)}: WebPPL "
                f"broadcast supports up to rank 2"
            ],
        )
    # Innermost: a function() returning the value.
    inner_value = self._render_arg(ctx, value)
    current = inner_value
    for size in reversed(target_shape):
        body = self._fresh(ctx, "rbody")
        ctx.sb.vertex(body, "statement_block")
        self._emit_return_statement(ctx, body, current)
        lam = self._function_expression(ctx, (), body)
        current = self._call(
            ctx,
            self._ident(ctx, "repeat"),
            (self._number_literal(ctx, size), lam),
        )
    return current

assert_no_dangling_refs

assert_no_dangling_refs(ir: IRProgram) -> None

Raise if the IR contains an IRArgRef whose name is not bound by a declaration, a previous step, or an input.

Source code in src/quivers/transpile/renderers/_base.py
def assert_no_dangling_refs(ir: IRProgram) -> None:
    """Raise if the IR contains an `IRArgRef` whose name is not bound
    by a declaration, a previous step, or an input."""
    declared: set[str] = {inp.name for inp in ir.inputs}
    # ``__row_var__`` is the sentinel Lower writes into observe arg
    # threads to mark "the renderer's per-row loop variable; name
    # bound at render time". Each renderer substitutes the sentinel
    # for its actual loop variable (Stan ``n``, BUGS ``n``, NumPyro's
    # implicit plate index, ...). It is a structural marker, not a
    # true free name, so it does not count as dangling.
    declared.add("__row_var__")
    _walk_for_refs(ir.body, declared, ir.name)

assert_no_dropped_param_map

assert_no_dropped_param_map(ir: IRProgram, target: str) -> None

Reject scalar family arguments whose reference width cannot match the site.

A declared conditional kernel may carry a learned parameter map from its domain to its family heads. Those learned weights do not occur in emitted source or sample sites. The IR must thus not reduce such a map to a bare reference with a different static width.

Scalar arguments may broadcast from width one. Indexed references, mixture arguments, and values gathered through an observation fibration are excluded because their effective widths follow other rules. Families whose argument ranks are unavailable are also skipped.

Source code in src/quivers/transpile/renderers/_base.py
def assert_no_dropped_param_map(ir: IRProgram, target: str) -> None:
    """Reject scalar family arguments whose reference width cannot match the site.

    A declared conditional kernel may carry a learned parameter map from its
    domain to its family heads. Those learned weights do not occur in emitted
    source or sample sites. The IR must thus not reduce such a map to a bare
    reference with a different static width.

    Scalar arguments may broadcast from width one. Indexed references, mixture
    arguments, and values gathered through an observation fibration are
    excluded because their effective widths follow other rules. Families whose
    argument ranks are unavailable are also skipped.
    """
    extents = _BindingExtents()
    for inp in ir.inputs:
        extents = extents.bind(inp.name, _static_extent(inp.plate))
    _walk_for_param_maps(ir.body, extents, ir.name, target)

assert_no_lists

assert_no_lists(ir: IRProgram) -> None

Raise if the IR contains an IRArgList / IRArgMatrix in a position a backend does not support.

The default implementation is permissive: list / matrix args are legal in the IR; only renderers that lack a native list / matrix literal raise. Per-renderer assert_no_lists overrides plug in the per-target rejection rule.

Source code in src/quivers/transpile/renderers/_base.py
def assert_no_lists(ir: IRProgram) -> None:
    """Raise if the IR contains an `IRArgList` /
    [`IRArgMatrix`][quivers.transpile.ir.IRArgMatrix] in a position a
    backend does not support.

    The default implementation is permissive: list / matrix args are
    legal in the IR; only renderers that lack a native list / matrix
    literal raise. Per-renderer `assert_no_lists` overrides plug in
    the per-target rejection rule.
    """
    del ir