Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reflection: allow names to return using-ed names #54609

Merged
merged 1 commit into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Julia v1.12 Release Notes
New language features
---------------------

- A new keyword argument `usings::Bool` has been added to `names`. By using this, we can now
find all the names available in module `A` by `names(A; all=true, imported=true, usings=true)`. ([#54609])

Language changes
----------------

Expand All @@ -17,7 +20,7 @@ Language changes
may pave the way for inference to be able to intelligently re-use the old
results, once the new method is deleted. ([#53415])

- Macro expansion will no longer eargerly recurse into into `Expr(:toplevel)`
- Macro expansion will no longer eagerly recurse into into `Expr(:toplevel)`
expressions returned from macros. Instead, macro expansion of `:toplevel`
expressions will be delayed until evaluation time. This allows a later
expression within a given `:toplevel` expression to make use of macros
Expand Down
19 changes: 12 additions & 7 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,33 @@ function fullname(m::Module)
end

"""
names(x::Module; all::Bool = false, imported::Bool = false)
names(x::Module; all::Bool=false, imported::Bool=false, usings::Bool=false) -> Vector{Symbol}

Get a vector of the public names of a `Module`, excluding deprecated names.
If `all` is true, then the list also includes non-public names defined in the module,
deprecated names, and compiler-generated names.
If `imported` is true, then names explicitly imported from other modules
are also included. Names are returned in sorted order.
are also included.
If `usings` is true, then names explicitly imported via `using` are also included.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also implicitly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing that out. I plan to address this issue as well in #54659.

Names are returned in sorted order.

As a special case, all names defined in `Main` are considered \"public\",
since it is not idiomatic to explicitly mark names from `Main` as public.

!!! note
`sym ∈ names(SomeModule)` does *not* imply `isdefined(SomeModule, sym)`.
`names` will return symbols marked with `public` or `export`, even if
`names` may return symbols marked with `public` or `export`, even if
they are not defined in the module.

!!! warning
`names` may return duplicate names. The duplication happens, e.g. if an `import`ed name
conflicts with an already existing identifier.

See also: [`Base.isexported`](@ref), [`Base.ispublic`](@ref), [`Base.@locals`](@ref), [`@__MODULE__`](@ref).
"""
names(m::Module; all::Bool = false, imported::Bool = false) =
sort!(unsorted_names(m; all, imported))
unsorted_names(m::Module; all::Bool = false, imported::Bool = false) =
ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint), m, all, imported)
names(m::Module; kwargs...) = sort!(unsorted_names(m; kwargs...))
unsorted_names(m::Module; all::Bool=false, imported::Bool=false, usings::Bool=false) =
ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint, Cint), m, all, imported, usings)

