Skip to content

feat/transpile-pipeline: LetExprCall function names are emitted verbatim, with no per-target symbol mapping or import #53

Description

@pkuchmiichuk

Summary

A LetExprCall (a function call inside a let, e.g. erf(u)) is emitted with its func string rendered as a bare identifier in every target. There is no per-target name mapping and no import injection, so any function that is not already in the target's namespace produces undefined-name output:

  • In the Python-family renderers (pyro, numpyro, pymc, edward2), erf(u) is emitted as bare erf(u) with no accompanying import. It needs torch.erf (Pyro), jax.scipy.special.erf (NumPyro), or pytensor.tensor.erf (PyMC).

Minimal reproduction

Builds a Module with let m = erf(u) and transpiles it:

from quivers.dsl.ast_nodes.objects import DiscreteConstructor, TypeName
from quivers.dsl.ast_nodes.declarations import ObjectDecl, ProgramDecl, ExportDecl, TypeFromExpr
from quivers.dsl.ast_nodes.expressions import ExprIdent
from quivers.dsl.ast_nodes.program_steps import (
    SampleStep, ObserveStep, LetStep, DrawArgScalar, DrawArgName,
)
from quivers.dsl.ast_nodes.let_expressions import LetExprCall, LetExprVar
from quivers.dsl.ast_nodes.module import Module
import quivers.transpile as T

def sca(x): return DrawArgScalar(value=float(x))
Resp = TypeName(name="Resp")

module = Module(statements=(
    ObjectDecl(name="Resp",
               init=TypeFromExpr(expr=DiscreteConstructor(constructor="FinSet", args=("100",)))),
    ProgramDecl(name="model", domain=Resp, codomain=Resp, draws=(
        SampleStep(vars=("u",),     morphism="Normal",  args=(sca(0.0), sca(1.0))),
        SampleStep(vars=("sigma",), morphism="Uniform", args=(sca(0.0), sca(1.0))),
        LetStep(name="m", value=LetExprCall(func="erf", args=(LetExprVar(name="u"),))),
        ObserveStep(var="y", morphism="Normal",
                    args=(DrawArgName(text="m"), DrawArgName(text="sigma")), index=Resp),
    ), return_vars=("y",)),
    ExportDecl(expr=ExprIdent(name="model")),
))

for target in ("pyro", "numpyro", "pymc"):
    print(f"----- {target} -----")
    print(T.transpile(module, target=target).decode())

Expected behavior

erf (and other math builtins) are rendered as the target's own symbol, with the required import emitted. If a builtin has no mapping for a given target, transpilation should raise rather than emit an undefined name.

Root cause

renderers/_python_helpers.py:342-351 renders LetExprCall by emitting expr.func as a bare identifier, with no builtin-name → target-symbol table and no import injection:

if isinstance(expr, LetExprCall):
    c = ctx.v(ctx.fresh("call"), "call")
    fn = ctx.v(ctx.fresh("fn"), "identifier")
    ctx.literal(fn, expr.func)
    ctx.e(c, fn, "function")
    args = ctx.v(ctx.fresh("args"), "argument_list")
    for a in expr.args:
        ctx.e(args, render_let_expr_python(ctx, a), "child_of")
    ctx.e(c, args, "arguments")
    return c

The native (torch) let-expression compiler resolves ~86 primitives (erf, exp, log, ...) to their torch symbols at compile time, but the transpile renderers do not apply an equivalent per-target mapping.

Suggested fix

Add a per-target builtin table that maps each supported function name to the target's symbol and records the imports it requires (e.g. erftorch.erf + import torch), and emit those imports in the module preamble.

Environment

  • Python 3.14.3
  • Quivers branch feat/transpile-pipeline
  • panproto 0.56.1
  • didactic 0.9.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions