From 1fd87cf59f8531d7ba6a2e46d332953946c88ab9 Mon Sep 17 00:00:00 2001 From: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:01:14 +0000 Subject: [PATCH 01/14] Remove dependencies associated with the removed `GraphInfo` (#90) --- Project.toml | 6 ++---- test/Project.toml | 4 ---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Project.toml b/Project.toml index 98eb0ed..ba73c43 100644 --- a/Project.toml +++ b/Project.toml @@ -3,19 +3,17 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" keywords = ["probablistic programming"] license = "MIT" desc = "Common interfaces for probabilistic programming" -version = "0.7" +version = "0.7.1" [deps] AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001" DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" -SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] AbstractMCMC = "2, 3, 4, 5" DensityInterface = "0.4" -Setfield = "0.8.2, 1" Random = "1.6" -SparseArrays = "1.6" +Setfield = "0.8.2, 1" julia = "~1.6.6, 1.7.3" diff --git a/test/Project.toml b/test/Project.toml index 0770c5c..21903a1 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,16 +1,12 @@ [deps] -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" -SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] -Distributions = "0.25" Documenter = "0.26.3, 0.27" InvertedIndices = "1" OffsetArrays = "1" From f3fbce0932030372d0a7bca1325f78792583395d Mon Sep 17 00:00:00 2001 From: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:25:57 +0000 Subject: [PATCH 02/14] Move to `Accessors.jl` from `Setfield.jl` (#91) * move to Accessors * fix tests * fix test error * add compat * version bump * Update src/varname.jl Co-authored-by: Tor Erlend Fjelde * Update src/varname.jl * remove type piracy in `show` function * fix print behavior * removed composition of a varname to a lens * update doc * remove `Setfield` * add some type stability tests and additional doctests * fix test error * Update src/varname.jl Co-authored-by: Tor Erlend Fjelde * copy functions from Setfield and recover the interpolation abilities * fix some comments --------- Co-authored-by: Tor Erlend Fjelde --- Project.toml | 7 +- src/AbstractPPL.jl | 2 +- src/varname.jl | 379 +++++++++++++++++++++++++++------------------ test/Project.toml | 3 +- test/varname.jl | 39 +++-- 5 files changed, 262 insertions(+), 168 deletions(-) diff --git a/Project.toml b/Project.toml index ba73c43..becfdbe 100644 --- a/Project.toml +++ b/Project.toml @@ -3,17 +3,18 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" keywords = ["probablistic programming"] license = "MIT" desc = "Common interfaces for probabilistic programming" -version = "0.7.1" +version = "0.8.0" [deps] AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001" +Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" [compat] AbstractMCMC = "2, 3, 4, 5" +Accessors = "0.1" DensityInterface = "0.4" Random = "1.6" -Setfield = "0.8.2, 1" julia = "~1.6.6, 1.7.3" diff --git a/src/AbstractPPL.jl b/src/AbstractPPL.jl index 1be28de..775576a 100644 --- a/src/AbstractPPL.jl +++ b/src/AbstractPPL.jl @@ -3,7 +3,7 @@ module AbstractPPL # VarName export VarName, getsym, - getlens, + getoptic, inspace, subsumes, subsumedby, diff --git a/src/varname.jl b/src/varname.jl index a0267e3..a3ca1dc 100644 --- a/src/varname.jl +++ b/src/varname.jl @@ -1,70 +1,70 @@ -using Setfield -using Setfield: PropertyLens, ComposedLens, IdentityLens, IndexLens, DynamicIndexLens +using Accessors +using Accessors: ComposedOptic, PropertyLens, IndexLens, DynamicIndexLens +using MacroTools + +const ALLOWED_OPTICS = Union{typeof(identity),PropertyLens,IndexLens,ComposedOptic} """ - VarName{sym}(lens::Lens=IdentityLens()) + VarName{sym}(optic=identity) -A variable identifier for a symbol `sym` and lens `lens`. +A variable identifier for a symbol `sym` and optic `optic`. The Julia variable in the model corresponding to `sym` can refer to a single value or to a hierarchical array structure of univariate, multivariate or matrix variables. The field `lens` stores the indices requires to access the random variable from the Julia variable indicated by `sym` -as a tuple of tuples. Each element of the tuple thereby contains the indices of one lens +as a tuple of tuples. Each element of the tuple thereby contains the indices of one optic operation. -`VarName`s can be manually constructed using the `VarName{sym}(lens)` constructor, or from an -lens expression through the [`@varname`](@ref) convenience macro. +`VarName`s can be manually constructed using the `VarName{sym}(optic)` constructor, or from an +optic expression through the [`@varname`](@ref) convenience macro. # Examples -```jldoctest; setup=:(using Setfield) -julia> vn = VarName{:x}(Setfield.IndexLens((Colon(), 1)) ∘ Setfield.IndexLens((2, ))) -x[:,1][2] +```jldoctest; setup=:(using Accessors) +julia> vn = VarName{:x}(Accessors.IndexLens((Colon(), 1)) ⨟ Accessors.IndexLens((2, ))) +x[:, 1][2] -julia> getlens(vn) -(@lens _[Colon(), 1][2]) +julia> getoptic(vn) +(@o _[Colon(), 1][2]) julia> @varname x[:, 1][1+1] -x[:,1][2] +x[:, 1][2] ``` """ -struct VarName{sym,T<:Lens} - lens::T +struct VarName{sym,T} + optic::T - function VarName{sym}(lens=IdentityLens()) where {sym} - # TODO: Should we completely disallow or just `@warn` of limited support? - if !is_static_lens(lens) - error("attempted to construct `VarName` with dynamic lens of type $(nameof(typeof(lens)))") + function VarName{sym}(optic=identity) where {sym} + if !is_static_optic(typeof(optic)) + throw(ArgumentError("attempted to construct `VarName` with unsupported optic of type $(nameof(typeof(optic)))")) end - return new{sym,typeof(lens)}(lens) + return new{sym,typeof(optic)}(optic) end end """ - is_static_lens(l::Lens) - -Return `true` if `l` does not require runtime information to be resolved. + is_static_optic(l) -In particular it returns `false` for `Setfield.DynamicLens` and `Setfield.FunctionLens`. +Return `true` if `l` is one or a composition of `identity`, `PropertyLens`, and `IndexLens`; `false` if `l` is +one or a composition of `DynamicIndexLens`; and undefined otherwise. """ -is_static_lens(l::Lens) = is_static_lens(typeof(l)) -is_static_lens(::Type{<:Lens}) = false -is_static_lens(::Type{<:Union{PropertyLens, IndexLens, IdentityLens}}) = true -function is_static_lens(::Type{ComposedLens{LO, LI}}) where {LO, LI} - return is_static_lens(LO) && is_static_lens(LI) +is_static_optic(::Type{<:Union{typeof(identity),PropertyLens,IndexLens}}) = true +function is_static_optic(::Type{ComposedOptic{LO,LI}}) where {LO,LI} + return is_static_optic(LO) && is_static_optic(LI) end +is_static_optic(::Type{<:DynamicIndexLens}) = false # A bit of backwards compatibility. -VarName{sym}(indexing::Tuple) where {sym} = VarName{sym}(tupleindex2lens(indexing)) +VarName{sym}(indexing::Tuple) where {sym} = VarName{sym}(tupleindex2optic(indexing)) """ - VarName(vn::VarName, lens::Lens) + VarName(vn::VarName, optic) VarName(vn::VarName, indexing::Tuple) -Return a copy of `vn` with a new index `lens`/`indexing`. +Return a copy of `vn` with a new index `optic`/`indexing`. -```jldoctest; setup=:(using Setfield) -julia> VarName(@varname(x[1][2:3]), Setfield.IndexLens((2,))) +```jldoctest; setup=:(using Accessors) +julia> VarName(@varname(x[1][2:3]), Accessors.IndexLens((2,))) x[2] julia> VarName(@varname(x[1][2:3]), ((2,),)) @@ -74,16 +74,16 @@ julia> VarName(@varname(x[1][2:3])) x ``` """ -VarName(vn::VarName, lens::Lens = IdentityLens()) = VarName{getsym(vn)}(lens) +VarName(vn::VarName, optic=identity) = VarName{getsym(vn)}(optic) function VarName(vn::VarName, indexing::Tuple) - return VarName{getsym(vn)}(tupleindex2lens(indexing)) + return VarName{getsym(vn)}(tupleindex2optic(indexing)) end -tupleindex2lens(indexing::Tuple{}) = IdentityLens() -tupleindex2lens(indexing::Tuple{<:Tuple}) = IndexLens(first(indexing)) -function tupleindex2lens(indexing::Tuple) - return IndexLens(first(indexing)) ∘ tupleindex2lens(indexing[2:end]) +tupleindex2optic(indexing::Tuple{}) = identity +tupleindex2optic(indexing::Tuple{<:Tuple}) = IndexLens(first(indexing)) # TODO: rest? +function tupleindex2optic(indexing::Tuple) + return IndexLens(first(indexing)) ∘ tupleindex2optic(indexing[2:end]) end """ @@ -104,70 +104,90 @@ julia> getsym(@varname(y)) getsym(vn::VarName{sym}) where {sym} = sym """ - getlens(vn::VarName) + getoptic(vn::VarName) -Return the lens of the Julia variable used to generate `vn`. +Return the optic of the Julia variable used to generate `vn`. ## Examples ```jldoctest -julia> getlens(@varname(x[1][2:3])) -(@lens _[1][2:3]) +julia> getoptic(@varname(x[1][2:3])) +(@o _[1][2:3]) -julia> getlens(@varname(y)) -(@lens _) +julia> getoptic(@varname(y)) +identity (generic function with 1 method) ``` """ -getlens(vn::VarName) = vn.lens - +getoptic(vn::VarName) = vn.optic """ get(obj, vn::VarName{sym}) -Alias for `get(obj, PropertyLens{sym}() ∘ getlens(vn))`. +Alias for `getoptic(vn)(obj)`. + +# Example + +```jldoctest; setup = :(nt = (a = 1, b = (c = [1, 2, 3],)); name = :nt) +julia> get(nt, @varname(nt.a)) +1 + +julia> get(nt, @varname(nt.b.c[1])) +1 + +julia> get(nt, @varname(\$name.b.c[1])) +1 +``` """ -function Setfield.get(obj, vn::VarName{sym}) where {sym} - return Setfield.get(obj, PropertyLens{sym}() ∘ getlens(vn)) +function Base.get(obj, vn::VarName{sym}) where {sym} + return getoptic(vn)(obj) end """ set(obj, vn::VarName{sym}, value) -Alias for `set(obj, PropertyLens{sym}() ∘ getlens(vn), value)`. +Alias for `set(obj, PropertyLens{sym}() ⨟ getoptic(vn), value)`. + +# Example + +```jldoctest; setup = :(using AbstractPPL: Accessors; nt = (a = 1, b = (c = [1, 2, 3],)); name = :nt) +julia> Accessors.set(nt, @varname(a), 10) +(a = 10, b = (c = [1, 2, 3],)) + +julia> Accessors.set(nt, @varname(b.c[1]), 10) +(a = 1, b = (c = [10, 2, 3],)) +``` """ -function Setfield.set(obj, vn::VarName{sym}, value) where {sym} - return Setfield.set(obj, PropertyLens{sym}() ∘ getlens(vn), value) +function Accessors.set(obj, vn::VarName{sym}, value) where {sym} + return Accessors.set(obj, PropertyLens{sym}() ⨟ getoptic(vn), value) end -Base.hash(vn::VarName, h::UInt) = hash((getsym(vn), getlens(vn)), h) +Base.hash(vn::VarName, h::UInt) = hash((getsym(vn), getoptic(vn)), h) function Base.:(==)(x::VarName, y::VarName) - return getsym(x) == getsym(y) && getlens(x) == getlens(y) + return getsym(x) == getsym(y) && getoptic(x) == getoptic(y) end -# Allow compositions with lenses. -function Base.:∘(vn::VarName{sym,<:Lens}, lens::Lens) where {sym} - return VarName{sym}(getlens(vn) ∘ lens) -end - -function Base.show(io::IO, vn::VarName{<:Any,<:Lens}) - # No need to check `Setfield.has_atlens_support` since - # `VarName` does not allow dynamic lenses. +function Base.show(io::IO, vn::VarName{sym,T}) where {sym,T} print(io, getsym(vn)) - _print_application(io, getlens(vn)) + _show_optic(io, getoptic(vn)) end -# This is all just to allow to convert `Colon()` into `:`. -_print_application(io::IO, l::Lens) = Setfield.print_application(io, l) -function _print_application(io::IO, l::ComposedLens) - _print_application(io, l.outer) - _print_application(io, l.inner) +# modified from https://github.com/JuliaObjects/Accessors.jl/blob/01528a81fdf17c07436e1f3d99119d3f635e4c26/src/sugar.jl#L502 +function _show_optic(io::IO, optic) + opts = Accessors.deopcompose(optic) + inner = Iterators.takewhile(x -> applicable(_shortstring, "", x), opts) + outer = Iterators.dropwhile(x -> applicable(_shortstring, "", x), opts) + if !isempty(outer) + show(io, opcompose(outer...)) + print(io, " ∘ ") + end + shortstr = reduce(_shortstring, inner; init="") + print(io, shortstr) end -_print_application(io::IO, l::IndexLens) = - print(io, "[", join(map(prettify_index, l.indices), ","), "]") -# This is a bit weird but whatever. We're almost always going to -# `concretize` anyways. -_print_application(io::IO, l::DynamicIndexLens) = print(io, l, "(_)") + +_shortstring(prev, o::IndexLens) = "$prev[$(join(map(prettify_index, o.indices), ", "))]" +_shortstring(prev, ::typeof(identity)) = "$prev" +_shortstring(prev, o) = Accessors._shortstring(prev, o) prettify_index(x) = repr(x) prettify_index(::Colon) = ":" @@ -175,7 +195,7 @@ prettify_index(::Colon) = ":" """ Symbol(vn::VarName) -Return a `Symbol` represenation of the variable identifier `VarName`. +Return a `Symbol` representation of the variable identifier `VarName`. # Examples ```jldoctest @@ -266,7 +286,7 @@ Currently _not_ supported are: - Trailing ones: `x[2, 1]` does not subsume `x[2]` for a vector `x` """ function subsumes(u::VarName, v::VarName) - return getsym(u) == getsym(v) && subsumes(u.lens, v.lens) + return getsym(u) == getsym(v) && subsumes(getoptic(u), getoptic(v)) end # Idea behind `subsumes` for `Lens` is that we traverse the two lenses in parallel, @@ -274,20 +294,20 @@ end # `PropertyLens{:a}` and `PropertyLens{:b}` we immediately know that they do not subsume # each other since at the same level/depth they access different properties. # E.g. `x`, `x[1]`, i.e. `u` is always subsumed by `t` -subsumes(::IdentityLens, ::IdentityLens) = true -subsumes(::IdentityLens, ::Lens) = true -subsumes(::Lens, ::IdentityLens) = false +subsumes(::typeof(identity), ::typeof(identity)) = true +subsumes(::typeof(identity), ::ALLOWED_OPTICS) = true +subsumes(::ALLOWED_OPTICS, ::typeof(identity)) = false -subsumes(t::ComposedLens, u::ComposedLens) = +subsumes(t::ComposedOptic, u::ComposedOptic) = subsumes(t.outer, u.outer) && subsumes(t.inner, u.inner) # If `t` is still a composed lens, then there is no way it can subsume `u` since `u` is a # leaf of the "lens-tree". -subsumes(t::ComposedLens, u::PropertyLens) = false +subsumes(t::ComposedOptic, u::PropertyLens) = false # Here we need to check if `u.outer` (i.e. the next lens to be applied from `u`) is # subsumed by `t`, since this would mean that the rest of the composition is also subsumed # by `t`. -subsumes(t::PropertyLens, u::ComposedLens) = subsumes(t, u.outer) +subsumes(t::PropertyLens, u::ComposedOptic) = subsumes(t, u.inner) # For `PropertyLens` either they have the same `name` and thus they are indeed the same. subsumes(t::PropertyLens{name}, u::PropertyLens{name}) where {name} = true @@ -299,8 +319,8 @@ subsumes(t::PropertyLens, u::PropertyLens) = false # FIXME: Does not correctly handle cases such as `subsumes(x, x[:])` # (but neither did old implementation). subsumes( - t::Union{IndexLens,ComposedLens{<:IndexLens}}, - u::Union{IndexLens,ComposedLens{<:IndexLens}} + t::Union{IndexLens,ComposedOptic{<:ALLOWED_OPTICS,<:IndexLens}}, + u::Union{IndexLens,ComposedOptic{<:ALLOWED_OPTICS,<:IndexLens}} ) = subsumes_indices(t, u) @@ -317,7 +337,7 @@ const ≍ = uncomparable # Therefore we must recurse until we reach something that is NOT # indexing, and then consider the sequence of indices leading up to this. """ - subsumes_indices(t::Lens, u::Lens) + subsumes_indices(t, u) Return `true` if the indexing represented by `t` subsumes `u`. @@ -326,12 +346,12 @@ e.g. `_[1][2].a[2]` and `_[1][2].a`. In such a scenario we do the following: 1. Combine `[1][2]` into a `Tuple` of indices using [`combine_indices`](@ref). 2. Do the same for `[1][2]`. 3. Compare the two tuples from (1) and (2) using `subsumes_indices`. -4. Since we're still undecided, we call `subsume(@lens(_.a[2]), @lens(_.a))` +4. Since we're still undecided, we call `subsume(@o(_.a[2]), @o(_.a))` which then returns `false`. # Example -```jldoctest; setup=:(using Setfield; using AbstractPPL: subsumes_indices) -julia> t = @lens(_[1].a); u = @lens(_[1]); +```jldoctest; setup=:(using Accessors; using AbstractPPL: subsumes_indices) +julia> t = @o(_[1].a); u = @o(_[1]); julia> subsumes_indices(t, u) false @@ -339,22 +359,22 @@ false julia> subsumes_indices(u, t) true -julia> # `IdentityLens` subsumes all. - subsumes_indices(@lens(_), t) +julia> # `identity` subsumes all. + subsumes_indices(identity, t) true -julia> # None subsumes `IdentityLens`. - subsumes_indices(t, @lens(_)) +julia> # None subsumes `identity`. + subsumes_indices(t, identity) false -julia> AbstractPPL.subsumes(@lens(_[1][2].a[2]), @lens(_[1][2].a)) +julia> AbstractPPL.subsumes(@o(_[1][2].a[2]), @o(_[1][2].a)) false -julia> AbstractPPL.subsumes(@lens(_[1][2].a), @lens(_[1][2].a[2])) +julia> AbstractPPL.subsumes(@o(_[1][2].a), @o(_[1][2].a[2])) true ``` """ -function subsumes_indices(t::Lens, u::Lens) +function subsumes_indices(t::ALLOWED_OPTICS, u::ALLOWED_OPTICS) t_indices, t_next = combine_indices(t) u_indices, u_next = combine_indices(u) @@ -378,18 +398,18 @@ function subsumes_indices(t::Lens, u::Lens) end """ - combine_indices(lens) + combine_indices(optic) Return sequential indexing into a single `Tuple` of indices, e.g. `x[:][1][2]` becomes `((Colon(), ), (1, ), (2, ))`. The result is compatible with [`subsumes_indices`](@ref) for `Tuple` input. """ -combine_indices(lens::Lens) = (), lens -combine_indices(lens::IndexLens) = (lens.indices,), nothing -function combine_indices(lens::ComposedLens{<:IndexLens}) - indices, next = combine_indices(lens.inner) - return (lens.outer.indices, indices...), next +combine_indices(optic::ALLOWED_OPTICS) = (), optic +combine_indices(optic::IndexLens) = (optic.indices,), nothing +function combine_indices(optic::ComposedOptic{<:ALLOWED_OPTICS,<:IndexLens}) + indices, next = combine_indices(optic.outer) + return (optic.inner.indices, indices...), next end """ @@ -427,11 +447,11 @@ subsumes_index(i, j) = i == j An indexing object wrapping the range of a `Base.Slice` object representing the concrete indices a `:` indicates. Behaves the same, but prints differently, namely, still as `:`. """ -struct ConcretizedSlice{T, R} <: AbstractVector{T} +struct ConcretizedSlice{T,R} <: AbstractVector{T} range::R end -ConcretizedSlice(s::Base.Slice{R}) where {R} = ConcretizedSlice{eltype(s.indices), R}(s.indices) +ConcretizedSlice(s::Base.Slice{R}) where {R} = ConcretizedSlice{eltype(s.indices),R}(s.indices) Base.show(io::IO, s::ConcretizedSlice) = print(io, ":") Base.show(io::IO, ::MIME"text/plain", s::ConcretizedSlice) = print(io, "ConcretizedSlice(", s.range, ")") @@ -459,9 +479,8 @@ reconcretize_index(original_index, lowered_index) = lowered_index reconcretize_index(original_index::Colon, lowered_index::Base.Slice) = ConcretizedSlice(lowered_index) - """ - concretize(l::Lens, x) + concretize(l, x) Return `l` instantiated on `x`, i.e. any information related to the runtime shape of `x` is evaluated. This concerns `begin`, `end`, and `:` slices. @@ -470,12 +489,12 @@ Basically, every index is converted to a concrete value using `Base.to_index` on slices are only converted to `ConcretizedSlice` (as opposed to `Base.Slice{Base.OneTo}`), to keep the result close to the original indexing. """ -concretize(I::Lens, x) = I +concretize(I::ALLOWED_OPTICS, x) = I concretize(I::DynamicIndexLens, x) = concretize(IndexLens(I.f(x)), x) concretize(I::IndexLens, x) = IndexLens(reconcretize_index.(I.indices, to_indices(x, I.indices))) -function concretize(I::ComposedLens, x) - x_inner = get(x, I.outer) # TODO: get view here - return ComposedLens(concretize(I.outer, x), concretize(I.inner, x_inner)) +function concretize(I::ComposedOptic, x) + x_inner = I.inner(x) # TODO: get view here + return ComposedOptic(concretize(I.outer, x_inner), concretize(I.inner, x)) end """ @@ -485,11 +504,11 @@ Return `vn` concretized on `x`, i.e. any information related to the runtime shap evaluated. This concerns `begin`, `end`, and `:` slices. # Examples -```jldoctest; setup=:(using Setfield) +```jldoctest; setup=:(using Accessors) julia> x = (a = [1.0 2.0; 3.0 4.0; 5.0 6.0], ); -julia> getlens(@varname(x.a[1:end, end][:], true)) # concrete=true required for @varname -(@lens _.a[1:3, 2][:]) +julia> getoptic(@varname(x.a[1:end, end][:], true)) # concrete=true required for @varname +(@o _.a[1:3, 2][:]) julia> y = zeros(10, 10); @@ -497,11 +516,11 @@ julia> @varname(y[:], true) y[:] julia> # The underlying value is conretized, though: - AbstractPPL.getlens(AbstractPPL.concretize(@varname(y[:]), y)).indices[1] + AbstractPPL.getoptic(AbstractPPL.concretize(@varname(y[:]), y)).indices[1] ConcretizedSlice(Base.OneTo(100)) ``` """ -concretize(vn::VarName, x) = VarName(vn, concretize(getlens(vn), x)) +concretize(vn::VarName, x) = VarName(vn, concretize(getoptic(vn), x)) """ @varname(expr, concretize=false) @@ -521,7 +540,7 @@ concretized as `VarName` only supports non-dynamic indexing as determined by julia> x = (a = [1.0 2.0; 3.0 4.0; 5.0 6.0], ); julia> @varname(x.a[1:end, end][:], true) -x.a[1:3,2][:] +x.a[1:3, 2][:] julia> @varname(x.a[end], false) # disable concretization ERROR: LoadError: Variable name `x.a[end]` is dynamic and requires concretization! @@ -542,46 +561,46 @@ julia> # Potentially surprising behaviour, but this is equivalent to what Base d ### General indexing -Under the hood Setfield.jl's `Lens` are used for the indexing: +Under the hood `optic`s are used for the indexing: ```jldoctest -julia> getlens(@varname(x)) -(@lens _) +julia> getoptic(@varname(x)) +identity (generic function with 1 method) -julia> getlens(@varname(x[1])) -(@lens _[1]) +julia> getoptic(@varname(x[1])) +(@o _[1]) -julia> getlens(@varname(x[:, 1])) -(@lens _[Colon(), 1]) +julia> getoptic(@varname(x[:, 1])) +(@o _[Colon(), 1]) -julia> getlens(@varname(x[:, 1][2])) -(@lens _[Colon(), 1][2]) +julia> getoptic(@varname(x[:, 1][2])) +(@o _[Colon(), 1][2]) -julia> getlens(@varname(x[1,2][1+5][45][3])) -(@lens _[1, 2][6][45][3]) +julia> getoptic(@varname(x[1,2][1+5][45][3])) +(@o _[1, 2][6][45][3]) ``` This also means that we support property access: ```jldoctest -julia> getlens(@varname(x.a)) -(@lens _.a) +julia> getoptic(@varname(x.a)) +(@o _.a) -julia> getlens(@varname(x.a[1])) -(@lens _.a[1]) +julia> getoptic(@varname(x.a[1])) +(@o _.a[1]) -julia> x = (a = [(b = rand(2), )], ); getlens(@varname(x.a[1].b[end], true)) -(@lens _.a[1].b[2]) +julia> x = (a = [(b = rand(2), )], ); getoptic(@varname(x.a[1].b[end], true)) +(@o _.a[1].b[2]) ``` -Interpolation can be used for names (the base name as well as property names). Variables within -indices are always evaluated in the calling scope, in the same manner as `Setfield` does: +Interpolation can be used for variable names, or array name, but not the lhs of a `.` expression. +Variables within indices are always evaluated in the calling scope. ```jldoctest julia> name, i = :a, 10; julia> @varname(x.\$name[i, i+1]) -x.a[10,11] +x.a[10, 11] julia> @varname(\$name) a @@ -595,47 +614,43 @@ a.x[1] julia> @varname(b.\$name.x[1]) b.a.x[1] ``` - -!!! compat "Julia 1.5" - Using `begin` in an indexing expression to refer to the first index requires at least - Julia 1.5. """ -macro varname(expr::Union{Expr,Symbol}, concretize::Bool=Setfield.need_dynamic_lens(expr)) +macro varname(expr::Union{Expr,Symbol}, concretize::Bool=Accessors.need_dynamic_optic(expr)) return varname(expr, concretize) end varname(sym::Symbol) = :($(AbstractPPL.VarName){$(QuoteNode(sym))}()) varname(sym::Symbol, _) = varname(sym) -function varname(expr::Expr, concretize=Setfield.need_dynamic_lens(expr)) +function varname(expr::Expr, concretize=Accessors.need_dynamic_optic(expr)) if Meta.isexpr(expr, :ref) || Meta.isexpr(expr, :.) # Split into object/base symbol and lens. - sym_escaped, lens = Setfield.parse_obj_lens(expr) + sym_escaped, optics = _parse_obj_optic(expr) # Setfield.jl escapes the return symbol, so we need to unescape # to call `QuoteNode` on it. sym = drop_escape(sym_escaped) # This is to handle interpolated heads -- Setfield treats them differently: - # julia> Setfield.parse_obj_lens(@q $name.a) - # (:($(Expr(:escape, :_))), :((Setfield.compose)($(Expr(:escape, :name)), (Setfield.PropertyLens){:a}()))) - # julia> Setfield.parse_obj_lens(@q x.a) - # (:($(Expr(:escape, :x))), :((Setfield.compose)((Setfield.PropertyLens){:a}()))) + # julia> AbstractPPL._parse_obj_optics(Meta.parse("\$name.a")) + # (:($(Expr(:escape, :_))), (:($(Expr(:escape, :name))), :((PropertyLens){:a}()))) + # julia> AbstractPPL._parse_obj_optic(:(x.a)) + # (:($(Expr(:escape, :x))), :(Accessors.opticcompose((PropertyLens){:a}()))) if sym != :_ sym = QuoteNode(sym) else - sym = lens.args[2] - lens = Expr(:call, lens.args[1], lens.args[3:end]...) + sym = optics.args[2] + optics = Expr(:call, optics.args[1], optics.args[3:end]...) end if concretize return :( $(AbstractPPL.VarName){$sym}( - $(AbstractPPL.concretize)($lens, $sym_escaped) + $(AbstractPPL.concretize)($optics, $sym_escaped) ) ) - elseif Setfield.need_dynamic_lens(expr) + elseif Accessors.need_dynamic_optic(expr) error("Variable name `$(expr)` is dynamic and requires concretization!") else - :($(AbstractPPL.VarName){$sym}($lens)) + return :($(AbstractPPL.VarName){$sym}($optics)) end elseif Meta.isexpr(expr, :$, 1) return :($(AbstractPPL.VarName){$(esc(expr.args[1]))}()) @@ -650,6 +665,66 @@ function drop_escape(expr::Expr) return Expr(expr.head, map(x -> drop_escape(x), expr.args)...) end +function _parse_obj_optic(ex) + obj, optics = _parse_obj_optics(ex) + optic = Expr(:call, :(Accessors.opticcompose), optics...) + obj, optic +end + +# Accessors doesn't have the same support for interpolation, so copy and modify Setfield's parsing functions +is_interpolation(x) = x isa Expr && x.head == :$ + +function _parse_obj_optics_composite(lensexprs::Vector) + if isempty(lensexprs) + return esc(:_), () + else + obj, outermostlens = _parse_obj_optics(lensexprs[1]) + innerlenses = map(lensexprs[2:end]) do innerex + o, lens = _parse_obj_optics(innerex) + @assert o == esc(:_) + lens + end + return obj, (outermostlens, innerlenses...) + end +end + +function _parse_obj_optics(ex) + if @capture(ex, ∘(opticsexprs__)) + return _parse_obj_optics_composite(opticsexprs) + elseif is_interpolation(ex) + @assert length(ex.args) == 1 + return esc(:_), (esc(ex.args[1]),) + elseif @capture(ex, front_[indices__]) + obj, frontoptics = _parse_obj_optics(front) + if any(Accessors.need_dynamic_optic, indices) + @gensym collection + indices = Accessors.replace_underscore.(indices, collection) + dims = length(indices) == 1 ? nothing : 1:length(indices) + lindices = esc.(Accessors.lower_index.(collection, indices, dims)) + optics = :($(Accessors.DynamicIndexLens)($(esc(collection)) -> ($(lindices...),))) + else + index = esc(Expr(:tuple, indices...)) + optics = :($(Accessors.IndexLens)($index)) + end + elseif @capture(ex, front_.property_) + obj, frontoptics = _parse_obj_optics(front) + if property isa Union{Symbol,String} + optics = :($(Accessors.PropertyLens){$(QuoteNode(property))}()) + elseif is_interpolation(property) + optics = :($(Accessors.PropertyLens){$(esc(property.args[1]))}()) + else + throw(ArgumentError( + string("Error while parsing :($ex). Second argument to `getproperty` can only be", + "a `Symbol` or `String` literal, received `$property` instead.") + )) + end + else + obj = esc(ex) + return obj, () + end + obj, tuple(frontoptics..., optics) +end + """ @vsym(expr) diff --git a/test/Project.toml b/test/Project.toml index 21903a1..adeb617 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,14 +1,13 @@ [deps] +Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] Documenter = "0.26.3, 0.27" InvertedIndices = "1" OffsetArrays = "1" -Setfield = "0.7.1, 0.8, 1" julia = "1" diff --git a/test/varname.jl b/test/varname.jl index d9b52db..d5cb4f7 100644 --- a/test/varname.jl +++ b/test/varname.jl @@ -1,9 +1,11 @@ +using Accessors using InvertedIndices using OffsetArrays -using Setfield using AbstractPPL: ⊑, ⊒, ⋢, ⋣, ≍ +using AbstractPPL: Accessors +using AbstractPPL.Accessors: IndexLens, PropertyLens macro test_strict_subsumption(x, y) quote @@ -21,20 +23,18 @@ end @test @varname(A[:, 1][1+1]) == @varname(A[:, 1][2]) @test(@varname(A[:, 1][2]) == - VarName{:A}(@lens(_[:, 1]) ∘ @lens(_[2])) == - VarName{:A}(@lens(_[:, 1])) ∘ @lens(_[2]) == - VarName{:A}() ∘ @lens(_[:, 1]) ∘ @lens(_[2])) + VarName{:A}(@o(_[:, 1]) ⨟ @o(_[2]))) # concretization y = zeros(10, 10) x = (a = [1.0 2.0; 3.0 4.0; 5.0 6.0], ); @test @varname(y[begin, i], true) == @varname(y[1, 1:10]) - @test @varname(y[:], true) == @varname(y[1:100]) - @test @varname(y[:, begin], true) == @varname(y[1:10, 1]) - @test getlens(AbstractPPL.concretize(@varname(y[:]), y)).indices[1] === + @test get(y, @varname(y[:], true)) == get(y, @varname(y[1:100])) + @test get(y, @varname(y[:, begin], true)) == get(y, @varname(y[1:10, 1])) + @test getoptic(AbstractPPL.concretize(@varname(y[:]), y)).indices[1] === AbstractPPL.ConcretizedSlice(to_indices(y, (:,))[1]) - @test @varname(x.a[1:end, end][:], true) == @varname(x.a[1:3,2][1:3]) + @test get(x, @varname(x.a[1:end, end][:], true)) == get(x, @varname(x.a[1:3,2][1:3])) end @testset "subsumption with standard indexing" begin @@ -83,10 +83,29 @@ end @testset "non-standard indexing" begin A = rand(10, 10) - @test @varname(A[1, Not(3)], true) == @varname(A[1, [1, 2, 4, 5, 6, 7, 8, 9, 10]]) + @test get(A, @varname(A[1, Not(3)], true)) == get(A, @varname(A[1, [1, 2, 4, 5, 6, 7, 8, 9, 10]])) B = OffsetArray(A, -5, -5) # indices -4:5×-4:5 - @test @varname(B[1, :], true) == @varname(B[1, -4:5]) + @test collect(get(B, @varname(B[1, :], true))) == collect(get(B, @varname(B[1, -4:5]))) end + + @testset "type stability" begin + @inferred VarName{:a}() + @inferred VarName{:a}(IndexLens(1)) + @inferred VarName{:a}(IndexLens(1, 2)) + @inferred VarName{:a}(PropertyLens(:b)) + @inferred VarName{:a}(Accessors.opcompose(IndexLens(1), PropertyLens(:b))) + + a = [1, 2, 3] + @inferred get(a, @varname(a[1])) + + b = (a=[1, 2, 3],) + @inferred get(b, @varname(b.a[1])) + @inferred Accessors.set(b, @varname(a[1]), 10) + + c = (b=(a=[1, 2, 3],),) + @inferred get(c, @varname(c.b.a[1])) + @inferred Accessors.set(c, @varname(b.a[1]), 10) + end end From cc9c8edb0163aa9e57b5934caf1ff347d9962b2b Mon Sep 17 00:00:00 2001 From: Hong Ge <3279477+yebai@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:25:53 +0100 Subject: [PATCH 03/14] Remove MacroTools (#92) * Update varname.jl * Update Project.toml * use `MacroTools` from `Accessors` * stop using `MacroTools.@capture` * rename test * remove support for composite lens * Apply suggestions from code review Co-authored-by: David Widmann --------- Co-authored-by: Xianda Sun Co-authored-by: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Co-authored-by: David Widmann --- Project.toml | 1 - src/varname.jl | 34 +++++++++------------------------- test/varname.jl | 1 - 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/Project.toml b/Project.toml index becfdbe..d5fe1a2 100644 --- a/Project.toml +++ b/Project.toml @@ -9,7 +9,6 @@ version = "0.8.0" AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001" Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" -MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [compat] diff --git a/src/varname.jl b/src/varname.jl index a3ca1dc..adcd568 100644 --- a/src/varname.jl +++ b/src/varname.jl @@ -1,6 +1,5 @@ using Accessors using Accessors: ComposedOptic, PropertyLens, IndexLens, DynamicIndexLens -using MacroTools const ALLOWED_OPTICS = Union{typeof(identity),PropertyLens,IndexLens,ComposedOptic} @@ -671,30 +670,13 @@ function _parse_obj_optic(ex) obj, optic end -# Accessors doesn't have the same support for interpolation, so copy and modify Setfield's parsing functions -is_interpolation(x) = x isa Expr && x.head == :$ - -function _parse_obj_optics_composite(lensexprs::Vector) - if isempty(lensexprs) - return esc(:_), () - else - obj, outermostlens = _parse_obj_optics(lensexprs[1]) - innerlenses = map(lensexprs[2:end]) do innerex - o, lens = _parse_obj_optics(innerex) - @assert o == esc(:_) - lens - end - return obj, (outermostlens, innerlenses...) - end -end - +# Accessors doesn't have the same support for interpolation +# so this function is copied and altered from `Setfield._parse_obj_lens` function _parse_obj_optics(ex) - if @capture(ex, ∘(opticsexprs__)) - return _parse_obj_optics_composite(opticsexprs) - elseif is_interpolation(ex) - @assert length(ex.args) == 1 + if Meta.isexpr(ex, :$, 1) return esc(:_), (esc(ex.args[1]),) - elseif @capture(ex, front_[indices__]) + elseif Meta.isexpr(ex, :ref) && !isempty(ex.args) + front, indices... = ex.args obj, frontoptics = _parse_obj_optics(front) if any(Accessors.need_dynamic_optic, indices) @gensym collection @@ -706,11 +688,13 @@ function _parse_obj_optics(ex) index = esc(Expr(:tuple, indices...)) optics = :($(Accessors.IndexLens)($index)) end - elseif @capture(ex, front_.property_) + elseif Meta.isexpr(ex, :., 2) + front = ex.args[1] + property = ex.args[2].value # ex.args[2] is a QuoteNode obj, frontoptics = _parse_obj_optics(front) if property isa Union{Symbol,String} optics = :($(Accessors.PropertyLens){$(QuoteNode(property))}()) - elseif is_interpolation(property) + elseif Meta.isexpr(property, :$, 1) optics = :($(Accessors.PropertyLens){$(esc(property.args[1]))}()) else throw(ArgumentError( diff --git a/test/varname.jl b/test/varname.jl index d5cb4f7..b117f0e 100644 --- a/test/varname.jl +++ b/test/varname.jl @@ -89,7 +89,6 @@ end @test collect(get(B, @varname(B[1, :], true))) == collect(get(B, @varname(B[1, -4:5]))) end - @testset "type stability" begin @inferred VarName{:a}() @inferred VarName{:a}(IndexLens(1)) From 9d35b638e2bd98d707d72e3034f2890a07cef615 Mon Sep 17 00:00:00 2001 From: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:30:13 +0100 Subject: [PATCH 04/14] Fix `get` function (#93) * Fix `get` function * fix definition and tests * use `optic` instead of`.optic` * add tests for `set` * fix more test errors * fix doctest * version bump --- Project.toml | 2 +- src/varname.jl | 16 ++-------------- test/varname.jl | 42 ++++++++++++++++++++++++++++++------------ 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/Project.toml b/Project.toml index d5fe1a2..37cac96 100644 --- a/Project.toml +++ b/Project.toml @@ -3,7 +3,7 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" keywords = ["probablistic programming"] license = "MIT" desc = "Common interfaces for probabilistic programming" -version = "0.8.0" +version = "0.8.1" [deps] AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001" diff --git a/src/varname.jl b/src/varname.jl index adcd568..83d58ce 100644 --- a/src/varname.jl +++ b/src/varname.jl @@ -122,23 +122,11 @@ getoptic(vn::VarName) = vn.optic """ get(obj, vn::VarName{sym}) -Alias for `getoptic(vn)(obj)`. - -# Example - -```jldoctest; setup = :(nt = (a = 1, b = (c = [1, 2, 3],)); name = :nt) -julia> get(nt, @varname(nt.a)) -1 - -julia> get(nt, @varname(nt.b.c[1])) -1 - -julia> get(nt, @varname(\$name.b.c[1])) -1 +Alias for `(PropertyLens{sym}() ⨟ getoptic(vn))(obj)`. ``` """ function Base.get(obj, vn::VarName{sym}) where {sym} - return getoptic(vn)(obj) + return (PropertyLens{sym}() ⨟ getoptic(vn))(obj) end """ diff --git a/test/varname.jl b/test/varname.jl index b117f0e..6488c61 100644 --- a/test/varname.jl +++ b/test/varname.jl @@ -14,6 +14,19 @@ macro test_strict_subsumption(x, y) end end +function test_equal(o1::VarName{sym1}, o2::VarName{sym2}) where {sym1, sym2} + return sym1 === sym2 && test_equal(o1.optic, o2.optic) +end +function test_equal(o1::ComposedFunction, o2::ComposedFunction) + return test_equal(o1.inner, o2.inner) && test_equal(o1.outer, o2.outer) +end +function test_equal(o1::Accessors.IndexLens, o2::Accessors.IndexLens) + return test_equal(o1.indices, o2.indices) +end +function test_equal(o1, o2) + return o1 == o2 +end + @testset "varnames" begin @testset "construction & concretization" begin i = 1:10 @@ -27,14 +40,22 @@ end # concretization y = zeros(10, 10) - x = (a = [1.0 2.0; 3.0 4.0; 5.0 6.0], ); + x = (a = [1.0 2.0; 3.0 4.0; 5.0 6.0],); @test @varname(y[begin, i], true) == @varname(y[1, 1:10]) - @test get(y, @varname(y[:], true)) == get(y, @varname(y[1:100])) - @test get(y, @varname(y[:, begin], true)) == get(y, @varname(y[1:10, 1])) - @test getoptic(AbstractPPL.concretize(@varname(y[:]), y)).indices[1] === + @test test_equal(@varname(y[:], true), @varname(y[1:100])) + @test test_equal(@varname(y[:, begin], true), @varname(y[1:10, 1])) + @test getoptic(AbstractPPL.concretize(@varname(y[:]), y)).indices[1] === AbstractPPL.ConcretizedSlice(to_indices(y, (:,))[1]) - @test get(x, @varname(x.a[1:end, end][:], true)) == get(x, @varname(x.a[1:3,2][1:3])) + @test test_equal(@varname(x.a[1:end, end][:], true), @varname(x.a[1:3,2][1:3])) + end + + @testset "get & set" begin + x = (a = [1.0 2.0; 3.0 4.0; 5.0 6.0], b = 1.0); + @test get(x, @varname(a[1, 2])) == 2.0 + @test get(x, @varname(b)) == 1.0 + @test set(x, @varname(a[1, 2]), 10) == (a = [1.0 10.0; 3.0 4.0; 5.0 6.0], b = 1.0) + @test set(x, @varname(b), 10) == (a = [1.0 2.0; 3.0 4.0; 5.0 6.0], b = 10.0) end @testset "subsumption with standard indexing" begin @@ -83,10 +104,10 @@ end @testset "non-standard indexing" begin A = rand(10, 10) - @test get(A, @varname(A[1, Not(3)], true)) == get(A, @varname(A[1, [1, 2, 4, 5, 6, 7, 8, 9, 10]])) + @test test_equal(@varname(A[1, Not(3)], true), @varname(A[1, [1, 2, 4, 5, 6, 7, 8, 9, 10]])) B = OffsetArray(A, -5, -5) # indices -4:5×-4:5 - @test collect(get(B, @varname(B[1, :], true))) == collect(get(B, @varname(B[1, -4:5]))) + @test test_equal(@varname(B[1, :], true), @varname(B[1, -4:5])) end @testset "type stability" begin @@ -96,15 +117,12 @@ end @inferred VarName{:a}(PropertyLens(:b)) @inferred VarName{:a}(Accessors.opcompose(IndexLens(1), PropertyLens(:b))) - a = [1, 2, 3] - @inferred get(a, @varname(a[1])) - b = (a=[1, 2, 3],) - @inferred get(b, @varname(b.a[1])) + @inferred get(b, @varname(a[1])) @inferred Accessors.set(b, @varname(a[1]), 10) c = (b=(a=[1, 2, 3],),) - @inferred get(c, @varname(c.b.a[1])) + @inferred get(c, @varname(b.a[1])) @inferred Accessors.set(c, @varname(b.a[1]), 10) end end From a80a0f2bf042ea76b46f3728c9f334471149e235 Mon Sep 17 00:00:00 2001 From: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:16:39 +0100 Subject: [PATCH 05/14] =?UTF-8?q?Bring=20back=20`=E2=88=98`=20function=20b?= =?UTF-8?q?etween=20varname=20and=20optic=20(#95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add `∘` and `⨟` function between varname and optic * Apply suggestions from code review Co-authored-by: David Widmann --------- Co-authored-by: David Widmann --- src/varname.jl | 10 +++++++--- test/varname.jl | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/varname.jl b/src/varname.jl index 83d58ce..0f470a1 100644 --- a/src/varname.jl +++ b/src/varname.jl @@ -148,6 +148,10 @@ function Accessors.set(obj, vn::VarName{sym}, value) where {sym} return Accessors.set(obj, PropertyLens{sym}() ⨟ getoptic(vn), value) end +# Allow compositions with optic. +function Base.:∘(optic::ALLOWED_OPTICS, vn::VarName{sym,<:ALLOWED_OPTICS}) where {sym} + return VarName{sym}(optic ∘ getoptic(vn)) +end Base.hash(vn::VarName, h::UInt) = hash((getsym(vn), getoptic(vn)), h) function Base.:(==)(x::VarName, y::VarName) @@ -631,8 +635,8 @@ function varname(expr::Expr, concretize=Accessors.need_dynamic_optic(expr)) if concretize return :( $(AbstractPPL.VarName){$sym}( - $(AbstractPPL.concretize)($optics, $sym_escaped) - ) + $(AbstractPPL.concretize)($optics, $sym_escaped) + ) ) elseif Accessors.need_dynamic_optic(expr) error("Variable name `$(expr)` is dynamic and requires concretization!") @@ -687,7 +691,7 @@ function _parse_obj_optics(ex) else throw(ArgumentError( string("Error while parsing :($ex). Second argument to `getproperty` can only be", - "a `Symbol` or `String` literal, received `$property` instead.") + "a `Symbol` or `String` literal, received `$property` instead.") )) end else diff --git a/test/varname.jl b/test/varname.jl index 6488c61..a26e346 100644 --- a/test/varname.jl +++ b/test/varname.jl @@ -5,7 +5,7 @@ using OffsetArrays using AbstractPPL: ⊑, ⊒, ⋢, ⋣, ≍ using AbstractPPL: Accessors -using AbstractPPL.Accessors: IndexLens, PropertyLens +using AbstractPPL.Accessors: IndexLens, PropertyLens, ⨟ macro test_strict_subsumption(x, y) quote @@ -50,6 +50,11 @@ end @test test_equal(@varname(x.a[1:end, end][:], true), @varname(x.a[1:3,2][1:3])) end + @testset "compose and opcompose" begin + @test IndexLens(1) ∘ @varname(x.a) == @varname(x.a[1]) + @test @varname(x.a) ⨟ IndexLens(1) == @varname(x.a[1]) + end + @testset "get & set" begin x = (a = [1.0 2.0; 3.0 4.0; 5.0 6.0], b = 1.0); @test get(x, @varname(a[1, 2])) == 2.0 From 29cdd412a01ddee00975d95851a21c7cc3f4297a Mon Sep 17 00:00:00 2001 From: Hong Ge <3279477+yebai@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:17:48 +0100 Subject: [PATCH 06/14] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 37cac96..f4f73c8 100644 --- a/Project.toml +++ b/Project.toml @@ -3,7 +3,7 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" keywords = ["probablistic programming"] license = "MIT" desc = "Common interfaces for probabilistic programming" -version = "0.8.1" +version = "0.8.2" [deps] AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001" From 6fbeffde547e948bad166798192f6b59805163c6 Mon Sep 17 00:00:00 2001 From: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:57:21 +0100 Subject: [PATCH 07/14] Ignore `identity` optic when composing (#96) --- Project.toml | 2 +- src/varname.jl | 9 ++++++++- test/varname.jl | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index f4f73c8..e929d94 100644 --- a/Project.toml +++ b/Project.toml @@ -3,7 +3,7 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" keywords = ["probablistic programming"] license = "MIT" desc = "Common interfaces for probabilistic programming" -version = "0.8.2" +version = "0.8.3" [deps] AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001" diff --git a/src/varname.jl b/src/varname.jl index 0f470a1..05d20b6 100644 --- a/src/varname.jl +++ b/src/varname.jl @@ -150,7 +150,14 @@ end # Allow compositions with optic. function Base.:∘(optic::ALLOWED_OPTICS, vn::VarName{sym,<:ALLOWED_OPTICS}) where {sym} - return VarName{sym}(optic ∘ getoptic(vn)) + vn_optic = getoptic(vn) + if vn_optic == identity + return VarName{sym}(optic) + elseif optic == identity + return vn + else + return VarName{sym}(optic ∘ vn_optic) + end end Base.hash(vn::VarName, h::UInt) = hash((getsym(vn), getoptic(vn)), h) diff --git a/test/varname.jl b/test/varname.jl index a26e346..d0e3de7 100644 --- a/test/varname.jl +++ b/test/varname.jl @@ -53,6 +53,13 @@ end @testset "compose and opcompose" begin @test IndexLens(1) ∘ @varname(x.a) == @varname(x.a[1]) @test @varname(x.a) ⨟ IndexLens(1) == @varname(x.a[1]) + + @test @varname(x) ⨟ identity == @varname(x) + @test identity ∘ @varname(x) == @varname(x) + @test @varname(x.a) ⨟ identity == @varname(x.a) + @test identity ∘ @varname(x.a) == @varname(x.a) + @test @varname(x[1].b) ⨟ identity == @varname(x[1].b) + @test identity ∘ @varname(x[1].b) == @varname(x[1].b) end @testset "get & set" begin From b9cd48b24a7c159799a76dc08584c16c8e0b470c Mon Sep 17 00:00:00 2001 From: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:04:02 +0100 Subject: [PATCH 08/14] fix optic parsing function (#97) --- Project.toml | 2 +- src/varname.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index e929d94..b32ad51 100644 --- a/Project.toml +++ b/Project.toml @@ -3,7 +3,7 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" keywords = ["probablistic programming"] license = "MIT" desc = "Common interfaces for probabilistic programming" -version = "0.8.3" +version = "0.8.4" [deps] AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001" diff --git a/src/varname.jl b/src/varname.jl index 05d20b6..1ffa542 100644 --- a/src/varname.jl +++ b/src/varname.jl @@ -665,7 +665,7 @@ end function _parse_obj_optic(ex) obj, optics = _parse_obj_optics(ex) - optic = Expr(:call, :(Accessors.opticcompose), optics...) + optic = Expr(:call, Accessors.opticcompose, optics...) obj, optic end From a77e2473736a3f357d4e5412d95bf333a15ec07f Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Wed, 18 Sep 2024 13:57:30 +0100 Subject: [PATCH 09/14] Build and publish API docs (#101) * Add docs * Add CI to build docs * Fix wrong heading --- .github/workflows/Docs.yml | 38 +++++++++++++++++++++++++++ .github/workflows/DocsNav.yml | 49 +++++++++++++++++++++++++++++++++++ .github/workflows/TagBot.yml | 1 + .gitignore | 4 ++- docs/Project.toml | 2 ++ docs/make.jl | 18 +++++++++++++ docs/src/api.md | 32 +++++++++++++++++++++++ docs/src/index.md | 3 +++ src/varname.jl | 13 +++++++--- 9 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/Docs.yml create mode 100644 .github/workflows/DocsNav.yml create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/api.md create mode 100644 docs/src/index.md diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml new file mode 100644 index 0000000..73ee754 --- /dev/null +++ b/.github/workflows/Docs.yml @@ -0,0 +1,38 @@ +name: Documentation + +on: + push: + branches: + - main + tags: '*' + pull_request: + branches: + - main + +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +permissions: + contents: write + pull-requests: read + statuses: write + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + version: '1' + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key + JULIA_DEBUG: Documenter # Print `@debug` statements (https://github.com/JuliaDocs/Documenter.jl/issues/955) + run: julia --project=docs/ docs/make.jl diff --git a/.github/workflows/DocsNav.yml b/.github/workflows/DocsNav.yml new file mode 100644 index 0000000..14614d1 --- /dev/null +++ b/.github/workflows/DocsNav.yml @@ -0,0 +1,49 @@ +name: Add Navbar + +on: + page_build: # Triggers the workflow on push events to gh-pages branch + workflow_dispatch: # Allows manual triggering + schedule: + - cron: '0 0 * * 0' # Runs every week on Sunday at midnight (UTC) + +jobs: + add-navbar: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout gh-pages + uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Download insert_navbar.sh + run: | + curl -O https://raw.githubusercontent.com/TuringLang/turinglang.github.io/main/assets/scripts/insert_navbar.sh + chmod +x insert_navbar.sh + + - name: Update Navbar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name github-actions[bot] + git config user.email github-actions[bot]@users.noreply.github.com + + # Define the URL of the navbar to be used + NAVBAR_URL="https://raw.githubusercontent.com/TuringLang/turinglang.github.io/main/assets/scripts/TuringNavbar.html" + + # Update all HTML files in the current directory (gh-pages root) + ./insert_navbar.sh . $NAVBAR_URL + + # Remove the insert_navbar.sh file + rm insert_navbar.sh + + # Check if there are any changes + if [[ -n $(git status -s) ]]; then + git add . + git commit -m "Added navbar and removed insert_navbar.sh" + git push "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" gh-pages + else + echo "No changes to commit" + fi diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index 778c06f..f49313b 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -12,3 +12,4 @@ jobs: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.gitignore b/.gitignore index d93db51..4389dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ docs/site/ Manifest.toml # vs code environment -.vscode \ No newline at end of file +.vscode + +.DS_Store diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..dfa65cd --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,2 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..404169e --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,18 @@ +using Documenter +using AbstractPPL + +# Doctest setup +DocMeta.setdocmeta!(AbstractPPL, :DocTestSetup, :(using AbstractPPL); recursive=true) + +makedocs(; + sitename="AbstractPPL", + modules=[AbstractPPL], + pages=[ + "Home" => "index.md", + "API" => "api.md", + ], + checkdocs=:exports, + doctest=false, +) + +deploydocs(; repo="github.com/TuringLang/AbstractPPL.jl.git", push_preview=true) diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..a8d4321 --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,32 @@ +# API + +## VarNames + +```@docs +VarName +getsym +getoptic +inspace +subsumes +subsumedby +vsym +@varname +@vsym +``` + +## Abstract model functions + +```@docs +AbstractProbabilisticProgram +condition +decondition +logdensityof +AbstractContext +evaluate!! +``` + +## Abstract traces + +```@docs +AbstractModelTrace +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..03c3112 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,3 @@ +# AbstractPPL.jl + +A lightweight package containing interfaces and associated APIs for modelling languages for probabilistic programming. diff --git a/src/varname.jl b/src/varname.jl index 1ffa542..da1bf02 100644 --- a/src/varname.jl +++ b/src/varname.jl @@ -211,7 +211,7 @@ Base.Symbol(vn::VarName) = Symbol(string(vn)) # simplified symbol inspace(vn::Union{VarName, Symbol}, space::Tuple) Check whether `vn`'s variable symbol is in `space`. The empty tuple counts as the "universal space" -containing all variables. Subsumption (see [`subsume`](@ref)) is respected. +containing all variables. Subsumption (see [`subsumes`](@ref)) is respected. ## Examples @@ -322,6 +322,11 @@ subsumes( ) = subsumes_indices(t, u) +""" + subsumedby(t, u) + +True if `t` is subsumed by `u`, i.e., if `subsumes(u, t)` is true. +""" subsumedby(t, u) = subsumes(u, t) uncomparable(t, u) = t ⋢ u && u ⋢ t const ⊒ = subsumes @@ -513,7 +518,7 @@ julia> y = zeros(10, 10); julia> @varname(y[:], true) y[:] -julia> # The underlying value is conretized, though: +julia> # The underlying value is concretized, though: AbstractPPL.getoptic(AbstractPPL.concretize(@varname(y[:]), y)).indices[1] ConcretizedSlice(Base.OneTo(100)) ``` @@ -525,11 +530,11 @@ concretize(vn::VarName, x) = VarName(vn, concretize(getoptic(vn), x)) A macro that returns an instance of [`VarName`](@ref) given a symbol or indexing expression `expr`. -If `concretize` is `true`, the resulting expression will be wrapped in a [`concretize`](@ref) call. +If `concretize` is `true`, the resulting expression will be wrapped in a `concretize()` call. Note that expressions involving dynamic indexing, i.e. `begin` and/or `end`, will always need to be concretized as `VarName` only supports non-dynamic indexing as determined by -[`is_static_index`](@ref). See examples below. +`is_static_optic`. See examples below. ## Examples From 68ad70702a13ff9356fce9401a50ff0ad774dd0b Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Tue, 1 Oct 2024 12:52:18 +0100 Subject: [PATCH 10/14] Conversion of VarName to/from string (#100) * Fix typo in comment * Conversion of VarName to/from string * Add one more test * More thorough serialisation * Add doctests * Add API docs * Add warning to docstring Co-authored-by: Tor Erlend Fjelde * Add alternate implementation with StructTypes * Reduce calls to Meta.parse() It's only called for ConcretizedSlice now, which could potentially be removed too. * Restrict allowed ranges for ConcretizedSlice * Fix name of wrapper type * More tests * Remove unneeded extra method for ConcretizedSlice * Add StepRange support * Support arrays of integers as indices * Simplify implementation even more * Bump to 0.9.0 * Clean up old code, add docs * Allow de/serialisation methods to be extended * Update docs * Name functions more consistently --------- Co-authored-by: Tor Erlend Fjelde --- Project.toml | 3 +- docs/src/api.md | 9 +++ src/AbstractPPL.jl | 6 +- src/varname.jl | 147 ++++++++++++++++++++++++++++++++++++++++++++- test/varname.jl | 63 +++++++++++++++++++ 5 files changed, 225 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index b32ad51..beb0c59 100644 --- a/Project.toml +++ b/Project.toml @@ -3,12 +3,13 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" keywords = ["probablistic programming"] license = "MIT" desc = "Common interfaces for probabilistic programming" -version = "0.8.4" +version = "0.9.0" [deps] AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001" Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [compat] diff --git a/docs/src/api.md b/docs/src/api.md index a8d4321..0e7f3f8 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -14,6 +14,15 @@ vsym @vsym ``` +## VarName serialisation + +```@docs +index_to_dict +dict_to_index +varname_to_string +string_to_varname +``` + ## Abstract model functions ```@docs diff --git a/src/AbstractPPL.jl b/src/AbstractPPL.jl index 775576a..33fa417 100644 --- a/src/AbstractPPL.jl +++ b/src/AbstractPPL.jl @@ -10,7 +10,11 @@ export VarName, varname, vsym, @varname, - @vsym + @vsym, + index_to_dict, + dict_to_index, + varname_to_string, + string_to_varname # Abstract model functions diff --git a/src/varname.jl b/src/varname.jl index da1bf02..48d7c5c 100644 --- a/src/varname.jl +++ b/src/varname.jl @@ -1,5 +1,6 @@ using Accessors using Accessors: ComposedOptic, PropertyLens, IndexLens, DynamicIndexLens +using JSON: JSON const ALLOWED_OPTICS = Union{typeof(identity),PropertyLens,IndexLens,ComposedOptic} @@ -302,7 +303,7 @@ subsumes(t::ComposedOptic, u::ComposedOptic) = # If `t` is still a composed lens, then there is no way it can subsume `u` since `u` is a # leaf of the "lens-tree". subsumes(t::ComposedOptic, u::PropertyLens) = false -# Here we need to check if `u.outer` (i.e. the next lens to be applied from `u`) is +# Here we need to check if `u.inner` (i.e. the next lens to be applied from `u`) is # subsumed by `t`, since this would mean that the rest of the composition is also subsumed # by `t`. subsumes(t::PropertyLens, u::ComposedOptic) = subsumes(t, u.inner) @@ -752,3 +753,147 @@ function vsym(expr::Expr) error("Malformed variable name `$(expr)`!") end end + +# String constants for each index type that we support serialisation / +# deserialisation of +const _BASE_INTEGER_TYPE = "Base.Integer" +const _BASE_VECTOR_TYPE = "Base.Vector" +const _BASE_UNITRANGE_TYPE = "Base.UnitRange" +const _BASE_STEPRANGE_TYPE = "Base.StepRange" +const _BASE_ONETO_TYPE = "Base.OneTo" +const _BASE_COLON_TYPE = "Base.Colon" +const _CONCRETIZED_SLICE_TYPE = "AbstractPPL.ConcretizedSlice" +const _BASE_TUPLE_TYPE = "Base.Tuple" + +""" + index_to_dict(::Integer) + index_to_dict(::AbstractVector{Int}) + index_to_dict(::UnitRange) + index_to_dict(::StepRange) + index_to_dict(::Colon) + index_to_dict(::ConcretizedSlice{T, Base.OneTo{I}}) where {T, I} + index_to_dict(::Tuple) + +Convert an index `i` to a dictionary representation. +""" +index_to_dict(i::Integer) = Dict("type" => _BASE_INTEGER_TYPE, "value" => i) +index_to_dict(v::Vector{Int}) = Dict("type" => _BASE_VECTOR_TYPE, "values" => v) +index_to_dict(r::UnitRange) = Dict("type" => _BASE_UNITRANGE_TYPE, "start" => r.start, "stop" => r.stop) +index_to_dict(r::StepRange) = Dict("type" => _BASE_STEPRANGE_TYPE, "start" => r.start, "stop" => r.stop, "step" => r.step) +index_to_dict(r::Base.OneTo{I}) where {I} = Dict("type" => _BASE_ONETO_TYPE, "stop" => r.stop) +index_to_dict(::Colon) = Dict("type" => _BASE_COLON_TYPE) +index_to_dict(s::ConcretizedSlice{T,R}) where {T,R} = Dict("type" => _CONCRETIZED_SLICE_TYPE, "range" => index_to_dict(s.range)) +index_to_dict(t::Tuple) = Dict("type" => _BASE_TUPLE_TYPE, "values" => map(index_to_dict, t)) + +""" + dict_to_index(dict) + dict_to_index(symbol_val, dict) + +Convert a dictionary representation of an index `dict` to an index. + +Users can extend the functionality of `dict_to_index` (and hence `VarName` +de/serialisation) by extending this method along with [`index_to_dict`](@ref). +Specifically, suppose you have a custom index type `MyIndexType` and you want +to be able to de/serialise a `VarName` containing this index type. You should +then implement the following two methods: + +1. `AbstractPPL.index_to_dict(i::MyModule.MyIndexType)` should return a + dictionary representation of the index `i`. This dictionary must contain the + key `"type"`, and the corresponding value must be a string that uniquely + identifies the index type. Generally, it makes sense to use the name of the + type (perhaps prefixed with module qualifiers) as this value to avoid + clashes. The remainder of the dictionary can have any structure you like. + +2. Suppose the value of `index_to_dict(i)["type"]` is `"MyModule.MyIndexType"`. + You should then implement the corresponding method + `AbstractPPL.dict_to_index(::Val{Symbol("MyModule.MyIndexType")}, dict)`, + which should take the dictionary representation as the second argument and + return the original `MyIndexType` object. + +To see an example of this in action, you can look in the the AbstractPPL test +suite, which contains a test for serialising OffsetArrays. +""" +function dict_to_index(dict) + t = dict["type"] + if t == _BASE_INTEGER_TYPE + return dict["value"] + elseif t == _BASE_VECTOR_TYPE + return collect(Int, dict["values"]) + elseif t == _BASE_UNITRANGE_TYPE + return dict["start"]:dict["stop"] + elseif t == _BASE_STEPRANGE_TYPE + return dict["start"]:dict["step"]:dict["stop"] + elseif t == _BASE_ONETO_TYPE + return Base.OneTo(dict["stop"]) + elseif t == _BASE_COLON_TYPE + return Colon() + elseif t == _CONCRETIZED_SLICE_TYPE + return ConcretizedSlice(Base.Slice(dict_to_index(dict["range"]))) + elseif t == _BASE_TUPLE_TYPE + return tuple(map(dict_to_index, dict["values"])...) + else + # Will error if the method is not defined, but this hook allows users + # to extend this function + return dict_to_index(Val(Symbol(t)), dict) + end +end + +optic_to_dict(::typeof(identity)) = Dict("type" => "identity") +optic_to_dict(::PropertyLens{sym}) where {sym} = Dict("type" => "property", "field" => String(sym)) +optic_to_dict(i::IndexLens) = Dict("type" => "index", "indices" => index_to_dict(i.indices)) +optic_to_dict(c::ComposedOptic) = Dict("type" => "composed", "outer" => optic_to_dict(c.outer), "inner" => optic_to_dict(c.inner)) + +function dict_to_optic(dict) + if dict["type"] == "identity" + return identity + elseif dict["type"] == "index" + return IndexLens(dict_to_index(dict["indices"])) + elseif dict["type"] == "property" + return PropertyLens{Symbol(dict["field"])}() + elseif dict["type"] == "composed" + return dict_to_optic(dict["outer"]) ∘ dict_to_optic(dict["inner"]) + else + error("Unknown optic type: $(dict["type"])") + end +end + +varname_to_dict(vn::VarName) = Dict("sym" => getsym(vn), "optic" => optic_to_dict(getoptic(vn))) + +dict_to_varname(dict::Dict{<:AbstractString, Any}) = VarName{Symbol(dict["sym"])}(dict_to_optic(dict["optic"])) + +""" + varname_to_string(vn::VarName) + +Convert a `VarName` as a string, via an intermediate dictionary. This differs +from `string(vn)` in that concretised slices are faithfully represented (rather +than being pretty-printed as colons). + +For `VarName`s which index into an array, this function will only work if the +indices can be serialised. This is true for all standard Julia index types, but +if you are using custom index types, you will need to implement the +`index_to_dict` and `dict_to_index` methods for those types. See the +documentation of [`dict_to_index`](@ref) for instructions on how to do this. + +```jldoctest +julia> varname_to_string(@varname(x)) +"{\\"optic\\":{\\"type\\":\\"identity\\"},\\"sym\\":\\"x\\"}" + +julia> varname_to_string(@varname(x.a)) +"{\\"optic\\":{\\"field\\":\\"a\\",\\"type\\":\\"property\\"},\\"sym\\":\\"x\\"}" + +julia> y = ones(2); varname_to_string(@varname(y[:])) +"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"type\\":\\"Base.Colon\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}" + +julia> y = ones(2); varname_to_string(@varname(y[:], true)) +"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"range\\":{\\"stop\\":2,\\"type\\":\\"Base.OneTo\\"},\\"type\\":\\"AbstractPPL.ConcretizedSlice\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}" +``` +""" +varname_to_string(vn::VarName) = JSON.json(varname_to_dict(vn)) + +""" + string_to_varname(str::AbstractString) + +Convert a string representation of a `VarName` back to a `VarName`. The string +should have been generated by `varname_to_string`. +""" +string_to_varname(str::AbstractString) = dict_to_varname(JSON.parse(str)) diff --git a/test/varname.jl b/test/varname.jl index d0e3de7..7a92d1e 100644 --- a/test/varname.jl +++ b/test/varname.jl @@ -137,4 +137,67 @@ end @inferred get(c, @varname(b.a[1])) @inferred Accessors.set(c, @varname(b.a[1]), 10) end + + @testset "de/serialisation of VarNames" begin + y = ones(10) + z = ones(5, 2) + vns = [ + @varname(x), + @varname(ä), + @varname(x.a), + @varname(x.a.b), + @varname(var"x.a"), + @varname(x[1]), + @varname(var"x[1]"), + @varname(x[1:10]), + @varname(x[1:3:10]), + @varname(x[1, 2]), + @varname(x[1, 2:5]), + @varname(x[:]), + @varname(x.a[1]), + @varname(x.a[1:10]), + @varname(x[1].a), + @varname(y[:]), + @varname(y[begin:end]), + @varname(y[end]), + @varname(y[:], false), + @varname(y[:], true), + @varname(z[:], false), + @varname(z[:], true), + @varname(z[:][:], false), + @varname(z[:][:], true), + @varname(z[:,:], false), + @varname(z[:,:], true), + @varname(z[2:5,:], false), + @varname(z[2:5,:], true), + ] + for vn in vns + @test string_to_varname(varname_to_string(vn)) == vn + end + + # For this VarName, the {de,}serialisation works correctly but we must + # test in a different way because equality comparison of structs with + # vector fields (such as Accessors.IndexLens) compares the memory + # addresses rather than the contents (thus vn_vec == vn_vec2 returns + # false). + vn_vec = @varname(x[[1, 2, 5, 6]]) + vn_vec2 = string_to_varname(varname_to_string(vn_vec)) + @test hash(vn_vec) == hash(vn_vec2) + end + + @testset "de/serialisation of VarNames with custom index types" begin + using OffsetArrays: OffsetArrays, Origin + weird = Origin(4)(ones(10)) + vn = @varname(weird[:], true) + + # This won't work as we don't yet know how to handle OffsetArray + @test_throws MethodError varname_to_string(vn) + + # Now define the relevant methods + AbstractPPL.index_to_dict(o::OffsetArrays.IdOffsetRange{I, R}) where {I,R} = Dict("type" => "OffsetArrays.OffsetArray", "parent" => AbstractPPL.index_to_dict(o.parent), "offset" => o.offset) + AbstractPPL.dict_to_index(::Val{Symbol("OffsetArrays.OffsetArray")}, d) = OffsetArrays.IdOffsetRange(AbstractPPL.dict_to_index(d["parent"]), d["offset"]) + + # Serialisation should now work + @test string_to_varname(varname_to_string(vn)) == vn + end end From 1cbcd1c39547bb17732b33d2b2f7aef1edc818af Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Tue, 1 Oct 2024 13:04:21 +0100 Subject: [PATCH 11/14] Add compat bounds for JSON (#102) --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index beb0c59..2a74776 100644 --- a/Project.toml +++ b/Project.toml @@ -16,5 +16,6 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" AbstractMCMC = "2, 3, 4, 5" Accessors = "0.1" DensityInterface = "0.4" +JSON = "0.19 - 0.21" Random = "1.6" julia = "~1.6.6, 1.7.3" From 7af5cf5442de826c2d4a878a8942a78f531880f8 Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Tue, 8 Oct 2024 14:42:32 +0100 Subject: [PATCH 12/14] Update CompatHelper.yml (#105) --- .github/workflows/CompatHelper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 0cb6d66..a757962 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -12,5 +12,5 @@ jobs: - name: CompatHelper.main() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} # for triggering CI + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} run: julia -e 'using CompatHelper; CompatHelper.main(; subdirs=["", "test"])' From a94d1674b1ba8fa3ab1fed57f3168d67e811c19d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:01:45 +0100 Subject: [PATCH 13/14] CompatHelper: bump compat for Documenter to 1 for package test, (keep existing compat) (#106) Co-authored-by: CompatHelper Julia --- test/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Project.toml b/test/Project.toml index adeb617..5a41251 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -7,7 +7,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] -Documenter = "0.26.3, 0.27" +Documenter = "0.26.3, 0.27, 1" InvertedIndices = "1" OffsetArrays = "1" julia = "1" From 82e1e4af2d47a4a20162f6731ac3edd912b7a5a1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:28:00 +0100 Subject: [PATCH 14/14] CompatHelper: add new compat entry for Accessors at version 0.1 for package test, (keep existing compat) (#107) Co-authored-by: CompatHelper Julia Co-authored-by: Penelope Yong --- test/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Project.toml b/test/Project.toml index 5a41251..dbc641b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -7,6 +7,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] +Accessors = "0.1" Documenter = "0.26.3, 0.27, 1" InvertedIndices = "1" OffsetArrays = "1"