Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Julia v1.11 Release Notes

New language features
---------------------
* `ScopedValue` implement dynamic scope with inheritance across tasks ([#50958]).

Language changes
----------------
Expand Down
12 changes: 8 additions & 4 deletions base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,6 @@ using .Libc: getpid, gethostname, time, memcpy, memset, memmove, memcmp
const libblas_name = "libblastrampoline" * (Sys.iswindows() ? "-5" : "")
const liblapack_name = libblas_name

# Logging
include("logging.jl")
using .CoreLogging

# Concurrency (part 2)
# Note that `atomics.jl` here should be deprecated
Core.eval(Threads, :(include("atomics.jl")))
Expand All @@ -344,6 +340,14 @@ include("task.jl")
include("threads_overloads.jl")
include("weakkeydict.jl")

# ScopedValues
include("scopedvalues.jl")
using .ScopedValues

# Logging
include("logging.jl")
using .CoreLogging

include("env.jl")

# functions defined in Random
Expand Down
2 changes: 1 addition & 1 deletion base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
# result::Any
# exception::Any
# backtrace::Any
# logstate::Any
# scope::Any
# code::Any
#end

Expand Down
5 changes: 5 additions & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,11 @@ export
sprint,
summary,

# ScopedValue
scoped,
@scoped,
ScopedValue,

# logging
@debug,
@info,
Expand Down
16 changes: 4 additions & 12 deletions base/logging.jl
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,10 @@ end

LogState(logger) = LogState(LogLevel(_invoked_min_enabled_level(logger)), logger)

const CURRENT_LOGSTATE = ScopedValue{Union{Nothing, LogState}}(nothing)

function current_logstate()
logstate = current_task().logstate
logstate = CURRENT_LOGSTATE[]
return (logstate !== nothing ? logstate : _global_logstate)::LogState
end

Expand All @@ -506,17 +508,7 @@ end
return nothing
end

function with_logstate(f::Function, logstate)
@nospecialize
t = current_task()
old = t.logstate
try
t.logstate = logstate
f()
finally
t.logstate = old
end
end
with_logstate(f::Function, logstate) = @scoped(CURRENT_LOGSTATE => logstate, f())

#-------------------------------------------------------------------------------
# Control of the current logger and early log filtering
Expand Down
175 changes: 175 additions & 0 deletions base/scopedvalues.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

module ScopedValues

export ScopedValue, scoped, @scoped

"""
ScopedValue(x)

Create a container that propagates values across dynamic scopes.
Use [`scoped`](@ref) to create and enter a new dynamic scope.

Values can only be set when entering a new dynamic scope,
and the value referred to will be constant during the
execution of a dynamic scope.

Dynamic scopes are propagated across tasks.

# Examples
```jldoctest
julia> const svar = ScopedValue(1);

julia> svar[]
1

julia> scoped(svar => 2) do
svar[]
end
2

julia> svar[]
1
```

!!! compat "Julia 1.11"
Scoped values were introduced in Julia 1.11. In Julia 1.8+ a compatible
implementation is available from the package ScopedValues.jl.
"""
mutable struct ScopedValue{T}
const initial_value::T
end

Base.eltype(::Type{ScopedValue{T}}) where {T} = T

##
# Notes on the implementation.
# We want lookup to be unreasonably fast.
# - IDDict/Dict are ~10ns
# - ImmutableDict is faster up to about ~15 entries
# - ScopedValue are meant to be constant, Immutabilty
# is thus a boon
# - If we were to use IDDict/Dict we would need to split
# the cache portion and the value portion of the hash-table,
# the value portion is read-only/write-once, but the cache version
# would need a lock which makes ImmutableDict incredibly attractive.
# We could also use task-local-storage, but that added about 12ns.
# - Values are GC'd when scopes become unreachable, one could use
# a WeakKeyDict to also ensure that values get GC'd when ScopedValues
# become unreachable.
# - Scopes are an inline implementation of an ImmutableDict, if we wanted
# be really fancy we could use a CTrie or HAMT.

mutable struct Scope
const parent::Union{Nothing, Scope}
const key::ScopedValue
const value::Any
Scope(parent, key::ScopedValue{T}, value::T) where T = new(parent, key, value)
end
Scope(parent, key::ScopedValue{T}, value) where T =
Scope(parent, key, convert(T, value))

function Scope(scope, pairs::Pair{<:ScopedValue}...)
for pair in pairs
scope = Scope(scope, pair...)
end
return scope
end

"""
current_scope()::Union{Nothing, Scope}

Return the current dynamic scope.
"""
current_scope() = current_task().scope::Union{Nothing, Scope}

function Base.show(io::IO, scope::Scope)
print(io, Scope, "(")
seen = Set{ScopedValue}()
while scope !== nothing
if scope.key ∉ seen
if !isempty(seen)
print(io, ", ")
end
print(io, typeof(scope.key), "@")
show(io, Base.objectid(scope.key))
print(io, " => ")
show(IOContext(io, :typeinfo => eltype(scope.key)), scope.value)
push!(seen, scope.key)
end
scope = scope.parent
end
print(io, ")")
end

function Base.getindex(var::ScopedValue{T})::T where T
scope = current_scope()
while scope !== nothing
if scope.key === var
return scope.value::T
end
scope = scope.parent
end
return var.initial_value
end

function Base.show(io::IO, var::ScopedValue)
print(io, ScopedValue)
print(io, '{', eltype(var), '}')
print(io, '(')
show(IOContext(io, :typeinfo => eltype(var)), var[])
print(io, ')')
end

"""
scoped(f, (var::ScopedValue{T} => val::T)...)

Execute `f` in a new scope with `var` set to `val`.
"""
function scoped(f, pair::Pair{<:ScopedValue}, rest::Pair{<:ScopedValue}...)
@nospecialize
ct = Base.current_task()
current_scope = ct.scope::Union{Nothing, Scope}
scope = Scope(current_scope, pair, rest...)
ct.scope = scope
try
return f()
finally
ct.scope = current_scope
end
end

scoped(@nospecialize(f)) = f()

"""
@scoped vars... expr

Macro version of `scoped(f, vars...)` but with `expr` instead of `f` function.
This is similar to using [`scoped`](@ref) with a `do` block, but avoids creating
a closure.
"""
macro scoped(exprs...)
ex = last(exprs)
if length(exprs) > 1
exprs = exprs[1:end-1]
else
exprs = ()
end
for expr in exprs
if expr.head !== :call || first(expr.args) !== :(=>)
error("@scoped expects arguments of the form `A => 2` got $expr")
end
end
exprs = map(esc, exprs)
ct = gensym(:ct)
current_scope = gensym(:current_scope)
body = Expr(:tryfinally, esc(ex), :($(ct).scope = $(current_scope)))
quote
$(ct) = $(Base.current_task)()
$(current_scope) = $(ct).scope::$(Union{Nothing, Scope})
$(ct).scope = $(Scope)($(current_scope), $(exprs...))
$body
end
end

end # module ScopedValues
1 change: 1 addition & 0 deletions doc/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ BaseDocs = [
"base/arrays.md",
"base/parallel.md",
"base/multi-threading.md",
"base/scopedvalues.md",
"base/constants.md",
"base/file.md",
"base/io-network.md",
Expand Down
Loading