• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37570

pending completion
#37570

push

local

web-flow
Make sure Core.Compiler can throw kwarg mismatch errors (#50174)

The _new_NamedTuple helper was in a Base-only branch, causing

```

julia> Core.eval(Core.Compiler, quote f(;a=1) = a end)
f (generic function with 1 method)

julia> Core.Compiler.f(;b=2)
ERROR: UndefVarError: `_new_NamedTuple` not defined
Stacktrace:
 [1] macro expansion
   @ Core.Compiler ./namedtuple.jl:0 [inlined]
 [2] structdiff(a::@NamedTuple{b::Int64}, b::Type{NamedTuple{(:a,)}})
   @ Core.Compiler ./namedtuple.jl:421
 [3] top-level scope
   @ REPL[2]:1
```

After this change, we have the expected
```
julia> Core.eval(Core.Compiler, quote f(;a=1) = a end)
f (generic function with 1 method)

julia> Core.Compiler.f(;b=2)
ERROR: MethodError: no method matching f(; b::Int64)

Closest candidates are:
  f(; a) got unsupported keyword argument "b"
   @ Core REPL[13]:1

Stacktrace:
 [1] kwerr(kw::@NamedTuple{b::Int64}, args::Function)
   @ Core.Compiler ./error.jl:165
 [2] top-level scope
   @ REPL[14]:1
```

2 of 2 new or added lines in 1 file covered. (100.0%)

72937 of 84140 relevant lines covered (86.69%)

36259858.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.92
/stdlib/Dates/src/parse.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
### Parsing utilities
4

5
_directives(::Type{DateFormat{S,T}}) where {S,T} = T.parameters
15✔
6

7
character_codes(df::Type{DateFormat{S,T}}) where {S,T} = character_codes(_directives(df))
10✔
8
function character_codes(directives::Core.SimpleVector)
15✔
9
    letters = sizehint!(Char[], length(directives))
15✔
10
    for (i, directive) in enumerate(directives)
30✔
11
        if directive <: DatePart
129✔
12
            letter = first(directive.parameters)
69✔
13
            push!(letters, letter)
69✔
14
        end
15
    end
243✔
16
    return letters
15✔
17
end
18

19
genvar(t::DataType) = Symbol(lowercase(string(nameof(t))))
127✔
20

21
"""
22
    tryparsenext_core(str::AbstractString, pos::Int, len::Int, df::DateFormat, raise=false)
23

24
Parse the string according to the directives within the `DateFormat`. Parsing will start at
25
character index `pos` and will stop when all directives are used or we have parsed up to
26
the end of the string, `len`. When a directive cannot be parsed the returned value
27
will be `nothing` if `raise` is false otherwise an exception will be thrown.
28

29
If successful, return a 3-element tuple `(values, pos, num_parsed)`:
30
* `values::Tuple`: A tuple which contains a value
31
  for each `DatePart` within the `DateFormat` in the order
32
  in which they occur. If the string ends before we finish parsing all the directives
33
  the missing values will be filled in with default values.
34
* `pos::Int`: The character index at which parsing stopped.
35
* `num_parsed::Int`: The number of values which were parsed and stored within `values`.
36
  Useful for distinguishing parsed values from default values.
37
"""
38
@generated function tryparsenext_core(str::AbstractString, pos::Int, len::Int,
13✔
39
                                      df::DateFormat, raise::Bool=false)
40
    directives = _directives(df)
5✔
41
    letters = character_codes(directives)
5✔
42

43
    tokens = Type[CONVERSION_SPECIFIERS[letter] for letter in letters]
10✔
44
    value_names = Symbol[genvar(t) for t in tokens]
10✔
45
    value_defaults = Tuple(CONVERSION_DEFAULTS[t] for t in tokens)
5✔
46

47
    # Pre-assign variables to defaults. Allows us to use `@goto done` without worrying about
48
    # unassigned variables.
49
    assign_defaults = Expr[]
5✔
50
    for (name, default) in zip(value_names, value_defaults)
10✔
51
        push!(assign_defaults, quote
23✔
52
            $name = $default
48✔
53
        end)
54
    end
41✔
55

56
    vi = 1
5✔
57
    parsers = Expr[]
5✔
58
    for i = 1:length(directives)
5✔
59
        if directives[i] <: DatePart
43✔
60
            name = value_names[vi]
23✔
61
            vi += 1
23✔
62
            push!(parsers, quote
23✔
63
                pos > len && @goto done
47✔
64
                let val = tryparsenext(directives[$i], str, pos, len, locale)
94✔
65
                    val === nothing && @goto error
47✔
66
                    $name, pos = val
47✔
67
                end
68
                num_parsed += 1
47✔
69
                directive_index += 1
47✔
70
            end)
71
        else
72
            push!(parsers, quote
20✔
73
                pos > len && @goto done
37✔
74
                let val = tryparsenext(directives[$i], str, pos, len, locale)
72✔
75
                    val === nothing && @goto error
36✔
76
                    delim, pos = val
36✔
77
                end
78
                directive_index += 1
36✔
79
            end)
80
        end
81
    end
43✔
82

83
    return quote
5✔
84
        directives = df.tokens
13✔
85
        locale::DateLocale = df.locale
13✔
86

87
        num_parsed = 0
13✔
88
        directive_index = 1
13✔
89

90
        $(assign_defaults...)
×
91
        $(parsers...)
×
92

93
        pos > len || @goto error
12✔
94

95
        @label done
×
96
        return $(Expr(:tuple, value_names...)), pos, num_parsed
13✔
97

98
        @label error
×
99
        if raise
×
100
            if directive_index > length(directives)
×
101
                throw(ArgumentError("Found extra characters at the end of date time string"))
×
102
            else
103
                d = directives[directive_index]
×
104
                throw(ArgumentError("Unable to parse date time. Expected directive $d at char $pos"))
×
105
            end
106
        end
107
        return nothing
×
108
    end
109
end
110

111
"""
112
    tryparsenext_internal(::Type{<:TimeType}, str, pos, len, df::DateFormat, raise=false)
113

114
Parse the string according to the directives within the `DateFormat`. The specified `TimeType`
115
type determines the type of and order of tokens returned. If the given `DateFormat` or string
116
does not provide a required token a default value will be used. When the string cannot be
117
parsed the returned value will be `nothing` if `raise` is false otherwise an exception will
118
be thrown.
119

120
If successful, returns a 2-element tuple `(values, pos)`:
121
* `values::Tuple`: A tuple which contains a value
122
  for each token as specified by the passed in type.
123
* `pos::Int`: The character index at which parsing stopped.
124
"""
125
@generated function tryparsenext_internal(::Type{T}, str::AbstractString, pos::Int, len::Int,
26✔
126
                                          df::DateFormat, raise::Bool=false) where T<:TimeType
127
    letters = character_codes(df)
20✔
128

129
    tokens = Type[CONVERSION_SPECIFIERS[letter] for letter in letters]
20✔
130
    value_names = Symbol[genvar(t) for t in tokens]
20✔
131

132
    output_tokens = CONVERSION_TRANSLATIONS[T]
10✔
133
    output_names = Symbol[genvar(t) for t in output_tokens]
68✔
134
    output_defaults = Tuple(CONVERSION_DEFAULTS[t] for t in output_tokens)
10✔
135

136
    # Pre-assign output variables to defaults. Ensures that all output variables are
137
    # assigned as the value tuple returned from `tryparsenext_core` may not include all
138
    # of the required variables.
139
    assign_defaults = Expr[
68✔
140
        quote
141
            $name = $default
57✔
142
        end
143
        for (name, default) in zip(output_names, output_defaults)
144
    ]
145

146
    # Unpacks the value tuple returned by `tryparsenext_core` into separate variables.
147
    value_tuple = Expr(:tuple, value_names...)
10✔
148

149
    return quote
10✔
150
        val = tryparsenext_core(str, pos, len, df, raise)
13✔
151
        val === nothing && return nothing
13✔
152
        values, pos, num_parsed = val
13✔
153
        $(assign_defaults...)
×
154
        $value_tuple = values
13✔
155
        return $(Expr(:tuple, output_names...)), pos
13✔
156
    end
157
end
158

159
@inline function tryparsenext_base10(str::AbstractString, i::Int, len::Int, min_width::Int=1, max_width::Int=0)
173✔
160
    i > len && return nothing
191✔
161
    min_pos = min_width <= 0 ? i : i + min_width - 1
202✔
162
    max_pos = max_width <= 0 ? len : min(i + max_width - 1, len)
155✔
163
    d::Int64 = 0
155✔
164
    @inbounds while i <= max_pos
155✔
165
        c, ii = iterate(str, i)::Tuple{Char, Int}
838✔
166
        if '0' <= c <= '9'
419✔
167
            d = d * 10 + (c - '0')
365✔
168
        else
169
            break
54✔
170
        end
171
        i = ii
365✔
172
    end
365✔
173
    if i <= min_pos
155✔
174
        return nothing
×
175
    else
176
        return d, i
155✔
177
    end
178
end
179

180
@inline function tryparsenext_word(str::AbstractString, i, len, locale, maxchars=0)
×
181
    word_start, word_end = i, 0
×
182
    max_pos = maxchars <= 0 ? len : min(len, nextind(str, i, maxchars-1))
×
183
    @inbounds while i <= max_pos
×
184
        c, ii = iterate(str, i)::Tuple{Char, Int}
×
185
        if isletter(c)
×
186
            word_end = i
×
187
        else
188
            break
×
189
        end
190
        i = ii
×
191
    end
×
192
    if word_end == 0
×
193
        return nothing
×
194
    else
195
        return SubString(str, word_start, word_end), i
×
196
    end
197
end
198

199
function Base.parse(::Type{DateTime}, s::AbstractString, df::typeof(ISODateTimeFormat))
18✔
200
    i, end_pos = firstindex(s), lastindex(s)
18✔
201
    i > end_pos && throw(ArgumentError("Cannot parse an empty string as a DateTime"))
18✔
202

203
    local dy
×
204
    dm = dd = Int64(1)
18✔
205
    th = tm = ts = tms = Int64(0)
18✔
206

207
    let val = tryparsenext_base10(s, i, end_pos, 1)
36✔
208
        val === nothing && @goto error
18✔
209
        dy, i = val
18✔
210
        i > end_pos && @goto error
18✔
211
    end
212

213
    c, i = iterate(s, i)::Tuple{Char, Int}
36✔
214
    c != '-' && @goto error
18✔
215
    i > end_pos && @goto done
18✔
216

217
    let val = tryparsenext_base10(s, i, end_pos, 1, 2)
36✔
218
        val === nothing && @goto error
18✔
219
        dm, i = val
18✔
220
        i > end_pos && @goto done
18✔
221
    end
222

223
    c, i = iterate(s, i)::Tuple{Char, Int}
36✔
224
    c != '-' && @goto error
18✔
225
    i > end_pos && @goto done
18✔
226

227
    let val = tryparsenext_base10(s, i, end_pos, 1, 2)
36✔
228
        val === nothing && @goto error
18✔
229
        dd, i = val
18✔
230
        i > end_pos && @goto done
18✔
231
    end
232

233
    c, i = iterate(s, i)::Tuple{Char, Int}
34✔
234
    c != 'T' && @goto error
17✔
235
    i > end_pos && @goto done
17✔
236

237
    let val = tryparsenext_base10(s, i, end_pos, 1, 2)
34✔
238
        val === nothing && @goto error
17✔
239
        th, i = val
17✔
240
        i > end_pos && @goto done
17✔
241
    end
242

243
    c, i = iterate(s, i)::Tuple{Char, Int}
34✔
244
    c != ':' && @goto error
17✔
245
    i > end_pos && @goto done
17✔
246

247
    let val = tryparsenext_base10(s, i, end_pos, 1, 2)
34✔
248
        val === nothing && @goto error
17✔
249
        tm, i = val
17✔
250
        i > end_pos && @goto done
17✔
251
    end
252

253
    c, i = iterate(s, i)::Tuple{Char, Int}
34✔
254
    c != ':' && @goto error
17✔
255
    i > end_pos && @goto done
17✔
256

257
    let val = tryparsenext_base10(s, i, end_pos, 1, 2)
34✔
258
        val === nothing && @goto error
17✔
259
        ts, i = val
17✔
260
        i > end_pos && @goto done
17✔
261
    end
262

263
    c, i = iterate(s, i)::Tuple{Char, Int}
14✔
264
    c != '.' && @goto error
7✔
265
    i > end_pos && @goto done
3✔
266

267
    let val = tryparsenext_base10(s, i, end_pos, 1, 3)
6✔
268
        val === nothing && @goto error
3✔
269
        tms, j = val
3✔
270
        tms *= 10 ^ (3 - (j - i))
3✔
271
        j > end_pos || @goto error
3✔
272
    end
273

274
    @label done
×
275
    return DateTime(dy, dm, dd, th, tm, ts, tms)
13✔
276

277
    @label error
×
278
    throw(ArgumentError("Invalid DateTime string"))
5✔
279
end
280

281
function Base.parse(::Type{T}, str::AbstractString, df::DateFormat=default_format(T)) where T<:TimeType
34✔
282
    pos, len = firstindex(str), lastindex(str)
34✔
283
    pos > len && throw(ArgumentError("Cannot parse an empty string as a Date or Time"))
13✔
284
    val = tryparsenext_internal(T, str, pos, len, df, true)
26✔
285
    @assert val !== nothing
13✔
286
    values, endpos = val
13✔
287
    return T(values...)::T
13✔
288
end
289

290
function Base.tryparse(::Type{T}, str::AbstractString, df::DateFormat=default_format(T)) where T<:TimeType
×
291
    pos, len = firstindex(str), lastindex(str)
×
292
    pos > len && return nothing
×
293
    res = tryparsenext_internal(T, str, pos, len, df, false)
×
294
    res === nothing && return nothing
×
295
    values, endpos = res
×
296
    if validargs(T, values...) === nothing
×
297
        # TODO: validargs gets called twice, since it's called again in the T constructor
298
        return T(values...)::T
×
299
    end
300
    return nothing
×
301
end
302

303
"""
304
    parse_components(str::AbstractString, df::DateFormat) -> Array{Any}
305

306
Parse the string into its components according to the directives in the `DateFormat`.
307
Each component will be a distinct type, typically a subtype of Period. The order of the
308
components will match the order of the `DatePart` directives within the `DateFormat`. The
309
number of components may be less than the total number of `DatePart`.
310
"""
311
@generated function parse_components(str::AbstractString, df::DateFormat)
×
312
    letters = character_codes(df)
×
313
    tokens = Type[CONVERSION_SPECIFIERS[letter] for letter in letters]
×
314

315
    return quote
×
316
        pos, len = firstindex(str), lastindex(str)
×
317
        val = tryparsenext_core(str, pos, len, df, #=raise=#true)
×
318
        @assert val !== nothing
×
319
        values, pos, num_parsed = val
×
320
        types = $(Expr(:tuple, tokens...))
×
321
        result = Vector{Any}(undef, num_parsed)
×
322
        for (i, typ) in enumerate(types)
×
323
            i > num_parsed && break
×
324
            result[i] = typ(values[i])  # Constructing types takes most of the time
×
325
        end
×
326
        return result
×
327
    end
328
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc