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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions