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

JuliaLang / julia / #37632

26 Sep 2023 06:44AM UTC coverage: 86.999% (-0.9%) from 87.914%
#37632

push

local

web-flow
inference: make `throw` block deoptimization concrete-eval friendly (#49235)

The deoptimization can sometimes destroy the effects analysis and
disable [semi-]concrete evaluation that is otherwise possible. This is
because the deoptimization was designed with the type domain
profitability in mind (#35982), and hasn't been adequately considering
the effects domain.

This commit makes the deoptimization aware of the effects domain more
and enables the `throw` block deoptimization only when the effects
already known to be ineligible for concrete-evaluation.

In our current effect system, `ALWAYS_FALSE`/`false` means that the
effect can not be refined to `ALWAYS_TRUE`/`true` anymore (unless given
user annotation later). Therefore we can enable the `throw` block
deoptimization without hindering the chance of concrete-evaluation when
any of the following conditions are met:
- `effects.consistent === ALWAYS_FALSE`
- `effects.effect_free === ALWAYS_FALSE`
- `effects.terminates === false`
- `effects.nonoverlayed === false`

Here are some numbers:

| Metric | master | this commit | #35982 reverted (set
`unoptimize_throw_blocks=false`) |

|-------------------------|-----------|-------------|--------------------------------------------|
| Base (seconds) | 15.579300 | 15.206645 | 15.296319 |
| Stdlibs (seconds) | 17.919013 | 17.667094 | 17.738128 |
| Total (seconds) | 33.499279 | 32.874737 | 33.035448 |
| Precompilation (seconds) | 49.967516 | 49.421121 | 49.999998 |
| First time `plot(rand(10,3))` [^1] | `2.476678 seconds (11.74 M
allocations)` | `2.430355 seconds (11.77 M allocations)` | `2.514874
seconds (11.64 M allocations)` |
| First time `solve(prob, QNDF())(5.0)` [^2] | `4.469492 seconds (15.32
M allocations)` | `4.499217 seconds (15.41 M allocations)` | `4.470772
seconds (15.38 M allocations)` |

[^1]: With disabling precompilation of Plots.jl.
[^2]: With disabling precompilation of OrdinaryDiffEq.

These numbers ma... (continued)

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

73407 of 84377 relevant lines covered (87.0%)

11275130.05 hits per line

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

80.39
/stdlib/REPL/src/docview.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
## Code for searching and viewing documentation
4

5
using Markdown
6

7
using Base.Docs: catdoc, modules, DocStr, Binding, MultiDoc, keywords, isfield, namify, bindingexpr,
8
    defined, resolve, getdoc, meta, aliasof, signature
9

10
import Base.Docs: doc, formatdoc, parsedoc, apropos
11

12
using Base: with_output_color, mapany
13

14
import REPL
15

16
using InteractiveUtils: subtypes
17

18
using Unicode: normalize
19

20
## Help mode ##
21

22
# This is split into helpmode and _helpmode to easier unittest _helpmode
23
function helpmode(io::IO, line::AbstractString, mod::Module=Main)
28✔
24
    internal_accesses = Set{Pair{Module,Symbol}}()
28✔
25
    quote
20✔
26
        docs = $REPL.insert_hlines($(REPL._helpmode(io, line, mod, internal_accesses)))
27
        $REPL.insert_internal_warning(docs, $internal_accesses)
28
    end
29
end
30
helpmode(line::AbstractString, mod::Module=Main) = helpmode(stdout, line, mod)
10✔
31

32
# A hack to make the line entered at the REPL available at trimdocs without
33
# passing the string through the entire mechanism.
34
const extended_help_on = Ref{Any}(nothing)
35

36
function _helpmode(io::IO, line::AbstractString, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
108✔
37
    line = strip(line)
108✔
38
    ternary_operator_help = (line == "?" || line == "?:")
128✔
39
    if startswith(line, '?') && !ternary_operator_help
64✔
40
        line = line[2:end]
1✔
41
        extended_help_on[] = nothing
1✔
42
        brief = false
1✔
43
    else
44
        extended_help_on[] = line
63✔
45
        brief = true
63✔
46
    end
47
    # interpret anything starting with # or #= as asking for help on comments
48
    if startswith(line, "#")
128✔
49
        if startswith(line, "#=")
×
50
            line = "#="
×
51
        else
52
            line = "#"
×
53
        end
54
    end
55
    x = Meta.parse(line, raise = false, depwarn = false)
64✔
56
    assym = Symbol(line)
64✔
57
    expr =
137✔
58
        if haskey(keywords, assym) || Base.isoperator(assym) || isexpr(x, :error) ||
59
            isexpr(x, :invalid) || isexpr(x, :incomplete)
60
            # Docs for keywords must be treated separately since trying to parse a single
61
            # keyword such as `function` would throw a parse error due to the missing `end`.
62
            assym
13✔
63
        elseif isexpr(x, (:using, :import))
66✔
64
            (x::Expr).head
4✔
65
        else
66
            # Retrieving docs for macros requires us to make a distinction between the text
67
            # `@macroname` and `@macroname()`. These both parse the same, but are used by
68
            # the docsystem to return different results. The first returns all documentation
69
            # for `@macroname`, while the second returns *only* the docs for the 0-arg
70
            # definition if it exists.
71
            (isexpr(x, :macrocall, 1) && !endswith(line, "()")) ? quot(x) : x
115✔
72
        end
73
    # the following must call repl(io, expr) via the @repl macro
74
    # so that the resulting expressions are evaluated in the Base.Docs namespace
75
    :($REPL.@repl $io $expr $brief $mod $internal_accesses)
64✔
76
end
77
_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod)
28✔
78

79
# Print horizontal lines between each docstring if there are multiple docs
80
function insert_hlines(docs)
22✔
81
    if !isa(docs, Markdown.MD) || !haskey(docs.meta, :results) || isempty(docs.meta[:results])
37✔
82
        return docs
18✔
83
    end
84
    docs = docs::Markdown.MD
×
85
    v = Any[]
4✔
86
    for (n, doc) in enumerate(docs.content)
8✔
87
        push!(v, doc)
4✔
88
        n == length(docs.content) || push!(v, Markdown.HorizontalRule())
4✔
89
    end
4✔
90
    return Markdown.MD(v)
4✔
91
end
92

93
function formatdoc(d::DocStr)
3,059✔
94
    buffer = IOBuffer()
3,059✔
95
    for part in d.text
6,118✔
96
        formatdoc(buffer, d, part)
3,617✔
97
    end
4,175✔
98
    Markdown.MD(Any[Markdown.parse(seekstart(buffer))])
3,059✔
99
end
100
@noinline formatdoc(buffer, d, part) = print(buffer, part)
3,617✔
101

102
function parsedoc(d::DocStr)
5,849✔
103
    if d.object === nothing
5,849✔
104
        md = formatdoc(d)
3,059✔
105
        md.meta[:module] = d.data[:module]
3,059✔
106
        md.meta[:path]   = d.data[:path]
3,059✔
107
        d.object = md
3,059✔
108
    end
109
    d.object
5,849✔
110
end
111

112
## Trimming long help ("# Extended help")
113

114
struct Message  # For direct messages to the terminal
115
    msg    # AbstractString
2✔
116
    fmt    # keywords to `printstyled`
117
end
118
Message(msg) = Message(msg, ())
×
119

120
function Markdown.term(io::IO, msg::Message, columns)
×
121
    printstyled(io, msg.msg; msg.fmt...)
×
122
end
123

124
trimdocs(doc, brief::Bool) = doc
2✔
125

126
function trimdocs(md::Markdown.MD, brief::Bool)
51✔
127
    brief || return md
55✔
128
    md, trimmed = _trimdocs(md, brief)
47✔
129
    if trimmed
47✔
130
        line = extended_help_on[]
2✔
131
        line = isa(line, AbstractString) ? line : ""
2✔
132
        push!(md.content, Message("Extended help is available with `??$line`", (color=Base.info_color(), bold=true)))
2✔
133
    end
134
    return md
47✔
135
end
136

137
function _trimdocs(md::Markdown.MD, brief::Bool)
87✔
138
    content, trimmed = [], false
87✔
139
    for c in md.content
87✔
140
        if isa(c, Markdown.Header{1}) && isa(c.text, AbstractArray) && !isempty(c.text)
203✔
141
            item = c.text[1]
20✔
142
            if isa(item, AbstractString) &&
71✔
143
                lowercase(item) ∈ ("extended help",
144
                                   "extended documentation",
145
                                   "extended docs")
146
                trimmed = true
×
147
                break
2✔
148
            end
149
        end
150
        c, trm = _trimdocs(c, brief)
201✔
151
        trimmed |= trm
201✔
152
        push!(content, c)
201✔
153
    end
201✔
154
    return Markdown.MD(content, md.meta), trimmed
87✔
155
end
156

157
_trimdocs(md, brief::Bool) = md, false
161✔
158

159

160
is_tuple(expr) = false
24✔
161
is_tuple(expr::Expr) = expr.head == :tuple
×
162

163
struct Logged{F}
164
    f::F
43✔
165
    mod::Module
166
    collection::Set{Pair{Module,Symbol}}
167
end
168
function (la::Logged)(m::Module, s::Symbol)
31✔
169
    m !== la.mod && !Base.ispublic(m, s) && push!(la.collection, m => s)
31✔
170
    la.f(m, s)
31✔
171
end
172
(la::Logged)(args...) = la.f(args...)
×
173

174
function log_nonpublic_access(expr::Expr, mod::Module, internal_access::Set{Pair{Module,Symbol}})
199✔
175
    if expr.head === :. && length(expr.args) == 2 && !is_tuple(expr.args[2])
199✔
176
        Expr(:call, Logged(getproperty, mod, internal_access), log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
24✔
177
    elseif expr.head === :call && expr.args[1] === Base.Docs.Binding
175✔
178
        Expr(:call, Logged(Base.Docs.Binding, mod, internal_access), log_nonpublic_access.(expr.args[2:end], (mod,), (internal_access,))...)
19✔
179
    else
180
        Expr(expr.head, log_nonpublic_access.(expr.args, (mod,), (internal_access,))...)
156✔
181
    end
182
end
183
log_nonpublic_access(expr, ::Module, _) = expr
27✔
184

185
function insert_internal_warning(md::Markdown.MD, internal_access::Set{Pair{Module,Symbol}})
20✔
186
    if !isempty(internal_access)
20✔
187
        items = Any[Any[Markdown.Paragraph(Any[Markdown.Code("", s)])] for s in sort("$mod.$sym" for (mod, sym) in internal_access)]
12✔
188
        admonition = Markdown.Admonition("warning", "Warning", Any[
12✔
189
            Markdown.Paragraph(Any["The following bindings may be internal; they may change or be removed in future versions:"]),
190
            Markdown.List(items, -1, false)])
191
        pushfirst!(md.content, admonition)
6✔
192
    end
193
    md
20✔
194
end
195
function insert_internal_warning(other, internal_access::Set{Pair{Module,Symbol}})
×
196
    println("oops.")
×
197
    other
×
198
end
199

200
"""
201
    Docs.doc(binding, sig)
202

203
Return all documentation that matches both `binding` and `sig`.
204

205
If `getdoc` returns a non-`nothing` result on the value of the binding, then a
206
dynamic docstring is returned instead of one based on the binding itself.
207
"""
208
function doc(binding::Binding, sig::Type = Union{})
215✔
209
    if defined(binding)
215✔
210
        result = getdoc(resolve(binding), sig)
175✔
211
        result === nothing || return result
×
212
    end
213
    results, groups = DocStr[], MultiDoc[]
184✔
214
    # Lookup `binding` and `sig` for matches in all modules of the docsystem.
215
    for mod in modules
184✔
216
        dict = meta(mod; autoinit=false)
26,578✔
217
        isnothing(dict) && continue
26,578✔
218
        if haskey(dict, binding)
13,379✔
219
            multidoc = dict[binding]
90✔
220
            push!(groups, multidoc)
90✔
221
            for msig in multidoc.order
90✔
222
                sig <: msig && push!(results, multidoc.docs[msig])
90✔
223
            end
90✔
224
        end
225
    end
13,473✔
226
    if isempty(groups)
184✔
227
        # When no `MultiDoc`s are found that match `binding` then we check whether `binding`
228
        # is an alias of some other `Binding`. When it is we then re-run `doc` with that
229
        # `Binding`, otherwise if it's not an alias then we generate a summary for the
230
        # `binding` and display that to the user instead.
231
        alias = aliasof(binding)
96✔
232
        alias == binding ? summarize(alias, sig) : doc(alias, sig)
96✔
233
    else
234
        # There was at least one match for `binding` while searching. If there weren't any
235
        # matches for `sig` then we concatenate *all* the docs from the matching `Binding`s.
236
        if isempty(results)
88✔
237
            for group in groups, each in group.order
3✔
238
                push!(results, group.docs[each])
3✔
239
            end
6✔
240
        end
241
        # Get parsed docs and concatenate them.
242
        md = catdoc(mapany(parsedoc, results)...)
88✔
243
        # Save metadata in the generated markdown.
244
        if isa(md, Markdown.MD)
88✔
245
            # We don't know how to insert an internal symbol warning into non-markdown
246
            # content, so we don't.
247
            md.meta[:results] = results
86✔
248
            md.meta[:binding] = binding
86✔
249
            md.meta[:typesig] = sig
86✔
250
        end
251
        return md
88✔
252
    end
253
end
254

255
# Some additional convenience `doc` methods that take objects rather than `Binding`s.
256
doc(obj::UnionAll) = doc(Base.unwrap_unionall(obj))
×
257
doc(object, sig::Type = Union{}) = doc(aliasof(object, typeof(object)), sig)
296✔
258
doc(object, sig...)              = doc(object, Tuple{sig...})
×
259

260
function lookup_doc(ex)
49✔
261
    if isa(ex, Expr) && ex.head !== :(.) && Base.isoperator(ex.head)
30✔
262
        # handle syntactic operators, e.g. +=, ::, .=
263
        ex = ex.head
×
264
    end
265
    if haskey(keywords, ex)
49✔
266
        return parsedoc(keywords[ex])
6✔
267
    elseif Meta.isexpr(ex, :incomplete)
25✔
268
        return :($(Markdown.md"No documentation found."))
×
269
    elseif !isa(ex, Expr) && !isa(ex, Symbol)
25✔
270
        return :($(doc)($(typeof)($(esc(ex)))))
1✔
271
    end
272
    if isa(ex, Symbol) && Base.isoperator(ex)
54✔
273
        str = string(ex)
6✔
274
        isdotted = startswith(str, ".")
12✔
275
        if endswith(str, "=") && Base.operator_precedence(ex) == Base.prec_assignment && ex !== :(:=)
6✔
276
            op = chop(str)
2✔
277
            eq = isdotted ? ".=" : "="
2✔
278
            return Markdown.parse("`x $op= y` is a synonym for `x $eq x $op y`")
2✔
279
        elseif isdotted && ex !== :(..)
4✔
280
            op = str[2:end]
2✔
281
            if op in ("&&", "||")
3✔
282
                return Markdown.parse("`x $ex y` broadcasts the boolean operator `$op` to `x` and `y`. See [`broadcast`](@ref).")
×
283
            else
284
                return Markdown.parse("`x $ex y` is akin to `broadcast($op, x, y)`. See [`broadcast`](@ref).")
1✔
285
            end
286
        end
287
    end
288
    binding = esc(bindingexpr(namify(ex)))
43✔
289
    if isexpr(ex, :call) || isexpr(ex, :macrocall) || isexpr(ex, :where)
48✔
290
        sig = esc(signature(ex))
4✔
291
        :($(doc)($binding, $sig))
4✔
292
    else
293
        :($(doc)($binding))
35✔
294
    end
295
end
296

297
# Object Summaries.
298
# =================
299

300
function summarize(binding::Binding, sig)
96✔
301
    io = IOBuffer()
96✔
302
    if defined(binding)
96✔
303
        binding_res = resolve(binding)
86✔
304
        if !isa(binding_res, Module)
86✔
305
            if Base.ispublic(binding.mod, binding.var)
3✔
306
                println(io, "No documentation found for public symbol.\n")
×
307
            else
308
                println(io, "No documentation found for private symbol.\n")
3✔
309
            end
310
        end
311
        summarize(io, binding_res, binding)
86✔
312
    else
313
        println(io, "No documentation found.\n")
10✔
314
        quot = any(isspace, sprint(print, binding)) ? "'" : ""
10✔
315
        if Base.isbindingresolved(binding.mod, binding.var)
10✔
316
            println(io, "Binding ", quot, "`", binding, "`", quot, " exists, but has not been assigned a value.")
1✔
317
        else
318
            println(io, "Binding ", quot, "`", binding, "`", quot, " does not exist.")
9✔
319
        end
320
    end
321
    md = Markdown.parse(seekstart(io))
96✔
322
    # Save metadata in the generated markdown.
323
    md.meta[:results] = DocStr[]
96✔
324
    md.meta[:binding] = binding
96✔
325
    md.meta[:typesig] = sig
96✔
326
    return md
96✔
327
end
328

329
function summarize(io::IO, λ::Function, binding::Binding)
×
330
    kind = startswith(string(binding.var), '@') ? "macro" : "`Function`"
×
331
    println(io, "`", binding, "` is a ", kind, ".")
×
332
    println(io, "```\n", methods(λ), "\n```")
×
333
end
334

335
function summarize(io::IO, TT::Type, binding::Binding)
3✔
336
    println(io, "# Summary")
3✔
337
    T = Base.unwrap_unionall(TT)
3✔
338
    if T isa DataType
3✔
339
        println(io, "```")
3✔
340
        print(io,
6✔
341
            Base.isabstracttype(T) ? "abstract type " :
342
            Base.ismutabletype(T)  ? "mutable struct " :
343
            Base.isstructtype(T) ? "struct " :
344
            "primitive type ")
345
        supert = supertype(T)
3✔
346
        println(io, T)
3✔
347
        println(io, "```")
3✔
348
        if !Base.isabstracttype(T) && T.name !== Tuple.name && !isempty(fieldnames(T))
3✔
349
            println(io, "# Fields")
×
350
            println(io, "```")
×
351
            pad = maximum(length(string(f)) for f in fieldnames(T))
×
352
            for (f, t) in zip(fieldnames(T), fieldtypes(T))
×
353
                println(io, rpad(f, pad), " :: ", t)
×
354
            end
×
355
            println(io, "```")
×
356
        end
357
        subt = subtypes(TT)
3✔
358
        if !isempty(subt)
3✔
359
            println(io, "# Subtypes")
×
360
            println(io, "```")
×
361
            for t in subt
×
362
                println(io, Base.unwrap_unionall(t))
×
363
            end
×
364
            println(io, "```")
×
365
        end
366
        if supert != Any
3✔
367
            println(io, "# Supertype Hierarchy")
3✔
368
            println(io, "```")
3✔
369
            Base.show_supertypes(io, T)
3✔
370
            println(io)
3✔
371
            println(io, "```")
3✔
372
        end
373
    elseif T isa Union
×
374
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
×
375
        println(io, "# Union Composed of Types")
×
376
        for T1 in Base.uniontypes(T)
×
377
            println(io, " - `", Base.rewrap_unionall(T1, TT), "`")
×
378
        end
×
379
    else # unreachable?
380
        println(io, "`", binding, "` is of type `", typeof(TT), "`.\n")
×
381
    end
382
end
383

384
function find_readme(m::Module)::Union{String, Nothing}
83✔
385
    mpath = pathof(m)
83✔
386
    isnothing(mpath) && return nothing
93✔
387
    !isfile(mpath) && return nothing # modules in sysimage, where src files are omitted
10✔
388
    path = dirname(mpath)
10✔
389
    top_path = pkgdir(m)
20✔
390
    while true
20✔
391
        for file in readdir(path; join=true, sort=true)
20✔
392
            isfile(file) && (basename(lowercase(file)) in ["readme.md", "readme"]) || continue
150✔
393
            return file
2✔
394
        end
86✔
395
        path == top_path && break # go no further than pkgdir
36✔
396
        path = dirname(path) # work up through nested modules
10✔
397
    end
10✔
398
    return nothing
8✔
399
end
400
function summarize(io::IO, m::Module, binding::Binding; nlines::Int = 200)
166✔
401
    readme_path = find_readme(m)
83✔
402
    public = Base.ispublic(binding.mod, binding.var) ? "public" : "internal"
83✔
403
    if isnothing(readme_path)
85✔
404
        println(io, "No docstring or readme file found for $public module `$m`.\n")
81✔
405
    else
406
        println(io, "No docstring found for $public module `$m`.")
2✔
407
    end
408
    exports = filter!(!=(nameof(m)), names(m))
83✔
409
    if isempty(exports)
83✔
410
        println(io, "Module does not have any public names.")
16✔
411
    else
412
        println(io, "# Public names")
67✔
413
        print(io, "  `")
67✔
414
        join(io, exports, "`, `")
67✔
415
        println(io, "`\n")
67✔
416
    end
417
    if !isnothing(readme_path)
85✔
418
        readme_lines = readlines(readme_path)
2✔
419
        isempty(readme_lines) && return  # don't say we are going to print empty file
2✔
420
        println(io, "# Displaying contents of readme found at `$(readme_path)`")
2✔
421
        for line in first(readme_lines, nlines)
2✔
422
            println(io, line)
400✔
423
        end
402✔
424
        length(readme_lines) > nlines && println(io, "\n[output truncated to first $nlines lines]")
83✔
425
    end
426
end
427

428
function summarize(io::IO, @nospecialize(T), binding::Binding)
3✔
429
    T = typeof(T)
3✔
430
    println(io, "`", binding, "` is of type `", T, "`.\n")
3✔
431
    summarize(io, T, binding)
3✔
432
end
433

434
# repl search and completions for help
435

436

437
quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x
120,633✔
438

439
function repl_search(io::IO, s::Union{Symbol,String}, mod::Module)
23✔
440
    pre = "search:"
23✔
441
    print(io, pre)
23✔
442
    printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre))
23✔
443
    println(io, "\n")
23✔
444
end
445

446
# TODO: document where this is used
447
repl_search(s, mod::Module) = repl_search(stdout, s, mod)
×
448

449
function repl_corrections(io::IO, s, mod::Module)
6✔
450
    print(io, "Couldn't find ")
6✔
451
    quot = any(isspace, s) ? "'" : ""
6✔
452
    print(io, quot)
6✔
453
    printstyled(io, s, color=:cyan)
6✔
454
    print(io, quot, '\n')
6✔
455
    print_correction(io, s, mod)
6✔
456
end
457
repl_corrections(s) = repl_corrections(stdout, s)
×
458

459
# inverse of latex_symbols Dict, lazily created as needed
460
const symbols_latex = Dict{String,String}()
461
function symbol_latex(s::String)
43✔
462
    if isempty(symbols_latex) && isassigned(Base.REPL_MODULE_REF)
43✔
463
        for (k,v) in Iterators.flatten((REPLCompletions.latex_symbols,
1✔
464
                                        REPLCompletions.emoji_symbols))
465
            symbols_latex[v] = k
3,699✔
466
        end
3,701✔
467

468
        # Overwrite with canonical mapping when a symbol has several completions (#39148)
469
        merge!(symbols_latex, REPLCompletions.symbols_latex_canonical)
