From 698c3066d13591e718cb39b8a7a082ea4c8e0552 Mon Sep 17 00:00:00 2001 From: hardik-xi11 Date: Thu, 30 Apr 2026 13:36:22 +0530 Subject: [PATCH 1/2] Throw KeyError for missing Property --- HISTORY.md | 4 ++++ Project.toml | 2 +- src/varnamedtuple.jl | 4 ++-- src/varnamedtuple/getset.jl | 27 ++++++++++++++++++--------- test/logdensityfunction.jl | 2 +- test/varinfo.jl | 13 +++++++++++++ test/varnamedtuple.jl | 12 ++++++++++++ 7 files changed, 51 insertions(+), 13 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index a9a82fc86..a1d040ec8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,7 @@ +# 0.41.7 + +Accessing a nonexistent variable in a `VarNamedTuple` now throws a `KeyError` with the original `VarName`, instead of an opaque `type NamedTuple has no field ...` error. + # 0.41.6 Add a `factorize::Bool` keyword argument for `pointwise_logdensities(model, values)`, which controls whether pointwise logdensities for factorisable distributions (e.g. `MvNormal`, `product_distribution`, etc.) are returned as a single log-density for the whole distribution, or as an array of log-densities for each factor. diff --git a/Project.toml b/Project.toml index 8e09a07bf..f7820a2c5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "DynamicPPL" uuid = "366bfd00-2699-11ea-058f-f148b4cae6d8" -version = "0.41.6" +version = "0.41.7" [deps] ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" diff --git a/src/varnamedtuple.jl b/src/varnamedtuple.jl index aa5ed56e7..8c087ff4f 100644 --- a/src/varnamedtuple.jl +++ b/src/varnamedtuple.jl @@ -226,7 +226,7 @@ function AbstractPPL.hasvalue(vnt::VarNamedTuple, vn::VarName, dist::LKJCholesky for k in keys(val) # VarNamedTuples have VarNames as keys, PartialArrays have Index optics. subvn = val isa VarNamedTuple ? prefix(k, vn) : AbstractPPL.append_optic(vn, k) - dval[subvn] = _getindex_optic(val, k) + dval[subvn] = _getindex_optic(val, k, subvn) end return AbstractPPL.hasvalue(dval, vn, dist) end @@ -244,7 +244,7 @@ function AbstractPPL.getvalue(vnt::VarNamedTuple, vn::VarName, dist::LKJCholesky for k in keys(val) # VarNamedTuples have VarNames as keys, PartialArrays have Index optics. subvn = val isa VarNamedTuple ? prefix(k, vn) : AbstractPPL.append_optic(vn, k) - dval[subvn] = _getindex_optic(val, k) + dval[subvn] = _getindex_optic(val, k, subvn) end return AbstractPPL.getvalue(dval, vn, dist) end diff --git a/src/varnamedtuple/getset.jl b/src/varnamedtuple/getset.jl index df436b3f9..5c22815fa 100644 --- a/src/varnamedtuple/getset.jl +++ b/src/varnamedtuple/getset.jl @@ -11,7 +11,7 @@ const IndexWithoutChild = AbstractPPL.Index{<:Tuple,<:NamedTuple,AbstractPPL.Ide _unimplemented() = error("Not implemented") """ - DynamicPPL._getindex_optic(collection, optic::AbstractPPL.Optic) + DynamicPPL._getindex_optic(collection, optic::AbstractPPL.Optic, orig_vn::VarName) DynamicPPL._getindex_optic(collection, vn::VarName) Access the value in `collection` at the location specified by the given `optic`. If a `VarName` @@ -29,14 +29,23 @@ the leaf of the VNT i.e. a value, we could still handle pure `Index` optics if t an `AbstractArray`, but otherwise the only valid optic is `Iden`. """ function _getindex_optic(vnt::VarNamedTuple, vn::VarName) - return _getindex_optic(vnt, AbstractPPL.varname_to_optic(vn)) + return _getindex_optic(vnt, AbstractPPL.varname_to_optic(vn), vn) end -@inline _getindex_optic(@nospecialize(x::Any), ::AbstractPPL.Iden) = x -@inline _getindex_optic(x::Any, o::AbstractPPL.AbstractOptic) = o(x) -function _getindex_optic(vnt::VarNamedTuple, optic::AbstractPPL.Property{S}) where {S} - return _getindex_optic(getindex(vnt.data, S), optic.child) +function _getindex_optic(vnt::VarNamedTuple, vn::VarName, orig_vn) + return _getindex_optic(vnt, AbstractPPL.varname_to_optic(vn), orig_vn) end -function _getindex_optic(pa::PartialArray, optic::AbstractPPL.Index) + +@inline _getindex_optic(@nospecialize(x::Any), ::AbstractPPL.Iden, orig_vn) = x +@inline _getindex_optic(x::Any, o::AbstractPPL.AbstractOptic, orig_vn) = o(x) +function _getindex_optic( + vnt::VarNamedTuple, optic::AbstractPPL.Property{S}, orig_vn +) where {S} + if !haskey(vnt.data, S) + throw(KeyError(orig_vn)) + end + return _getindex_optic(getindex(vnt.data, S), optic.child, orig_vn) +end +function _getindex_optic(pa::PartialArray, optic::AbstractPPL.Index, orig_vn) coptic = AbstractPPL.concretize_top_level(optic, pa.data) child_value = if _is_multiindex(pa, coptic.ix...; coptic.kw...) && @@ -49,9 +58,9 @@ function _getindex_optic(pa::PartialArray, optic::AbstractPPL.Index) else getindex(pa, coptic.ix...; coptic.kw...) end - return _getindex_optic(child_value, optic.child) + return _getindex_optic(child_value, optic.child, orig_vn) end -function _getindex_optic(arr::AbstractArray, optic::IndexWithoutChild) +function _getindex_optic(arr::AbstractArray, optic::IndexWithoutChild, orig_vn) coptic = AbstractPPL.concretize_top_level(optic, arr) return Base.getindex(arr, coptic.ix...; coptic.kw...) end diff --git a/test/logdensityfunction.jl b/test/logdensityfunction.jl index 107772c61..ab50ef0d2 100644 --- a/test/logdensityfunction.jl +++ b/test/logdensityfunction.jl @@ -348,7 +348,7 @@ end @test_throws ArgumentError to_vector_params(vecvals, ldf) accs = OnlyAccsVarInfo(VectorParamAccumulator(ldf)) - @test_throws ErrorException init!!( + @test_throws KeyError init!!( extra_model, accs, InitFromPrior(), transform_strategy ) end diff --git a/test/varinfo.jl b/test/varinfo.jl index 9e909b4cc..f563e0fde 100644 --- a/test/varinfo.jl +++ b/test/varinfo.jl @@ -87,6 +87,19 @@ end vi, TransformedValue(x, NoTransform()), Normal(), vn, x ) @test !isempty(vi) + + @testset "KeyError for missing varname" begin + @model function test_model() + x ~ Normal() + return nothing + end + vi2 = VarInfo(test_model()) + # KeyError propagates from VarNamedTuple through VarInfo + @test_throws KeyError DynamicPPL.getindex_internal(vi2, @varname(y)) + @test_throws KeyError DynamicPPL.get_transformed_value(vi2, @varname(y)) + # Direct VarNamedTuple access also throws KeyError + @test_throws KeyError vi2.values[@varname(y)] + end end @testset "get/set/acclogp" begin diff --git a/test/varnamedtuple.jl b/test/varnamedtuple.jl index ed83c96fc..cc5f0dac3 100644 --- a/test/varnamedtuple.jl +++ b/test/varnamedtuple.jl @@ -346,6 +346,18 @@ Base.size(st::SizedThing) = st.size end end + @testset "KeyError for missing properties" begin + vnt = @vnt begin + x.a := 1.0 + end + # Should throw KeyError for missing top-level symbol + @test_throws KeyError vnt[@varname(y)] + # Should throw KeyError for missing nested property + @test_throws KeyError vnt[@varname(x.b)] + # Sanity check: accessing existing property should work + @test vnt[@varname(x.a)] == 1.0 + end + @testset "haskey on PartialArray" begin @testset "no ALBs" begin vnt = @vnt begin From fbb24dd37b4958ced99aac8b8117b8282fdefc31 Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Fri, 1 May 2026 18:49:38 +0100 Subject: [PATCH 2/2] Update src/varnamedtuple/getset.jl --- src/varnamedtuple/getset.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/varnamedtuple/getset.jl b/src/varnamedtuple/getset.jl index 5c22815fa..788a7b155 100644 --- a/src/varnamedtuple/getset.jl +++ b/src/varnamedtuple/getset.jl @@ -27,6 +27,9 @@ Note that it is only valid to index into a `VarNamedTuple` with a `Property` opt `PartialArray` with an `Index` optic. Other combinations are not valid. When we have reached the leaf of the VNT i.e. a value, we could still handle pure `Index` optics if the value is an `AbstractArray`, but otherwise the only valid optic is `Iden`. + +`orig_vn` is used to keep track of the original VarName used to index into a VarNamedTuple, +and is only for error reporting purposes. """ function _getindex_optic(vnt::VarNamedTuple, vn::VarName) return _getindex_optic(vnt, AbstractPPL.varname_to_optic(vn), vn)