Skip to content

Release 4.9.0: audit fixes, CI matrix, and error-message improvements - #329

Open
bvdmitri wants to merge 27 commits into
mainfrom
4.9.0
Open

bvdmitri wants to merge 27 commits into
mainfrom
4.9.0

Conversation

@bvdmitri

@bvdmitri bvdmitri commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Collects the batch of fixes from @docxology's August 2026 audit, together with the CI changes
needed to land them, into a single release branch.

Issues closed

Closes #304 — multi-/named-output submodels silently ignored where { ... } options
Closes #305 — UndefVarError: opt in the duplicate form-constraint @warn
Closes #306 — dead variableref_checked_collection_typeof referencing an unbound ref
Closes #307 — @model keyword arguments are now rejected instead of silently dropped
Closes #308 — iterating a ragged/sparse ResizableArray now errors descriptively
Closes #309 — broadcast lowering of mixed positional/keyword arguments
Closes #310 — clear error when a multi-output submodel call has the wrong LHS arity
Closes #311 — multiple anonymous variables in one context no longer collapse to one key
Closes #328 — the comma form f(a, b = c) is split into keyword arguments

(Closing keywords are listed here rather than relying on the individual pull requests, since
GitHub only fires them on merge into the default branch.)

Deliberately not closed

#313 (GraphViz libgvc InitError) was closed separately as not reproducible — GraphViz
0.2.0, 0.2.1 and 0.3.0 all initialize correctly on Julia 1.12, and the full suite passes there.
The compat bump in #322 lands on its own merits, not as a fix for it. The reporter's underlying
point was still valid, though, and is addressed here: the extension was never exercised on 1.12
by CI, and now is.

Breaking

#307 / #326: keyword arguments in an @model signature now raise an error. Previously they
were parsed, silently dropped, and warned about, so the only real signal was a later
UndefVarError in the body. This is the reason for a minor rather than a patch bump.

CI changes

  • test matrix extended to Julia 1.12 and 1.13
  • opened added to the pull_request trigger types — it was missing, so a pull request opened
    non-draft and never pushed to again received no CI at all
  • concurrency groups on CI and Test dependent packages so superseded runs are cancelled,
    with main exempt

Included pull requests

#314, #315, #316 (@docxology), and #322, #323, #324, #325, #326, #327, #330.

Supersedes #319 and #320 (CompatHelper duplicates of #322).

🤖 Generated with Claude Code

docxology and others added 13 commits August 1, 2026 17:24
…bmodels

Inline where { constraints = ... } (and other context options) were silently
dropped for multi-output and named-output submodel calls because the generated
make_node! built the child Context without NodeCreationOptions. Mirror the
single-output path.
The 'already has functional form constraint' warnings referenced undefined
locals opt and constraint_data inside lazy"...", producing UndefVarError
instead of the intended informative warning. Use bound getconstraint(...).
Unused function referenced an unbound local 'ref' (latent UndefVarError).
GraphViz 0.3.0 (Graphviz_jll v15) is the current release; the `[compat]`
entry pinned us to the 0.2 series. Verified on Julia 1.10, 1.11 and 1.12
that 0.3.0 resolves, the `GraphPPLGraphVizExt` extension precompiles and
loads, `GraphViz.Graph(::String)` constructs and `GraphViz.load` is still
an extensible generic, so `ext/GraphPPLGraphVizExt.jl` needs no changes.

Full test suite passes on Julia 1.12 with 0.3.0, including the
`ext/graphviz_integration_tests.jl` test item.

Refs #313. Note this is a compat bump, not a fix for that report: the
`UndefVarError: libgvc` there could not be reproduced here — GraphViz
0.2.0, 0.2.1 and 0.3.0 all initialize correctly on Julia 1.12.

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

Mirrors the existing "inline constraints on submodel calls" test item for both
LHS forms the fix touches: Tuple LHS `(a, b) ~ sub(...)` and NamedTuple LHS
`(a = ma, b = mb) ~ sub(...)`.

Asserts that `where { constraints = ... }` reaches the child context
(`get(context_options(inner_context), :constraints, nothing) isa Constraints`)
and that the requested factorization actually materializes on the inner nodes.

Verified the test fails on `main` (4 failures, 2 per LHS form) and passes with
the `__options__` propagation fix.

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

The warning is meant to say "this node already has constraint X, so the new
constraint Y will not be applied". As written it interpolated the *new*
constraint into the slot for the existing one, producing "already has
functional form constraint Y applied, therefore it will not be applied".

Read the existing constraint from the node via `getextra` and name the
skipped one, and restore the `lazy"..."` string.

