Skip to content

Commit 14beaa7

Browse files
torfjeldeyebai
andcommitted
Introduce traits for contexts (#286)
This is motivated by the potential introduction of more contexts, e.g. #278, and has been brought up as an alternative (and better) approach to achieve parts of what we want to achieve in #254 . (I hope you're proud of me @devmotion ) ## Current state of things Currently, if one wants to implement a new `AbstractContext` one _at least_ has to implement the following methods: ```julia tilde_assume(...) tilde_observe(...) dot_tilde_assume(...) dot_tilde_observe(...) ``` But there are also other methods that _should_ be implemented but generally aren't properly handled, e.g. `matchingvalue`. And there might be more methods in the future, e.g. `contextual_isassumption` in #254. ## This sucks This means that: 1. Implementing a new behavior for `AbstractContext`, e.g. `contextual_isassumption`, requires you to: 1. Find all implementations of `AbstractContext`, which is non-trivial! Most are here in DPPL, but some are in Turing.jl, and eventually we also want packages outside of the Turing.jl-umbrella to extend DPPL using contexts. 2. Implement the method for that particular context. 2. Implementing a new `AbstractContext`, e.g. `Turing.OptimizationContext`, requires you to find all the methods to implement and then do so. Again, non-trivial. This combinatorial blow up essentially means that we're super-reluctant to introduce new behaviors or new contexts, for good reasons. And the stupid thing is that in most cases a context is only trying to modify maybe one or two "behaviors", e.g. `MiniBatchContext` only wants to change the `*tilde_observe` methods, and otherwise just defer to whatever implementation is available for its "childcontext" `minibatchcontext.context`. ## Goal A new `AbstractContext` should only (up to an additive factor) have to implement the behavior it _wants to change_, not _all_ behaviors. E.g. `MiniBatchContext` should really only have to overload `*tilde_observe`. ## Solution (this PR) The above was the motivation for #254, but there we wanted a rather strict separation between certain types of contexts which we're reluctant to add due to its restrictive nature (in particular given how "recently" contexts where introduced). This PR takes are more extensible and less restrictive approach of introducing some traits for `AbstractContext`. As a starter I've just introduced a couple of traits to allow code-sharing between "parent contexts", e.g. `MiniBatchContext` and `ConditionContext` (from #278), and "leaf-contexts", e.g. `DefaultContext` and `PriorContext` (which IMO should have been a wrapper-context itself). Ideally we'd also define a promotion-system, e.g. what do we do if we're asked to combine a `MiniBatchContext` and `DefaultContext`? Well in that case we could either 1. replace `minibatchcontext.context` with `DefaultContext()`, or 2. recursively "rewrap" `DefaultContext()` in `minibatch.context`. (1) has the issue that `MiniBatchContext` might be wrapping another context, e.g. `PrefixContext`, and so just replacing `.context` is dangerous. (2) seems like a better idea since `DefaultContext` should always be at the end of the stack, i.e. a "leaf", since it always exists the tilde-callstack (e.g. in `tilde_assume` we call `assume`, etc.). Such a promotion system will require some thought though, but this PR will allow us to experiment with this (on top of providing a good approach to code-sharing). ## Examples ### `GeneratedQuantitiesContext` In DPPL we have the `generated_quantities` but it sort of sucks because often these quantities are not relevant for sampling, thus we're adding unnecessary computation to the sampling process. One might want want to introduce a `@generated_quantities` macro that will only be executed when called from `generated_quantities`. This can easily be achieved with contexts: ```julia using DynamicPPL, Distributions, Random using DynamicPPL: AbstractContext, IsLeaf, IsParent, childcontext struct GeneratedQuantitiesContext{Ctx} <: AbstractContext context::Ctx end GeneratedQuantitiesContext() = GeneratedQuantitiesContext(DefaultContext()) # Define the `NodeTrait` for `GeneratedQuantitiesContext`. DynamicPPL.NodeTrait(context::GeneratedQuantitiesContext) = IsParent() DynamicPPL.childcontext(context::GeneratedQuantitiesContext) = context.context """ isgeneratedquantities(context) Return `true` if `context` wants evaluation of model to execute the `@generatedquantities` block. """ function isgeneratedquantities(context::AbstractContext) return isgeneratedquantities(DynamicPPL.NodeTrait(isgeneratedquantities, context), context) end # Define the behavior for the different `NodeType`s. isgeneratedquantities(::IsLeaf, context::AbstractContext) = false function isgeneratedquantities(::IsParent, context::AbstractContext) return isgeneratedquantities(childcontext(context)) end # Specific implementations of `isgeneratedquantities`. isgeneratedquantities(context::GeneratedQuantitiesContext) = true """ @generatedquantities f(x) Specify that `f(x)` should only if the model is run using `GeneratedQuantitiesContext`. """ macro generated_quantities(expr) return esc(generated_quantities_expr(expr)) end function generated_quantities_expr(expr) return quote if isgeneratedquantities(__context__) $expr end end end ``` And usage would be as follows: ```julia julia> @model function demo(x) @generated_quantities a = [] m ~ Normal() # "Expensive" piece of code that we don't want to compute # unless we're computing the generated quantities. @generated_quantities for i = 1:100 push!(a, m + randn()) end # Observe. x ~ Normal(m, 1.0) # Return additional fields if we're computing the generated quantities. @generated_quantities return (; x, m, logp = getlogp(__varinfo__), a) return nothing end demo (generic function with 1 method) julia> m = demo(1.0); var_info = VarInfo(m); julia> m(var_info) # (✓) returns nothing julia> m(var_info, GeneratedQuantitiesContext()) # (✓) returns everything (x = 1.0, m = 0.7451107426181028, logp = -2.147956342556143, a = Any[0.929285513523637, 0.5979631005709274, 3.1944251790696794, -0.611727924858586, 1.0561547812845788, 0.9694358994096283, 0.9130715096769692, 0.018196803783751103, 0.919216919507385, 0.5382931170515716 … 0.24049636440948963, 0.7868300598622132, 0.18210113151764207, 1.9568444848346909, 0.4512687443970105, 0.5155449377942058, 0.32900420588898294, 1.4274186203915957, 0.5599167955770905, -0.5351355146677438]) julia> m(Random.GLOBAL_RNG, GeneratedQuantitiesContext()) # (✓) just works even when wrapped in `SamplingContext` (x = 1.0, m = -0.7568553457880578, logp = -3.6675624266453637, a = Any[-0.4715652454870858, 2.0553700887015776, -0.6055293756513751, -1.6153963580018202, -0.6316036328835652, -0.9694585645577235, -1.234406947321252, 0.47075652867281415, -2.0403875768252187, -1.332736944079995 … -1.123179524380186, -0.4445013585772502, -1.2366853403014721, -0.2726171409415225, 0.050910231154342234, -1.937702826603367, -2.109933658872988, -0.8300603173278494, 0.076480161355589, -1.2666452596129179]) ``` This would also _just work_ for submodels, etc. This "conditional execution" could of course be generalized too. Such a conditional execution is very useful when you also want to work with Zygote, e.g. you don't want mutations in the model but for the post-processing steps, e.g. `predict` and `generated_quantities`, you do need it. But whether or not we want this particular `@generatedquantities` macro in DPPL is not the point; the point is that we _can_ implement such a thing. Even more importantly, I can easily implement this from the "outside", no needing to touch DPPL to do it. ### `ConditionContext` See #278. Co-authored-by: Hong Ge <[email protected]>
1 parent 99df54b commit 14beaa7

File tree

6 files changed

+314
-65
lines changed

6 files changed

+314
-65
lines changed

src/compiler.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,8 +501,14 @@ function matchingvalue(sampler::AbstractSampler, vi, value::FloatOrArrayType)
501501
end
502502

503503
function matchingvalue(context::AbstractContext, vi, value)
504+
return matchingvalue(NodeTrait(matchingvalue, context), context, vi, value)
505+
end
506+
function matchingvalue(::IsLeaf, context::AbstractContext, vi, value)
504507
return matchingvalue(SampleFromPrior(), vi, value)
505508
end
509+
function matchingvalue(::IsParent, context::AbstractContext, vi, value)
510+
return matchingvalue(childcontext(context), vi, value)
511+
end
506512
function matchingvalue(context::SamplingContext, vi, value)
507513
return matchingvalue(context.sampler, vi, value)
508514
end

src/context_implementations.jl

Lines changed: 51 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,27 @@ function tilde_assume(context::SamplingContext, right, vn, inds, vi)
3535
end
3636

3737
# Leaf contexts
38-
tilde_assume(::DefaultContext, right, vn, inds, vi) = assume(right, vn, vi)
38+
function tilde_assume(context::AbstractContext, args...)
39+
return tilde_assume(NodeTrait(tilde_assume, context), context, args...)
40+
end
41+
function tilde_assume(::IsLeaf, context::AbstractContext, right, vn, vinds, vi)
42+
return assume(right, vn, vi)
43+
end
44+
function tilde_assume(::IsParent, context::AbstractContext, args...)
45+
return tilde_assume(childcontext(context), args...)
46+
end
47+
48+
function tilde_assume(rng, context::AbstractContext, args...)
49+
return tilde_assume(NodeTrait(tilde_assume, context), rng, context, args...)
50+
end
3951
function tilde_assume(
40-
rng::Random.AbstractRNG, ::DefaultContext, sampler, right, vn, inds, vi
52+
::IsLeaf, rng, context::AbstractContext, sampler, right, vn, vinds, vi
4153
)
4254
return assume(rng, sampler, right, vn, vi)
4355
end
56+
function tilde_assume(::IsParent, rng, context::AbstractContext, args...)
57+
return tilde_assume(rng, childcontext(context), args...)
58+
end
4459

4560
function tilde_assume(context::PriorContext{<:NamedTuple}, right, vn, inds, vi)
4661
if haskey(context.vars, getsym(vn))
@@ -64,12 +79,6 @@ function tilde_assume(
6479
end
6580
return tilde_assume(rng, PriorContext(), sampler, right, vn, inds, vi)
6681
end
67-
function tilde_assume(::PriorContext, right, vn, inds, vi)
68-
return assume(right, vn, vi)
69-
end
70-
function tilde_assume(rng::Random.AbstractRNG, ::PriorContext, sampler, right, vn, inds, vi)
71-
return assume(rng, sampler, right, vn, vi)
72-
end
7382

7483
function tilde_assume(context::LikelihoodContext{<:NamedTuple}, right, vn, inds, vi)
7584
if haskey(context.vars, getsym(vn))
@@ -102,18 +111,9 @@ function tilde_assume(
102111
return assume(rng, sampler, NoDist(right), vn, vi)
103112
end
104113

105-
function tilde_assume(context::MiniBatchContext, right, vn, inds, vi)
106-
return tilde_assume(context.context, right, vn, inds, vi)
107-
end
108-
109-
function tilde_assume(rng, context::MiniBatchContext, sampler, right, vn, inds, vi)
110-
return tilde_assume(rng, context.context, sampler, right, vn, inds, vi)
111-
end
112-
113114
function tilde_assume(context::PrefixContext, right, vn, inds, vi)
114115
return tilde_assume(context.context, right, prefix(context, vn), inds, vi)
115116
end
116-
117117
function tilde_assume(rng, context::PrefixContext, sampler, right, vn, inds, vi)
118118
return tilde_assume(rng, context.context, sampler, right, prefix(context, vn), inds, vi)
119119
end
@@ -162,16 +162,16 @@ function tilde_observe(context::SamplingContext, right, left, vi)
162162
end
163163

164164
# Leaf contexts
165-
tilde_observe(::DefaultContext, right, left, vi) = observe(right, left, vi)
166-
function tilde_observe(::DefaultContext, sampler, right, left, vi)
167-
return observe(sampler, right, left, vi)
165+
function tilde_observe(context::AbstractContext, args...)
166+
return tilde_observe(NodeTrait(tilde_observe, context), context, args...)
167+
end
168+
tilde_observe(::IsLeaf, context::AbstractContext, args...) = observe(args...)
169+
function tilde_observe(::IsParent, context::AbstractContext, args...)
170+
return tilde_observe(childcontext(context), args...)
168171
end
172+
169173
tilde_observe(::PriorContext, right, left, vi) = 0
170174
tilde_observe(::PriorContext, sampler, right, left, vi) = 0
171-
tilde_observe(::LikelihoodContext, right, left, vi) = observe(right, left, vi)
172-
function tilde_observe(::LikelihoodContext, sampler, right, left, vi)
173-
return observe(sampler, right, left, vi)
174-
end
175175

176176
# `MiniBatchContext`
177177
function tilde_observe(context::MiniBatchContext, right, left, vi)
@@ -185,9 +185,6 @@ end
185185
function tilde_observe(context::PrefixContext, right, left, vname, vi)
186186
return tilde_observe(context.context, right, left, prefix(context, vname), vi)
187187
end
188-
function tilde_observe(context::PrefixContext, right, left, vi)
189-
return tilde_observe(context.context, right, left, vi)
190-
end
191188

192189
"""
193190
tilde_observe!(context, right, left, vname, vinds, vi)
@@ -288,9 +285,28 @@ function dot_tilde_assume(context::SamplingContext, right, left, vn, inds, vi)
288285
end
289286

290287
# `DefaultContext`
291-
function dot_tilde_assume(::DefaultContext, right, left, vns, inds, vi)
288+
function dot_tilde_assume(context::AbstractContext, args...)
289+
return dot_tilde_assume(NodeTrait(dot_tilde_assume, context), context, args...)
290+
end
291+
function dot_tilde_assume(rng, context::AbstractContext, args...)
292+
return dot_tilde_assume(rng, NodeTrait(dot_tilde_assume, context), context, args...)
293+
end
294+
295+
function dot_tilde_assume(::IsLeaf, ::AbstractContext, right, left, vns, inds, vi)
292296
return dot_assume(right, left, vns, vi)
293297
end
298+
function dot_tilde_assume(
299+
::IsLeaf, rng, ::AbstractContext, sampler, right, left, vns, inds, vi
300+
)
301+
return dot_assume(rng, sampler, right, vns, left, vi)
302+
end
303+
304+
function dot_tilde_assume(::IsParent, context::AbstractContext, args...)
305+
return dot_tilde_assume(childcontext(context), args...)
306+
end
307+
function dot_tilde_assume(rng, ::IsParent, context::AbstractContext, args...)
308+
return dot_tilde_assume(rng, childcontext(context), args...)
309+
end
294310

295311
function dot_tilde_assume(rng, ::DefaultContext, sampler, right, left, vns, inds, vi)
296312
return dot_assume(rng, sampler, right, vns, left, vi)
@@ -371,25 +387,6 @@ function dot_tilde_assume(
371387
dot_tilde_assume(rng, PriorContext(), sampler, right, left, vn, inds, vi)
372388
end
373389
end
374-
function dot_tilde_assume(context::PriorContext, right, left, vn, inds, vi)
375-
return dot_assume(right, left, vn, vi)
376-
end
377-
function dot_tilde_assume(
378-
rng::Random.AbstractRNG, context::PriorContext, sampler, right, left, vn, inds, vi
379-
)
380-
return dot_assume(rng, sampler, right, vn, left, vi)
381-
end
382-
383-
# `MiniBatchContext`
384-
function dot_tilde_assume(context::MiniBatchContext, right, left, vn, inds, vi)
385-
return dot_tilde_assume(context.context, right, left, vn, inds, vi)
386-
end
387-
388-
function dot_tilde_assume(
389-
rng, context::MiniBatchContext, sampler, right, left, vn, inds, vi
390-
)
391-
return dot_tilde_assume(rng, context.context, sampler, right, left, vn, inds, vi)
392-
end
393390

394391
# `PrefixContext`
395392
function dot_tilde_assume(context::PrefixContext, right, left, vn, inds, vi)
@@ -586,18 +583,16 @@ function dot_tilde_observe(context::SamplingContext, right, left, vi)
586583
end
587584

588585
# Leaf contexts
589-
dot_tilde_observe(::DefaultContext, right, left, vi) = dot_observe(right, left, vi)
590-
function dot_tilde_observe(::DefaultContext, sampler, right, left, vi)
591-
return dot_observe(sampler, right, left, vi)
586+
function dot_tilde_observe(context::AbstractContext, args...)
587+
return dot_tilde_observe(NodeTrait(tilde_observe, context), context, args...)
588+
end
589+
dot_tilde_observe(::IsLeaf, ::AbstractContext, args...) = dot_observe(args...)
590+
function dot_tilde_observe(::IsParent, context::AbstractContext, args...)
591+
return dot_tilde_observe(childcontext(context), args...)
592592
end
593+
593594
dot_tilde_observe(::PriorContext, right, left, vi) = 0
594595
dot_tilde_observe(::PriorContext, sampler, right, left, vi) = 0
595-
function dot_tilde_observe(context::LikelihoodContext, right, left, vi)
596-
return dot_observe(right, left, vi)
597-
end
598-
function dot_tilde_observe(context::LikelihoodContext, sampler, right, left, vi)
599-
return dot_observe(sampler, right, left, vi)
600-
end
601596

602597
# `MiniBatchContext`
603598
function dot_tilde_observe(context::MiniBatchContext, right, left, vi)

src/contexts.jl

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,124 @@
1+
# Fallback traits
2+
# TODO: Should this instead be `NoChildren()`, `HasChild()`, etc. so we allow plural too, e.g. `HasChildren()`?
3+
4+
"""
5+
NodeTrait(context)
6+
NodeTrait(f, context)
7+
8+
Specifies the role of `context` in the context-tree.
9+
10+
The officially supported traits are:
11+
- `IsLeaf`: `context` does not have any decendants.
12+
- `IsParent`: `context` has a child context to which we often defer.
13+
Expects the following methods to be implemented:
14+
- [`childcontext`](@ref)
15+
- [`setchildcontext`](@ref)
16+
"""
17+
abstract type NodeTrait end
18+
NodeTrait(_, context) = NodeTrait(context)
19+
20+
"""
21+
IsLeaf
22+
23+
Specifies that the context is a leaf in the context-tree.
24+
"""
25+
struct IsLeaf <: NodeTrait end
26+
"""
27+
IsParent
28+
29+
Specifies that the context is a parent in the context-tree.
30+
"""
31+
struct IsParent <: NodeTrait end
32+
33+
"""
34+
childcontext(context)
35+
36+
Return the descendant context of `context`.
37+
"""
38+
childcontext
39+
40+
"""
41+
setchildcontext(parent::AbstractContext, child::AbstractContext)
42+
43+
Reconstruct `parent` but now using `child` is its [`childcontext`](@ref),
44+
effectively updating the child context.
45+
46+
# Examples
47+
```jldoctest
48+
julia> ctx = SamplingContext();
49+
50+
julia> DynamicPPL.childcontext(ctx)
51+
DefaultContext()
52+
53+
julia> ctx_prior = DynamicPPL.setchildcontext(ctx, PriorContext()); # only compute the logprior
54+
55+
julia> DynamicPPL.childcontext(ctx_prior)
56+
PriorContext{Nothing}(nothing)
57+
```
58+
"""
59+
setchildcontext
60+
61+
"""
62+
leafcontext(context)
63+
64+
Return the leaf of `context`, i.e. the first descendant context that `IsLeaf`.
65+
"""
66+
leafcontext(context) = leafcontext(NodeTrait(leafcontext, context), context)
67+
leafcontext(::IsLeaf, context) = context
68+
leafcontext(::IsParent, context) = leafcontext(childcontext(context))
69+
70+
"""
71+
setleafcontext(left, right)
72+
73+
Return `left` but now with its leaf context replaced by `right`.
74+
75+
Note that this also works even if `right` is not a leaf context,
76+
in which case effectively append `right` to `left`, dropping the
77+
original leaf context of `left`.
78+
79+
# Examples
80+
```jldoctest
81+
julia> using DynamicPPL: leafcontext, setleafcontext, childcontext, setchildcontext, AbstractContext
82+
83+
julia> struct ParentContext{C} <: AbstractContext
84+
context::C
85+
end
86+
87+
julia> DynamicPPL.NodeTrait(::ParentContext) = DynamicPPL.IsParent()
88+
89+
julia> DynamicPPL.childcontext(context::ParentContext) = context.context
90+
91+
julia> DynamicPPL.setchildcontext(::ParentContext, child) = ParentContext(child)
92+
93+
julia> Base.show(io::IO, c::ParentContext) = print(io, "ParentContext(", childcontext(c), ")")
94+
95+
julia> ctx = ParentContext(ParentContext(DefaultContext()))
96+
ParentContext(ParentContext(DefaultContext()))
97+
98+
julia> # Replace the leaf context with another leaf.
99+
leafcontext(setleafcontext(ctx, PriorContext()))
100+
PriorContext{Nothing}(nothing)
101+
102+
julia> # Append another parent context.
103+
setleafcontext(ctx, ParentContext(DefaultContext()))
104+
ParentContext(ParentContext(ParentContext(DefaultContext())))
105+
```
106+
"""
107+
function setleafcontext(left, right)
108+
return setleafcontext(
109+
NodeTrait(setleafcontext, left), NodeTrait(setleafcontext, right), left, right
110+
)
111+
end
112+
function setleafcontext(::IsParent, ::IsParent, left, right)
113+
return setchildcontext(left, setleafcontext(childcontext(left), right))
114+
end
115+
function setleafcontext(::IsParent, ::IsLeaf, left, right)
116+
return setchildcontext(left, setleafcontext(childcontext(left), right))
117+
end
118+
setleafcontext(::IsLeaf, ::IsParent, left, right) = right
119+
setleafcontext(::IsLeaf, ::IsLeaf, left, right) = right
120+
121+
# Contexts
1122
"""
2123
SamplingContext(rng, sampler, context)
3124
@@ -11,6 +132,16 @@ struct SamplingContext{S<:AbstractSampler,C<:AbstractContext,R} <: AbstractConte
11132
sampler::S
12133
context::C
13134
end
135+
SamplingContext(sampler, context) = SamplingContext(Random.GLOBAL_RNG, sampler, context)
136+
SamplingContext(context::AbstractContext) = SamplingContext(SampleFromPrior(), context)
137+
SamplingContext(sampler::AbstractSampler) = SamplingContext(sampler, DefaultContext())
138+
SamplingContext() = SamplingContext(SampleFromPrior())
139+
140+
NodeTrait(context::SamplingContext) = IsParent()
141+
childcontext(context::SamplingContext) = context.context
142+
function setchildcontext(parent::SamplingContext, child)
143+
return SamplingContext(parent.rng, parent.sampler, child)
144+
end
14145

15146
"""
16147
struct DefaultContext <: AbstractContext end
@@ -19,6 +150,7 @@ The `DefaultContext` is used by default to compute log the joint probability of
19150
and parameters when running the model.
20151
"""
21152
struct DefaultContext <: AbstractContext end
153+
NodeTrait(context::DefaultContext) = IsLeaf()
22154

23155
"""
24156
struct PriorContext{Tvars} <: AbstractContext
@@ -32,6 +164,7 @@ struct PriorContext{Tvars} <: AbstractContext
32164
vars::Tvars
33165
end
34166
PriorContext() = PriorContext(nothing)
167+
NodeTrait(context::PriorContext) = IsLeaf()
35168

36169
"""
37170
struct LikelihoodContext{Tvars} <: AbstractContext
@@ -46,6 +179,7 @@ struct LikelihoodContext{Tvars} <: AbstractContext
46179
vars::Tvars
47180
end
48181
LikelihoodContext() = LikelihoodContext(nothing)
182+
NodeTrait(context::LikelihoodContext) = IsLeaf()
49183

50184
"""
51185
struct MiniBatchContext{Tctx, T} <: AbstractContext
@@ -66,6 +200,11 @@ end
66200
function MiniBatchContext(context=DefaultContext(); batch_size, npoints)
67201
return MiniBatchContext(context, npoints / batch_size)
68202
end
203+
NodeTrait(context::MiniBatchContext) = IsParent()
204+
childcontext(context::MiniBatchContext) = context.context
205+
function setchildcontext(parent::MiniBatchContext, child)
206+
return MiniBatchContext(child, parent.loglike_scalar)
207+
end
69208

70209
"""
71210
PrefixContext{Prefix}(context)
@@ -85,6 +224,12 @@ function PrefixContext{Prefix}(context::AbstractContext) where {Prefix}
85224
return PrefixContext{Prefix,typeof(context)}(context)
86225
end
87226

227+
NodeTrait(context::PrefixContext) = IsParent()
228+
childcontext(context::PrefixContext) = context.context
229+
function setchildcontext(parent::PrefixContext{Prefix}, child) where {Prefix}
230+
return PrefixContext{Prefix}(child)
231+
end
232+
88233
const PREFIX_SEPARATOR = Symbol(".")
89234

90235
function PrefixContext{PrefixInner}(

0 commit comments

Comments
 (0)