diff --git a/AGENTS.md b/AGENTS.md index b80460ae..f4548d40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,7 @@ DocStringExtensions adds abbreviations and templates to Julia's docsystem. An ab - CI runs Julia 1.0, the LTS, the latest release, and nightly. Code in `src/` and `test/` must parse and run on 1.0, so check any syntax or Base function newer than that against the floor. - The package leans on undocumented Base internals (`Base.kwarg_decl`, method table fields, binding partitions on 1.12+). The `Base assumptions` testset in `test/base_assumptions.jl` records each one. Add a case there when the code starts relying on another. - A user-facing change updates every page that shows the feature, in the same commit as the code. Check the docstring, the `Examples` module in `docs/Examples/src/` and its page in `docs/src/examples/`, `docs/src/setup.md`, and `CHANGELOG.md`. Search `docs/` for the names the change touches, then build the docs as CI does. The build runs the doctests and fails on a broken `@ref`. +- From Julia 1.6 the package runs interpreted (`@compiler_options compile = min` in `src/DocStringExtensions.jl`), and it ships no precompile workload, which would slow every dependent's load. Interpreted code costs nothing to compile, but a closure or generator handed to Base, as in `map`, `filter`, `any`, `sort!` with `by` or a comprehension, compiles a new specialization of the Base function on the first render. Write those as loops. - `format` methods can run in a stale world during macro expansion. Wrap binding lookups such as `Docs.resolve` and `methods` in `Base.invokelatest`, as the existing `format` methods do. ## Running tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 6752497f..487201e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Remove the precompilation workload and the PrecompileTools dependency, which made every package depending on DocStringExtensions slower to precompile and load. On Julia 1.6 and later the package runs interpreted, with inference on. Rendering the first docstring of a session takes about 0.2 s, against 1 s in v0.10.0 ([#313](https://github.com/JuliaDocs/DocStringExtensions.jl/issues/313), [#314](https://github.com/JuliaDocs/DocStringExtensions.jl/pull/314)) + ## [v0.10.1](https://github.com/JuliaDocs/DocStringExtensions.jl/releases/tag/v0.10.1) - 2026-10-20 ### Changed diff --git a/Project.toml b/Project.toml index 8e05ccde..0e0df51e 100644 --- a/Project.toml +++ b/Project.toml @@ -2,12 +2,8 @@ name = "DocStringExtensions" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" version = "0.10.1" -[deps] -PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" - [compat] DeepDiffs = "1" -PrecompileTools = "1" julia = "1" [extras] diff --git a/docs/make.jl b/docs/make.jl index a47b3c12..2a811dfd 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -18,7 +18,6 @@ const TOPICS = ["functions", "types", "enums", "modules", "templates"] makedocs( sitename = "DocStringExtensions.jl", modules = [DocStringExtensions, Examples], - checkdocs_ignored_modules = [DocStringExtensions._PrecompileWorkload], clean = false, pages = Any[ "Home" => "index.md", diff --git a/src/DocStringExtensions.jl b/src/DocStringExtensions.jl index f8770c64..959f58dc 100644 --- a/src/DocStringExtensions.jl +++ b/src/DocStringExtensions.jl @@ -37,7 +37,10 @@ $(EXPORTS) """ module DocStringExtensions -using PrecompileTools: @setup_workload, @compile_workload +# A docstring renders once per session, so interpreting the package costs less than compiling it. +@static if VERSION >= v"1.6" + Base.Experimental.@compiler_options compile = min +end # Exports. @@ -97,8 +100,6 @@ let λ = s -> isa(s, Symbol) ? getfield(DocStringExtensions, s) : s end end -include("precompile.jl") - __init__() = (setter!(template_hook); nothing) end # module diff --git a/src/abbreviations/fields.jl b/src/abbreviations/fields.jl index 4a69f344..7921ac27 100644 --- a/src/abbreviations/fields.jl +++ b/src/abbreviations/fields.jl @@ -128,7 +128,10 @@ first_line(text) = occursin('\n', text) ? "$(first(split(text, '\n'))) …" : te # Julia's Markdown reads an even run of backticks as LaTeX, so the fence is the shortest odd # run longer than any in `text`. A space keeps a backtick at either end out of the fence. function code_span(text) - longest = reduce(max, [length(m.match) for m in eachmatch(r"`+", text)]; init = 0) + longest = 0 + for m in eachmatch(r"`+", text) + longest = max(longest, length(m.match)) + end fence = "`"^(isodd(longest) ? longest + 2 : longest + 1) padding = startswith(text, '`') || endswith(text, '`') ? " " : "" return "$fence$padding$text$padding$fence" diff --git a/src/abbreviations/imports.jl b/src/abbreviations/imports.jl index 8ecd9087..3a2da1d6 100644 --- a/src/abbreviations/imports.jl +++ b/src/abbreviations/imports.jl @@ -28,13 +28,19 @@ julia> @doc Plots """ const IMPORTS = ModuleImports() +# Kept out of `format`: Julia compiles any method holding a `ccall`, even in an interpreted module. +module_usings(mod::Module) = ccall(:jl_module_usings, Any, (Any,), mod)::Vector{Any} + function format(::ModuleImports, buf, doc) binding = doc_binding(doc) object = Base.invokelatest(Docs.resolve, binding)::Module - imports = unique(Vector{Module}(ccall(:jl_module_usings, Any, (Any,), object)::Vector{Any})) + imports = Module[] + for mod in module_usings(object) + mod in imports || push!(imports, mod) + end return if !isempty(imports) println(buf) - for mod in sort(imports, by = string) + for mod in insertion_sort!(imports, (a, b) -> string(a) < string(b)) println(buf, " - `", mod, "`") end println(buf) diff --git a/src/abbreviations/instances.jl b/src/abbreviations/instances.jl index cd979783..0902e98f 100644 --- a/src/abbreviations/instances.jl +++ b/src/abbreviations/instances.jl @@ -28,7 +28,8 @@ julia> @doc Fruit const INSTANCES = EnumInstances() function format(::EnumInstances, buf, doc) - entries = Base.invokelatest(enum_entries ∘ Docs.resolve, doc_binding(doc))::Vector{Tuple{String, String}} + enum = Base.invokelatest(Docs.resolve, doc_binding(doc)) + entries = Base.invokelatest(enum_entries, enum)::Vector{Tuple{String, String}} if !isempty(entries) println(buf) for (name, value) in entries @@ -39,8 +40,13 @@ function format(::EnumInstances, buf, doc) return nothing end -enum_entries(enum::Type{<:Base.Enum}, prefix = "") = - [("$prefix$(Symbol(instance))", string(Integer(instance))) for instance in instances(enum)] +function enum_entries(enum::Type{<:Base.Enum}, prefix = "") + entries = Tuple{String, String}[] + for instance in instances(enum) + push!(entries, ("$prefix$(Symbol(instance))", string(Integer(instance)))) + end + return entries +end # EnumX.jl documents the module that holds its enum type `T`, and callers write `Module.Name`. enum_entries(mod::Module) = isdefined(mod, :T) ? enum_entries(mod.T, "$(nameof(mod)).") : Tuple{String, String}[] enum_entries(other, prefix = "") = Tuple{String, String}[] diff --git a/src/abbreviations/module_names.jl b/src/abbreviations/module_names.jl index c12276ab..5a287c60 100644 --- a/src/abbreviations/module_names.jl +++ b/src/abbreviations/module_names.jl @@ -38,14 +38,18 @@ const EXPORTS = ModuleExports() function format(::ModuleExports, buf, doc) object = Base.invokelatest(Docs.resolve, doc_binding(doc))::Module # `names` also returns `public` names on Julia 1.11 and later. - return print_name_list(buf, object, filter(sym -> Base.invokelatest(Base.isexported, object, sym)::Bool, names(object))) + exported = Symbol[] + for sym in names(object) + Base.invokelatest(Base.isexported, object, sym)::Bool && push!(exported, sym) + end + return print_name_list(buf, object, exported) end function print_name_list(buf, object::Module, syms) if !isempty(syms) println(buf) # Sorting ignores the `@` in macro names and sorts them in with others. - for sym in sort(syms, by = s -> lstrip(string(s), '@')) + for sym in insertion_sort!(copy(syms), (a, b) -> lstrip(string(a), '@') < lstrip(string(b), '@')) # Skip the module itself, since that's always exported. sym === nameof(object) && continue # We print linked names using Documenter.jl cross-reference syntax diff --git a/src/abbreviations/signatures.jl b/src/abbreviations/signatures.jl index e0cd635e..10686c33 100644 --- a/src/abbreviations/signatures.jl +++ b/src/abbreviations/signatures.jl @@ -76,21 +76,21 @@ function format(abbr::MethodSignatures, buf, doc) return if !isempty(groups) lines = String[] + io = IOBuffer() for group in groups, method in collapse_defaults(group, defaults) - push!( - lines, sprint( - io -> printmethod( - io, binding, func, method; - definition = abbr.definition, returns = abbr.returns, - brackets = abbr.defaults === :brackets - ) - ) + printmethod( + io, binding, func, method; + definition = abbr.definition, returns = abbr.returns, + brackets = abbr.defaults === :brackets ) + push!(lines, String(take!(io))) end println(buf) println(buf, "```julia") # A struct's typed constructor and its untyped fallback print the same signature. - foreach(line -> println(buf, line), unique(lines)) + for line in unique(lines) + println(buf, line) + end println(buf, "```\n") end end @@ -181,12 +181,18 @@ the method that takes every argument of `definition`. """ collapse_defaults(group, ::Nothing) = group function collapse_defaults(group, definition::DefinitionArguments) - matching = filter(m -> matches_definition(m, definition), group) - isempty(matching) && return group - longest = maximum(m -> length(arguments(m)), matching) - return filter(group) do m - !matches_definition(m, definition) || length(arguments(m)) == longest + longest = nothing + for m in group + matches_definition(m, definition) || continue + n = length(arguments(m)) + (longest === nothing || n > longest) && (longest = n) end + longest === nothing && return group + kept = Method[] + for m in group + (!matches_definition(m, definition) || length(arguments(m)) == longest) && push!(kept, m) + end + return kept end function format(tms::TypedMethodSignatures, buf, doc) @@ -227,9 +233,12 @@ function printed_typesig(func, method::Method, tuples) if idx === nothing n = length(arguments(method)) @static if Sys.iswindows() && VERSION < v"1.8" - idx = findlast(t -> has_arity(t, n), tuples) + candidates = length(tuples):-1:1 else - idx = findfirst(t -> has_arity(t, n), tuples) + candidates = 1:length(tuples) + end + for i in candidates + has_arity(tuples[i], n) && (idx = i; break) end end # Julia 1.12 and later can normalise `typesig` to `Tuple`, leaving no tuple of length `n`. diff --git a/src/abbreviations/subtypes.jl b/src/abbreviations/subtypes.jl index dd56121c..be207612 100644 --- a/src/abbreviations/subtypes.jl +++ b/src/abbreviations/subtypes.jl @@ -36,7 +36,8 @@ julia> @doc Shape const SUBTYPES = TypeSubtypes() function format(::TypeSubtypes, buf, doc) - names = Base.invokelatest(subtype_names ∘ Docs.resolve, doc_binding(doc))::Vector{String} + object = Base.invokelatest(Docs.resolve, doc_binding(doc)) + names = Base.invokelatest(subtype_names, object)::Vector{String} if !isempty(names) println(buf) for name in names diff --git a/src/abbreviations/typedef.jl b/src/abbreviations/typedef.jl index fe9edd0c..4625ac19 100644 --- a/src/abbreviations/typedef.jl +++ b/src/abbreviations/typedef.jl @@ -46,7 +46,10 @@ function print_params(buf, object, io) isempty(object.parameters) && return env = parameter_context(object, io) print(buf, "{") - join(buf, map(p -> parameter_text(p, env), object.parameters), ", ") + for (i, p) in enumerate(object.parameters) + i > 1 && print(buf, ", ") + print(buf, parameter_text(p, env)) + end print(buf, "}") return end diff --git a/src/methods.jl b/src/methods.jl index f3604d94..046d28ab 100644 --- a/src/methods.jl +++ b/src/methods.jl @@ -11,12 +11,14 @@ function struct_constructors(func, definition::StructDefinition, modname) if func isa UnionAll candidates = unique([candidates; constructor_methods(func, Tuple{Vararg{Any}})]) end - found = filter(candidates) do m - m.module == modname || return false - String(m.file) == definition.file && return m.line <= definition.last_line - return definition.wrapped && generated_constructor(func, m) + found = Method[] + for m in candidates + m.module == modname || continue + constructor = String(m.file) == definition.file ? m.line <= definition.last_line : + definition.wrapped && generated_constructor(func, m) + constructor && push!(found, m) end - return sort!(found; by = m -> (length(arguments(m)), string(m.sig))) + return insertion_sort!(found, (a, b) -> isless((length(arguments(a)), string(a.sig)), (length(arguments(b)), string(b.sig)))) end # The keyword constructor `Base.@kwdef` and similar macros generate, and the positional one @@ -50,22 +52,27 @@ function methodgroups(func, typesig, modname; exact = true) end func isa Type && typesig != Union{} && exact && append!(methods, callable_methods(func, typesig)) unique!(methods) - # Group methods by file and line number. - groups = groupby(Tuple{Symbol, Int}, Vector{Method}, methods) do m - (m.file, m.line), m + # Group the methods from `modname` by file and line number. + groups = Vector{Method}[] + for m in methods + m.module == modname || continue + index = findfirst_group(groups, m) + index === nothing ? push!(groups, Method[m]) : push!(groups[index], m) end - # Filter out methods from other modules and with non-matching signatures. + # Filter out methods with non-matching signatures. typesigs = alltypesigs(typesig) results = Vector{Method}[] - for (key, group) in groups - filter!(m -> m.module == modname, group) + for group in groups if exact # Julia 1.12 and later can normalise the union of a definition's signatures # into a single type, such as `Union{Tuple{}, Tuple{Any, Vararg{Any}}} == Tuple`. # When no method matches a component, keep the group if its signatures together # equal `typesig`. - matching = filter(m -> argsig(m) in typesigs, group) + matching = Method[] + for m in group + argsig(m) in typesigs && push!(matching, m) + end if !isempty(matching) || !signatures_equal(group, typesig) group = matching end @@ -73,10 +80,29 @@ function methodgroups(func, typesig, modname; exact = true) isempty(group) || push!(results, group) end - # Sort the groups by file and line. - sort!(results, lt = comparemethods, by = first) + return insertion_sort!(results, (a, b) -> comparemethods(first(a), first(b))) +end - return results +function findfirst_group(groups, m::Method) + for i in eachindex(groups) + first(groups[i]).file === m.file && first(groups[i]).line == m.line && return i + end + return nothing +end + +# `Base.sort!` compiles a specialisation for each `lt`, while this runs interpreted with the +# rest of the package. The lists it sorts hold a few methods. +function insertion_sort!(xs, lt) + for i in 2:length(xs) + x = xs[i] + j = i - 1 + while j >= 1 && lt(x, xs[j]) + xs[j + 1] = xs[j] + j -= 1 + end + xs[j + 1] = x + end + return xs end """ @@ -91,9 +117,13 @@ function documented_groups(groups, doc) path = get(doc.data, :path, nothing)::Union{String, Nothing} line = get(doc.data, :linenumber, nothing)::Union{Int, Nothing} (path === nothing || line === nothing) && return groups - after = filter(g -> String(first(g).file) == path && first(g).line >= line, groups) - isempty(after) && return groups - return [after[argmin(map(g -> first(g).line, after))]] + nearest = nothing + for group in groups + m = first(group) + String(m.file) == path && m.line >= line || continue + (nearest === nothing || m.line < first(nearest).line) && (nearest = group) + end + return nearest === nothing ? groups : [nearest] end # The argument types of `m`, without the function's own type or a macro's hidden arguments. @@ -111,7 +141,12 @@ function ismacro(m::Method) return startswith(String(m.name), "@") && length(params) >= 3 && params[2] === LineNumberNode && params[3] === Module end -ismacro(@nospecialize(f::Function)) = any(ismacro, Base.invokelatest(methods, f)::Base.MethodList) +function ismacro(@nospecialize(f::Function)) + for m in Base.invokelatest(methods, f)::Base.MethodList + ismacro(m) && return true + end + return false +end # The argument types `f` is called with for those in `sig`, which a macro's docstring gives as # its caller writes them. @@ -129,12 +164,22 @@ arguments, up to one past the most parameters any tuple declares. Larger counts varargs and compare the same. """ function signatures_equal(group, typesig) - sigs = map(argsig, group) + sigs = Any[] + for m in group + push!(sigs, argsig(m)) + end combined = Union{sigs...} - longest = mapreduce(t -> length(tuple_parameters(t)), max, [alltypesigs(typesig); sigs]; init = 0) - return all(0:(longest + 1)) do n - typeintersect(typesig, NTuple{n, Any}) == typeintersect(combined, NTuple{n, Any}) + longest = 0 + for t in sigs + longest = max(longest, length(tuple_parameters(t))) + end + for t in alltypesigs(typesig) + longest = max(longest, length(tuple_parameters(t))) + end + for n in 0:(longest + 1) + typeintersect(typesig, NTuple{n, Any}) == typeintersect(combined, NTuple{n, Any}) || return false end + return true end """ @@ -189,10 +234,12 @@ end # Sets `params[i]` to the part of `t` in the place of `vars[i]` within `pattern`. function match_parameters!(params, vars, pattern, t) if pattern isa TypeVar - i = findfirst(v -> v === pattern, vars) + i = findfirst_identical(pattern, vars) i === nothing || (params[i] = t) elseif pattern isa DataType && t isa DataType && pattern.name === t.name - foreach((p, q) -> match_parameters!(params, vars, p, q), pattern.parameters, t.parameters) + for i in 1:min(length(pattern.parameters), length(t.parameters)) + match_parameters!(params, vars, pattern.parameters[i], t.parameters[i]) + end end return params end @@ -255,12 +302,15 @@ every method that matches, so a more specific method whose signature overlaps `m skipped by its position in `methods(func, typesig)`. """ function method_return_type(func, method::Method, typesig) - matching = collect(Base.invokelatest(methods, func, typesig)::Base.MethodList) - idx = findfirst(m -> m === method, matching) + matching = Method[] + for m in Base.invokelatest(methods, func, typesig)::Base.MethodList + push!(matching, m) + end + idx = findfirst_identical(method, matching) idx === nothing && return signature_return_type(method, typesig) # Inference cannot run a generator on abstract argument types: Julia 1.0 throws and 1.10 # hits a `BoundsError` in Base. Other versions infer `Any` for a generated method itself. - any(hasgenerator, matching) && !isconcretetype(typesig) && return [] + any_generated(matching) && !isconcretetype(typesig) && return [] return Any[(Base.invokelatest(Base.return_types, func, typesig)::Vector{Any})[idx]] end @@ -273,14 +323,24 @@ function signature_return_type(method::Method, typesig) tt = typeintersect(method.sig, Base.rewrap_unionall(Tuple{Any, args.parameters...}, typesig)) tt === Union{} && return [] world = Base.get_world_counter() - matching = Any[match.method for match in Base.invokelatest(Base._methods_by_ftype, tt, -1, world)] - idx = findfirst(m -> m === method, matching) + matching = Method[] + for match in Base.invokelatest(Base._methods_by_ftype, tt, -1, world) + push!(matching, match.method) + end + idx = findfirst_identical(method, matching) idx === nothing && return [] - any(hasgenerator, matching) && !isconcretetype(tt) && return [] + any_generated(matching) && !isconcretetype(tt) && return [] typed = Base.invokelatest(Base.code_typed_by_type, tt; optimize = false, world = world) return Any[last(typed[idx])] end +function any_generated(methods) + for m in methods + hasgenerator(m) && return true + end + return false +end + if isdefined(Base, :hasgenerator) const hasgenerator = Base.hasgenerator else @@ -394,11 +454,17 @@ The methods matching the full call signature `tt`, a `Tuple` type of the callabl the argument types. """ function matched_methods(tt) - world = ccall(:jl_get_world_counter, UInt, ()) + found = Method[] # Julia 1.0 gives each match as an `svec` holding the method third. - return Method[m isa Core.SimpleVector ? m[3] : m.method for m in Base._methods_by_ftype(tt, -1, world)] + for m in Base._methods_by_ftype(tt, -1, world_counter()) + push!(found, m isa Core.SimpleVector ? m[3] : m.method) + end + return found end +# Julia compiles any method holding a `ccall`, even in an interpreted module. +world_counter() = ccall(:jl_get_world_counter, UInt, ()) + """ $(:SIGNATURES) @@ -438,46 +504,16 @@ function alltypesigs(sig)::Vector{Any} elseif isa(sig, Union) uniontypes(sig) elseif isa(sig, UnionAll) - Any[ - Base.rewrap_unionall(usig, sig) for - usig in uniontypes(Base.unwrap_unionall(sig)) - ] + sigs = Any[] + for usig in uniontypes(Base.unwrap_unionall(sig)) + push!(sigs, Base.rewrap_unionall(usig, sig)) + end + sigs else Any[sig] end end -""" -$(:SIGNATURES) - -A helper method for [`groupby`](@ref) that uses a pre-allocated `groups` `Dict`. -""" -function groupby!(f, groups, data) - for each in data - key, value = f(each) - push!(get!(groups, key, []), value) - end - return sort!(collect(groups), by = first) -end - -""" -$(:SIGNATURES) - -Group `data` using function `f` where key type is specified by `K` and group type by `V`. - -The function `f` takes a single argument, an element of `data`, and should return a 2-tuple -of `(computed_key, element)`. See the example below for details. - -# Examples - -```julia -groupby(Int, Vector{Int}, collect(1:10)) do num - mod(num, 3), num -end -``` -""" -groupby(f, K, V, data) = groupby!(f, Dict{K, V}(), data) - get_method_source(m::Method) = Base.uncompressed_ast(m)::Core.CodeInfo nargs(m::Method) = m.nargs @@ -512,13 +548,18 @@ function keywords(func, m::Method) if !isa(kwargs, Vector) || isempty(kwargs) return Symbol[] end - filter!(arg -> !occursin("#", string(arg)), kwargs) + named = Symbol[] + index = nothing + for arg in kwargs + occursin("#", string(arg)) && continue + push!(named, arg) + index === nothing && endswith(string(arg), "...") && (index = length(named)) + end # Keywords *may* not be sorted correctly. We move the vararg one to the end. - index = findfirst(arg -> endswith(string(arg), "..."), kwargs) if index != nothing - kwargs[index], kwargs[end] = kwargs[end], kwargs[index] + named[index], named[end] = named[end], named[index] end - return kwargs + return named end """ @@ -559,13 +600,28 @@ function keyword_types(method::Method, kws, vars) body = isempty(kws) ? nothing : keyword_body(method) body === nothing && return types, TypeVar[] own = typevars(body.sig) - shared = Dict(v.name => v for v in vars) - sig = isempty(own) ? body.sig : body.sig{[get(shared, v.name, v) for v in own]...} - names = get_method_source(body).slotnames[2:nargs(body)] - for (name, type) in zip(names, tuple_parameters(sig)[2:end]) - types[string(name)] = type + # A keyword type parameter named as one in `vars` is that parameter. + params = Any[] + keyword_only = TypeVar[] + for v in own + i = findlast_named(vars, v.name) + i === nothing && push!(keyword_only, v) + push!(params, i === nothing ? v : vars[i]) end - return types, filter(v -> !haskey(shared, v.name), own) + sig = isempty(own) ? body.sig : body.sig{params...} + names = get_method_source(body).slotnames + sigtypes = tuple_parameters(sig) + for i in 2:min(nargs(body), length(sigtypes)) + types[string(names[i])] = sigtypes[i] + end + return types, keyword_only +end + +function findlast_named(vars, name::Symbol) + for i in length(vars):-1:1 + vars[i].name === name && return i + end + return nothing end @@ -583,7 +639,11 @@ args = arguments(first(methods(f))) ``` """ function arguments(m::Method) - return String[isunnamed(arg) ? "_" : string(arg) for arg in slot_names(m)[(2 + hidden_arguments(m)):end]] + names = String[] + for arg in slot_names(m)[(2 + hidden_arguments(m)):end] + push!(names, isunnamed(arg) ? "_" : string(arg)) + end + return names end # The names of the argument slots of `m`, the callable itself first. Empty when `m` records none. diff --git a/src/precompile.jl b/src/precompile.jl deleted file mode 100644 index 23c19d1a..00000000 --- a/src/precompile.jl +++ /dev/null @@ -1,66 +0,0 @@ -# Representative docstrings expanded during precompilation -module _PrecompileWorkload - - using ..DocStringExtensions - - const PtrOrRef{T} = Union{Ptr{T}, Ref{T}} - - """ - $(TYPEDEF) - - $(TYPEDFIELDS) - """ - struct Point{T <: Real} - "x" - x::T - "y" - y::T - end - - """ - $(TYPEDSIGNATURES) - """ - point(x::T, y::T = zero(T)) where {T <: Real} = Point(x, y) - - """ - $(TYPEDSIGNATURES) - """ - scale(p::Point, s::Union{Integer, AbstractFloat} = 1; round::Bool = false) = p - - """ - $(TYPEDSIGNATURES) - """ - fill!(out::PtrOrRef{Point{Float64}}, xs::Vector{Float64}, n::Integer...) = nothing - - """ - $(SIGNATURES) - - $(METHODLIST) - """ - untyped(a, b = 1; c = 2) = a - - """ - $(FUNCTIONNAME) - - $(FIELDS) - """ - mutable struct Mutable - a::Int - end - -end - -@setup_workload begin - docstrs = [docstr for multidoc in values(Docs.meta(_PrecompileWorkload)) for docstr in values(multidoc.docs)] - @compile_workload begin - # `Docs.parsedoc` is defined by the REPL stdlib, so expand the abbreviations directly. - buf = IOBuffer() - for docstr in docstrs - for part in docstr.text - if part isa Abbreviation - format(part, buf, docstr) - end - end - end - end -end diff --git a/src/printmethod.jl b/src/printmethod.jl index 68ba9721..9f60d341 100644 --- a/src/printmethod.jl +++ b/src/printmethod.jl @@ -73,10 +73,12 @@ julia> String(take!(buffer)) function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Method; definition = nothing, returns = false, brackets = false) aliases = alias_context(method.module) env = unionall_env(typevars(method.sig), aliases) - typestring(t) = type_text(t, env) names = argument_names(method, definition) - args = map(names, argument_types(method, length(names))) do arg, t - arg == "_" && t !== Any ? "::$(typestring(t))" : arg + types = argument_types(method, length(names)) + args = String[] + for i in eachindex(names) + arg, t = names[i], types[i] + push!(args, arg == "_" && t !== Any ? "::$(type_text(t, env))" : arg) end method.isva && (args[end] = "$(args[end])...") kws = string.(keywords(func, method)) @@ -84,13 +86,13 @@ function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Meth instance = instance_type(func, method) vars = String[] name = if instance === nothing - callable_name(binding, constructor_parameters(func, method), typestring) + callable_name(binding, constructor_parameters(func, method), env) elseif func === Type # The binding `Type` does not say which types the callable takes, so print them as # their module names them, with any type parameter they use. used = used_typevars(typevars(method.sig), [instance]) env = IOContext(unionall_env(used, aliases), :module => method.module) - vars = String[typevar_text(v, env) for v in used] + vars = typevar_texts(used, env) instance_call(method, type_text(instance, env)) else instance_call(method, string(binding.var)) @@ -111,10 +113,12 @@ appears in `definition`. function argument_names(method::Method, definition) args = arguments(method) matches_definition(method, definition) || return args - return map(enumerate(args)) do (i, arg) + names = String[] + for i in eachindex(args) name, _ = definition.positional[i] - arg == "_" ? name : arg + push!(names, args[i] == "_" ? name : args[i]) end + return names end """ @@ -128,9 +132,12 @@ function matches_definition(method::Method, definition::DefinitionArguments) names = arguments(method) length(names) <= length(definition.positional) || return false # `_` stands for an argument whose name one side does not know, such as `@nospecialize(x)`. - return all(zip(names, definition.positional)) do (name, (expected, _)) - name == "_" || expected == "_" || name == expected + for i in eachindex(names) + name = names[i] + expected, _ = definition.positional[i] + name == "_" || expected == "_" || name == expected || return false end + return true end """ @@ -143,16 +150,20 @@ and leave `kws` unchanged. Leaves both unchanged when `method` does not match `d """ function append_defaults(args, kws, kwnames, method::Method, definition; brackets::Bool = false) matches_definition(method, definition) || return args, kws - args = map(enumerate(args)) do (i, arg) + with_defaults = String[] + for i in eachindex(args) + arg = args[i] _, default = definition.positional[i] - default === nothing ? arg : brackets ? "[$arg]" : "$arg=$default" + push!(with_defaults, default === nothing ? arg : brackets ? "[$arg]" : "$arg=$default") end - brackets && return args, kws - kws = map(kws, kwnames) do kw, name - default = get(definition.keywords, name, nothing) - default === nothing ? kw : "$kw=$default" + brackets && return with_defaults, kws + kws_with_defaults = String[] + for i in eachindex(kws) + kw = kws[i] + default = get(definition.keywords, kwnames[i], nothing) + push!(kws_with_defaults, default === nothing ? kw : "$kw=$default") end - return args, kws + return with_defaults, kws_with_defaults end """ @@ -197,7 +208,6 @@ function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Meth used = used_typevars([vars; kwvars], types) aliases = alias_context(method.module) env = unionall_env(used, aliases) - typestring(t) = type_text(t, env) argtypes = collect(body.types) if expanded_vararg(method) @@ -206,19 +216,23 @@ function printmethod(buffer::IOBuffer, binding::Docs.Binding, func, method::Meth argtypes = Any[argtypes[1:(end - n)]..., Vararg{n > 0 ? argtypes[end] : Any, n}] end - args = map((arg, t) -> typed_argument(arg, t, typestring), args, argtypes) - kws = map(kwnames) do kw + typed = String[] + for i in eachindex(args) + push!(typed, typed_argument(args[i], argtypes[i], env)) + end + kws = String[] + for kw in kwnames t = get(kwtypes, kw, Any) - t === Any ? kw : "$kw::$(typestring(t))" + push!(kws, t === Any ? kw : "$kw::$(type_text(t, env))") end - args, kws = append_defaults(args, kws, kwnames, method, definition; brackets = brackets) + args, kws = append_defaults(typed, kws, kwnames, method, definition; brackets = brackets) return printmethod_format( buffer, - instance === nothing ? callable_name(binding, params, typestring) : instance_call(method, typestring(instance)), + instance === nothing ? callable_name(binding, params, env) : instance_call(method, type_text(instance, env)), args, kws; - where_clause = where_clause([typevar_text(v, env) for v in used]), + where_clause = where_clause(typevar_texts(used, env)), return_type = return_annotation(returns, func, method, typesig), macrocall = ismacro(method) ) @@ -228,18 +242,30 @@ end function used_typevars(vars, types) # Innermost first: a type parameter used only in a later parameter's bound is still used. used = Any[] - for v in reverse(vars) + for i in length(vars):-1:1 + v = vars[i] isanonymous(v) && continue - if any(t -> Base.has_typevar(t, v), types) || - any(u -> Base.has_typevar(u.lb, v) || Base.has_typevar(u.ub, v), used) - pushfirst!(used, v) - end + (has_typevar_in(types, v) || bounds_have_typevar(used, v)) && pushfirst!(used, v) end return used end -# The positional argument `arg` of type `t`, as the typed `printmethod` prints it. -function typed_argument(arg, t, typestring) +function has_typevar_in(types, v::TypeVar) + for t in types + Base.has_typevar(t, v) && return true + end + return false +end + +function bounds_have_typevar(vars, v::TypeVar) + for u in vars + (Base.has_typevar(u.lb, v) || Base.has_typevar(u.ub, v)) && return true + end + return false +end + +# The positional argument `arg` of type `t`, as the typed `printmethod` prints it in `io`. +function typed_argument(arg, t, io) type = "" suffix = "" if Base.isvarargtype(t) @@ -248,11 +274,11 @@ function typed_argument(arg, t, typestring) if len === nothing suffix = "..." else - type = "::Vararg{$(typestring(t)), $len}" + type = "::Vararg{$(type_text(t, io)), $len}" end end if t !== Any && isempty(type) - type = "::$(typestring(t))" + type = "::$(type_text(t, io))" end # An unnamed argument with a type prints as `::T`. if arg == "_" && !isempty(type) @@ -290,9 +316,23 @@ where_clause(typevars) = length(typevars) == 1 ? " where $(typevars[1])" : " where {$(join(typevars, ", "))}" +function typevar_texts(vars, io) + texts = String[] + for v in vars + push!(texts, typevar_text(v, io)) + end + return texts +end + # The name `method` is called by, with the parameters of a constructor written as `T{P}(...)`. -callable_name(binding, params, typestring) = - params === nothing ? string(binding.var) : "$(binding.var){$(join(map(typestring, params), ", "))}" +function callable_name(binding, params, io) + params === nothing && return string(binding.var) + texts = String[] + for p in params + push!(texts, type_text(p, io)) + end + return "$(binding.var){$(join(texts, ", "))}" +end # The instance a callable method is called on, `(e::T)` or `(::T)`, with `type` printed for `T`. function instance_call(method::Method, type) diff --git a/src/type_text.jl b/src/type_text.jl index 88560b25..a02a5138 100644 --- a/src/type_text.jl +++ b/src/type_text.jl @@ -51,14 +51,22 @@ end # `t` as `show` prints it in `io`, with each free type variable of `names` written as its text. function named_text(@nospecialize(t), names, io::IO) - text = sprint(show, t; context = unionall_env(map(first, names), io)) + text = sprint(show, t; context = unionall_env(placeholders(names), io)) # Last first, so that the first placeholder is not replaced inside the tenth. - for (v, replacement) in reverse(names) - text = replace(text, string(v.name) => replacement) + for i in length(names):-1:1 + text = replace(text, string(names[i].first.name) => names[i].second) end return text end +function placeholders(names) + vars = TypeVar[] + for name in names + push!(vars, name.first) + end + return vars +end + # `t` with each anonymous type variable that `show` prints by its generated name in `io` replaced # by a free one that `show` prints by its name when it is in the `:unionall_env`. Appends each # replacement to `names` with its text: its bound when `writes_as_bound`, as in `<:Real`. @@ -77,7 +85,7 @@ function name_anonymous!(names, @nospecialize(t), io::IO) push!(names, v => bound_text(v, io)) return name_anonymous!(names, t{v}, io) end - text = alias_text(t, unionall_env(map(first, names), io)) + text = alias_text(t, unionall_env(placeholders(names), io)) text === nothing && return t # A placeholder for the whole `UnionAll`: Julia caches a type such as `Ref{X}` by type # equality, so `Ref{X}` rebuilt from a renamed `X` comes back with the generated name. @@ -87,8 +95,14 @@ function name_anonymous!(names, @nospecialize(t), io::IO) elseif t isa Union return Union{name_anonymous!(names, t.a, io), name_anonymous!(names, t.b, io)} elseif t isa DataType - params = Any[name_anonymous!(names, p, io) for p in t.parameters] - return all(map(===, params, collect(t.parameters))) ? t : t.name.wrapper{params...} + params = Any[] + changed = false + for p in t.parameters + named = name_anonymous!(names, p, io) + changed |= named !== p + push!(params, named) + end + return changed ? t.name.wrapper{params...} : t elseif istypeoftype(t) # From Julia 1.14 `Type{T}` is a `TypeEq`, not a `DataType`. p = name_anonymous!(names, type_parameter(t), io) @@ -116,7 +130,13 @@ function writes_as_bound(t::UnionAll) end istypeoftype(body) && return type_parameter(body) === v body isa DataType || return false - return count(p -> p === v, body.parameters) == 1 && count(p -> Base.has_typevar(p, v), body.parameters) == 1 + identical = 0 + containing = 0 + for p in body.parameters + identical += p === v + containing += Base.has_typevar(p, v) + end + return identical == 1 && containing == 1 end # `io` extended so that `type_text` also looks for aliases in `mod`, the module that defines @@ -143,9 +163,10 @@ function alias_text(t::UnionAll, io::IO) best === nothing && return nothing _, mod, name, args = best anonymous, _ = leading_anonymous(t) - texts = map(args) do arg - i = findfirst(v -> v === arg, anonymous) - i === nothing ? type_text(arg, io) : bound_text(anonymous[i], io) + texts = String[] + for arg in args + i = findfirst_identical(arg, anonymous) + push!(texts, i === nothing ? type_text(arg, io) : bound_text(anonymous[i], io)) end return "$(alias_name(mod, name, io)){$(join(texts, ", "))}" end @@ -180,7 +201,9 @@ function foreach_type(f, @nospecialize(t)) foreach_type(f, t.a) foreach_type(f, t.b) elseif t isa DataType - foreach(p -> foreach_type(f, p), t.parameters) + for p in t.parameters + foreach_type(f, p) + end elseif istypeoftype(t) foreach_type(f, type_parameter(t)) elseif Base.isvarargtype(t) @@ -195,7 +218,10 @@ function shows_anonymous(@nospecialize(t), text, io::IO) anonymous = Symbol[] foreach_type(part -> part isa UnionAll && isanonymous(part.var, io) && push!(anonymous, part.var.name), t) # Whole names only: `_A` also appears inside a name such as `MAX_A`. - return any(name -> occursin(Regex("(? Base.invokelatest(isalias, mod, name), - Base.invokelatest(names, mod; all = true, imported = true) -) +function module_aliases(mod::Module) + aliases = Symbol[] + for name in Base.invokelatest(names, mod; all = true, imported = true)::Vector{Symbol} + Base.invokelatest(isalias, mod, name)::Bool && push!(aliases, name) + end + return aliases +end # Whether `name` is a `const` of `mod` bound to a parametric alias, such as `Vector` or # `const ScalarOrRef{T} = Union{T, Ref{T}} where T`, and not to a type's own `UnionAll`. @@ -233,23 +262,37 @@ function alias_arguments(@nospecialize(alias), t::UnionAll) vars = typevars(alias) args = Vector{Any}(undef, length(vars)) match_alias!(args, vars, Base.unwrap_unionall(alias), body) || return nothing - all(i -> isassigned(args, i), eachindex(args)) || return nothing + for i in eachindex(args) + isassigned(args, i) || return nothing + end for v in anonymous - count(arg -> arg === v, args) == 1 || return nothing - any(arg -> arg !== v && Base.has_typevar(arg, v), args) && return nothing + identical = 0 + for arg in args + identical += arg === v + arg !== v && Base.has_typevar(arg, v) && return nothing + end + identical == 1 || return nothing end # The match reads `t` structurally, so only an alias that rebuilds `t` gives it. Julia # applies an alias to a type variable outside its bounds without complaint, so `t` must also # be a subtype of the alias. - fresh = Dict{TypeVar, TypeVar}(v => TypeVar(v.name, v.lb, v.ub) for v in anonymous) + fresh = TypeVar[] + for v in anonymous + push!(fresh, TypeVar(v.name, v.lb, v.ub)) + end + freshargs = Any[] + for arg in args + i = findfirst_identical(arg, anonymous) + push!(freshargs, i === nothing ? arg : fresh[i]) + end applied = try - alias{Any[arg isa TypeVar ? get(fresh, arg, arg) : arg for arg in args]...} + alias{freshargs...} catch err err isa TypeError || rethrow() return nothing end - for v in reverse(anonymous) - applied = UnionAll(fresh[v], applied) + for i in length(fresh):-1:1 + applied = UnionAll(fresh[i], applied) end return applied == t && t <: alias ? args : nothing end @@ -269,7 +312,7 @@ end # `pattern` reads as `t` with those bindings. A variable bound twice must bind equal parts. function match_alias!(args, vars, @nospecialize(pattern), @nospecialize(t)) if pattern isa TypeVar - i = findfirst(v -> v === pattern, vars) + i = findfirst_identical(pattern, vars) i === nothing && return pattern === t isassigned(args, i) && return same_parameter(args[i], t) args[i] = t @@ -280,17 +323,24 @@ function match_alias!(args, vars, @nospecialize(pattern), @nospecialize(t)) match_alias!(args, vars, pattern.var.ub, t.var.ub) || return false # The variable `pattern` binds stands for the one `t` binds, within the body. inner = Vector{Any}(undef, length(args) + 1) - foreach(i -> isassigned(args, i) && (inner[i] = args[i]), eachindex(args)) + for i in eachindex(args) + isassigned(args, i) && (inner[i] = args[i]) + end inner[end] = t.var match_alias!(inner, [vars; pattern.var], pattern.body, t.body) || return false - foreach(i -> isassigned(inner, i) && (args[i] = inner[i]), eachindex(args)) + for i in eachindex(args) + isassigned(inner, i) && (args[i] = inner[i]) + end return true elseif pattern isa Union return t isa Union && match_alias!(args, vars, pattern.a, t.a) && match_alias!(args, vars, pattern.b, t.b) elseif pattern isa DataType t isa DataType && pattern.name === t.name && length(pattern.parameters) == length(t.parameters) || return false - return all(i -> match_alias!(args, vars, pattern.parameters[i], t.parameters[i]), eachindex(pattern.parameters)) + for i in eachindex(pattern.parameters) + match_alias!(args, vars, pattern.parameters[i], t.parameters[i]) || return false + end + return true elseif istypeoftype(pattern) return istypeoftype(t) && match_alias!(args, vars, type_parameter(pattern), type_parameter(t)) elseif Base.isvarargtype(pattern) @@ -304,5 +354,17 @@ end # Whether two type parameters are the same: identical, or equal types. same_parameter(@nospecialize(a), @nospecialize(b)) = a === b || a isa Type && b isa Type && a == b +function findfirst_identical(@nospecialize(x), xs) + for i in eachindex(xs) + xs[i] === x && return i + end + return nothing +end + # `io` extended so that `show` prints each of `typevars` by name. -unionall_env(typevars, io = IOContext(devnull)) = foldl((io, v) -> IOContext(io, :unionall_env => v), typevars; init = io) +function unionall_env(typevars, io = IOContext(devnull)) + for v in typevars + io = IOContext(io, :unionall_env => v) + end + return io +end diff --git a/test/methods.jl b/test/methods.jl index dfdf79f7..c5746c5e 100644 --- a/test/methods.jl +++ b/test/methods.jl @@ -149,18 +149,6 @@ @test DSE.alltypesigs(Tuple{G} where {G}) == Any[Tuple{G} where {G}] end - @testset "groupby" begin - let groups = DSE.groupby(Int, Vector{Int}, collect(1:10)) do each - mod(each, 3), each - end - @test groups == Pair{Int, Vector{Int}}[ - 0 => [3, 6, 9], - 1 => [1, 4, 7, 10], - 2 => [2, 5, 8], - ] - end - end - @testset "comparemethods" begin let f = first(methods(M.f)), g = first(methods(M.g)) diff --git a/test/printmethod.jl b/test/printmethod.jl index 82427b8f..e978b187 100644 --- a/test/printmethod.jl +++ b/test/printmethod.jl @@ -191,15 +191,15 @@ end @testset "typed_argument" begin - typestring(t) = sprint(show, t) + io = IOContext(devnull) unbounded = last(Tuple{Vararg{Int8}}.parameters) counted = last((Tuple{Vararg{Int8, N}} where {N}).body.parameters) - @test DSE.typed_argument("x", Any, typestring) == "x" - @test DSE.typed_argument("x", Int8, typestring) == "x::Int8" - @test DSE.typed_argument("_", Int8, typestring) == "::Int8" - @test DSE.typed_argument("_", Any, typestring) == "_" - @test DSE.typed_argument("xs", unbounded, typestring) == "xs::Int8..." - @test DSE.typed_argument("_", unbounded, typestring) == "::Int8..." - @test DSE.typed_argument("xs", counted, typestring) == "xs::Vararg{Int8, N}" + @test DSE.typed_argument("x", Any, io) == "x" + @test DSE.typed_argument("x", Int8, io) == "x::Int8" + @test DSE.typed_argument("_", Int8, io) == "::Int8" + @test DSE.typed_argument("_", Any, io) == "_" + @test DSE.typed_argument("xs", unbounded, io) == "xs::Int8..." + @test DSE.typed_argument("_", unbounded, io) == "::Int8..." + @test DSE.typed_argument("xs", counted, io) == "xs::Vararg{Int8, N}" end end