From e194a395dfd76b980d00476cca6ef4a80c42b579 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 12:02:50 +0100 Subject: [PATCH 1/9] docs: note whole-variable semantics for `predict` and `fix` A variable drawn from a multivariate distribution in a single tilde-statement (e.g. `x ~ MvNormal(...)` / `filldist`) is a single random variable, not i.i.d. components. `predict` therefore cannot fix a subset of its components while resampling the rest, and `fix` cannot fix them independently. Add warning admonitions documenting this, referencing TuringLang/Turing.jl#2239. Co-Authored-By: Claude Opus 4.8 (1M context) --- ext/DynamicPPLMCMCChainsExt.jl | 9 +++++++++ src/model.jl | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/ext/DynamicPPLMCMCChainsExt.jl b/ext/DynamicPPLMCMCChainsExt.jl index dbce4edd5..921fcd57a 100644 --- a/ext/DynamicPPLMCMCChainsExt.jl +++ b/ext/DynamicPPLMCMCChainsExt.jl @@ -273,6 +273,15 @@ If `include_all` is `false`, the returned `Chains` will contain only those varia the samples in `chain`. This is useful when you want to sample only new variables from the posterior predictive distribution. +!!! warning "Variables are treated as they occur in the model" + A variable drawn from a multivariate distribution in a single tilde-statement + (e.g. `x ~ MvNormal(...)` or `x ~ filldist(Normal(), n)`) is a *single* random + variable, not a collection of i.i.d. components. `predict` cannot fix a subset of + such a variable's components while resampling the rest; if `chain` supplies only + some components, the whole variable is resampled from the prior. To treat components + individually, declare them in a loop, e.g. `for i in eachindex(x); x[i] ~ Normal(); end`. + See [TuringLang/Turing.jl#2239](https://github.com/TuringLang/Turing.jl/issues/2239). + # Examples ```jldoctest using AbstractMCMC, Distributions, DynamicPPL, Random diff --git a/src/model.jl b/src/model.jl index 6d6a23c28..215acbccf 100644 --- a/src/model.jl +++ b/src/model.jl @@ -522,6 +522,14 @@ Return a `Model` which now treats the variables in `values` as fixed. See also: [`unfix`](@ref), [`fixed`](@ref) +!!! warning "Fixing applies to whole variables" + Variables are treated as they occur in the model. A variable drawn from a multivariate + distribution in a single tilde-statement (e.g. `x ~ MvNormal(...)`) is a *single* random + variable, so a subset of its components cannot be fixed independently; only fixing the + variable in its entirety is supported. Declare components in a loop (`x[i] ~ ...`) if you + need to fix them individually. + See [TuringLang/Turing.jl#2239](https://github.com/TuringLang/Turing.jl/issues/2239). + # Examples ## Simple univariate model ```jldoctest fix From 5243226239250c98ae06db38645d9ac409c81b5b Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 12:38:49 +0100 Subject: [PATCH 2/9] Give an informative error when only part of a multivariate variable is specified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supplying a subset of a variable that the model samples as a single multivariate draw (e.g. `x[1:10]` when the model draws `x ~ MvNormal(zeros(20), ...)`) has never worked: `predict` silently resampled the whole variable from the prior, `fix` silently collapsed it to the supplied length, and `condition` threw an opaque `DimensionMismatch`. This is therefore not a behaviour change — it only replaces those silent or confusing failures with a single, informative error. Add `_check_supplied_shape(dist, supplied, vn)`, dispatched on the supplied representation (a materialised value for `condition`/`fix`, or the parameter `VarNamedTuple` for `predict`/`InitFromParams`), which throws one clear error referencing TuringLang/Turing.jl#2239. Only multivariate distributions are checked; per-index (`x[i] ~ ...`) declarations and correctly-sized whole-variable values are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/compiler.jl | 68 +++++++++++++++++++++++++++++++++++++++++--- src/contexts/init.jl | 3 ++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/compiler.jl b/src/compiler.jl index 0c53deef8..54c9411b7 100644 --- a/src/compiler.jl +++ b/src/compiler.jl @@ -127,6 +127,58 @@ function contextual_isfixed(context::AbstractContext, vn) end end +""" + _check_supplied_shape(dist, supplied, vn) + +Validate that the value(s) supplied for `vn` — whether by `condition`, `fix`, or `predict` — +are consistent with the multivariate distribution `dist` that `vn` is `~`-tied to, and +return `supplied`. This catches the unsupported case of supplying only part of a variable +that the model samples as a single multivariate draw (e.g. `x[1:10]` when the model draws +`x ~ MvNormal(zeros(20), ...)`), which would otherwise silently collapse `x` (`fix`), +resample the whole variable (`predict`), or throw an opaque `DimensionMismatch` +(`condition`). See https://github.com/TuringLang/Turing.jl/issues/2239. + +`supplied` is dispatched on to cover both callers: a materialised value (from `condition` +/`fix`) or the parameter `VarNamedTuple` (from `predict`/`InitFromParams`). Only +`MultivariateDistribution`s are checked; everything else is a no-op. +""" +_check_supplied_shape(_, supplied, _) = supplied +function _check_supplied_shape( + dist::Distributions.MultivariateDistribution, supplied::AbstractArray, vn +) + length(supplied) == length(dist) || + _throw_incomplete_multivariate(vn, length(supplied), dist) + return supplied +end +function _check_supplied_shape( + dist::Distributions.MultivariateDistribution, params::VarNamedTuples.VarNamedTuple, vn +) + # Only a reference to the *whole* variable can be partial; an absent indexed leaf + # (`x[i]`) is simply a new variable to be sampled from the prior. + AbstractPPL.getoptic(vn) isa AbstractPPL.Iden || return params + sym = AbstractPPL.getsym(vn) + haskey(params.data, sym) || return params + val = params.data[sym] + if val isa VarNamedTuples.PartialArray + n = count(val.mask) + # `n == 0` means nothing was supplied (the caller falls back to the prior); a full, + # correctly-sized array would already have been found by `hasvalue`. + (n == 0 || n == length(dist)) || _throw_incomplete_multivariate(vn, n, dist) + end + return params +end +@noinline function _throw_incomplete_multivariate(vn, n, dist) + return error( + "A value with $(n) element(s) was supplied for `$(vn)`, but the model samples " * + "`$(vn)` as a single $(length(dist))-dimensional random variable. Supplying a " * + "subset of such a variable (via `condition`, `fix`, or `predict` with a chain from " * + "a differently-sized model) is not supported. To treat its components " * + "individually, declare them in a loop, e.g. " * + "`for i in eachindex($(vn)); $(vn)[i] ~ ...; end`. " * + "See https://github.com/TuringLang/Turing.jl/issues/2239.", + ) +end + # If we're working with, say, a `Symbol`, then we're not going to `view`. maybe_view(x) = x maybe_view(x::Expr) = :(@views($x)) @@ -486,8 +538,12 @@ function generate_tilde(left, right) # need to use Accessors.set to safely set it. $(assign_or_set!!( left, - :($(DynamicPPL.getfixed_nested)( - __model__.context, $(DynamicPPL.prefix)(__model__.context, $vn) + :($(DynamicPPL._check_supplied_shape)( + $dist, + $(DynamicPPL.getfixed_nested)( + __model__.context, $(DynamicPPL.prefix)(__model__.context, $vn) + ), + $vn, )), vn, )) @@ -505,8 +561,12 @@ function generate_tilde(left, right) $supplied_val = if $(DynamicPPL.inargnames)($vn, __model__) $(maybe_view(left)) else - $(DynamicPPL.getconditioned_nested)( - __model__.context, $(DynamicPPL.prefix)(__model__.context, $vn) + $(DynamicPPL._check_supplied_shape)( + $dist, + $(DynamicPPL.getconditioned_nested)( + __model__.context, $(DynamicPPL.prefix)(__model__.context, $vn) + ), + $vn, ) end diff --git a/src/contexts/init.jl b/src/contexts/init.jl index 23c2515c3..90aa0ccbd 100644 --- a/src/contexts/init.jl +++ b/src/contexts/init.jl @@ -188,6 +188,9 @@ function init( TransformedValue(x, NoTransform()) end else + # Fail loudly if only a subset of a whole multivariate variable was supplied (#2239); + # otherwise fall back for a genuinely-absent variable. + _check_supplied_shape(dist, p.params, vn) p.fallback === nothing && error("No value was provided for the variable `$(vn)`.") init(rng, vn, dist, p.fallback) end From 61c590cfcfd15f0335e619a211667b744fb59b6b Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 12:46:54 +0100 Subject: [PATCH 3/9] Test partial-multivariate error for predict, fix, and condition Cover the #2239 misuse cases: predicting with a chain from a differently-sized model, and `fix`/`condition` of a single index of a multivariate variable, all now raise the informative error. A whole-variable `fix` of the correct size is kept as a positive control. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/conditionfix.jl | 14 ++++++++++++++ test/model.jl | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/test/conditionfix.jl b/test/conditionfix.jl index ef91a67b6..ab1907db4 100644 --- a/test/conditionfix.jl +++ b/test/conditionfix.jl @@ -541,6 +541,20 @@ end end end +@testset "partial multivariate variable errors (#2239)" begin + @model function mvc(n) + m ~ MvNormal(zeros(n), 1.0) + return m + end + # Supplying only part of a variable the model samples as one multivariate draw is + # unsupported: `fix` used to silently collapse it and `condition` to throw an opaque + # `DimensionMismatch`. Both must now raise the informative #2239 error. + @test_throws "2239" fix(mvc(3), Dict(@varname(m[1]) => 1.0))() + @test_throws "2239" condition(mvc(3), Dict(@varname(m[1]) => 1.0))() + # A whole-variable value of the correct size is still honoured. + @test fix(mvc(3), Dict(@varname(m) => [1.0, 2.0, 3.0]))() == [1.0, 2.0, 3.0] +end + @info "Completed $(@__FILE__) in $(now() - __now__)." end # module diff --git a/test/model.jl b/test/model.jl index 2bc94cf65..52f215332 100644 --- a/test/model.jl +++ b/test/model.jl @@ -642,6 +642,19 @@ const GDEMO_DEFAULT = DynamicPPL.TestUtils.demo_assume_observe_literal() pdns = DynamicPPL.predict(fmodel, chn[[:b]]) @test Set(keys(pdns)) == Set([:x]) end + + @testset "errors on partial multivariate variable (#2239)" begin + @model function mv_partial(n) + s ~ truncated(Normal(0, 1); lower=0) + m ~ MvNormal(zeros(n), 1.0) + return y ~ MvNormal(m, s) + end + # A chain from the n=10 model carries only `m[1:10]`; predicting with the + # n=20 model asks to fill part of the single multivariate variable `m`, + # which is unsupported and must error rather than silently resample it. + chn10 = make_chain_from_prior(mv_partial(10) | (; y=zeros(10)), 5) + @test_throws "2239" DynamicPPL.predict(mv_partial(20), chn10) + end end end From fad7778619a98cc1f958e065a13d3120955394e9 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 12:54:40 +0100 Subject: [PATCH 4/9] Minimise: trim helper docs/comments and simplify the predict test model No behaviour change. Shorten the `_check_supplied_shape` docstring and the `init` comment, and reduce the predict misuse test to the smallest model that still triggers the error (just the multivariate variable, no extra latent or observation). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/compiler.jl | 16 +++++----------- src/contexts/init.jl | 3 +-- test/model.jl | 12 +++++------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/compiler.jl b/src/compiler.jl index 54c9411b7..a94c8ee5b 100644 --- a/src/compiler.jl +++ b/src/compiler.jl @@ -130,17 +130,11 @@ end """ _check_supplied_shape(dist, supplied, vn) -Validate that the value(s) supplied for `vn` — whether by `condition`, `fix`, or `predict` — -are consistent with the multivariate distribution `dist` that `vn` is `~`-tied to, and -return `supplied`. This catches the unsupported case of supplying only part of a variable -that the model samples as a single multivariate draw (e.g. `x[1:10]` when the model draws -`x ~ MvNormal(zeros(20), ...)`), which would otherwise silently collapse `x` (`fix`), -resample the whole variable (`predict`), or throw an opaque `DimensionMismatch` -(`condition`). See https://github.com/TuringLang/Turing.jl/issues/2239. - -`supplied` is dispatched on to cover both callers: a materialised value (from `condition` -/`fix`) or the parameter `VarNamedTuple` (from `predict`/`InitFromParams`). Only -`MultivariateDistribution`s are checked; everything else is a no-op. +Return `supplied`, but error (see TuringLang/Turing.jl#2239) if it specifies only part of a +variable that the model samples as a single multivariate draw (e.g. `x[1:10]` for +`x ~ MvNormal(zeros(20), ...)`). `supplied` is either a materialised value (`condition` +/`fix`) or the parameter `VarNamedTuple` (`predict`/`InitFromParams`); non-multivariate +`dist`s are a no-op. """ _check_supplied_shape(_, supplied, _) = supplied function _check_supplied_shape( diff --git a/src/contexts/init.jl b/src/contexts/init.jl index 90aa0ccbd..50686c2be 100644 --- a/src/contexts/init.jl +++ b/src/contexts/init.jl @@ -188,8 +188,7 @@ function init( TransformedValue(x, NoTransform()) end else - # Fail loudly if only a subset of a whole multivariate variable was supplied (#2239); - # otherwise fall back for a genuinely-absent variable. + # Error if only part of a whole multivariate variable was supplied (#2239). _check_supplied_shape(dist, p.params, vn) p.fallback === nothing && error("No value was provided for the variable `$(vn)`.") init(rng, vn, dist, p.fallback) diff --git a/test/model.jl b/test/model.jl index 52f215332..ad5e223f9 100644 --- a/test/model.jl +++ b/test/model.jl @@ -645,14 +645,12 @@ const GDEMO_DEFAULT = DynamicPPL.TestUtils.demo_assume_observe_literal() @testset "errors on partial multivariate variable (#2239)" begin @model function mv_partial(n) - s ~ truncated(Normal(0, 1); lower=0) - m ~ MvNormal(zeros(n), 1.0) - return y ~ MvNormal(m, s) + return m ~ MvNormal(zeros(n), 1.0) end - # A chain from the n=10 model carries only `m[1:10]`; predicting with the - # n=20 model asks to fill part of the single multivariate variable `m`, - # which is unsupported and must error rather than silently resample it. - chn10 = make_chain_from_prior(mv_partial(10) | (; y=zeros(10)), 5) + # The n=10 chain carries only `m[1:10]`; predicting with the n=20 model + # asks to fill part of the single multivariate `m`, which must error + # rather than silently resample it. + chn10 = make_chain_from_prior(mv_partial(10), 5) @test_throws "2239" DynamicPPL.predict(mv_partial(20), chn10) end end From a548bb83d9ebb71e40170362f7e48c8cd198bb13 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 12:57:17 +0100 Subject: [PATCH 5/9] Add HISTORY entry and bump to 0.42.2 Document the new informative error for partial specification of a multivariate variable via condition/fix/predict (Turing#2239). Non-breaking: the case never worked, so this only replaces a silent or opaque failure with a clear message. Co-Authored-By: Claude Opus 4.8 (1M context) --- HISTORY.md | 6 ++++++ Project.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 7f90d3af7..509990344 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,9 @@ +# 0.42.2 + +`condition`, `fix`, and `predict` now raise an informative error when only part of a variable that the model samples as a single multivariate draw is supplied (e.g. `x[1:10]` when the model draws `x ~ MvNormal(zeros(20), ...)`). + +This case has never worked: `predict` silently resampled the whole variable from the prior, `fix` silently collapsed it to the supplied length, and `condition` threw an opaque `DimensionMismatch`. It is therefore not a behaviour change, only a clearer failure. To sample the components individually, declare them in a loop, e.g. `for i in eachindex(x); x[i] ~ ...; end`. See [Turing.jl#2239](https://github.com/TuringLang/Turing.jl/issues/2239). + # 0.42.1 Fixed a type-inference failure that made nested submodels (a `~ to_submodel(...)` statement inside a model that is itself evaluated as a submodel) very slow. diff --git a/Project.toml b/Project.toml index cbabc7812..911593316 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "DynamicPPL" uuid = "366bfd00-2699-11ea-058f-f148b4cae6d8" -version = "0.42.1" +version = "0.42.2" [deps] ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" From 526ac26ea48c9cea3041bae8a2c1a196c9513da7 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 12:59:34 +0100 Subject: [PATCH 6/9] Reconcile predict/fix docstrings with the new error behaviour The docstring notes previously described the old silent behaviour (predict resampling the whole variable from the prior); update them to state that supplying only part of a multivariate variable now raises an error. Co-Authored-By: Claude Opus 4.8 (1M context) --- ext/DynamicPPLMCMCChainsExt.jl | 8 ++++---- src/model.jl | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ext/DynamicPPLMCMCChainsExt.jl b/ext/DynamicPPLMCMCChainsExt.jl index 921fcd57a..c33c67668 100644 --- a/ext/DynamicPPLMCMCChainsExt.jl +++ b/ext/DynamicPPLMCMCChainsExt.jl @@ -276,10 +276,10 @@ predictive distribution. !!! warning "Variables are treated as they occur in the model" A variable drawn from a multivariate distribution in a single tilde-statement (e.g. `x ~ MvNormal(...)` or `x ~ filldist(Normal(), n)`) is a *single* random - variable, not a collection of i.i.d. components. `predict` cannot fix a subset of - such a variable's components while resampling the rest; if `chain` supplies only - some components, the whole variable is resampled from the prior. To treat components - individually, declare them in a loop, e.g. `for i in eachindex(x); x[i] ~ Normal(); end`. + variable, not a collection of i.i.d. components. `predict` cannot fill in a subset of + such a variable's components: if `chain` supplies only some of them, an error is + raised. To treat components individually, declare them in a loop, e.g. + `for i in eachindex(x); x[i] ~ Normal(); end`. See [TuringLang/Turing.jl#2239](https://github.com/TuringLang/Turing.jl/issues/2239). # Examples diff --git a/src/model.jl b/src/model.jl index 215acbccf..b2dfccb4d 100644 --- a/src/model.jl +++ b/src/model.jl @@ -525,9 +525,9 @@ See also: [`unfix`](@ref), [`fixed`](@ref) !!! warning "Fixing applies to whole variables" Variables are treated as they occur in the model. A variable drawn from a multivariate distribution in a single tilde-statement (e.g. `x ~ MvNormal(...)`) is a *single* random - variable, so a subset of its components cannot be fixed independently; only fixing the - variable in its entirety is supported. Declare components in a loop (`x[i] ~ ...`) if you - need to fix them individually. + variable, so a subset of its components cannot be fixed independently; attempting to do + so raises an error. Declare components in a loop (`x[i] ~ ...`) if you need to fix them + individually. See [TuringLang/Turing.jl#2239](https://github.com/TuringLang/Turing.jl/issues/2239). # Examples From 619a92069170092212fba60253d5a6382f52e28b Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 13:00:54 +0100 Subject: [PATCH 7/9] Tighten the 0.42.2 HISTORY entry for clarity and brevity Co-Authored-By: Claude Opus 4.8 (1M context) --- HISTORY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 509990344..d4d090a11 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,8 +1,8 @@ # 0.42.2 -`condition`, `fix`, and `predict` now raise an informative error when only part of a variable that the model samples as a single multivariate draw is supplied (e.g. `x[1:10]` when the model draws `x ~ MvNormal(zeros(20), ...)`). +`condition`, `fix`, and `predict` now raise an informative error when given only part of a variable that the model samples as a single multivariate draw (e.g. `x[1:10]` when `x ~ MvNormal(zeros(20), ...)`). -This case has never worked: `predict` silently resampled the whole variable from the prior, `fix` silently collapsed it to the supplied length, and `condition` threw an opaque `DimensionMismatch`. It is therefore not a behaviour change, only a clearer failure. To sample the components individually, declare them in a loop, e.g. `for i in eachindex(x); x[i] ~ ...; end`. See [Turing.jl#2239](https://github.com/TuringLang/Turing.jl/issues/2239). +This is not a behaviour change: the partial case never worked, previously failing silently or with an opaque `DimensionMismatch`. To handle components individually, use a loop: `for i in eachindex(x); x[i] ~ ...; end`. See [Turing.jl#2239](https://github.com/TuringLang/Turing.jl/issues/2239). # 0.42.1 From e50f2d4b48ff55f8538120aa9111c9db3fe27107 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 13:04:32 +0100 Subject: [PATCH 8/9] Drop the issue URL from the error message Keep the error self-contained and actionable (the loop workaround); the #2239 reference stays in the docstrings and HISTORY. Tests now match on a stable phrase from the message rather than the issue number. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/compiler.jl | 3 +-- test/conditionfix.jl | 4 ++-- test/model.jl | 4 +++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/compiler.jl b/src/compiler.jl index a94c8ee5b..affc108ba 100644 --- a/src/compiler.jl +++ b/src/compiler.jl @@ -168,8 +168,7 @@ end "subset of such a variable (via `condition`, `fix`, or `predict` with a chain from " * "a differently-sized model) is not supported. To treat its components " * "individually, declare them in a loop, e.g. " * - "`for i in eachindex($(vn)); $(vn)[i] ~ ...; end`. " * - "See https://github.com/TuringLang/Turing.jl/issues/2239.", + "`for i in eachindex($(vn)); $(vn)[i] ~ ...; end`.", ) end diff --git a/test/conditionfix.jl b/test/conditionfix.jl index ab1907db4..f45adab5e 100644 --- a/test/conditionfix.jl +++ b/test/conditionfix.jl @@ -549,8 +549,8 @@ end # Supplying only part of a variable the model samples as one multivariate draw is # unsupported: `fix` used to silently collapse it and `condition` to throw an opaque # `DimensionMismatch`. Both must now raise the informative #2239 error. - @test_throws "2239" fix(mvc(3), Dict(@varname(m[1]) => 1.0))() - @test_throws "2239" condition(mvc(3), Dict(@varname(m[1]) => 1.0))() + @test_throws "declare them in a loop" fix(mvc(3), Dict(@varname(m[1]) => 1.0))() + @test_throws "declare them in a loop" condition(mvc(3), Dict(@varname(m[1]) => 1.0))() # A whole-variable value of the correct size is still honoured. @test fix(mvc(3), Dict(@varname(m) => [1.0, 2.0, 3.0]))() == [1.0, 2.0, 3.0] end diff --git a/test/model.jl b/test/model.jl index ad5e223f9..82c63ff68 100644 --- a/test/model.jl +++ b/test/model.jl @@ -651,7 +651,9 @@ const GDEMO_DEFAULT = DynamicPPL.TestUtils.demo_assume_observe_literal() # asks to fill part of the single multivariate `m`, which must error # rather than silently resample it. chn10 = make_chain_from_prior(mv_partial(10), 5) - @test_throws "2239" DynamicPPL.predict(mv_partial(20), chn10) + @test_throws "declare them in a loop" DynamicPPL.predict( + mv_partial(20), chn10 + ) end end end From 4a9e25a076cd32c7ef4cf1e4d8f46e911eecd845 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Wed, 8 Jul 2026 13:15:48 +0100 Subject: [PATCH 9/9] Throw ArgumentError and add a no-false-positive predict test Address review feedback: raise `ArgumentError` (consistent with `check_tilde_rhs` /`check_dot_tilde_rhs`) instead of a generic `error`, and match it by type in the tests rather than by a fragile message substring. Add a regression test that per-index (`.~`) variables still grow correctly under `predict` without being falsely flagged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/compiler.jl | 16 +++++++++------- test/conditionfix.jl | 4 ++-- test/model.jl | 17 ++++++++++++++--- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/compiler.jl b/src/compiler.jl index affc108ba..7b5438932 100644 --- a/src/compiler.jl +++ b/src/compiler.jl @@ -162,13 +162,15 @@ function _check_supplied_shape( return params end @noinline function _throw_incomplete_multivariate(vn, n, dist) - return error( - "A value with $(n) element(s) was supplied for `$(vn)`, but the model samples " * - "`$(vn)` as a single $(length(dist))-dimensional random variable. Supplying a " * - "subset of such a variable (via `condition`, `fix`, or `predict` with a chain from " * - "a differently-sized model) is not supported. To treat its components " * - "individually, declare them in a loop, e.g. " * - "`for i in eachindex($(vn)); $(vn)[i] ~ ...; end`.", + return throw( + ArgumentError( + "A value with $(n) element(s) was supplied for `$(vn)`, but the model samples " * + "`$(vn)` as a single $(length(dist))-dimensional random variable. Supplying a " * + "subset of such a variable (via `condition`, `fix`, or `predict` with a chain " * + "from a differently-sized model) is not supported. To treat its components " * + "individually, declare them in a loop, e.g. " * + "`for i in eachindex($(vn)); $(vn)[i] ~ ...; end`.", + ), ) end diff --git a/test/conditionfix.jl b/test/conditionfix.jl index f45adab5e..8765fbdc5 100644 --- a/test/conditionfix.jl +++ b/test/conditionfix.jl @@ -549,8 +549,8 @@ end # Supplying only part of a variable the model samples as one multivariate draw is # unsupported: `fix` used to silently collapse it and `condition` to throw an opaque # `DimensionMismatch`. Both must now raise the informative #2239 error. - @test_throws "declare them in a loop" fix(mvc(3), Dict(@varname(m[1]) => 1.0))() - @test_throws "declare them in a loop" condition(mvc(3), Dict(@varname(m[1]) => 1.0))() + @test_throws ArgumentError fix(mvc(3), Dict(@varname(m[1]) => 1.0))() + @test_throws ArgumentError condition(mvc(3), Dict(@varname(m[1]) => 1.0))() # A whole-variable value of the correct size is still honoured. @test fix(mvc(3), Dict(@varname(m) => [1.0, 2.0, 3.0]))() == [1.0, 2.0, 3.0] end diff --git a/test/model.jl b/test/model.jl index 82c63ff68..aab216694 100644 --- a/test/model.jl +++ b/test/model.jl @@ -651,9 +651,20 @@ const GDEMO_DEFAULT = DynamicPPL.TestUtils.demo_assume_observe_literal() # asks to fill part of the single multivariate `m`, which must error # rather than silently resample it. chn10 = make_chain_from_prior(mv_partial(10), 5) - @test_throws "declare them in a loop" DynamicPPL.predict( - mv_partial(20), chn10 - ) + @test_throws ArgumentError DynamicPPL.predict(mv_partial(20), chn10) + end + + @testset "per-index variables are not falsely flagged (#2239)" begin + @model function iid(k) + x = Vector{Float64}(undef, k) + x .~ Normal() + return x + end + # Each `x[i]` is its own variable, so predicting with a larger model must + # not error; the extra indices are simply sampled from the prior. + chn10 = make_chain_from_prior(iid(10), 5) + pred = DynamicPPL.predict(iid(20), chn10) + @test Symbol("x[20]") in keys(pred) end end end