Skip to content

Commit f9b857e

Browse files
committed
Merge branch 'main' into sp/ternary-unexpected-kw
2 parents 15c028c + 7951c0e commit f9b857e

File tree

5 files changed

+142
-24
lines changed

5 files changed

+142
-24
lines changed

src/parser.jl

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -201,25 +201,6 @@ function bump_semicolon_trivia(ps)
201201
end
202202
end
203203

204-
# Like @assert, but always enabled and calls internal_error()
205-
macro check(ex, msgs...)
206-
msg = isempty(msgs) ? ex : msgs[1]
207-
if isa(msg, AbstractString)
208-
msg = msg
209-
elseif !isempty(msgs) && (isa(msg, Expr) || isa(msg, Symbol))
210-
msg = :(string($(esc(msg))))
211-
else
212-
msg = string(msg)
213-
end
214-
return :($(esc(ex)) ? nothing : internal_error($msg))
215-
end
216-
217-
# Parser internal error, used as an assertion failure for cases we expect can't
218-
# happen.
219-
@noinline function internal_error(strs...)
220-
error("Internal error: ", strs...)
221-
end
222-
223204
#-------------------------------------------------------------------------------
224205
# Parsing-specific predicates on tokens/kinds
225206
#

src/utils.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
# Internal error, used as assertion failure for cases we expect can't happen.
2+
@noinline function internal_error(strs...)
3+
error("Internal error: ", strs...)
4+
end
5+
6+
# Like @assert, but always enabled and calls internal_error()
7+
macro check(ex, msgs...)
8+
msg = isempty(msgs) ? ex : msgs[1]
9+
if isa(msg, AbstractString)
10+
msg = msg
11+
elseif !isempty(msgs) && (isa(msg, Expr) || isa(msg, Symbol))
12+
msg = :(string($(esc(msg))))
13+
else
14+
msg = string(msg)
15+
end
16+
return :($(esc(ex)) ? nothing : internal_error($msg))
17+
end
18+
119