1✔
470
    end
471

472
    return get(symbols_latex, s, "")
43✔
473
end
474
function repl_latex(io::IO, s0::String)
23✔
475
    # This has rampant `Core.Box` problems (#15276). Use the tricks of
476
    # https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
477
    # We're changing some of the values so the `let` trick isn't applicable.
478
    s::String = s0
23✔
479
    latex::String = symbol_latex(s)
23✔
480
    if isempty(latex)
23✔
481
        # Decompose NFC-normalized identifier to match tab-completion
482
        # input if the first search came up empty.
483
        s = normalize(s, :NFD)
20✔
484
        latex = symbol_latex(s)
20✔
485
    end
486
    if !isempty(latex)
23✔
487
        print(io, "\"")
3✔
488
        printstyled(io, s, color=:cyan)
3✔
489
        print(io, "\" can be typed by ")
3✔
490
        printstyled(io, latex, "<tab>", color=:cyan)
3✔
491
        println(io, '\n')
3✔
492
    elseif any(c -> haskey(symbols_latex, string(c)), s)
102✔
493
        print(io, "\"")
1✔
494
        printstyled(io, s, color=:cyan)
1✔
495
        print(io, "\" can be typed by ")
1✔
496
        state::Char = '\0'
1✔
497
        with_output_color(:cyan, io) do io
