Skip to content

Commit

Permalink
More "thread-safe" LazyString (JuliaLang#44936)
Browse files Browse the repository at this point in the history
* More "thread-safe" LazyString
* Serialize rendered string
* Move some array methods to essentials.jl

Co-authored-by: Jameson Nash <[email protected]>
  • Loading branch information
tkf and vtjnash committed Apr 13, 2022
1 parent 88fcf44 commit b6a6875
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 20 deletions.
7 changes: 1 addition & 6 deletions base/array.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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) ]
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion base/essentials.jl
Original file line number Diff line number Diff line change
@@ -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}
Expand Down
4 changes: 4 additions & 0 deletions base/expr.jl
Original file line number Diff line number Diff line change
@@ -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 ##

"""
Expand Down
3 changes: 1 addition & 2 deletions base/meta.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 25 additions & 9 deletions base/strings/lazy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

"""
Expand All @@ -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
```
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions stdlib/Serialization/src/Serialization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions stdlib/Serialization/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit b6a6875

Please sign in to comment.