-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ScopedVariables are containers whose observed value depends the current dynamic scope. This implementation is inspired by https://openjdk.org/jeps/446 A scope is introduced with the `scoped` function that takes a lambda to execute within the new scope. The value of a `ScopedVariable` is constant within that scope and can only be set upon introduction of a new scope. Scopes are propagated across tasks boundaries. In contrast to #35833 the storage of the per-scope data is assoicated with the ScopedVariables object and does not require copies upon scope entry. This also means that libraries can use scoped variables without paying for scoped variables introduces in other libraries. Finding the current value of a ScopedVariable, involves walking the scope chain upwards and checking if the scoped variable has a value for the current or one of its parent scopes. This means the cost of a lookup scales with the depth of the dynamic scoping. This could be amortized by using a task-local cache.
- Loading branch information
Showing
10 changed files
with
199 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -163,7 +163,7 @@ | |
# result::Any | ||
# exception::Any | ||
# backtrace::Any | ||
# logstate::Any | ||
# scope::Any | ||
# code::Any | ||
#end | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -648,6 +648,10 @@ export | |
sprint, | ||
summary, | ||
|
||
# ScopedVariable | ||
scoped, | ||
ScopedVariable, | ||
|
||
# logging | ||
@debug, | ||
@info, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# This file is a part of Julia. License is MIT: https://julialang.org/license | ||
|
||
module ScopedVariables | ||
|
||
export ScopedVariable, scoped | ||
|
||
mutable struct Scope | ||
const parent::Union{Nothing, Scope} | ||
end | ||
|
||
current_scope() = current_task().scope::Union{Nothing, Scope} | ||
|
||
""" | ||
ScopedVariable(x) | ||
Create a container that propagates values across scopes. | ||
Use [`scoped`](@ref) to create and enter a new scope. | ||
Values can only be set when entering a new scope, | ||
and the value referred to will be constant during the | ||
execution of a scope. | ||
Dynamic scopes are propagated across tasks. | ||
# Examples | ||
```jldoctest | ||
julia> const svar = ScopedVariable(1); | ||
julia> svar[] | ||
1 | ||
julia> scoped(svar => 2) do | ||
svar[] | ||
end | ||
2 | ||
``` | ||
!!! compat "Julia 1.11" | ||
This method requires at least Julia 1.11. In Julia 1.7+ this | ||
is available from the package ScopedVariables.jl. | ||
""" | ||
mutable struct ScopedVariable{T} | ||
const values::WeakKeyDict{Scope, T} | ||
const initial_value::T | ||
ScopedVariable{T}(initial_value) where {T} = new{T}(WeakKeyDict{Scope, T}(), initial_value) | ||
end | ||
ScopedVariable(initial_value::T) where {T} = ScopedVariable{T}(initial_value) | ||
|
||
Base.eltype(::Type{ScopedVariable{T}}) where {T} = T | ||
|
||
function Base.getindex(var::ScopedVariable{T})::T where T | ||
scope = current_scope() | ||
if scope === nothing | ||
return var.initial_value | ||
end | ||
@lock var.values begin | ||
while scope !== nothing | ||
if haskey(var.values.ht, scope) | ||
return var.values.ht[scope] | ||
end | ||
scope = scope.parent | ||
end | ||
end | ||
return var.initial_value | ||
end | ||
|
||
function Base.show(io::IO, var::ScopedVariable) | ||
print(io, ScopedVariable) | ||
print(io, '{', eltype(var), '}') | ||
print(io, '(') | ||
show(io, var[]) | ||
print(io, ')') | ||
end | ||
|
||
function __set_var!(scope::Scope, var::ScopedVariable{T}, val::T) where T | ||
# PRIVATE API! Wrong usage will break invariants of ScopedVariable. | ||
if scope === nothing | ||
error("ScopedVariable: Currently not in scope.") | ||
end | ||
@lock var.values begin | ||
if haskey(var.values.ht, scope) | ||
error("ScopedVariable: Variable is already set for this scope.") | ||
end | ||
var.values[scope] = val | ||
end | ||
end | ||
|
||
""" | ||
scoped(f, var::ScopedVariable{T} => val::T) | ||
Execute `f` in a new scope with `var` set to `val`. | ||
""" | ||
function scoped(f, pair::Pair{<:ScopedVariable{T}, T}) where T | ||
@nospecialize | ||
ct = Base.current_task() | ||
current_scope = ct.scope::Union{Nothing, Scope} | ||
try | ||
scope = Scope(current_scope) | ||
__set_var!(scope, pair...) | ||
ct.scope = scope | ||
return f() | ||
finally | ||
ct.scope = current_scope | ||
end | ||
end | ||
|
||
""" | ||
scoped(f, vars...::ScopedVariable{T} => val::T) | ||
Execute `f` in a new scope with each scoped variable set to the provided `val`. | ||
""" | ||
function scoped(f, pairs::Pair{<:ScopedVariable}...) | ||
@nospecialize | ||
ct = Base.current_task() | ||
current_scope = ct.scope::Union{Nothing, Scope} | ||
try | ||
scope = Scope(current_scope) | ||
for (var, val) in pairs | ||
__set_var!(scope, var, val) | ||
end | ||
ct.scope = scope | ||
return f() | ||
finally | ||
ct.scope = current_scope | ||
end | ||
end | ||
|
||
end # module ScopedVariables |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# This file is a part of Julia. License is MIT: https://julialang.org/license | ||
|
||
const svar1 = ScopedVariable(1) | ||
|
||
@testset "errors" begin | ||
var = ScopedVariable(1) | ||
@test_throws MethodError var[] = 2 | ||
scoped() do | ||
@test_throws MethodError var[] = 2 | ||
end | ||
end | ||
|
||
const svar = ScopedVariable(1) | ||
@testset "inheritance" begin | ||
@test svar[] == 1 | ||
scoped() do | ||
@test svar[] == 1 | ||
scoped() do | ||
@test svar[] == 1 | ||
end | ||
scoped(svar => 2) do | ||
@test svar[] == 2 | ||
end | ||
@test svar[] == 1 | ||
end | ||
@test svar[] == 1 | ||
end | ||
|
||
const svar_float = ScopedVariable(1.0) | ||
|
||
@testset "multiple scoped variables" begin | ||
scoped(svar => 2, svar_float => 2.0) do | ||
@test svar[] == 2 | ||
@test svar_float[] == 2.0 | ||
end | ||
end | ||
|
||
import Base.Threads: @spawn | ||
@testset "tasks" begin | ||
@test fetch(@spawn begin | ||
svar[] | ||
end) == 1 | ||
scoped(svar => 2) do | ||
@test fetch(@spawn begin | ||
svar[] | ||
end) == 2 | ||
end | ||
end |