1✔
498
            for c in s
1✔
499
                cstr = string(c)
8✔
500
                if haskey(symbols_latex, cstr)
8✔
501
                    latex = symbols_latex[cstr]
8✔
502
                    if length(latex) == 3 && latex[2] in ('^','_')
11✔
503
                        # coalesce runs of sub/superscripts
504
                        if state != latex[2]
12✔
505
                            '\0' != state && print(io, "<tab>")
2✔
506
                            print(io, latex[1:2])
2✔
507
                            state = latex[2]
4✔
508
                        end
509
                        print(io, latex[3])
6✔
510
                    else
511
                        if '\0' != state
2✔
512
                            print(io, "<tab>")
1✔
513
                            state = '\0'
1✔
514
                        end
515
                        print(io, latex, "<tab>")
10✔
516
                    end
517
                else
518
                    if '\0' != state
×
519
                        print(io, "<tab>")
×
520
                        state = '\0'
×
521
                    end
522
                    print(io, c)
×
523
                end
524
            end
8✔
525
            '\0' != state && print(io, "<tab>")
1✔
526
        end
527
        println(io, '\n')
20✔
528
    end
529
end
530
repl_latex(s::String) = repl_latex(stdout, s)
×
531

532
macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end
533
macro repl(io, ex, brief, mod, internal_accesses) repl(io, ex; brief, mod, internal_accesses) end
50✔
534

