Skip to content

feat/transpile-pipeline: nested arithmetic in let expressions loses its parentheses #52

Description

@pkuchmiichuk

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

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