Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a LazyString type, for places (like errors) which we would prefer to defer the actual work #33711

Merged
merged 1 commit into from
Jan 20, 2022
Merged
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 @@ -102,6 +102,7 @@ Standard library changes
* `extrema` now supports `init` keyword argument ([#36265], [#43604]).
* Intersect returns a result with the eltype of the type-promoted eltypes of the two inputs ([#41769]).
* `Iterators.countfrom` now accepts any type that defines `+`. ([#37747])
* The `LazyString` and the `lazy"str"` macro were added to support delayed construction of error messages in error paths. ([#33711])

#### InteractiveUtils
* A new macro `@time_imports` for reporting any time spent importing packages and their dependencies ([#41612])
Expand Down
4 changes: 4 additions & 0 deletions base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ include("refpointer.jl")
include("checked.jl")
using .Checked

# Lazy strings
include("strings/lazy.jl")

# array structures
include("indices.jl")
include("array.jl")
Expand Down Expand Up @@ -200,6 +203,7 @@ include("dict.jl")
include("abstractset.jl")
include("set.jl")

# Strings
include("char.jl")
include("strings/basic.jl")
include("strings/string.jl")
Expand Down
34 changes: 20 additions & 14 deletions base/abstractarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -914,19 +914,21 @@ end
# copy from an some iterable object into an AbstractArray
function copyto!(dest::AbstractArray, dstart::Integer, src, sstart::Integer)
if (sstart < 1)
throw(ArgumentError(string("source start offset (",sstart,") is < 1")))
throw(ArgumentError(LazyString("source start offset (",sstart,") is < 1")))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these cases be using the string macro for consistency? It seems you've included strings/lazy.jl before abstractarray.jl during bootstrap so I guess it should work?

[edit] ah, I see that some uses of string() are just for keeping indentation under control. It sure would be nice if it was easier to reuse the parsing logic from #40753 so we had consistency of escaping rules for these kind of "almost but not quite a normal string" type of custom string literals. Rather than needing the raw string escaping rules.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did the straightforward replacement. We can change these for consistency in a followup.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly fine by me.

end
y = iterate(src)
for j = 1:(sstart-1)
if y === nothing
throw(ArgumentError(string("source has fewer elements than required, ",
"expected at least ",sstart,", got ",j-1)))
throw(ArgumentError(LazyString(
"source has fewer elements than required, ",
"expected at least ", sstart,", got ", j-1)))
end
y = iterate(src, y[2])
end
if y === nothing
throw(ArgumentError(string("source has fewer elements than required, ",
"expected at least ",sstart,", got ",sstart-1)))
throw(ArgumentError(LazyString(
"source has fewer elements than required, ",
"expected at least ",sstart," got ", sstart-1)))
end
i = Int(dstart)
while y !== nothing
Expand All @@ -940,19 +942,22 @@ end

# this method must be separate from the above since src might not have a length
function copyto!(dest::AbstractArray, dstart::Integer, src, sstart::Integer, n::Integer)
n < 0 && throw(ArgumentError(string("tried to copy n=", n, " elements, but n should be nonnegative")))
n < 0 && throw(ArgumentError(LazyString("tried to copy n=",n,
", elements, but n should be nonnegative")))
n == 0 && return dest
dmax = dstart + n - 1
inds = LinearIndices(dest)
if (dstart ∉ inds || dmax ∉ inds) | (sstart < 1)
sstart < 1 && throw(ArgumentError(string("source start offset (",sstart,") is < 1")))
sstart < 1 && throw(ArgumentError(LazyString("source start offset (",
sstart,") is < 1")))
throw(BoundsError(dest, dstart:dmax))
end
y = iterate(src)
for j = 1:(sstart-1)
if y === nothing
throw(ArgumentError(string("source has fewer elements than required, ",
"expected at least ",sstart,", got ",j-1)))
throw(ArgumentError(LazyString(
"source has fewer elements than required, ",
"expected at least ",sstart,", got ",j-1)))
end
y = iterate(src, y[2])
end
Expand Down Expand Up @@ -1064,7 +1069,8 @@ function copyto!(dest::AbstractArray, dstart::Integer,
src::AbstractArray, sstart::Integer,
n::Integer)
n == 0 && return dest
n < 0 && throw(ArgumentError(string("tried to copy n=", n, " elements, but n should be nonnegative")))
n < 0 && throw(ArgumentError(LazyString("tried to copy n=",
n," elements, but n should be nonnegative")))
destinds, srcinds = LinearIndices(dest), LinearIndices(src)
(checkbounds(Bool, destinds, dstart) && checkbounds(Bool, destinds, dstart+n-1)) || throw(BoundsError(dest, dstart:dstart+n-1))
(checkbounds(Bool, srcinds, sstart) && checkbounds(Bool, srcinds, sstart+n-1)) || throw(BoundsError(src, sstart:sstart+n-1))
Expand All @@ -1082,12 +1088,12 @@ end
function copyto!(B::AbstractVecOrMat{R}, ir_dest::AbstractRange{Int}, jr_dest::AbstractRange{Int},
A::AbstractVecOrMat{S}, ir_src::AbstractRange{Int}, jr_src::AbstractRange{Int}) where {R,S}
if length(ir_dest) != length(ir_src)
throw(ArgumentError(string("source and destination must have same size (got ",
length(ir_src)," and ",length(ir_dest),")")))
throw(ArgumentError(LazyString("source and destination must have same size (got ",
length(ir_src)," and ",length(ir_dest),")")))
end
if length(jr_dest) != length(jr_src)
throw(ArgumentError(string("source and destination must have same size (got ",
length(jr_src)," and ",length(jr_dest),")")))
throw(ArgumentError(LazyString("source and destination must have same size (got ",
length(jr_src)," and ",length(jr_dest),")")))
end
@boundscheck checkbounds(B, ir_dest, jr_dest)
@boundscheck checkbounds(A, ir_src, jr_src)
Expand Down
2 changes: 2 additions & 0 deletions base/compiler/compiler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ add_with_overflow(x::T, y::T) where {T<:SignedInt} = checked_sadd_int(x, y)
add_with_overflow(x::T, y::T) where {T<:UnsignedInt} = checked_uadd_int(x, y)
add_with_overflow(x::Bool, y::Bool) = (x+y, false)

include("strings/lazy.jl")

# core array operations
include("indices.jl")
include("array.jl")
Expand Down
2 changes: 2 additions & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export
IOStream,
LinRange,
Irrational,
LazyString,
Matrix,
MergeSort,
Missing,
Expand Down Expand Up @@ -986,6 +987,7 @@ export
@v_str, # version number
@raw_str, # raw string with no interpolation/unescaping
@NamedTuple,
@lazy_str, # lazy string

# documentation
@text_str,
Expand Down
11 changes: 6 additions & 5 deletions base/math.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ using Core.Intrinsics: sqrt_llvm
using .Base: IEEEFloat

@noinline function throw_complex_domainerror(f::Symbol, x)
throw(DomainError(x, string("$f will only return a complex result if called with a ",
"complex argument. Try $f(Complex(x)).")))
throw(DomainError(x,
LazyString(f," will only return a complex result if called with a complex argument. Try ", f,"(Complex(x)).")))
end
@noinline function throw_exp_domainerror(x)
throw(DomainError(x, string("Exponentiation yielding a complex result requires a ",
"complex argument.\nReplace x^y with (x+0im)^y, ",
"Complex(x)^y, or similar.")))
throw(DomainError(x, LazyString(
"Exponentiation yielding a complex result requires a ",
"complex argument.\nReplace x^y with (x+0im)^y, ",
"Complex(x)^y, or similar.")))
end

# non-type specific math functions
Expand Down
63 changes: 63 additions & 0 deletions base/strings/lazy.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
LazyString <: AbstractString

A lazy representation of string interpolation. This is useful when a string
needs to be constructed in a context where performing the actual interpolation
and string construction is unnecessary or undesirable (e.g. in error paths
of functions).

This type is designed to be cheap to construct at runtime, trying to offload
as much work as possible to either the macro or later printing operations.

!!! compat "Julia 1.8"
`LazyString` requires Julia 1.8 or later.
"""
mutable struct LazyString <: AbstractString
parts::Tuple
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to allocate an Array here, so we do not need to create a whole new Tuple type and can examine and iterate it more easily later?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be be an immutable array once that's merged, but currently the analysis in #43852 is not strong enough to model the mutation of the mutable array at the moment. I think for the current use cases, a tuple is mostly fine, since the types will be known at compile time and are stable. Plus it's the error path anyway, which we heavily pessimize and in particular, don't infer, so it'll allocate tons of tuple types anyway.

# Created on first access
str::String
LazyString(args...) = new(args)
end

"""
lazy"str"

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.

!!! compat "Julia 1.8"
`lazy"str"` requires Julia 1.8 or later.
"""
macro lazy_str(text)
parts = Any[]
lastidx = idx = 1
while (idx = findnext('$', text, idx)) !== nothing
lastidx < idx && push!(parts, text[lastidx:idx-1])
idx += 1
expr, idx = Meta.parseatom(text, idx; filename=string(__source__.file))
push!(parts, esc(expr))
lastidx = idx
end
lastidx <= lastindex(text) && push!(parts, text[lastidx:end])
:(LazyString($(parts...)))
end

function String(l::LazyString)
if !isdefined(l, :str)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any worry about race conditions for code like this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We can use an atomic acquire and cmpswap release here to fix that. (Or perhaps create an "assign_once" type, that manages this pattern for us?)

l.str = sprint() do io
for p in l.parts
print(io, p)
end
end
end
return l.str
end

hash(s::LazyString, h::UInt64) = hash(String(s), h)
lastindex(s::LazyString) = lastindex(String(s))
iterate(s::LazyString) = iterate(String(s))
iterate(s::LazyString, i::Integer) = iterate(String(s), i)
isequal(a::LazyString, b::LazyString) = isequal(String(a), String(b))
==(a::LazyString, b::LazyString) = (String(a) == String(b))
ncodeunits(s::LazyString) = ncodeunits(String(s))
Loading