Summary
A let binding whose value is a nested arithmetic expression (e.g. (a + b) * c) is emitted without the grouping parentheses by several backends, producing an expression with different operator precedence. No error is raised.
The Stan and Julia (gen, turing) renderers emit correct parentheses. The Python (pyro, numpyro, pymc, edward2), BUGS (jags, bugs), and JavaScript (webppl) renderers drop them.
The Python renderer already ships a helper for this (python_paren in renderers/_python_helpers.py) whose docstring states "The Python pretty printer drops parens around nested binary_operator children, which scrambles operator precedence", but the LetExprBinOp / LetExprUnaryOp rendering does not use it.
Minimal reproduction
Builds a Module with let m = (a + b) * c and transpiles it to each target:
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 LetExprBinOp, LetExprVar
from quivers.dsl.ast_nodes.module import Module
import quivers.transpile as T
def sca(x): return DrawArgScalar(value=float(x))
a, b, c = LetExprVar(name="a"), LetExprVar(name="b"), LetExprVar(name="c")
Resp = TypeName(name="Resp")
# m = (a + b) * c
m_value = LetExprBinOp(op="*", left=LetExprBinOp(op="+", left=a, right=b), right=c)
module = Module(statements=(
ObjectDecl(name="Resp",
init=TypeFromExpr(expr=DiscreteConstructor(constructor="FinSet", args=("100",)))),
ProgramDecl(name="model", domain=Resp, codomain=Resp, draws=(
SampleStep(vars=("a",), morphism="Normal", args=(sca(0.0), sca(1.0))),
SampleStep(vars=("b",), morphism="Normal", args=(sca(0.0), sca(1.0))),
SampleStep(vars=("c",), 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=m_value),
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 ("stan", "gen", "turing", "pyro", "numpyro", "pymc", "edward2", "jags", "bugs", "webppl"):
src = T.transpile(module, target=target).decode()
line = next(l.strip() for l in src.splitlines() if l.strip().startswith(("m ", "m=", "m[", "var m")))
print(f"{target:8}: {line}")
Expected behavior
Every target emits m = (a + b) * c (or its language equivalent), preserving the grouping — as Stan and Julia already do.
Root cause
In renderers/_python_helpers.py, render_let_expr_python builds the binary_operator / unary_operator vertices and wires the child operands directly, without wrapping nested operators in python_paren:
if isinstance(expr, LetExprBinOp):
b = ctx.v(ctx.fresh("bop"), "binary_operator")
ctx.constraint(b, "field:operator", expr.op)
ctx.constraint(b, "chose-alt-fingerprint", expr.op)
ctx.e(b, render_let_expr_python(ctx, expr.left), "left")
ctx.e(b, render_let_expr_python(ctx, expr.right), "right")
return b
The BUGS and JavaScript renderers (_bugs_helpers.py, _javascript_helpers.py) emit the same unparenthesized output, so they have the analogous omission. The Stan and Julia renderers emit correct grouping, so a correct reference already exists in-tree.
Suggested fix
When rendering a LetExprBinOp / LetExprUnaryOp, wrap any child operand that is itself a LetExprBinOp / LetExprUnaryOp in the grouping construct (python_paren for the Python family, and the BUGS / JavaScript equivalents) before wiring it as left / right / argument.
Environment
- Python 3.14.3
- Quivers branch
feat/transpile-pipeline
- panproto 0.56.1
- didactic 0.9.0
Summary
A
letbinding whose value is a nested arithmetic expression (e.g.(a + b) * c) is emitted without the grouping parentheses by several backends, producing an expression with different operator precedence. No error is raised.The Stan and Julia (
gen,turing) renderers emit correct parentheses. The Python (pyro,numpyro,pymc,edward2), BUGS (jags,bugs), and JavaScript (webppl) renderers drop them.The Python renderer already ships a helper for this (
python_pareninrenderers/_python_helpers.py) whose docstring states "The Python pretty printer drops parens around nested binary_operator children, which scrambles operator precedence", but theLetExprBinOp/LetExprUnaryOprendering does not use it.Minimal reproduction
Builds a
Modulewithlet m = (a + b) * cand transpiles it to each target:Expected behavior
Every target emits
m = (a + b) * c(or its language equivalent), preserving the grouping — as Stan and Julia already do.Root cause
In
renderers/_python_helpers.py,render_let_expr_pythonbuilds thebinary_operator/unary_operatorvertices and wires the child operands directly, without wrapping nested operators inpython_paren:The BUGS and JavaScript renderers (
_bugs_helpers.py,_javascript_helpers.py) emit the same unparenthesized output, so they have the analogous omission. The Stan and Julia renderers emit correct grouping, so a correct reference already exists in-tree.Suggested fix
When rendering a
LetExprBinOp/LetExprUnaryOp, wrap any child operand that is itself aLetExprBinOp/LetExprUnaryOpin the grouping construct (python_parenfor the Python family, and the BUGS / JavaScript equivalents) before wiring it asleft/right/argument.Environment
feat/transpile-pipeline