Also adds the regression test the PR was missing: applies a form constraint
twice (marginal and message variants) and asserts a warning is emitted and the
original constraint is preserved. Verified the test errors against the
pre-fix `@warn` (UndefVarError: opt) and passes with the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Base.iterate` indexed by the reported (max-extent) `size` without an
`isassigned` guard, so iterating a ragged or sparsely filled array raised a
bare `BoundsError` pointing at an internal vector, with no hint about the real
problem or the supported alternative.

`iterate` now checks `isassigned` and raises an error naming the unassigned
index and pointing at `GraphPPL.vec`, which already guards with `isassigned`
and is the supported way to traverse ragged arrays.

Note it deliberately does *not* skip unassigned slots. `ResizableArray <:
AbstractArray` and there is no `length`/`IteratorSize` override, so
`length == prod(size)`; skipping would break the iteration protocol and leave
`collect`/`map` results holding uninitialized memory. Failing loudly is the
safer contract for a package that must build factor graphs deterministically.

Also guards the empty case in the 0-argument method, which previously
destructured `iterate(CartesianIndices(...))` unconditionally.

Closes #308.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er of LHS outputs

A multi-output submodel call must provide exactly as many outputs on the LHS as
there are interfaces left unspecified on the RHS. When it did not, the total
arity matched no generated `make_node!` method and dispatch failed with a
`MethodError` that said nothing about the real problem.

Note this happens one layer earlier than #310 assumed: `prepare_interfaces_multi`
is never reached, because the dispatch sites pass
`static(length(rhs_interfaces) + length(lhs_interface))` and the generated
methods only exist for the model's own `StaticInt{num_interfaces}`.

Adds a less specific `make_node!` fallback for `Composite` nodes with `Tuple` or
`NamedTuple` LHS that reports the mismatch, and updates the test that previously
pinned the `MethodError` text.

Closes #310.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Anonymous variables were all registered under the constant key
`VariableNameAnonymous`, and `Context`'s `setindex!` is insert-or-update, so a
context creating more than one anonymous variable kept only the last in
`individual_variables`. The graph itself was correct, but anything enumerating
the context registry (notably `VarDict`) silently missed the earlier ones.

Adds `add_anonymous_node!`, mirroring `add_constant_node!`, which registers
under `to_symbol(VariableNameAnonymous, label.global_counter)`. The node
property `name` is unchanged, so `is_anonymous` and
`as_variable(VariableNameAnonymous)` are unaffected.

Closes #311.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keyword arguments in a `@model` function signature were parsed and then silently
dropped, with only a `@warn`. The body never saw them, so a user writing
`@model function m(x; a = 1)` got no `a` and a later `UndefVarError` as the only
real signal, and the warning is easy to miss in a noisy REPL.

Raise an error at macro expansion instead, consistent with unsupported
positional arguments at the call site, and name the offending keyword arguments
plus the supported form.

BREAKING: `@model` definitions carrying keyword arguments now fail to expand
rather than silently ignoring them. Nothing in this repository relied on the old
behaviour: no test or doc asserted the warning, and no `@model` signature with
keyword arguments exists in `src/`, `test/`, `docs/` or `README.md`. Downstream
packages that defined such signatures will need to drop them, but their keyword
arguments were never in effect to begin with.

Closes #307.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…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>
Three changes to the workflows ahead of the 4.9.0 batch:

- Extend the test matrix to Julia 1.12 and 1.13. 1.12 in particular
  exercises the GraphViz extension load path reported in #313, which
  was previously never covered by CI.

- Add `opened` to the `pull_request` trigger types. It was missing, so
  a pull request opened non-draft and never pushed to again received no
  CI at all -- #315 sat for six weeks with zero runs because of this,
  and #322-#327 each needed a manual `workflow_dispatch`.

- Add a concurrency group so a new run supersedes one still in flight
  for the same ref. `main` is exempt, since its runs are the record of
  what actually passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix: remove dead `variableref_checked_collection_typeof` (unbound `ref`)
@codecov

codecov Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.98246% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.04%. Comparing base (4cc7c6c) to head (849fd25).

Files with missing lines Patch % Lines
src/model_macro.jl 91.66% 3 Missing ⚠️
src/resizable_array.jl 90.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #329      +/-   ##
==========================================
+ Coverage   90.72%   91.04%   +0.32%     
==========================================
  Files          16       16              
  Lines        2263     2301      +38     
==========================================
+ Hits         2053     2095      +42     
+ Misses        210      206       -4     

☔ 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.

fix: repair duplicate form-constraint `@warn` (UndefVarError `opt`)
fix: propagate `__options__` to child Context for multi-/named-output submodels
…error

fix: throw a descriptive error when iterating a sparse `ResizableArray`
fix: clear error when a multi-output submodel call has the wrong number of LHS outputs
fix: register each anonymous variable under a unique context key
…xed-args

# Conflicts:
#	test/graph_construction_tests.jl
fix!: reject keyword arguments in `@model` signatures instead of warning
bvdmitri and others added 2 commits September 21, 2026 10:22
fix: correct the lowering of mixed positional/keyword broadcast arguments
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bvdmitri and others added 4 commits September 21, 2026 11:01
… 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>
`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>
fix: split mixed positional/keyword arguments in the comma form `f(a, b = c)`

This branch has not been deployed

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