Skip to content

Commit f290339

Browse files
authored
Add lazy string type (#33711)
1 parent cc96240 commit f290339

File tree

11 files changed

+157
-71
lines changed

11 files changed

+157
-71
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Standard library changes
102102
* `extrema` now supports `init` keyword argument ([#36265], [#43604]).
103103
* Intersect returns a result with the eltype of the type-promoted eltypes of the two inputs ([#41769]).
104104
* `Iterators.countfrom` now accepts any type that defines `+`. ([#37747])
105+
* The `LazyString` and the `lazy"str"` macro were added to support delayed construction of error messages in error paths. ([#33711])
105106

106107
#### InteractiveUtils
107108
* A new macro `@time_imports` for reporting any time spent importing packages and their dependencies ([#41612])

base/Base.jl

+4
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ include("refpointer.jl")
123123
include("checked.jl")
124124
using .Checked
125125

126+
# Lazy strings
127+
include("strings/lazy.jl")
128+
126129
# array structures
127130
include("indices.jl")
128131
include("array.jl")
@@ -200,6 +203,7 @@ include("dict.jl")
200203
include("abstractset.jl")
201204
include("set.jl")
202205

206+
# Strings
203207
include("char.jl")
204208
include("strings/basic.jl")
205209
include("strings/string.jl")

base/abstractarray.jl

+20-14
Original file line numberDiff line numberDiff line change
@@ -914,19 +914,21 @@ end
914914
# copy from an some iterable object into an AbstractArray
915915
function copyto!(dest::AbstractArray, dstart::Integer, src, sstart::Integer)
916916
if (sstart < 1)
917-
throw(ArgumentError(string("source start offset (",sstart,") is < 1")))
917+
throw(ArgumentError(LazyString("source start offset (",sstart,") is < 1")))
918918
end
919919
y = iterate(src)
920920
for j = 1:(sstart-1)
921921
if y === nothing
922-
throw(ArgumentError(string("source has fewer elements than required, ",
923-
"expected at least ",sstart,", got ",j-1)))
922+
throw(ArgumentError(LazyString(
923+
"source has fewer elements than required, ",
924+
"expected at least ", sstart,", got ", j-1)))
924925
end
925926
y = iterate(src, y[2])
926927
end
927928
if y === nothing
928-
throw(ArgumentError(string("source has fewer elements than required, ",
929-
"expected at least ",sstart,", got ",sstart-1)))
929+
throw(ArgumentError(LazyString(
930+
"source has fewer elements than required, ",
931+
"expected at least ",sstart," got ", sstart-1)))
930932
end
931933
i = Int(dstart)
932934
while y !== nothing
@@ -940,19 +942,22 @@ end
940942

941943
# this method must be separate from the above since src might not have a length
942944
function copyto!(dest::AbstractArray, dstart::Integer, src, sstart::Integer, n::Integer)
943-
n < 0 && throw(ArgumentError(string("tried to copy n=", n, " elements, but n should be nonnegative")))
945+
n < 0 && throw(ArgumentError(LazyString("tried to copy n=",n,
946+
", elements, but n should be nonnegative")))
944947
n == 0 && return dest
945948
dmax = dstart + n - 1
946949
inds = LinearIndices(dest)
947950
if (dstart inds || dmax inds) | (sstart < 1)
948-
sstart < 1 && throw(ArgumentError(string("source start offset (",sstart,") is < 1")))
951+
sstart < 1 && throw(ArgumentError(LazyString("source start offset (",
952+
sstart,") is < 1")))
949953
throw(BoundsError(dest, dstart:dmax))
950954
end
951955
y = iterate(src)
952956
for j = 1:(sstart-1)
953957
if y === nothing
954-
throw(ArgumentError(string("source has fewer elements than required, ",
955-
"expected at least ",sstart,", got ",j-1)))
958+
throw(ArgumentError(LazyString(
959+
"source has fewer elements than required, ",
960+
"expected at least ",sstart,", got ",j-1)))
956961
end
957962
y = iterate(src, y[2])
958963
end
@@ -1064,7 +1069,8 @@ function copyto!(dest::AbstractArray, dstart::Integer,
10641069
src::AbstractArray, sstart::Integer,
10651070
n::Integer)
10661071
n == 0 && return dest
1067-
n < 0 && throw(ArgumentError(string("tried to copy n=", n, " elements, but n should be nonnegative")))
1072+
n < 0 && throw(ArgumentError(LazyString("tried to copy n=",
1073+
n," elements, but n should be nonnegative")))
10681074
destinds, srcinds = LinearIndices(dest), LinearIndices(src)
10691075
(checkbounds(Bool, destinds, dstart) && checkbounds(Bool, destinds, dstart+n-1)) || throw(BoundsError(dest, dstart:dstart+n-1))
10701076
(checkbounds(Bool, srcinds, sstart) && checkbounds(Bool, srcinds, sstart+n-1)) || throw(BoundsError(src, sstart:sstart+n-1))
@@ -1082,12 +1088,12 @@ end
10821088
function copyto!(B::AbstractVecOrMat{R}, ir_dest::AbstractRange{Int}, jr_dest::AbstractRange{Int},
10831089
A::AbstractVecOrMat{S}, ir_src::AbstractRange{Int}, jr_src::AbstractRange{Int}) where {R,S}
10841090
if length(ir_dest) != length(ir_src)
1085-
throw(ArgumentError(string("source and destination must have same size (got ",
1086-
length(ir_src)," and ",length(ir_dest),")")))
1091+
throw(ArgumentError(LazyString("source and destination must have same size (got ",
1092+
length(ir_src)," and ",length(ir_dest),")")))
10871093
end
10881094
if length(jr_dest) != length(jr_src)
1089-
throw(ArgumentError(string("source and destination must have same size (got ",
1090-
length(jr_src)," and ",length(jr_dest),")")))
1095+
throw(ArgumentError(LazyString("source and destination must have same size (got ",
1096+
length(jr_src)," and ",length(jr_dest),")")))
10911097
end
10921098
@boundscheck checkbounds(B, ir_dest, jr_dest)
10931099
@boundscheck checkbounds(A, ir_src, jr_src)

base/compiler/compiler.jl

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ add_with_overflow(x::T, y::T) where {T<:SignedInt} = checked_sadd_int(x, y)
6767
add_with_overflow(x::T, y::T) where {T<:UnsignedInt} = checked_uadd_int(x, y)
6868
add_with_overflow(x::Bool, y::Bool) = (x+y, false)
6969

70+
include("strings/lazy.jl")
71+
7072
# core array operations
7173
include("indices.jl")
7274
include("array.jl")

base/exports.jl

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export
5757
IOStream,
5858
LinRange,
5959
Irrational,
60+
LazyString,
6061
Matrix,
6162
MergeSort,
6263
Missing,
@@ -986,6 +987,7 @@ export
986987
@v_str, # version number
987988
@raw_str, # raw string with no interpolation/unescaping
988989
@NamedTuple,
990+
@lazy_str, # lazy string
989991

990992
# documentation
991993
@text_str,

base/math.jl

+6-5
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ using Core.Intrinsics: sqrt_llvm
3030
using .Base: IEEEFloat
3131

3232
@noinline function throw_complex_domainerror(f::Symbol, x)
33-
throw(DomainError(x, string("$f will only return a complex result if called with a ",
34-
"complex argument. Try $f(Complex(x)).")))
33+
throw(DomainError(x,
34+
LazyString(f," will only return a complex result if called with a complex argument. Try ", f,"(Complex(x)).")))
3535
end
3636
@noinline function throw_exp_domainerror(x)
37-
throw(DomainError(x, string("Exponentiation yielding a complex result requires a ",
38-
"complex argument.\nReplace x^y with (x+0im)^y, ",
39-
"Complex(x)^y, or similar.")))
37+
throw(DomainError(x, LazyString(
38+
"Exponentiation yielding a complex result requires a ",
39+
"complex argument.\nReplace x^y with (x+0im)^y, ",
40+
"Complex(x)^y, or similar.")))
4041
end
4142

4243
# non-type specific math functions

base/strings/lazy.jl

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
LazyString <: AbstractString
3+
4+
A lazy representation of string interpolation. This is useful when a string
5+
needs to be constructed in a context where performing the actual interpolation
6+
and string construction is unnecessary or undesirable (e.g. in error paths
7+
of functions).
8+
9+
This type is designed to be cheap to construct at runtime, trying to offload
10+
as much work as possible to either the macro or later printing operations.
11+
12+
!!! compat "Julia 1.8"
13+
`LazyString` requires Julia 1.8 or later.
14+
"""
15+
mutable struct LazyString <: AbstractString
16+
parts::Tuple
17+
# Created on first access
18+
str::String
19+
LazyString(args...) = new(args)
20+
end
21+
22+
"""
23+
lazy"str"
24+
25+
Create a [`LazyString`](@ref) using regular string interpolation syntax.
26+
Note that interpolations are *evaluated* at LazyString construction time,
27+
but *printing* is delayed until the first access to the string.
28+
29+
!!! compat "Julia 1.8"
30+
`lazy"str"` requires Julia 1.8 or later.
31+
"""
32+
macro lazy_str(text)
33+
parts = Any[]
34+
lastidx = idx = 1
35+
while (idx = findnext('$', text, idx)) !== nothing
36+
lastidx < idx && push!(parts, text[lastidx:idx-1])
37+
idx += 1
38+
expr, idx = Meta.parseatom(text, idx; filename=string(__source__.file))
39+
push!(parts, esc(expr))
40+
lastidx = idx
41+
end
42+
lastidx <= lastindex(text) && push!(parts, text[lastidx:end])
43+
:(LazyString($(parts...)))
44+
end
45+
46+
function String(l::LazyString)
47+
if !isdefined(l, :str)
48+
l.str = sprint() do io
49+
for p in l.parts
50+
print(io, p)
51+
end
52+
end
53+
end
54+
return l.str
55+
end
56+
57+
hash(s::LazyString, h::UInt64) = hash(String(s), h)
58+
lastindex(s::LazyString) = lastindex(String(s))
59+
iterate(s::LazyString) = iterate(String(s))
60+
iterate(s::LazyString, i::Integer) = iterate(String(s), i)
61+
isequal(a::LazyString, b::LazyString) = isequal(String(a), String(b))
62+
==(a::LazyString, b::LazyString) = (String(a) == String(b))
63+
ncodeunits(s::LazyString) = ncodeunits(String(s))

0 commit comments

Comments
 (0)