From b6a68754799e9212391f7ae7b5b7c58b2b442efc Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 12 Apr 2022 19:29:37 -0700 Subject: [PATCH] More "thread-safe" LazyString (#44936) * More "thread-safe" LazyString * Serialize rendered string * Move some array methods to essentials.jl Co-authored-by: Jameson Nash --- base/array.jl | 7 +---- base/essentials.jl | 9 +++++- base/expr.jl | 4 +++ base/meta.jl | 3 +- base/show.jl | 2 -- base/strings/lazy.jl | 34 +++++++++++++++++------ stdlib/Serialization/src/Serialization.jl | 2 ++ stdlib/Serialization/test/runtests.jl | 8 ++++++ 8 files changed, 49 insertions(+), 20 deletions(-) diff --git a/base/array.jl b/base/array.jl index 5dd9a5660f54a..8f642600f0347 100644 --- a/base/array.jl +++ b/base/array.jl @@ -120,7 +120,7 @@ const DenseVecOrMat{T} = Union{DenseVector{T}, DenseMatrix{T}} ## Basic functions ## -import Core: arraysize, arrayset, arrayref, const_arrayref +using Core: arraysize, arrayset, const_arrayref vect() = Vector{Any}() vect(X::T...) where {T} = T[ X[i] for i = 1:length(X) ] @@ -212,7 +212,6 @@ function bitsunionsize(u::Union) return sz end -length(a::Array) = arraylen(a) elsize(@nospecialize _::Type{A}) where {T,A<:Array{T}} = aligned_sizeof(T) sizeof(a::Array) = Core.sizeof(a) @@ -920,10 +919,6 @@ julia> getindex(A, "a") """ function getindex end -# This is more complicated than it needs to be in order to get Win64 through bootstrap -@eval getindex(A::Array, i1::Int) = arrayref($(Expr(:boundscheck)), A, i1) -@eval getindex(A::Array, i1::Int, i2::Int, I::Int...) = (@inline; arrayref($(Expr(:boundscheck)), A, i1, i2, I...)) - # Faster contiguous indexing using copyto! for AbstractUnitRange and Colon function getindex(A::Array, I::AbstractUnitRange{<:Integer}) @inline diff --git a/base/essentials.jl b/base/essentials.jl index af965e045cbb5..498c6f8f4f196 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -1,11 +1,18 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -using Core: CodeInfo, SimpleVector, donotdelete +using Core: CodeInfo, SimpleVector, donotdelete, arrayref const Callable = Union{Function,Type} const Bottom = Union{} +# Define minimal array interface here to help code used in macros: +length(a::Array) = arraylen(a) + +# This is more complicated than it needs to be in order to get Win64 through bootstrap +eval(:(getindex(A::Array, i1::Int) = arrayref($(Expr(:boundscheck)), A, i1))) +eval(:(getindex(A::Array, i1::Int, i2::Int, I::Int...) = (@inline; arrayref($(Expr(:boundscheck)), A, i1, i2, I...)))) + """ AbstractSet{T} diff --git a/base/expr.jl b/base/expr.jl index 7352c7863f8e1..4e01ec41ef0ae 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -1,5 +1,9 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license +isexpr(@nospecialize(ex), heads) = isa(ex, Expr) && in(ex.head, heads) +isexpr(@nospecialize(ex), heads, n::Int) = isa(ex, Expr) && in(ex.head, heads) && length(ex.args) == n +const is_expr = isexpr + ## symbols ## """ diff --git a/base/meta.jl b/base/meta.jl index fcf66a7a787b2..cf59d3fa3274e 100644 --- a/base/meta.jl +++ b/base/meta.jl @@ -74,8 +74,7 @@ julia> Meta.isexpr(ex, :call, 2) true ``` """ -isexpr(@nospecialize(ex), heads) = isa(ex, Expr) && in(ex.head, heads) -isexpr(@nospecialize(ex), heads, n::Int) = isa(ex, Expr) && in(ex.head, heads) && length(ex.args) == n +isexpr """ replace_sourceloc!(location, expr) diff --git a/base/show.jl b/base/show.jl index f2d8f958a0b24..fdf6470f4a881 100644 --- a/base/show.jl +++ b/base/show.jl @@ -1481,8 +1481,6 @@ function operator_associativity(s::Symbol) return :left end -const is_expr = isexpr - is_quoted(ex) = false is_quoted(ex::QuoteNode) = true is_quoted(ex::Expr) = is_expr(ex, :quote, 1) || is_expr(ex, :inert, 1) diff --git a/base/strings/lazy.jl b/base/strings/lazy.jl index 4d918807a3576..c7f03d42b4d49 100644 --- a/base/strings/lazy.jl +++ b/base/strings/lazy.jl @@ -20,12 +20,25 @@ See also [`lazy"str"`](@ref). !!! compat "Julia 1.8" `LazyString` requires Julia 1.8 or later. + +# Extended help +## Safety properties for concurrent programs + +A lazy string itself does not introduce any concurrency problems even if it is printed in +multiple Julia tasks. However, if `print` methods on a captured value can have a +concurrency issue when invoked without synchronizations, printing the lazy string may cause +an issue. Furthermore, the `print` methods on the captured values may be invoked multiple +times, though only exactly one result will be returned. + +!!! compat "Julia 1.9" + `LazyString` is safe in the above sense in Julia 1.9 and later. """ mutable struct LazyString <: AbstractString - parts::Tuple + const parts::Tuple # Created on first access - str::String - LazyString(args...) = new(args) + @atomic str::Union{String,Nothing} + global _LazyString(parts, str) = new(parts, str) + LazyString(args...) = new(args, nothing) end """ @@ -35,6 +48,8 @@ Create a [`LazyString`](@ref) using regular string interpolation syntax. Note that interpolations are *evaluated* at LazyString construction time, but *printing* is delayed until the first access to the string. +See [`LazyString`](@ref) documentation for the safety properties for concurrent programs. + # Examples ``` @@ -63,14 +78,15 @@ macro lazy_str(text) end function String(l::LazyString) - if !isdefined(l, :str) - l.str = sprint() do io - for p in l.parts - print(io, p) - end + old = @atomic :acquire l.str + old === nothing || return old + str = sprint() do io + for p in l.parts + print(io, p) end end - return l.str + old, ok = @atomicreplace :acquire_release :acquire l.str nothing => str + return ok ? str : (old::String) end hash(s::LazyString, h::UInt64) = hash(String(s), h) diff --git a/stdlib/Serialization/src/Serialization.jl b/stdlib/Serialization/src/Serialization.jl index 0f69e686bb473..a35813b44f13e 100644 --- a/stdlib/Serialization/src/Serialization.jl +++ b/stdlib/Serialization/src/Serialization.jl @@ -1565,5 +1565,7 @@ function deserialize(s::AbstractSerializer, ::Type{T}) where T<:Base.GenericCond return cond end +serialize(s::AbstractSerializer, l::LazyString) = + invoke(serialize, Tuple{AbstractSerializer,Any}, s, Base._LazyString((), string(l))) end diff --git a/stdlib/Serialization/test/runtests.jl b/stdlib/Serialization/test/runtests.jl index ceacb7f33c27a..104b3e97d6118 100644 --- a/stdlib/Serialization/test/runtests.jl +++ b/stdlib/Serialization/test/runtests.jl @@ -642,3 +642,11 @@ let c1 = Threads.Condition() unlock(c2) wait(t) end + +@testset "LazyString" begin + l1 = lazy"a $1 b $2" + l2 = deserialize(IOBuffer(sprint(serialize, l1))) + @test l2.str === l1.str + @test l2 == l1 + @test l2.parts === () +end