535
function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
46✔
536
    str = string(s)
23✔
537
    quote
29✔
538
        repl_latex($io, $str)
539
        repl_search($io, $str, $mod)
540
        $(if !isdefined(mod, s) && !Base.isbindingresolved(mod, s) && !haskey(keywords, s) && !Base.isoperator(s)
541
               # n.b. we call isdefined for the side-effect of resolving the binding, if possible
542
               :(repl_corrections($io, $str, $mod))
6✔
543
          end)
544
        $(_repl(s, brief, mod, internal_accesses))
545
    end
546
end
547
isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3])
25✔
548

549
repl(io::IO, ex::Expr; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief, mod, internal_accesses)
50✔
550
repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = :(apropos($io, $str))
2✔
551
repl(io::IO, other; brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing) = esc(:(@doc $other)) # TODO: track internal_accesses
2✔
552
#repl(io::IO, other) = lookup_doc(other) # TODO
553

554
repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod)
×
555

556
function _repl(x, brief::Bool=true, mod::Module=Main, internal_accesses::Union{Nothing, Set{Pair{Module,Symbol}}}=nothing)
47✔
557
    if isexpr(x, :call)
40✔
558
        x = x::Expr
×
559
        # determine the types of the values
560
        kwargs = nothing
×
561
        pargs = Any[]
×
562
        for arg in x.args[2:end]
×
563
            if isexpr(arg, :parameters)
×
564
                kwargs = mapany(arg.args) do kwarg
