fix: split mixed positional/keyword arguments in the comma form f(a, b = c) - #330
Merged
Merged
Conversation
… b = c)` In Julia `f(a, b = c)` and `f(a; b = c)` are the same call. Inside a `@model` body they were not: only the semicolon form worked, and the comma form failed during macro expansion with `syntax: invalid named tuple element ...`, pointing at generated code rather than at the user's line. `convert_to_kwargs_expression` was all-or-nothing -- it rewrote a call into the keyword form only when `is_kwargs_expression(args)` held, which requires *every* argument to be `:kw`/`:parameters`. Anything mixed fell through untouched, so the `:kw` node stayed among the positional arguments and `combine_args` emitted a tuple literal containing it, which is not valid syntax. Both spellings are now normalized to the same `(positional, keywords)` split. The trap here is that Julia puts an explicit `;` group *first*, in a `:parameters` node, so a naive "split off the trailing keywords" breaks calls like `Normal(0, 1; a = 1, b = 2)`; `split_positional_and_keyword_args` handles both shapes, and rebuilding an already-correct semicolon call reproduces it exactly. The same normalization is applied in `convert_to_anonymous`, which runs *after* `convert_to_kwargs_expression` in the pipeline and generates fresh tilde expressions for nested calls -- splicing the captured arguments back verbatim reintroduced the comma form there. This is the path the original report hit: a nested deterministic call such as `mixed_det(1.0, s = 2.0)` now builds. What this does and does not buy: - A nested deterministic call over constants is evaluated directly, so mixed arguments genuinely work there. That is the shape reported in the issue. - A node that has to be materialized still cannot take both, by design. It now reaches that stated limitation instead of failing as invalid syntax. The materialized-node error is also rewritten. It said "MixedArguments not supported for rhs_interfaces when node has to be materialized" -- `rhs_interfaces` means nothing to someone writing a model, and this change makes the error easier to reach. It now names the function, reports what it got, and says what to do. Tests assert the contract directly: the comma and semicolon forms are indistinguishable, across `~`, `.~` and `:=`, including the `Normal(0, 1; a = 1, b = 2)` case a naive fix would break. Closes #328 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 4.9.0 #330 +/- ##
==========================================
+ Coverage 90.96% 91.04% +0.08%
==========================================
Files 16 16
Lines 2279 2301 +22
==========================================
+ Hits 2073 2095 +22
Misses 206 206 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`makedocs` runs with `missing_docs` as an error, so every docstring in the module has to appear in a canonical `@docs` block. The three new helpers had docstrings but no entry, which failed the Documentation job. Listed next to `is_kwargs_expression` and `convert_to_kwargs_expression`, which is where the rest of the model macro pipeline lives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Building the docs required `Pkg.develop(PackageSpec(path=pwd()))` first, which writes an absolute, machine-specific path into `docs/Project.toml`. A `[sources]` entry pointing at `".."` does the same job, is portable, and makes `julia --project=docs docs/make.jl` work directly from a fresh checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In Julia
f(a, b = c)andf(a; b = c)are the same call. Inside a@modelbody they were not — only the semicolon form worked, and the comma form failed during macro expansion withwhich points at generated code rather than the user's line.
Root cause
convert_to_kwargs_expressionwas all-or-nothing: it rewrote a call into the keyword form only whenis_kwargs_expression(args)held, and that requires every argument to be:kw/:parameters. Anything mixed fell through untouched, so the:kwnode stayed among the positional arguments andcombine_argsemitted a tuple literal containing it:argskwargscombine_argsNormal(m; var = v)[:m][:(var = v)]MixedArguments((m,), (var = v,))✅Normal(m, var = v)[:m, :(var = v)]nothing(m, $(Expr(:kw, :var, :v)))❌The trap the issue warns about is real: Julia puts an explicit
;group first, in a:parametersnode, so a naive "split off the trailing keywords" breaksNormal(0, 1; a = 1, b = 2).split_positional_and_keyword_argshandles both shapes, and rebuilding an already-correct semicolon call reproduces it exactly.The second half
convert_anonymous_variablesruns afterconvert_to_kwargs_expressionin the pipeline (src/backends/default.jl:8-20) and generates fresh tilde expressions for nested calls, splicing the captured arguments back verbatim — which reintroduced the comma form. That is the path the original report hit, since its repro is a nested call. Fixing onlyconvert_to_kwargs_expressionleft the reported case still broken;reconstruct_callcloses it.What this does and does not buy
Error message
The materialized-node error is also rewritten. It read
MixedArguments not supported for rhs_interfaces when node has to be materialized—rhs_interfacesmeans nothing to a model author, and this change makes the error easier to reach. It now names the function, reports what it got, and says what to do.Known adjacent gap, not addressed here
A nested deterministic call with mixed arguments and a stochastic argument fails with
MethodError: no method matching filter(::typeof(is_nodelabel), ::MixedArguments)—materialize_anonymous_variable!has noMixedArgumentsmethod. Verified this is pre-existing on4.9.0and reachable through the semicolon form identically, so it is a separate bug rather than a regression from this change. This PR's contract is that the two spellings behave the same, which now holds in that case too.Tests
Asserted directly as "the comma and semicolon forms are indistinguishable", across
~,.~and:=— including theNormal(0, 1; a = 1, b = 2)regression guard, both spellings at once (f(a, b = c; d = e)), and a unit testitem for the splitter.Verified red before the
src/change (5 failed, 6 errored) and green after. Full local suite: 265/265 test items, 76866 pass, 1 broken (pre-existing), Aqua included.Closes #328