Skip to content

Commit ad6e4bb

Browse files
authored
bindings: Add native automatic re-export feature (#59859)
This implements a native version of automatic re-export, similar to the macro from Reexport.jl. Doing this natively in the binding system has two key advantages: 1. It properly participates in binding resolution and world ages. If a new binding is Revise'd into a re-exported module, this will now propagate. 2. The re-exported bindings are allocated lazily, improving performance. An accessor for this functionality is provided as `@Base.Experimental.reexport`. However, the only supported argument to this macro is single `using` statement (unlike the Reexport.jl version, which has more syntax). It is my expectation that Reexport.jl will be updated to make use of the underlying functionality here directly, the base version of the macro is mostly for testing. There are a few design warts here - in particular, we inherit the module name exporting behavior (JuliaLang/Reexport.jl#39). However, I think that would be best addressed by not exporting the module name itself from the modules but rather introducing the module name as an additional binding on `using` statements. However, this would be potentially breaking (even if the effect is unlikely to be seen in practice), so I'm not proposing it here. The Reexport.jl version of the macro can do whatever it needs to, including creating an explicit non-exported binding to suppress the automatic re-export for this if desired. Partially written by Claude Code. Co-authored-by: Keno Fischer <[email protected]>
1 parent 906d64e commit ad6e4bb

File tree

11 files changed

+288
-35
lines changed

11 files changed

+288
-35
lines changed

base/experimental.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,4 +695,55 @@ function wait_with_timeout(c::GenericCondition; first::Bool=false, timeout::Real
695695
end
696696
end
697697

698+
"""
699+
Base.Experimental.@reexport using Module
700+
701+
Automatically re-export all exported names from a module when using it.
702+
703+
# Examples
704+
705+
```jldoctest
706+
julia> module A
707+
export foo
708+
foo() = "foo from A"
709+
end
710+
A
711+
712+
julia> module B
713+
using Base.Experimental: @reexport
714+
@reexport using ..A
715+
# Now B exports foo, even though it's defined in A
716+
end
717+
B
718+
719+
julia> using .B
720+
721+
julia> foo()
722+
"foo from A"
723+
```
724+
725+
!!! warning
726+
This interface is experimental and subject to change or removal without notice.
727+
"""
728+
macro reexport(ex)
729+
if !Meta.isexpr(ex, :using) || isempty(ex.args)
730+
error("@reexport must be used with a `using` statement, e.g., `@reexport using MyModule`")
731+
end
732+
733+
# Check for `using Foo: x, y` syntax (not supported)
734+
if any(arg -> Meta.isexpr(arg, :(:)), ex.args)
735+
error("@reexport does not support `using Module: names` syntax")
736+
end
737+
738+
# Generate _eval_using calls for each module in the using statement
739+
calls = Expr(:block)
740+
for mod_path in ex.args
741+
push!(calls.args, :($(Core._eval_using)($(__module__), $(QuoteNode(mod_path)), $(Base.JL_MODULE_USING_REEXPORT))))
742+
end
743+
push!(calls.args, Expr(:latestworld))
744+
push!(calls.args, :nothing)
745+
746+
return esc(calls)
747+
end
748+
698749
end # module

base/module.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ using A.B => _module_using(Main, Expr(:., :A, :B))
133133
134134
See also [`_using`](@ref Core._using).
135135
"""
136-
function _eval_using(to::Module, path::Expr)
136+
function _eval_using(to::Module, path::Expr, flags::UInt8=UInt8(0))
137137
from = eval_import_path_all(to, path, "using")
138-
Core._using(to, from)
138+
Core._using(to, from, flags)
139139
is_package = length(path.args) == 1 && path.args[1] !== :.
140140
if to == Main && is_package
141141
Core._import(to, from, nameof(from))

base/runtime_internals.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,15 @@ const PARTITION_KIND_BACKDATED_CONST = 0xb
253253
const PARTITION_FLAG_EXPORTED = 0x10
254254
const PARTITION_FLAG_DEPRECATED = 0x20
255255
const PARTITION_FLAG_DEPWARN = 0x40
256+
const PARTITION_FLAG_IMPLICITLY_EXPORTED = 0x80
256257

257258
const PARTITION_MASK_KIND = 0x0f
258259
const PARTITION_MASK_FLAG = 0xf0
259260

260261
const BINDING_FLAG_ANY_IMPLICIT_EDGES = 0x8
261262

263+
const JL_MODULE_USING_REEXPORT = 0x1
264+
262265
is_defined_const_binding(kind::UInt8) = (kind == PARTITION_KIND_CONST || kind == PARTITION_KIND_CONST_IMPORT || kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_BACKDATED_CONST)
263266
is_some_const_binding(kind::UInt8) = (is_defined_const_binding(kind) || kind == PARTITION_KIND_UNDEF_CONST)
264267
is_some_imported(kind::UInt8) = (kind == PARTITION_KIND_IMPLICIT_GLOBAL || kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_EXPLICIT || kind == PARTITION_KIND_IMPORTED)

base/show.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3294,6 +3294,10 @@ function print_partition(io::IO, partition::Core.BindingPartition)
32943294
if (partition.kind & PARTITION_FLAG_EXPORTED) != 0
32953295
print(io, "exported")
32963296
end
3297+
if (partition.kind & PARTITION_FLAG_IMPLICITLY_EXPORTED) != 0
3298+
first ? (first = false) : print(io, ",")
3299+
print(io, "re-exported")
3300+
end
32973301
if (partition.kind & PARTITION_FLAG_DEPRECATED) != 0
32983302
first ? (first = false) : print(io, ",")
32993303
print(io, "deprecated")

src/builtins.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,10 +1579,15 @@ JL_CALLABLE(jl_f__import)
15791579
// _using(to::Module, from::Module)
15801580
JL_CALLABLE(jl_f__using)
15811581
{
1582-
JL_NARGS(_using, 2, 2);
1582+
JL_NARGS(_using, 2, 3);
15831583
JL_TYPECHK(_using, module, args[0]);
15841584
JL_TYPECHK(_using, module, args[1]);
1585-
jl_module_using((jl_module_t *)args[0], (jl_module_t *)args[1]);
1585+
size_t flags = 0;
1586+
if (nargs == 3) {
1587+
JL_TYPECHK(_using, uint8, args[2]);
1588+
flags = jl_unbox_uint8(args[2]);
1589+
}
1590+
jl_module_using((jl_module_t *)args[0], (jl_module_t *)args[1], flags);
15861591
return jl_nothing;
15871592
}
15881593

src/gc-stock.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,9 +2168,9 @@ STATIC_INLINE void gc_mark_module_binding(jl_ptls_t ptls, jl_module_t *mb_parent
21682168
jl_value_t *obj_parent = (jl_value_t *)mb_parent;
21692169
struct _jl_module_using *objary_begin = (struct _jl_module_using *)mb_parent->usings.items;
21702170
struct _jl_module_using *objary_end = objary_begin + nusings;
2171-
static_assert(sizeof(struct _jl_module_using) == 3*sizeof(void *), "Mismatch in _jl_module_using size");
2171+
static_assert(sizeof(struct _jl_module_using) == 4*sizeof(void *), "Mismatch in _jl_module_using size");
21722172
static_assert(offsetof(struct _jl_module_using, mod) == 0, "Expected `mod` at the beginning of _jl_module_using");
2173-
gc_mark_objarray(ptls, obj_parent, (jl_value_t**)objary_begin, (jl_value_t**)objary_end, 3, nptr);
2173+
gc_mark_objarray(ptls, obj_parent, (jl_value_t**)objary_begin, (jl_value_t**)objary_end, 4, nptr);
21742174
}
21752175
else {
21762176
gc_mark_push_remset(ptls, (jl_value_t *)mb_parent, nptr);

src/julia.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,9 @@ static const uint8_t PARTITION_FLAG_DEPRECATED = 0x20;
765765
// implies _DEPRECATED. However, the reverse is not true. Such bindings are usually used for functions,
766766
// where calling the function itself will provide a (better) deprecation warning/error.
767767
static const uint8_t PARTITION_FLAG_DEPWARN = 0x40;
768+
// _IMPLICITLY_EXPORTED: This binding partition is implicitly exported via @reexport. Unlike _EXPORTED,
769+
// this flag is set during implicit resolution and can be removed if the resolution changes.
770+
static const uint8_t PARTITION_FLAG_IMPLICITLY_EXPORTED = 0x80;
768771

769772
#if defined(_COMPILER_MICROSOFT_)
770773
#define JL_ALIGNED_ATTR(alignment) \
@@ -853,6 +856,8 @@ typedef struct _jl_module_t {
853856
int8_t max_methods;
854857
// If cleared no binding partition in this module has PARTITION_FLAG_EXPORTED and min_world > jl_require_world.
855858
_Atomic(int8_t) export_set_changed_since_require_world;
859+
// Set if this module has any reexport usings (used to bypass fast-path in implicit resolution)
860+
_Atomic(int8_t) has_reexports;
856861
jl_mutex_t lock;
857862
intptr_t hash;
858863
} jl_module_t;
@@ -861,8 +866,12 @@ struct _jl_module_using {
861866
jl_module_t *mod;
862867
size_t min_world;
863868
size_t max_world;
869+
size_t flags;
864870
};
865871

872+
// Flags for _jl_module_using.flags
873+
static const uint8_t JL_MODULE_USING_REEXPORT = 0x1;
874+
866875
struct _jl_globalref_t {
867876
JL_DATA_TYPE
868877
jl_module_t *mod;
@@ -2038,7 +2047,7 @@ JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val(jl_binding_t *b JL_
20382047
JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val2(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED, enum jl_partition_kind);
20392048
JL_DLLEXPORT void jl_module_import(jl_task_t *ct, jl_module_t *to, jl_module_t *from, jl_sym_t *asname, jl_sym_t *s, int explici);
20402049
JL_DLLEXPORT void jl_import_module(jl_task_t *ct, jl_module_t *m, jl_module_t *import, jl_sym_t *asname);
2041-
JL_DLLEXPORT void jl_module_using(jl_module_t *to, jl_module_t *from);
2050+
JL_DLLEXPORT void jl_module_using(jl_module_t *to, jl_module_t *from, size_t flags);
20422051
int jl_module_public_(jl_module_t *from, jl_sym_t *s, int exported, size_t new_world);
20432052
JL_DLLEXPORT int jl_is_imported(jl_module_t *m, jl_sym_t *s);
20442053
JL_DLLEXPORT int jl_module_exports_p(jl_module_t *m, jl_sym_t *var);

src/julia_internal.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -916,19 +916,19 @@ typedef struct _modstack_t {
916916
// The analyzer doesn't like looking through the arraylist, so just model the
917917
// access for it using this function
918918
STATIC_INLINE struct _jl_module_using *module_usings_getidx(jl_module_t *m JL_PROPAGATES_ROOT, size_t i) JL_NOTSAFEPOINT {
919-
return (struct _jl_module_using *)&(m->usings.items[3*i]);
919+
return (struct _jl_module_using *)&(m->usings.items[4*i]);
920920
}
921921
STATIC_INLINE jl_module_t *module_usings_getmod(jl_module_t *m JL_PROPAGATES_ROOT, size_t i) JL_NOTSAFEPOINT {
922922
return module_usings_getidx(m, i)->mod;
923923
}
924924
#endif
925925

926926
STATIC_INLINE size_t module_usings_length(jl_module_t *m) JL_NOTSAFEPOINT {
927-
return m->usings.len/3;
927+
return m->usings.len/4;
928928
}
929929

930930
STATIC_INLINE size_t module_usings_max(jl_module_t *m) JL_NOTSAFEPOINT {
931-
return m->usings.max/3;
931+
return m->usings.max/4;
932932
}
933933

934934
JL_DLLEXPORT jl_sym_t *jl_module_name(jl_module_t *m) JL_NOTSAFEPOINT;
@@ -1029,6 +1029,10 @@ STATIC_INLINE int jl_bkind_is_real_constant(enum jl_partition_kind kind) JL_NOTS
10291029
return kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_CONST || kind == PARTITION_KIND_CONST_IMPORT;
10301030
}
10311031

1032+
STATIC_INLINE int jl_bpart_is_exported(uint8_t flags) JL_NOTSAFEPOINT {
1033+
return flags & (PARTITION_FLAG_EXPORTED | PARTITION_FLAG_IMPLICITLY_EXPORTED);
1034+
}
1035+
10321036
JL_DLLEXPORT jl_binding_partition_t *jl_get_binding_partition(jl_binding_t *b JL_PROPAGATES_ROOT, size_t world) JL_GLOBALLY_ROOTED;
10331037
JL_DLLEXPORT jl_binding_partition_t *jl_get_binding_partition_with_hint(jl_binding_t *b JL_PROPAGATES_ROOT, jl_binding_partition_t *previous_part, size_t world) JL_GLOBALLY_ROOTED;
10341038
JL_DLLEXPORT jl_binding_partition_t *jl_get_binding_partition_all(jl_binding_t *b JL_PROPAGATES_ROOT, size_t min_world, size_t max_world) JL_GLOBALLY_ROOTED;

0 commit comments

Comments
 (0)