×
565
                    if kwarg isa Symbol
566
                        kwarg = :($kwarg::Any)
567
                    elseif isexpr(kwarg, :kw)
568
                        lhs = kwarg.args[1]
569
                        rhs = kwarg.args[2]
570
                        if lhs isa Symbol
571
                            if rhs isa Symbol
572
                                kwarg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
573
                            else
574
                                kwarg.args[1] = :($lhs::typeof($rhs))
575
                            end
576
                        end
577
                    end
578
                    kwarg
579
                end
580
            elseif isexpr(arg, :kw)
×
581
                if kwargs === nothing
×
582
                    kwargs = Any[]
×
583
                end
584
                lhs = arg.args[1]
×
585
                rhs = arg.args[2]
×
586
                if lhs isa Symbol
×
587
                    if rhs isa Symbol
×
588
                        arg.args[1] = :($lhs::(@isdefined($rhs) ? typeof($rhs) : Any))
×
589
                    else
590
                        arg.args[1] = :($lhs::typeof($rhs))
×
591
                    end
592
                end
593
                push!(kwargs, arg)
×
594
            else
595
                if arg isa Symbol
×
596
                    arg = :($arg::(@isdefined($arg) ? typeof($arg) : Any))
×
597
                elseif !isexpr(arg, :(::))
×
598
                    arg = :(::typeof($arg))
×
599
                end
600
                push!(pargs, arg)
×
601
            end
602
        end
×
603
        if kwargs === nothing
×
604
            x.args = Any[x.args[1], pargs...]
×
605
        else
606
            x.args = Any[x.args[1], Expr(:parameters, kwargs...), pargs...]
×
607
        end
608
    end
609
    #docs = lookup_doc(x) # TODO
610
    docs = esc(:(@doc $x))
57✔
611
    docs = if isfield(x)
40✔
612
        quote
19✔
613
            if isa($(esc(x.args[1])), DataType)
614
                fielddoc($(esc(x.args[1])), $(esc(x.args[2])))
615
            else
616
                $docs
617
            end
618
        end
619
    else
620
        docs
45✔
621
    end
622
    docs = log_nonpublic_access(macroexpand(mod, docs), mod, internal_accesses)
47✔
623
    :(REPL.trimdocs($docs, $brief))
47✔
624
end
625

626
"""
627
    fielddoc(binding, field)
628

629
Return documentation for a particular `field` of a type if it exists.
630
"""
631
function fielddoc(binding::Binding, field::Symbol)
4✔
632
    for mod in modules
4✔
633
        dict = meta(mod; autoinit=false)
584✔
634
        isnothing(dict) && continue
584✔
635
        if haskey(dict, binding)
292✔
636
            multidoc = dict[binding]
×
637
            if haskey(multidoc.docs, Union{})
×
638
                fields = multidoc.docs[Union{}].data[:fields]
×
639
                if haskey(fields, field)
×
640
                    doc = fields[field]
×
641
                    return isa(doc, Markdown.MD) ? doc : Markdown.parse(doc)
×
642
                end
643
            end
644
        end
645
    end
296✔
646
    fs = fieldnames(resolve(binding))
4✔
647
    fields = isempty(fs) ? "no fields" : (length(fs) == 1 ? "field " : "fields ") *
7✔
648
                                          join(("`$f`" for f in fs), ", ", ", and ")
649
    Markdown.parse("`$(resolve(binding))` has $fields.")
4✔
650
end
651

652
# As with the additional `doc` methods, this converts an object to a `Binding` first.
653
fielddoc(object, field::Symbol) = fielddoc(aliasof(object, typeof(object)), field)
4✔
654

655

656
# Search & Rescue
657
# Utilities for correcting user mistakes and (eventually)
658
# doing full documentation searches from the repl.
659

660
# Fuzzy Search Algorithm
661

662
function matchinds(needle, haystack; acronym::Bool = false)
474✔
663
    chars = collect(needle)
237✔
664
    is = Int[]
237✔
665
    lastc = '\0'
×
666
    for (i, char) in enumerate(haystack)
474✔
667
        while !isempty(chars) && isspace(first(chars))
2,056✔
668
            popfirst!(chars) # skip spaces
4✔
669
        end
4✔
670
        isempty(chars) && break
1,039✔
671
        if lowercase(char) == lowercase(chars[1]) &&
1,142✔
672
           (!acronym || !isletter(lastc))
673
            push!(is, i)
311✔
674
            popfirst!(chars)
311✔
675
        end
676
        lastc = char
×
677
    end
1,013✔
678
    return is
237✔
679
end
680

681
longer(x, y) = length(x) ≥ length(y) ? (x, true) : (y, false)
174✔
682

683
bestmatch(needle, haystack) =
174✔
684
    longer(matchinds(needle, haystack, acronym = true),
685
           matchinds(needle, haystack))
686

687
# Optimal string distance: Counts the minimum number of insertions, deletions,
688
# transpositions or substitutions to go from one string to the other.
689
function string_distance(a::AbstractString, lena::Integer, b::AbstractString, lenb::Integer)
98,211✔
690
    if lena > lenb
98,211✔
691
        a, b = b, a
×
692
        lena, lenb = lenb, lena
×
693
    end
694
    start = 0
98,211✔
695
    for (i, j) in zip(a, b)
195,056✔
696
        if a == b
96,980✔
697
            start += 1
183✔
698
        else
699
            break
×
700
        end
701
    end
318✔
702
    start == lena && return lenb - start
98,211✔
703
    vzero = collect(1:(lenb - start))
964,373✔
704
    vone = similar(vzero)
96,797✔
705
    prev_a, prev_b = first(a), first(b)
