Skip to content

fix: correct the lowering of mixed positional/keyword broadcast arguments - #327

Open
bvdmitri wants to merge 1 commit into
mainfrom
fix/309-broadcast-mixed-args
Open

bvdmitri wants to merge 1 commit into
mainfrom
fix/309-broadcast-mixed-args

Conversation

@bvdmitri

Copy link
Copy Markdown
Member

Closes #309.

Problem

combine_broadcast_args(args::Vector, kwargs::Vector) (src/model_macro.jl:617-628) was wrong in
both halves, not just the keyword half the issue identifies:

GraphPPL.MixedArguments($(Expr(:tuple, args...)), NamedTuple{$(Tuple(kwargs_keys))}(args))
  • the positional half splices the original argument expressions, which refer to the outer,
    un-broadcast collections — not the per-element slots;
  • the keyword half builds the NamedTuple from the whole closure tuple, whose arity is
    length(positional) + length(keywords), so the arity mismatches.

Inside the broadcasted expression, args is the varargs tail of the closure at model_macro.jl:734
(do ilhs, args...), populated at :726 as vcat(positional, kwarg_values) — positional slices first,
keyword values after.

Change

Both halves are now sliced out of that runtime tuple by index, emitted as a tuple literal and a
named-tuple literal (mirroring the non-broadcast combine_args) so that proxy_args wraps each element
individually and the two halves stay a Tuple and a NamedTuple, as
MixedArguments{A <: Tuple, K <: NamedTuple} requires:

GraphPPL.MixedArguments((args[1], args[2]), (τ = args[3], θ = args[4]))

Scope: what this does and does not fix

This fixes the lowering. It does not make stochastic mixed-argument broadcasts build, because
MixedArguments is rejected for any node that must be materialized
(graph_engine.jl:2038) — the non-broadcast ~ has exactly the same limitation, so the two paths stay
consistent. Lifting that is a feature, not a bug fix, and is out of scope here.

The user-visible effect is that z .~ sub(mu; y = sg) now reports the real, documented limitation:

MixedArguments not supported for rhs_interfaces when node has to be materialized

instead of MethodError: no method matching tuple(::GraphPPL.VariableRef{...}).

Known remaining gap (pre-existing, not addressed here)

The comma form f(a, b = c) is still not split into positional + keyword at all.
convert_to_kwargs_expression only converts when every argument is a keyword
(is_kwargs_expression(::Vector) requires all), so b = c stays among the positional arguments and the
call still fails with an opaque tuple MethodError. This affects ~, .~ and := alike and is
independent of this fix — x ~ Normal(mixed(1.0, s = 2.0), 1.0) fails identically on main.

I prototyped a split here but backed it out: Julia puts :parameters first in the argument AST, so a
naive trailing-keyword split breaks the existing convert_to_kwargs_expression test at
test/model_macro_tests.jl:553. It deserves its own issue and its own careful patch. Use the explicit
semicolon form f(a; b = c) meanwhile.

Tests

  • Updated the two tests that pinned the buggy shape"combine_broadcast_args"
    (model_macro_tests.jl:1448) and the codegen baseline in "convert_tilde_expression" (Test 13). These
    pinning the wrong output is why the bug survived.
  • New graph-level test item in test/graph_construction_tests.jl asserting the mixed broadcast reaches
    the stated MixedArguments limitation rather than a MethodError, and that keyword-only broadcasts
    still build.

Verified the new test fails on main and passes here. Full suite on Julia 1.12:
259/259 test items, all passing.

🤖 Generated with Claude Code

…ents

`combine_broadcast_args(args::Vector, kwargs::Vector)` built both halves of the
`MixedArguments` wrongly, not just the keyword half as #309 reports:

- the positional half spliced the original argument expressions, which refer to
  the outer, un-broadcast collections rather than the per-element slots;
- the keyword half called `NamedTuple{keys}(args)` on the *whole* broadcast
  closure tuple, whose arity is `length(positional) + length(keywords)`.

Inside the broadcasted expression `args` is the varargs tail of the closure,
holding positional slices first and keyword values after, so both halves are now
sliced out of it by index. They are emitted as a tuple literal and a named-tuple
literal, mirroring the non-broadcast `combine_args`, so `proxy_args` wraps each
element individually and the halves stay a `Tuple` and a `NamedTuple` as
`MixedArguments{A <: Tuple, K <: NamedTuple}` requires.

Updates the two tests that pinned the buggy generated shape.

Closes #309.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.73%. Comparing base (4cc7c6c) to head (8c75153).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #327      +/-   ##
==========================================
+ Coverage   90.72%   90.73%   +0.01%     
==========================================
  Files          16       16              
  Lines        2263     2266       +3     
==========================================
+ Hits         2053     2056       +3     
  Misses        210      210              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Broadcast lhs .~ f(args...; kwargs...) crashes with MethodError when mixing positional and keyword args

1 participant