"""
isexported(m::Module, s::Symbol) -> Bool
Expand Down
51 changes: 42 additions & 9 deletions src/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -991,10 +991,19 @@ JL_DLLEXPORT jl_value_t *jl_module_usings(jl_module_t *m)
return (jl_value_t*)a;
}

JL_DLLEXPORT jl_value_t *jl_module_names(jl_module_t *m, int all, int imported)
uint8_t _binding_is_from_explicit_using(jl_binding_t *b) {
jl_binding_t *owner = jl_atomic_load_relaxed(&b->owner);
return (owner != NULL && owner != b && !b->imported);
}

void _append_symbol_to_bindings_array(jl_array_t* a, jl_sym_t *name) {
jl_array_grow_end(a, 1);
//XXX: change to jl_arrayset if array storage allocation for Array{Symbols,1} changes:
jl_array_ptr_set(a, jl_array_dim0(a)-1, (jl_value_t*)name);
}

void append_module_names(jl_array_t* a, jl_module_t *m, int all, int imported, int usings)
{
jl_array_t *a = jl_alloc_array_1d(jl_array_symbol_type, 0);
JL_GC_PUSH1(&a);
jl_svec_t *table = jl_atomic_load_relaxed(&m->bindings);
for (size_t i = 0; i < jl_svec_len(table); i++) {
jl_binding_t *b = (jl_binding_t*)jl_svecref(table, i);
Expand All @@ -1003,15 +1012,39 @@ JL_DLLEXPORT jl_value_t *jl_module_names(jl_module_t *m, int all, int imported)
jl_sym_t *asname = b->globalref->name;
int hidden = jl_symbol_name(asname)[0]=='#';
int main_public = (m == jl_main_module && !(asname == jl_eval_sym || asname == jl_include_sym));
if ((b->publicp ||
if (((b->publicp) ||
(imported && b->imported) ||
(usings && _binding_is_from_explicit_using(b)) ||
(jl_atomic_load_relaxed(&b->owner) == b && !b->imported && (all || main_public))) &&
(all || (!b->deprecated && !hidden))) {
jl_array_grow_end(a, 1);
// n.b. change to jl_arrayset if array storage allocation for Array{Symbols,1} changes:
jl_array_ptr_set(a, jl_array_dim0(a)-1, (jl_value_t*)asname);
(all || (!b->deprecated && !hidden)))
_append_symbol_to_bindings_array(a, asname);
}
}

void append_exported_names(jl_array_t* a, jl_module_t *m, int all)
{
jl_svec_t *table = jl_atomic_load_relaxed(&m->bindings);
for (size_t i = 0; i < jl_svec_len(table); i++) {
jl_binding_t *b = (jl_binding_t*)jl_svecref(table, i);
if ((void*)b == jl_nothing)
break;
if (b->exportp && (all || !b->deprecated))
_append_symbol_to_bindings_array(a, b->globalref->name);
}
}

JL_DLLEXPORT jl_value_t *jl_module_names(jl_module_t *m, int all, int imported, int usings)
{
jl_array_t *a = jl_alloc_array_1d(jl_array_symbol_type, 0);
JL_GC_PUSH1(&a);
append_module_names(a, m, all, imported, usings);
if (usings) {
// If `usings` is specified, traverse the list of `using`-ed modules and incorporate
// the names exported by those modules into the list.
for(int i=(int)m->usings.len-1; i >= 0; --i) {
jl_module_t *usinged = module_usings_getidx(m, i);
append_exported_names(a, usinged, all);
}
table = jl_atomic_load_relaxed(&m->bindings);
}
JL_GC_POP();
return (jl_value_t*)a;
Expand Down
122 changes: 108 additions & 14 deletions test/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,18 @@ not_const = 1
# For curmod_*
include("testenv.jl")

module TestMod36529
x36529 = 0
y36529 = 1
export y36529
end

module TestMod7648
using Test
import Base.convert
import ..curmod_name, ..curmod
export a9475, foo9475, c7648, foo7648, foo7648_nomethods, Foo7648
using ..TestMod36529: x36529 # doesn't import TestMod36529 or y36529, even though it's exported
export a9475, c7648, f9475, foo7648, foo7648_nomethods, Foo7648

const c7648 = 8
d7648 = 9
Expand All @@ -142,10 +149,11 @@ module TestModSub9475
using Test
using ..TestMod7648
import ..curmod_name
export a9475, foo9475
export a9475, f9475, f54609
a9475 = 5
b9475 = 7
foo9475(x) = x
f9475(x) = x
f54609(x) = x
let
@test Base.binding_module(@__MODULE__, :a9475) == @__MODULE__
@test Base.binding_module(@__MODULE__, :c7648) == TestMod7648
Expand All @@ -169,18 +177,104 @@ let
@test Base.binding_module(TestMod7648, :d7648) == TestMod7648
@test Base.binding_module(TestMod7648, :a9475) == TestMod7648.TestModSub9475
@test Base.binding_module(TestMod7648.TestModSub9475, :b9475) == TestMod7648.TestModSub9475
@test Set(names(TestMod7648))==Set([:TestMod7648, :a9475, :foo9475, :c7648, :foo7648, :foo7648_nomethods, :Foo7648])
@test Set(names(TestMod7648, all = true)) == Set([:TestMod7648, :TestModSub9475, :a9475, :foo9475, :c7648, :d7648, :f7648,
:foo7648, Symbol("#foo7648"), :foo7648_nomethods, Symbol("#foo7648_nomethods"),
:Foo7648, :eval, Symbol("#eval"), :include, Symbol("#include")])
@test Set(names(TestMod7648, all = true, imported = true)) == Set([:TestMod7648, :TestModSub9475, :a9475, :foo9475, :c7648, :d7648, :f7648,
:foo7648, Symbol("#foo7648"), :foo7648_nomethods, Symbol("#foo7648_nomethods"),
:Foo7648, :eval, Symbol("#eval"), :include, Symbol("#include"),
:convert, :curmod_name, :curmod])
defaultset = Set(Symbol[:Foo7648, :TestMod7648, :a9475, :c7648, :f9475, :foo7648, :foo7648_nomethods])
allset = defaultset ∪ Set(Symbol[
Symbol("#eval"), Symbol("#foo7648"), Symbol("#foo7648_nomethods"), Symbol("#include"),
:TestModSub9475, :d7648, :eval, :f7648, :include])
imported = Set(Symbol[:convert, :curmod_name, :curmod])
usings_from_Test = Set(Symbol[
Symbol("@inferred"), Symbol("@test"), Symbol("@test_broken"), Symbol("@test_deprecated"),
Symbol("@test_logs"), Symbol("@test_nowarn"), Symbol("@test_skip"), Symbol("@test_throws"),
Symbol("@test_warn"), Symbol("@testset"), :GenericArray, :GenericDict, :GenericOrder,
:GenericSet, :GenericString, :LogRecord, :Test, :TestLogger, :TestSetException,
:detect_ambiguities, :detect_unbound_args])
usings_from_Base = delete!(Set(names(Module(); usings=true)), :anonymous) # the name of the anonymous module itself
usings = Set(Symbol[:x36529, :TestModSub9475, :f54609]) ∪ usings_from_Test ∪ usings_from_Base
@test Set(names(TestMod7648)) == defaultset
@test Set(names(TestMod7648, all=true)) == allset
@test Set(names(TestMod7648, all=true, imported=true)) == allset ∪ imported
@test Set(names(TestMod7648, usings=true)) == defaultset ∪ usings
@test Set(names(TestMod7648, all=true, usings=true)) == allset ∪ usings
@test isconst(TestMod7648, :c7648)
@test !isconst(TestMod7648, :d7648)
end

# tests for `names(...; usings=true)`

baremodule Test54609Simple
module Inner
export exported
global exported::Int = 1
global unexported::Int = 0
end
using Base: @assume_effects
using .Inner
end
let usings = names(Test54609Simple; usings=true)
@test Symbol("@assume_effects") ∈ usings
@test :Base ∉ usings
@test :exported ∈ usings
@test :unexported ∉ usings
end # baremodule Test54609Simple

baremodule _Test54609Complex
export exported_new
using Base: @deprecate_binding
global exported_new = nothing
@deprecate_binding exported_old exported_new
end # baremodule _Test54609Complex
baremodule Test54609Complex
using .._Test54609Complex
end # baremodule Test54609Complex
let usings = names(Test54609Complex; usings=true)
@test :exported_new ∈ usings
@test :exported_old ∉ usings
@test :_Test54609Complex ∈ usings # should include the `using`ed module itself
usings_all = names(Test54609Complex; usings=true, all=true)
@test :exported_new ∈ usings_all
@test :exported_old ∈ usings_all # deprecated names should be included with `all=true`
end

module TestMod54609
module M1
const m1_x = 1
export m1_x
end
module M2
const m2_x = 1
export m2_x
end
module A
module B
f(x) = 1
secret = 1
module Inner2 end
end
module C
x = 1
y = 2
export y
end
using .B: f
using .C
using ..M1
import ..M2
end
end # module TestMod54609
let defaultset = Set((:A,))
imported = Set((:M2,))
usings_from_Base = delete!(Set(names(Module(); usings=true)), :anonymous) # the name of the anonymous module itself
usings = Set((:A, :f, :C, :y, :M1, :m1_x)) ∪ usings_from_Base
allset = Set((:A, :B, :C, :eval, :include, Symbol("#eval"), Symbol("#include")))
@test Set(names(TestMod54609.A)) == defaultset
@test Set(names(TestMod54609.A, imported=true)) == defaultset ∪ imported
@test Set(names(TestMod54609.A, usings=true)) == defaultset ∪ usings
@test Set(names(TestMod54609.A, all=true)) == allset
@test Set(names(TestMod54609.A, all=true, usings=true)) == allset ∪ usings
@test Set(names(TestMod54609.A, imported=true, usings=true)) == defaultset ∪ imported ∪ usings
@test Set(names(TestMod54609.A, all=true, imported=true, usings=true)) == allset ∪ imported ∪ usings
end

let
using .TestMod7648
@test Base.binding_module(@__MODULE__, :a9475) == TestMod7648.TestModSub9475
Expand All @@ -189,10 +283,10 @@ let
@test parentmodule(foo7648, (Any,)) == TestMod7648
@test parentmodule(foo7648) == TestMod7648
@test parentmodule(foo7648_nomethods) == TestMod7648
@test parentmodule(foo9475, (Any,)) == TestMod7648.TestModSub9475
@test parentmodule(foo9475) == TestMod7648.TestModSub9475
@test parentmodule(f9475, (Any,)) == TestMod7648.TestModSub9475
@test parentmodule(f9475) == TestMod7648.TestModSub9475
@test parentmodule(Foo7648) == TestMod7648
@test parentmodule(first(methods(foo9475))) == TestMod7648.TestModSub9475
@test parentmodule(first(methods(f9475))) == TestMod7648.TestModSub9475
@test parentmodule(first(methods(foo7648))) == TestMod7648
@test nameof(Foo7648) === :Foo7648
@test basename(functionloc(foo7648, (Any,))[1]) == "reflection.jl"
Expand Down