96,797✔
706
    current = 0
×
707
    for (i, ai) in enumerate(a)
193,594✔
708
        i > start || (prev_a = ai; continue)
304,638✔
709
        left = i - start - 1
304,638✔
710
        current = i - start
304,638✔
711
        transition_next = 0
×
712
        for (j, bj) in enumerate(b)
609,276✔
713
            j > start || (prev_b = bj; continue)
3,230,128✔
714
            # No need to look beyond window of lower right diagonal
715
            above = current
×
716
            this_transition = transition_next
×
717
            transition_next = vone[j - start]
3,230,128✔
718
            vone[j - start] = current = left
3,230,128✔
719
            left = vzero[j - start]
3,230,128✔
720
            if ai != bj
3,230,128✔
721
                # Minimum between substitution, deletion and insertion
722
                current = min(current + 1, above + 1, left + 1)
3,129,369✔
723
                if i > start + 1 && j > start + 1 && ai == prev_b && prev_a == bj
3,129,369✔
724
                    current = min(current, (this_transition += 1))
3,950✔
725
                end
726
            end
727
            vzero[j - start] = current
3,230,128✔
728
            prev_b = bj
×
729
        end
6,155,618✔
730
        prev_a = ai
304,638✔
731
    end
512,479✔
732
    current
96,797✔
733
end
734

735
function fuzzyscore(needle::AbstractString, haystack::AbstractString)
12,251✔
736
    lena, lenb = length(needle), length(haystack)
98,211✔
737
    1 - (string_distance(needle, lena, haystack, lenb) / max(lena, lenb))
98,211✔
738
end
739

740
function fuzzysort(search::String, candidates::Vector{String})
43✔
741
    scores = map(cand -> fuzzyscore(search, cand), candidates)
86,003✔
742
    candidates[sortperm(scores)] |> reverse
43✔
743
end
744

745
# Levenshtein Distance
746

747
function levenshtein(s1, s2)
12,554✔
748
    a, b = collect(s1), collect(s2)
12,554✔
749
    m = length(a)
12,554✔
750
    n = length(b)
12,554✔
751
    d = Matrix{Int}(undef, m+1, n+1)
12,554✔
752

753
    d[1:m+1, 1] = 0:m
71,964✔
754
    d[1, 1:n+1] = 0:n
129,529✔
755

756
    for i = 1:m, j = 1:n
59,381✔
757
        d[i+1,j+1] = min(d[i  , j+1] + 1,
412,962✔
758
                         d[i+1, j  ] + 1,
759
                         d[i  , j  ] + (a[i] != b[j]))
760
    end
447,264✔
761

762
    return d[m+1, n+1]
12,554✔
763
end
764

765
function levsort(search::String, candidates::Vector{String})
6✔
766
    scores = map(cand -> (Float64(levenshtein(search, cand)), -fuzzyscore(search, cand)), candidates)
12,118✔
767
    candidates = candidates[sortperm(scores)]
6✔
768
    i = 0
6✔
769
    for outer i = 1:length(candidates)
12✔
770
        levenshtein(search, candidates[i]) > 3 && break
442✔
771
    end
436✔
772
    return candidates[1:i]
6✔
773
end
774

775
# Result printing
776

777
function printmatch(io::IO, word, match)
118✔
778
    is, _ = bestmatch(word, match)
174✔
779
    for (i, char) = enumerate(match)
236✔
780
        if i in is
1,499✔
781
            printstyled(io, char, bold=true)
244✔
782
        else
783
            print(io, char)
288✔
784
        end
785
    end
532✔
786
end
787

788
function printmatches(io::IO, word, matches; cols::Int = _displaysize(io)[2])
46✔
789
    total = 0
23✔
790
    for match in matches
23✔
791
        total + length(match) + 1 > cols && break
141✔
792
        fuzzyscore(word, match) < 0.5 && break
135✔
793
        print(io, " ")
118✔
794
        printmatch(io, word, match)
118✔
795
        total += length(match) + 1
118✔
796
    end
141✔
797
end
798

799
printmatches(args...; cols::Int = _displaysize(stdout)[2]) = printmatches(stdout, args..., cols = cols)
×
800

801
function print_joined_cols(io::IO, ss::Vector{String}, delim = "", last = delim; cols::Int = _displaysize(io)[2])
12✔
802
    i = 0
6✔
803
    total = 0
6✔
804
    for outer i = 1:length(ss)
12✔
805
        total += length(ss[i])
64✔
806
        total + max(i-2,0)*length(delim) + (i>1 ? 1 : 0)*length(last) > cols && (i-=1; break)
68✔
807
    end
60✔
808
    join(io, ss[1:i], delim, last)
6✔
809
end
810

811
print_joined_cols(args...; cols::Int = _displaysize(stdout)[2]) = print_joined_cols(stdout, args...; cols=cols)
×
812

813
function print_correction(io::IO, word::String, mod::Module)
6✔
814
    cors = map(quote_spaces, levsort(word, accessible(mod)))
6✔
815
    pre = "Perhaps you meant "
6✔
816
    print(io, pre)
6✔
817
    print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre))
6✔
818
    println(io)
6✔
819
    return
6✔
820
end
821

822
# TODO: document where this is used
823
print_correction(word, mod::Module) = print_correction(stdout, word, mod)
×
824

825
# Completion data
826

827

828
moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)
49✔
829

830
filtervalid(names) = filter(x->!occursin(r"#", x), map(string, names))
149,182✔
831

