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. erf → torch.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
Summary
A
LetExprCall(a function call inside alet, e.g.erf(u)) is emitted with itsfuncstring 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:pyro,numpyro,pymc,edward2),erf(u)is emitted as bareerf(u)with no accompanying import. It needstorch.erf(Pyro),jax.scipy.special.erf(NumPyro), orpytensor.tensor.erf(PyMC).Minimal reproduction
Builds a
Modulewithlet m = erf(u)and transpiles it: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-351rendersLetExprCallby emittingexpr.funcas a bareidentifier, with no builtin-name → target-symbol table and no import injection: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.
erf→torch.erf+import torch), and emit those imports in the module preamble.Environment
feat/transpile-pipeline