220
"""
321
Like printstyled, but allows providing RGB colors for true color terminals

src/value_parsing.jl

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ function julia_string_to_number(str::AbstractString, kind)
2121
return x
2222
elseif kind == K"Float"
2323
if !startswith(str,"0x") && 'f' in str && !('p' in str)
24-
# This is kind of awful. Should we have a separate Float32 literal
25-
# type produced by the lexer? The `f` suffix is nonstandard after all.
26-
return Base.parse(Float32, replace(str, 'f'=>'e'))
24+
# TODO: re-detecting Float32 here is kind of awful. Should have a
25+
# separate Float32 literal type produced by the lexer?
26+
x, code = _parse_float(Float32, str)
2727
else
28-
return Base.parse(Float64, str)
28+
x, code = _parse_float(Float64, str)
2929
end
30+
return code === :ok ? x :
31+
code === :underflow ? x : # < TODO: emit warning somehow?
32+
#=code === :overflow=# ErrorVal()
3033
elseif kind == K"HexInt"
3134
ndigits = length(str)-2
3235
return ndigits <= 2 ? Base.parse(UInt8, str) :
@@ -66,6 +69,95 @@ function julia_string_to_number(str::AbstractString, kind)
6669
end
6770

6871

72+
#-------------------------------------------------------------------------------
73+
"""
74+
Like `Base.parse(Union{Float64,Float32}, str)`, but permits float underflow
75+
76+
Parse a Float64. str[firstind:lastind] must be a valid floating point literal
77+
string. If the value is outside Float64 range.
78+
"""
79+
function _parse_float(::Type{T}, str::String,
80+
firstind::Integer, lastind::Integer) where {T} # force specialize with where {T}
81+
strsize = lastind - firstind + 1
82+
bufsz = 50
83+
if strsize < bufsz
84+
buf = Ref{NTuple{bufsz, UInt8}}()
85+
ptr = Base.unsafe_convert(Ptr{UInt8}, pointer_from_objref(buf))
86+
GC.@preserve str buf begin
87+
unsafe_copyto!(ptr, pointer(str, firstind), strsize)
88+
# Ensure ptr is null terminated
89+
unsafe_store!(ptr, UInt8(0), strsize + 1)
90+
_unsafe_parse_float(T, ptr, strsize)
91+
end
92+
else
93+
# Slow path with allocation.
94+
buf = Vector{UInt8}(str[firstind:lastind])
95+
push!(buf, 0x00)
96+
ptr = pointer(buf)
97+
GC.@preserve buf _unsafe_parse_float(T, ptr, strsize)
98+
end
99+
end
100+
101+
function _parse_float(T, str::String)
102+
_parse_float(T, str, firstindex(str), lastindex(str))
103+
end
104+
105+
# Internals of _parse_float, split into a separate function to avoid some
106+
# apparent codegen issues https://github.com/JuliaLang/julia/issues/46509
107+
# (perhaps we don't want the `buf` in `GC.@preserve buf` to be stack allocated
108+
# on one branch and heap allocated in another?)
109+
@inline function _unsafe_parse_float(::Type{Float64}, ptr, strsize)
110+
Libc.errno(0)
111+
endptr = Ref{Ptr{UInt8}}(C_NULL)
112+
x = @ccall jl_strtod_c(ptr::Ptr{UInt8}, endptr::Ptr{Ptr{UInt8}})::Cdouble
113+
@check endptr[] == ptr + strsize
114+
status = :ok
115+
if Libc.errno() == Libc.ERANGE
116+
# strtod man page:
117+
# * If the correct value would cause overflow, plus or
118+
# minus HUGE_VAL, HUGE_VALF, or HUGE_VALL is returned and
119+
# ERANGE is stored in errno.
120+
# * If the correct value would cause underflow, a value with
121+
# magnitude no larger than DBL_MIN, FLT_MIN, or LDBL_MIN is
122+
# returned and ERANGE is stored in errno.
123+
status = abs(x) < 1 ? :underflow : :overflow
124+
end
125+
return (x, status)
126+
end
127+
128+
@inline function _unsafe_parse_float(::Type{Float32}, ptr, strsize)
129+
# Convert float exponent 'f' to 'e' for strtof, eg, 1.0f0 => 1.0e0
130+
# Presumes we can modify the data in ptr!
131+
for p in ptr+strsize-1:-1:ptr
132+
if unsafe_load(p) == UInt8('f')
133+
unsafe_store!(p, UInt8('e'))
134+
break
135+
end
136+
end
137+
Libc.errno(0)
138+
endptr = Ref{Ptr{UInt8}}(C_NULL)
139+
status = :ok
140+
@static if Sys.iswindows()
141+
# Call strtod here and convert to Float32 on the Julia side because
142+
# strtof seems buggy on windows and doesn't set ERANGE correctly on
143+
# overflow. See also
144+
# https://github.com/JuliaLang/julia/issues/46544
145+
x = Float32(@ccall jl_strtod_c(ptr::Ptr{UInt8}, endptr::Ptr{Ptr{UInt8}})::Cdouble)
146+
if isinf(x)
147+
status = :overflow
148+
# Underflow not detected, but that will only be a warning elsewhere.
149+
end
150+
else
151+
x = @ccall jl_strtof_c(ptr::Ptr{UInt8}, endptr::Ptr{Ptr{UInt8}})::Cfloat
152+
end
153+
@check endptr[] == ptr + strsize
154+
if Libc.errno() == Libc.ERANGE
155+
status = abs(x) < 1 ? :underflow : :overflow
156+
end
157+
return (x, status)
158+
end
159+
160+
69161
#-------------------------------------------------------------------------------
70162
is_indentation(c) = c == ' ' || c == '\t'
71163

test/parser.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,7 @@ tests = [
648648
"```cmd```" => "(macrocall :(Core.var\"@cmd\") \"cmd\")"
649649
# literals
650650
"42" => "42"
651+
"1.0e-1000" => "0.0"
651652
"0x123456789abcdefp+0" => "8.19855292164869e16"
652653
# closing tokens
653654
")" => "(error)"

test/value_parsing.jl

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
using JuliaSyntax:
22
julia_string_to_number,
3-
unescape_julia_string
3+
unescape_julia_string,
4+
_parse_float
5+
6+
@testset "Float parsing" begin
7+
# Float64
8+
@test _parse_float(Float64, "123", 1, 3) === (123.0, :ok)
9+
@test _parse_float(Float64, "123", 2, 3) === (23.0, :ok)
10+
@test _parse_float(Float64, "123", 2, 2) === (2.0, :ok)
11+
@test _parse_float(Float64, "1.3", 1, 3) === (1.3, :ok)
12+
@test _parse_float(Float64, "1.3e2", 1, 5) === (1.3e2, :ok)
13+
@test _parse_float(Float64, "1.0e-1000", 1, 9) === (0.0, :underflow)
14+
@test _parse_float(Float64, "1.0e+1000", 1, 9) === (Inf, :overflow)
15+
# Slow path (exceeds static buffer size)
16+
@test _parse_float(Float64, "0.000000000000000000000000000000000000000000000000000000000001") === (1e-60, :ok)
17+
18+
# Float32
19+
@test _parse_float(Float32, "123", 1, 3) === (123.0f0, :ok)
20+
@test _parse_float(Float32, "1.3f2", 1, 5) === (1.3f2, :ok)
21+
if !Sys.iswindows()
22+
@test _parse_float(Float32, "1.0f-50", 1, 7) === (0.0f0, :underflow)
23+
end
24+
@test _parse_float(Float32, "1.0f+50", 1, 7) === (Inf32, :overflow)
25+
26+
# Assertions
27+
@test_throws ErrorException _parse_float(Float64, "x", 1, 1)
28+
@test_throws ErrorException _parse_float(Float64, "1x", 1, 2)
29+
end
430

531
hexint(s) = julia_string_to_number(s, K"HexInt")
632
binint(s) = julia_string_to_number(s, K"BinInt")

0 commit comments

Comments
 (0)