832
accessible(mod::Module) =
49✔
833
    Symbol[filter!(s -> !Base.isdeprecated(mod, s), names(mod, all=true, imported=true));
100,722✔
834
           map(names, moduleusings(mod))...;
835
           collect(keys(Base.Docs.keywords))] |> unique |> filtervalid
836

837
function doc_completions(name, mod::Module=Main)
46✔
838
    res = fuzzysort(name, accessible(mod))
46✔
839

840
    # to insert an entry like `raw""` for `"@raw_str"` in `res`
841
    ms = match.(r"^@(.*?)_str$", res)
86✔
842
    idxs = findall(!isnothing, ms)
86✔
843

844
    # avoid messing up the order while inserting
845
    for i in reverse!(idxs)
43✔
846
        c = only((ms[i]::AbstractMatch).captures)
1,122✔
847
        insert!(res, i, "$(c)\"\"")
1,122✔
848
    end
604✔
849
    res
43✔
850
end
851
doc_completions(name::Symbol) = doc_completions(string(name), mod)
×
852

853

854
# Searching and apropos
855

856
# Docsearch simply returns true or false if an object contains the given needle
857
docsearch(haystack::AbstractString, needle) = occursin(needle, haystack)
5,907✔
858
docsearch(haystack::Symbol, needle) = docsearch(string(haystack), needle)
×
859
docsearch(::Nothing, needle) = false
×
860
function docsearch(haystack::Array, needle)
×
861
    for elt in haystack
×
862
        docsearch(elt, needle) && return true
×
863
    end
×
864
    false
×
865
end
866
function docsearch(haystack, needle)
×
867
    @warn "Unable to search documentation of type $(typeof(haystack))" maxlog=1
×
868
    false
×
869
end
870

871
## Searching specific documentation objects
872
function docsearch(haystack::MultiDoc, needle)
5,370✔
873
    for v in values(haystack.docs)
10,740✔
874
        docsearch(v, needle) && return true
5,753✔
875
    end
3,114✔
876
    false
2,348✔
877
end
878

879
function docsearch(haystack::DocStr, needle)
5,753✔
880
    docsearch(parsedoc(haystack), needle) && return true
5,753✔
881
    if haskey(haystack.data, :fields)
2,731✔
882
        for doc in values(haystack.data[:fields])
268✔
883
            docsearch(doc, needle) && return true
10✔
884
        end
10✔
885
    end
886
    false
2,731✔
887
end
888

889
## doc search
890

891
## Markdown search simply strips all markup and searches plain text version
892
docsearch(haystack::Markdown.MD, needle) = docsearch(stripmd(haystack.content), needle)
5,897✔
893

894
"""
895
    stripmd(x)
896

897
Strip all Markdown markup from x, leaving the result in plain text. Used
898
internally by apropos to make docstrings containing more than one markdown
899
element searchable.
900
"""
901
stripmd(@nospecialize x) = string(x) # for random objects interpolated into the docstring
×
902
stripmd(x::AbstractString) = x  # base case
84,886✔
903
stripmd(x::Nothing) = " "
42✔
904
stripmd(x::Vector) = string(map(stripmd, x)...)
30,293✔
905

906
stripmd(x::Markdown.BlockQuote) = "$(stripmd(x.content))"
×
907
stripmd(x::Markdown.Admonition) = "$(stripmd(x.content))"
975✔
908
stripmd(x::Markdown.Bold) = "$(stripmd(x.text))"
33✔
909
stripmd(x::Markdown.Code) = "$(stripmd(x.code))"
39,328✔
910
stripmd(x::Markdown.Header) = stripmd(x.text)
2,222✔
911
stripmd(x::Markdown.HorizontalRule) = " "
68✔
912
stripmd(x::Markdown.Image) = "$(stripmd(x.alt)) $(x.url)"
4✔
913
stripmd(x::Markdown.Italic) = "$(stripmd(x.text))"
292✔
914
stripmd(x::Markdown.LaTeX) = "$(x.formula)"
364✔
915
stripmd(x::Markdown.LineBreak) = " "
34✔
916
stripmd(x::Markdown.Link) = "$(stripmd(x.text)) $(x.url)"
5,598✔
917
stripmd(x::Markdown.List) = join(map(stripmd, x.items), " ")
570✔
918
stripmd(x::Markdown.MD) = join(map(stripmd, x.content), " ")
5,885✔
919
stripmd(x::Markdown.Paragraph) = stripmd(x.content)
12,995✔
920
stripmd(x::Markdown.Footnote) = "$(stripmd(x.id)) $(stripmd(x.text))"
82✔
921
stripmd(x::Markdown.Table) =
17✔
922
    join([join(map(stripmd, r), " ") for r in x.rows], " ")
923

924
"""
925
    apropos([io::IO=stdout], pattern::Union{AbstractString,Regex})
926

927
Search available docstrings for entries containing `pattern`.
928

929
When `pattern` is a string, case is ignored. Results are printed to `io`.
930

931
`apropos` can be called from the help mode in the REPL by wrapping the query in double quotes:
932
```
933
help?> "pattern"
934
```
935
"""
936
apropos(string) = apropos(stdout, string)
×
937
apropos(io::IO, string) = apropos(io, Regex("\\Q$string", "i"))
1✔
938

939
function apropos(io::IO, needle::Regex)
2✔
940
    for mod in modules
2✔
941
        # Module doc might be in README.md instead of the META dict
942
        docsearch(doc(mod), needle) && println(io, mod)
144✔
943
        dict = meta(mod; autoinit=false)
288✔
944
        isnothing(dict) && continue
288✔
945
        for (k, v) in dict
288✔
946
            docsearch(v, needle) && println(io, k)
5,370✔
947
        end
5,370✔
948
    end
144✔
949
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