/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
JuliaLowering/test/scopes.jl
1 159 строк
53 KB
Em Chu
[JuliaLowering] Force `GlobalRef` for function-returning macro (#62565)
30 июл 2026, 20:40
Не верифицирован
30 июл 2026, 20:40
57bf0fb
Код
Авторство
О чём код?
test_mod = Module() #------------------------------------------------------------------------------- # Scopes @test JuliaLowering.include_string(test_mod, """ let y = 0 x = 1 let x = x + 1 y = x end (x, y) end """) == (1, 2) JuliaLowering.include_string(test_mod, """ x = 101 y = 202 """) @test test_mod.x == 101 @test test_mod.y == 202 @test JuliaLowering.include_string(test_mod, "x + y") == 303 @test JuliaLowering.include_string(test_mod, """ begin local x = 1 local x = 2 let (x,y) = (:x,:y) (y,x) end end """) === (:y,:x) # Types on left hand side of type decls refer to the outer scope # (In the flisp implementation they refer to the inner scope, but this seems # like a bug.) # edit: Using flisp semantics for now; see test below. @test_broken JuliaLowering.include_string(test_mod, """ let x::Int = 10.0 local Int = Float64 x end """) === 10 # The type in a let type decl must apply (and be re-evaluated) on assignments # to the variable from closures capturing it; hoisting the type into a # temporary outside the closure broke lowering of the closure body. @test JuliaLowering.include_string(test_mod, """ let x::Int = 1.0 f = function () x = 2.0 end f() x end """) === 2 # Closures in let syntax can only capture values from the outside # (In the flisp implementation it captures from inner scope, but this is # inconsistent with let assignment where the rhs refers to the outer scope and # thus seems like a bug.) @test_broken JuliaLowering.include_string(test_mod, """ begin local y = :outer_y let f() = y local y = :inner_y f() end end """) === :outer_y #= | old\new: || global | local | arg | sparam | |----------++------------+-------+--------------------+--------------------| | global || no-op | (*) | | | | local || error (*) | no-op | | | | arg || shadow(??) | error | error (not unique) | | | sparam || shadow(??) | error | error (sparam/arg) | error (not unique) | =# @testset "Conflicts in the same local scope" begin # no-op cases. It would probably be clearer (but breaking) if these were # errors like the conflict cases (two of the same decl should never do # anything, and the user might be expecting two variables). @testset "global,global" begin s = "function (); global g; global g; 1; end" @test JuliaLowering.include_string(test_mod, s) isa Function end @testset "local,local" begin s = "function (); local l; local l; end" @test JuliaLowering.include_string(test_mod, s) isa Function end # locals may not overlap args/sparams/globals @testset "global,local/local,global" begin s = "function (); global g; local g; end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) end @testset "arg,local" begin s = "function (x); local x; end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) end @testset "sparam,local" begin s = "function (a::s) where {s}; local s; end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) end # globals may overlap args or sparams (buggy?) TODO: decide whether it's # worth replicating this behaviour. We would likely need to copy the way # flisp nests an extra scope block in every lambda. @testset "arg,global" begin local f s = "function (a); global a = 1; a; end" @test_broken f = JuliaLowering.include_string(test_mod, s) @test_broken f isa Function @test_broken f(999) === 1 @test_broken isdefinedglobal(test_mod, :a) end @testset "sparam,global" begin local f s = "function (a::s) where {s}; global s = 1; s; end" @test_broken f = JuliaLowering.include_string(test_mod, s) @test_broken f isa Function @test_broken f(999) === 1 @test_broken isdefinedglobal(test_mod, :s) end # sp/arg conflict @testset "arg,sparam" begin s = "function (a) where {a}; end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) end @testset "arg,arg" begin s = "function (a,a); end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) end @testset "sparam,sparam" begin s = "function () where {s,s}; end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) end # (not in table) destructured args are handled internally like locals, but # should have similar conflict rules to arguments @testset "destructured-arg,destructured-arg/arg/local/sp/global" begin s = "function ((x,x)); end" # this works in flisp; should it? @test_throws LoweringError JuliaLowering.include_string(test_mod, s) s = "function ((x,y),x); end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) s = "function ((x,y)) where {x}; end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) s = "function ((x,y)); global x; x; end" @test_throws LoweringError JuliaLowering.include_string(test_mod, s) # quirk: flisp is OK with this s = "function ((x,y)); local x; end" @test JuliaLowering.include_string(test_mod, s) isa Function end end # Switch to Core.eval for sanity-checking expr_eval(mod, ex) = JuliaLowering.eval(mod, ex) enable_softscope(e...) = Expr(:block, Expr(:softscope, true), e...) wrap_none(e...) = Expr(:block, e...) wrap_neutral(e...) = Expr(:try, # use try so that a value is returned Expr(:block, e...), :catchvar, Expr(:block, Expr(:call, rethrow, :catchvar))) wrap_func(e...) = Expr(:call, Expr(:function, Expr(:tuple), Expr(:block, e...))) wrap_hard(e...) = Expr(:let, Expr(:block), Expr(:block, e...)) decls(e...) = Expr(:block, :(name = false), :(local lname = false), :(global gname = false), e...) decls_none(e...) = decls(e...) decls_neutral(e...) = wrap_neutral(decls(), e...) decls_hard(e...) = :(let lname = false # takes a different code path in flisp name = false global gname = false $(e...) end) decls_func(e...) = :((function (argname::spname = false) where spname name = false local lname = false global gname = false $(e...) end)(#=called=#)) lhs_names = (:name, :lname, :gname, :argname, :spname) #= simple test that ``` distraction_scope_begin local_scope_begin lhs = "resolve me" lhs *= '!' lhs local_scope_end distraction_scope_end === "resolve me!" ``` =# @testset "explicit locals and globals in local scope shadowing outer vars" begin local jl_mod = Module() local fl_mod = Module() @testset for soft_mode in (false, true), decls_s in (decls_func, decls_hard, decls_neutral, decls_none), local_s in (wrap_func, wrap_hard, wrap_neutral), lhs in lhs_names, assign_ex in (:(local $lhs = "resolve me"; $lhs *= '!'; $lhs), :(global $lhs = "resolve me"; $lhs *= '!'; $lhs)) ex = decls_s(local_s(assign_ex)) soft_mode && (ex = enable_softscope(ex)) if lhs == :spname && decls_s == decls_func && assign_ex.args[1].head === :local # flisp specifically disallows locals shadowing sparams; why? @test_broken fl_eval(fl_mod, ex) === "resolve me!" elseif lhs in (:spname, :argname) && decls_s != decls_func continue else reference_ok = fl_eval(fl_mod, ex) === "resolve me!" !reference_ok && @error("shadow test failed: flisp produced unexpected result; fix that or JL scope tests:\n", ex) @test reference_ok end ok = expr_eval(jl_mod, ex) === "resolve me!" !ok && @error("shadow test failed:\n", ex) @test ok end end @test JuliaLowering.include_string(test_mod, """ global g_shadow_sp_bound = Number function f_g_shadow_sp_bound(x::g_shadow_sp_bound) where { g_shadow_sp_bound<:g_shadow_sp_bound } (x, g_shadow_sp_bound) end f_g_shadow_sp_bound(1) """) == (1, Int) # For each distinct outer scope, declaration scope, and assignment scope, and # each kind of variable (lhs_names) in the declaration scope, set the same name # to true from the inner scope @testset "Behaviour of `=` in local scope (shadow or assign-existing)" begin expected_outer_vals = Dict{Tuple{Bool, Function, Function, Function}, Tuple}( (false, decls_func, wrap_hard, wrap_func ) => (true,true,true,true), (false, decls_func, wrap_hard, wrap_hard ) => (true,true,true,true), (false, decls_func, wrap_hard, wrap_neutral) => (true,true,true,true), (false, decls_func, wrap_neutral, wrap_func ) => (true,true,true,true), (false, decls_func, wrap_neutral, wrap_hard ) => (true,true,true,true), (false, decls_func, wrap_neutral, wrap_neutral) => (true,true,true,true), (false, decls_func, wrap_none, wrap_func ) => (true,true,true,true), (false, decls_func, wrap_none, wrap_hard ) => (true,true,true,true), (false, decls_func, wrap_none, wrap_neutral) => (true,true,true,true), (false, decls_hard, wrap_hard, wrap_func ) => (true,true,true), (false, decls_hard, wrap_hard, wrap_hard ) => (true,true,true), (false, decls_hard, wrap_hard, wrap_neutral) => (true,true,true), (false, decls_hard, wrap_neutral, wrap_func ) => (true,true,true), (false, decls_hard, wrap_neutral, wrap_hard ) => (true,true,true), (false, decls_hard, wrap_neutral, wrap_neutral) => (true,true,true), (false, decls_hard, wrap_none, wrap_func ) => (true,true,true), (false, decls_hard, wrap_none, wrap_hard ) => (true,true,true), (false, decls_hard, wrap_none, wrap_neutral) => (true,true,true), (false, decls_neutral, wrap_hard, wrap_func ) => (true,true,true), (false, decls_neutral, wrap_hard, wrap_hard ) => (true,true,true), (false, decls_neutral, wrap_hard, wrap_neutral) => (true,true,true), (false, decls_neutral, wrap_neutral, wrap_func ) => (true,true,true), (false, decls_neutral, wrap_neutral, wrap_hard ) => (true,true,true), (false, decls_neutral, wrap_neutral, wrap_neutral) => (true,true,true), (false, decls_neutral, wrap_none, wrap_func ) => (true,true,true), (false, decls_neutral, wrap_none, wrap_hard ) => (true,true,true), (false, decls_neutral, wrap_none, wrap_neutral) => (true,true,true), (false, decls_none, wrap_hard, wrap_func ) => (false,true,false), (false, decls_none, wrap_hard, wrap_hard ) => (false,true,false), (false, decls_none, wrap_hard, wrap_neutral) => (false,true,false), (false, decls_none, wrap_neutral, wrap_func ) => (false,true,false), (false, decls_none, wrap_neutral, wrap_hard ) => (false,true,false), (false, decls_none, wrap_neutral, wrap_neutral) => (false,true,false), (false, decls_none, wrap_none, wrap_func ) => (false,true,false), (false, decls_none, wrap_none, wrap_hard ) => (false,true,false), (false, decls_none, wrap_none, wrap_neutral) => (false,true,false), (true, decls_func, wrap_hard, wrap_func ) => (true,true,true,true), (true, decls_func, wrap_hard, wrap_hard ) => (true,true,true,true), (true, decls_func, wrap_hard, wrap_neutral) => (true,true,true,true), (true, decls_func, wrap_neutral, wrap_func ) => (true,true,true,true), (true, decls_func, wrap_neutral, wrap_hard ) => (true,true,true,true), (true, decls_func, wrap_neutral, wrap_neutral) => (true,true,true,true), (true, decls_func, wrap_none, wrap_func ) => (true,true,true,true), (true, decls_func, wrap_none, wrap_hard ) => (true,true,true,true), (true, decls_func, wrap_none, wrap_neutral) => (true,true,true,true), (true, decls_hard, wrap_hard, wrap_func ) => (true,true,true), (true, decls_hard, wrap_hard, wrap_hard ) => (true,true,true), (true, decls_hard, wrap_hard, wrap_neutral) => (true,true,true), (true, decls_hard, wrap_neutral, wrap_func ) => (true,true,true), (true, decls_hard, wrap_neutral, wrap_hard ) => (true,true,true), (true, decls_hard, wrap_neutral, wrap_neutral) => (true,true,true), (true, decls_hard, wrap_none, wrap_func ) => (true,true,true), (true, decls_hard, wrap_none, wrap_hard ) => (true,true,true), (true, decls_hard, wrap_none, wrap_neutral) => (true,true,true), (true, decls_neutral, wrap_hard, wrap_func ) => (true,true,true), (true, decls_neutral, wrap_hard, wrap_hard ) => (true,true,true), (true, decls_neutral, wrap_hard, wrap_neutral) => (true,true,true), (true, decls_neutral, wrap_neutral, wrap_func ) => (true,true,true), (true, decls_neutral, wrap_neutral, wrap_hard ) => (true,true,true), (true, decls_neutral, wrap_neutral, wrap_neutral) => (true,true,true), (true, decls_neutral, wrap_none, wrap_func ) => (true,true,true), (true, decls_neutral, wrap_none, wrap_hard ) => (true,true,true), (true, decls_neutral, wrap_none, wrap_neutral) => (true,true,true), (true, decls_none, wrap_hard, wrap_func ) => (false,true,false), (true, decls_none, wrap_hard, wrap_hard ) => (false,true,false), (true, decls_none, wrap_hard, wrap_neutral) => (false,true,false), (true, decls_none, wrap_neutral, wrap_func ) => (false,true,false), (true, decls_none, wrap_neutral, wrap_hard ) => (false,true,false), (true, decls_none, wrap_neutral, wrap_neutral) => (true,true,true), (true, decls_none, wrap_none, wrap_func ) => (false,true,false), (true, decls_none, wrap_none, wrap_hard ) => (false,true,false), (true, decls_none, wrap_none, wrap_neutral) => (true,true,true), ) expected_s(b::Bool) = b ? "assignment to outer var" : "brand-new var" local jl_mod = Module() local fl_mod = Module() @testset for ((soft_mode, decls_s, middle_s, assign_s), results) in expected_outer_vals, (lhs_i, lhs) in enumerate(lhs_names) ex = decls_s(middle_s(assign_s(:($lhs = true))), lhs) soft_mode && (ex = enable_softscope(ex)) if lhs in (:argname, :spname) && decls_s !== decls_func continue elseif lhs === :spname @test_throws LoweringError expr_eval(jl_mod, ex) else @assert !isdefined(jl_mod, lhs) && !isdefined(fl_mod, lhs) expected = results[lhs_i] reference_ok = fl_eval(fl_mod, ex) === expected !reference_ok && @error("flisp produced unexpected result; fix that or JL scope tests:\n", "expected $(expected_s(expected)), got $(expected_s(!expected))\n", ex) @test reference_ok ok = expr_eval(jl_mod, ex) === expected !ok && @error("expected $(expected_s(expected)), got $(expected_s(!expected))\n", ex) @test ok end Core.@latestworld for mod in (jl_mod, fl_mod), n in (:gname, :name) isdefined(mod, n) && Base.delete_binding(mod, n) end Core.@latestworld end end @testset "global declarations at top level are ignored in assignment resolution" begin suggest_global(e) = :(begin; global declared_unassigned_global; $e; end) for soft_mode in (true, false), scope in (wrap_func, wrap_hard, wrap_neutral) ex = scope(:(declared_unassigned_global = true)) soft_mode && (ex = enable_softscope(ex)) expr_eval(test_mod, ex) global_assigned = @invokelatest isdefined(test_mod, :declared_unassigned_global) global_assigned && error("global should not be assigned. settings: $soft_mode $scope\n") @test !global_assigned end @testset "soft scope isn't top level" begin ex = quote begin for i in 1:1; global soft_assigned_explicit_global = 1; end for i in 1:1; soft_assigned_explicit_global = 2; end end end expr_eval(test_mod, enable_softscope(ex)) @test test_mod.soft_assigned_explicit_global === 1 end end # Distinct from the stateful "existing global" check (probably to get around the # case where the global only becomes existing within the expression being # lowered) @testset "assignments at top level can influence assignment resolution in soft scopes" begin for soft_mode in (true, false), s1 in (wrap_neutral, (e)->wrap_neutral(wrap_neutral(e))), g_assign in (:(assigned_global = false), :(global assigned_global = false)) inner_assign_islocal = s1(Expr( :block, :(assigned_global = true), Expr(:(=), :out, Expr(:islocal, :assigned_global)))) for ex in (Expr(:block, :(local out), inner_assign_islocal, g_assign, :out), Expr(:block, :(local out), g_assign, inner_assign_islocal, :out)) if soft_mode ex = enable_softscope(ex) ok = expr_eval(test_mod, ex) === false !ok && error("expected assignment to global\n", ex) @test ok else # some of these produce warning in flisp ok = expr_eval(test_mod, ex) === true !ok && error("expected assignment to local\n", ex) @test ok end Base.delete_binding(test_mod, :assigned_global) end end end module ambiguous_local global x::Int = 0 end function resolve_and_get_bindings( mod::Module, ex; world::UInt = Base.get_world_counter(), soft_scope::Union{Nothing,Bool} = nothing, ) est = JuliaLowering.expr_to_est(ex) ex0 = JuliaLowering.rebase_layers(est, mod, JuliaLowering.JL_NEW_SYNTAX_VERSION) ex1 = JuliaLowering.expand_forms_1(ex0, world, true) ctx2, ex2 = JuliaLowering.expand_forms_2(ex1, world) ctx3, _ = JuliaLowering.resolve_scopes(ctx2, ex2; soft_scope) return ctx3.bindings.info end @testset "internal keyword body bindings" begin bindings = resolve_and_get_bindings(Module(), :(f(; x=1) = x)) kw_body_bindings = filter(b -> startswith(b.name, "#kw_body#"), bindings) @test !isempty(kw_body_bindings) @test all(b -> b.is_internal, kw_body_bindings) end @testset "is_ambiguous_local" begin # Assignment in for loop within begin block after toplevel assignment let bindings = resolve_and_get_bindings(ambiguous_local, :(for _ = 1:10; x = 1; end)) binfo = only(filter(b->b.name=="x", bindings)) @test binfo.kind === :local @test binfo.is_ambiguous_local end # while loop let bindings = resolve_and_get_bindings(ambiguous_local, :(while x < 5; x += 1; break; end)) binfos = filter(b->b.name=="x", bindings) @test length(binfos) == 2 binfo = only(filter(b->b.kind==:local, binfos)) @test binfo.is_ambiguous_local @test count(b->b.kind==:global, binfos) == 1 end # No ambiguity inside a function (hard scope) let bindings = resolve_and_get_bindings(ambiguous_local, :(function f() for _ = 1:10 x = 1 end end)) binfo = only(filter(b->b.name=="x", bindings)) @test binfo.kind === :local @test !binfo.is_ambiguous_local end # No ambiguity when shadowing global variable does not exist let bindings = resolve_and_get_bindings(ambiguous_local, :(for _ = 1:10; y = 1; end)) binfo = only(filter(b->b.name=="y", bindings)) @test binfo.kind === :local @test !binfo.is_ambiguous_local end # Explicit `global` should not produce ambiguous local let bindings = resolve_and_get_bindings(ambiguous_local, :(for _ = 1:10; global x = 1; end)) binfo = only(filter(b->b.name=="x", bindings)) @test binfo.kind === :global end # Block containing a toplevel assignment preceding a permeable scope let bindings = resolve_and_get_bindings(Module(), quote x = 0 for _ = 1:10 x = 1 end end) binfos = filter(b->b.name=="x", bindings) @test length(binfos) == 2 binfo = only(filter(b->b.kind==:local, binfos)) @test binfo.is_ambiguous_local @test count(b->b.kind==:global, binfos) == 1 end # Block containing a permeable scope followed by a toplevel assignment let bindings = resolve_and_get_bindings(Module(), quote for _ = 1:10 x = 1 end x = 0 end) binfos = filter(b->b.name=="x", bindings) @test length(binfos) == 2 binfo = only(filter(b->b.kind==:local, binfos)) @test binfo.is_ambiguous_local @test count(b->b.kind==:global, binfos) == 1 end # For some reason, flisp can avoid ambiguity when there is an additional `global` annotation. # JuliaLowering may want to follow suit, but it would be better to first decide on the details of this behaviour. let bindings = resolve_and_get_bindings(Module(), quote global x = 0 for _ = 1:10 x = 1 end end) binfos = filter(b->b.name=="x", bindings) @test length(binfos) == 2 binfo = only(filter(b->b.kind==:local, binfos)) @test_broken !binfo.is_ambiguous_local @test count(b->b.kind==:global, binfos) == 1 end let bindings = resolve_and_get_bindings(Module(), quote for _ = 1:10 x = 1 end global x = 0 end) binfos = filter(b->b.name=="x", bindings) @test length(binfos) == 2 binfo = only(filter(b->b.kind==:local, binfos)) @test_broken !binfo.is_ambiguous_local @test count(b->b.kind==:global, binfos) == 1 end @testset "soft_scope kwarg override" begin # Without soft_scope, x becomes an ambiguous local let bindings = resolve_and_get_bindings(ambiguous_local, :(for _ = 1:10; x = 1; end)) binfo = only(filter(b->b.name=="x", bindings)) @test binfo.kind === :local @test binfo.is_ambiguous_local end # With soft_scope=true, x stays global (no local created) let bindings = resolve_and_get_bindings(ambiguous_local, :(for _ = 1:10; x = 1; end); soft_scope=true) binfo = only(filter(b->b.name=="x", bindings)) @test binfo.kind === :global end end @testset "world-age propagation" begin let m = Module() Core.eval(m, :(global x = 0)) bindings = resolve_and_get_bindings(m, :(for _ = 1:10; x = 1; end); world=Base.get_world_counter()) binfo = only(filter(b->b.name=="x", bindings)) @test binfo.kind === :local @test binfo.is_ambiguous_local end let m = Module() Core.eval(m, :(global x = 0)) bindings = resolve_and_get_bindings(m, :(for _ = 1:10; x = 1; end); world=Base.get_world_counter(), soft_scope=true) binfo = only(filter(b->b.name=="x", bindings)) @test binfo.kind === :global end end end @testset "unescaped macro expansions introduce a hygienic scope" begin @eval test_mod module macro_mod macro m(x); x; end macro mesc(x); esc(x); end end # A function not wrapped in anything is made a macro-module global (#32026) JuliaLowering.include_string(test_mod, "macro_mod.@m function f_bug_1(); 1; end") @test isdefined(test_mod.macro_mod, :f_bug_1) JuliaLowering.include_string(test_mod, "macro_mod.@m function f_bug_2 end") @test isdefined(test_mod.macro_mod, :f_bug_2) JuliaLowering.include_string(test_mod, "macro_mod.@m f_bug_3(x) = 1") @test isdefined(test_mod.macro_mod, :f_bug_3) JuliaLowering.include_string(test_mod, "macro_mod.@m f_bug_4(x)::Int = 1") @test isdefined(test_mod.macro_mod, :f_bug_4) # (wrapped def is fine) JuliaLowering.include_string(test_mod, "macro_mod.@m begin; f_local_1(x) = 1; end") @test !isdefined(test_mod.macro_mod, :f_local_1) @test !isdefined(test_mod, :f_local_1) JuliaLowering.include_string(test_mod, "macro_mod.@mesc function f_nonlocal_2(); 1; end") @test isdefined(test_mod, :f_nonlocal_2) # An unescaped const should not error coming from an old-style macro @test JuliaLowering.include_string(test_mod, "macro_mod.@m const c_local_1 = 1") == 1 # The const may be escaped into test_mod JuliaLowering.include_string(test_mod, "macro_mod.@mesc const c_nonlocal_2 = 1") @test isdefined(test_mod, :c_nonlocal_2) JuliaLowering.include_string(test_mod, "macro_mod.@mesc const c_nonlocal_3 = 1"; expr_compat_mode=true) @test isdefined(test_mod, :c_nonlocal_3) end fl_eval(test_mod, :(macro old_hyg(x); x; end)) fl_eval(test_mod, :(macro old_esc(x); Expr(:escape, x); end)) # caller modules, where test_mod is the macro module module jl_mod import ..test_mod.@old_hyg import ..test_mod.@old_esc end module fl_mod import ..test_mod.@old_hyg import ..test_mod.@old_esc end # In flisp, with no escaping: # - Top level globals are unhygienic and declared in the calling module # - this includes abstract, primitive, and struct types # Not yet explicitly handled or tested: # - Top-level functions are unhygienic and declared in the macro's module # - Top-level `x=y` implicitly declares hygienic globals (but it is not breaking # to make them local) # # See https://github.com/JuliaLang/julia/issues/53667 for more quirks @testset "compat: macro hygiene exemptions for explicit globals" begin # desirable side of this behaviour where global decls passed as arguments to # non-escaping macros appear to have automatic hygiene (it's assumed that # declaring a global in the body of a macro is usually intended to produce a # global in the calling module). @testset "passed as an argument" for (ctx, mod, run) in [ ("flisp reference (delete if fail)", fl_mod, x->fl_eval(fl_mod, x)), ("jl expr_compat_mode=true", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=true)), ("jl expr_compat_mode=false", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=false))] @testset for str in [ # "@old_hyg const GENSYM = 1; GENSYM == 1" # flisp mangles, JL counts local "@old_hyg(global GENSYM = 1); GENSYM == 1" "@old_hyg(global GENSYM::Int = 1); GENSYM == 1" "@old_hyg(global (((GENSYM,),),) = (((1,),),)); GENSYM == 1" "(()->(@old_hyg global GENSYM = 1))() == 1" "@old_hyg(global const GENSYM = 1); GENSYM == 1" "@old_hyg(const global GENSYM = 1); GENSYM == 1" "@old_hyg(struct GENSYM end); GENSYM isa Type" "@old_hyg(struct GENSYM; x::Int; GENSYM(x) = new(x); end); GENSYM(42).x == 42" "@old_hyg(struct GENSYM{T}; x::T; end); GENSYM(42).x == 42" "@old_hyg(abstract type GENSYM end); GENSYM isa Type" "@old_hyg(primitive type GENSYM 8 end); GENSYM isa Type" # global functions: flisp encounters errors # "@old_hyg(global GENSYM(x) = x); GENSYM isa Function" # "@old_hyg(global function GENSYM(x)\nx\nend); GENSYM isa Function" # "@old_hyg(let\n global GENSYM(x) = x\nend); GENSYM isa Function" # "@old_hyg(let\n global function GENSYM(x)\nx\nend \nend); GENSYM isa Function" ] @gensym gen_global_sym prog_str = "#="*ctx*"=# "*replace( str, "GENSYM"=>"var\""*(string(gen_global_sym))*"\"") prog = JuliaSyntax.parseall(SyntaxTree, prog_str) @test run(prog) context=prog_str Core.@latestworld @test Base.isdefinedglobal(mod, gen_global_sym) context=prog_str @test !Base.isdefinedglobal(test_mod, gen_global_sym) context=prog_str end # these shouldn't resolve (JL implementation shouldn't be too lax) @testset for str in [ "begin; global GENSYM=1; @old_hyg(GENSYM) == 1; end" "begin; global GENSYM::Int=1; @old_hyg(GENSYM) == 1; end" "begin; global (((GENSYM,),),) = (((1,),),); @old_hyg(GENSYM) == 1; end" "begin; (()->(global GENSYM = 1; @old_hyg(GENSYM)))() == 1; end" "begin; global const GENSYM = 1; @old_hyg(GENSYM) == 1; end" "begin; const global GENSYM = 1; @old_hyg(GENSYM) == 1; end" "begin; struct GENSYM end; @old_hyg(GENSYM) isa Type; end" "begin; abstract type GENSYM end; @old_hyg(GENSYM) isa Type; end" "begin; primitive type GENSYM 8 end; @old_hyg(GENSYM) isa Type; end" ] @gensym gen_global_sym prog_str = "#="*ctx*"=# "*replace( str, "GENSYM"=>"var\""*(string(gen_global_sym))*"\"") prog = JuliaSyntax.parseall(SyntaxTree, prog_str) @test_throws string(gen_global_sym) run(prog) context=prog_str Core.@latestworld @test Base.isdefinedglobal(mod, gen_global_sym) context=prog_str @test !Base.isdefinedglobal(test_mod, gen_global_sym) context=prog_str end end # bad side: the same global decl coming from the body of the macro behaves # as if it was passed as a macro argument. It would have been more # consistent to require escaping here, since now it's impossible to # represent a global declaration in the macro module. @testset "from the macro body" for (ctx, mod, run) in [ ("flisp reference (delete if fail)", fl_mod, x->fl_eval(fl_mod, x)), ("jl expr_compat_mode=true", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=true)), ("jl expr_compat_mode=false", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=false))] # interpolating global name here appears to hit a bug fl_eval(test_mod, :(macro old_hyg_globalvar(str); quote global old_hyg_globalvar_G = $str end end)) run(:(import ..test_mod.@old_hyg_globalvar)) Core.@latestworld let s = "ran old_hyg_globalvar" @test run(:(@old_hyg_globalvar $s)) == s context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_globalvar_G) context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_globalvar_G) context=ctx end Base.delete_binding(mod, :old_hyg_globalvar_G) Base.delete_binding(test_mod, :old_hyg_globalvar_G) fl_eval(test_mod, :(macro old_hyg_globalvar_typed(str); quote global old_hyg_globalvar_typed_G::String = $str end end)) run(:(import ..test_mod.@old_hyg_globalvar_typed)) Core.@latestworld let s = "ran old_hyg_globalvar_typed" @test run(:(@old_hyg_globalvar_typed $s)) == s context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_globalvar_typed_G) context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_globalvar_typed_G) context=ctx end Base.delete_binding(mod, :old_hyg_globalvar_typed_G) Base.delete_binding(test_mod, :old_hyg_globalvar_typed_G) fl_eval(test_mod, :(macro old_hyg_globalvar_tuple(str); quote global (((old_hyg_globalvar_tuple_G,),),) = ((($str,),),) end end)) run(:(import ..test_mod.@old_hyg_globalvar_tuple)) Core.@latestworld let s = "ran old_hyg_globalvar_tuple" @test run(:(@old_hyg_globalvar_tuple $s)) == (((s,),),) context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_globalvar_tuple_G) context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_globalvar_tuple_G) context=ctx end Base.delete_binding(mod, :old_hyg_globalvar_tuple_G) Base.delete_binding(test_mod, :old_hyg_globalvar_tuple_G) fl_eval(test_mod, :(macro old_hyg_globalvar_in_lam(str); quote (()->(global old_hyg_globalvar_in_lam_G = $str))() end end)) run(:(import ..test_mod.@old_hyg_globalvar_in_lam)) Core.@latestworld let s = "ran old_hyg_globalvar_in_lam" @test run(:(@old_hyg_globalvar_in_lam $s)) == s context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_globalvar_in_lam_G) context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_globalvar_in_lam_G) context=ctx end Base.delete_binding(mod, :old_hyg_globalvar_in_lam_G) Base.delete_binding(test_mod, :old_hyg_globalvar_in_lam_G) fl_eval(test_mod, :(macro old_hyg_globalvar_const(str); quote global const old_hyg_globalvar_const_G = $str end end)) run(:(import ..test_mod.@old_hyg_globalvar_const)) Core.@latestworld let s = "ran old_hyg_globalvar_const" @test run(:(@old_hyg_globalvar_const $s)) == s context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_globalvar_const_G) context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_globalvar_const_G) context=ctx end Base.delete_binding(mod, :old_hyg_globalvar_const_G) Base.delete_binding(test_mod, :old_hyg_globalvar_const_G) fl_eval(test_mod, :(macro old_hyg_const_globalvar(str); quote const global old_hyg_const_globalvar_G = $str end end)) run(:(import ..test_mod.@old_hyg_const_globalvar)) Core.@latestworld let s = "ran old_hyg_const_globalvar" @test run(:(@old_hyg_const_globalvar $s)) == s context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_const_globalvar_G) context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_const_globalvar_G) context=ctx end Base.delete_binding(mod, :old_hyg_const_globalvar_G) Base.delete_binding(test_mod, :old_hyg_const_globalvar_G) fl_eval(test_mod, :(macro old_hyg_struct(str); quote struct old_hyg_struct_G end end end)) run(:(import ..test_mod.@old_hyg_struct)) Core.@latestworld let s = "ran old_hyg_struct" @test run(:(@old_hyg_struct $s)) === nothing context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_struct_G) context=ctx @test mod.old_hyg_struct_G isa Type context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_struct_G) context=ctx end Base.delete_binding(mod, :old_hyg_struct_G) Base.delete_binding(test_mod, :old_hyg_struct_G) fl_eval(test_mod, :(macro old_hyg_abstract_type(str); quote abstract type old_hyg_abstract_type_G end end end)) run(:(import ..test_mod.@old_hyg_abstract_type)) Core.@latestworld let s = "ran old_hyg_abstract_type" @test run(:(@old_hyg_abstract_type $s)) === nothing context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_abstract_type_G) context=ctx @test mod.old_hyg_abstract_type_G isa Type context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_abstract_type_G) context=ctx end Base.delete_binding(mod, :old_hyg_abstract_type_G) Base.delete_binding(test_mod, :old_hyg_abstract_type_G) fl_eval(test_mod, :(macro old_hyg_primitive_type(str); quote primitive type old_hyg_primitive_type_G 8 end end end)) run(:(import ..test_mod.@old_hyg_primitive_type)) Core.@latestworld let s = "ran old_hyg_primitive_type" @test run(:(@old_hyg_primitive_type $s)) === nothing context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_primitive_type_G) context=ctx @test mod.old_hyg_primitive_type_G isa Type context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_primitive_type_G) context=ctx end Base.delete_binding(mod, :old_hyg_primitive_type_G) Base.delete_binding(test_mod, :old_hyg_primitive_type_G) end end # The hygiene exemption above applies only within the scope containing the # `global` declaration: the declaration creates a global in the calling module, # but other references to the same name elsewhere in the expansion are still # hygienic, resolving in the macro's module. @testset "(AI) compat: hygiene exemption is confined to the declaring scope" begin # f() should throw an UndefVarError for the macro-module (hygienic) global undef_in_test_mod(f, name) = begin err = try; f(); catch e; e; end err isa UndefVarError && err.var === name && err.scope === test_mod end @testset "references outside the declaring scope" for (ctx, mod, run) in [ ("flisp reference (delete if fail)", fl_mod, x->fl_eval(fl_mod, x)), ("jl expr_compat_mode=true", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=true)), ("jl expr_compat_mode=false", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=false))] fl_eval(test_mod, :(macro old_hyg_g_in_let(); quote let global old_hyg_g_in_let_G = 1 end old_hyg_g_in_let_G end end)) run(:(import ..test_mod.@old_hyg_g_in_let)) Core.@latestworld @test undef_in_test_mod(()->run(:(@old_hyg_g_in_let)), :old_hyg_g_in_let_G) context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_g_in_let_G) && getglobal(mod, :old_hyg_g_in_let_G) == 1 context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_g_in_let_G) context=ctx Base.delete_binding(mod, :old_hyg_g_in_let_G) Base.delete_binding(test_mod, :old_hyg_g_in_let_G) fl_eval(test_mod, :(macro old_hyg_g_in_lam(); quote (()->(global old_hyg_g_in_lam_G = 2))() old_hyg_g_in_lam_G end end)) run(:(import ..test_mod.@old_hyg_g_in_lam)) Core.@latestworld @test undef_in_test_mod(()->run(:(@old_hyg_g_in_lam)), :old_hyg_g_in_lam_G) context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_g_in_lam_G) && getglobal(mod, :old_hyg_g_in_lam_G) == 2 context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_g_in_lam_G) context=ctx Base.delete_binding(mod, :old_hyg_g_in_lam_G) Base.delete_binding(test_mod, :old_hyg_g_in_lam_G) fl_eval(test_mod, :(macro old_hyg_g_sibling(); quote let global old_hyg_g_sibling_G = 3 end let old_hyg_g_sibling_G end end end)) run(:(import ..test_mod.@old_hyg_g_sibling)) Core.@latestworld @test undef_in_test_mod(()->run(:(@old_hyg_g_sibling)), :old_hyg_g_sibling_G) context=ctx Core.@latestworld @test Base.isdefinedglobal(mod, :old_hyg_g_sibling_G) && getglobal(mod, :old_hyg_g_sibling_G) == 3 context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_g_sibling_G) context=ctx Base.delete_binding(mod, :old_hyg_g_sibling_G) Base.delete_binding(test_mod, :old_hyg_g_sibling_G) end @testset "rescoping conflicts" for (ctx, mod, run) in [ # flisp is quite unpredictable here: segfaults, local-form-assigns-global # ("flisp reference (delete if fail)", fl_mod, x->fl_eval(fl_mod, x)), ("jl expr_compat_mode=true", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=true)), ("jl expr_compat_mode=false", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=false))] fl_eval(test_mod, :(macro old_hyg_g_rescope_conflict_old(); quote global bad = 1 end end)) run(:(import ..test_mod.@old_hyg_g_rescope_conflict_old)) Core.@latestworld @test_throws "conflicts with an existing" run(:( let bad = 1; @old_hyg_g_rescope_conflict_old(); end)) context=ctx @test_throws "conflicts with an existing" run(:( ((bad)->@old_hyg_g_rescope_conflict_old())(1))) context=ctx @test_throws "conflicts with an existing" run(:( (function old_hyg_g_rescope_conflict_old_F() where bad @old_hyg_g_rescope_conflict_old() end)())) context=ctx fl_eval(test_mod, :(macro old_hyg_g_rescope_conflict_new1(); :(let local bad = 1 global bad = 2 end) end)) run(:(import ..test_mod.@old_hyg_g_rescope_conflict_new1)) Core.@latestworld @test_throws "unhygienic global" run(:(@old_hyg_g_rescope_conflict_new1())) context=ctx fl_eval(test_mod, :(macro old_hyg_g_rescope_conflict_new2(); :(let global bad = 2 local bad = 1 end) end)) run(:(import ..test_mod.@old_hyg_g_rescope_conflict_new2)) Core.@latestworld @test_throws "unhygienic global" run(:(@old_hyg_g_rescope_conflict_new2())) context=ctx fl_eval(test_mod, :(macro old_hyg_g_rescope_conflict_new3(); :(let local bad global bad end) end)) run(:(import ..test_mod.@old_hyg_g_rescope_conflict_new3)) Core.@latestworld @test_throws "conflicts with an existing local variable" run(:(@old_hyg_g_rescope_conflict_new3())) context=ctx fl_eval(test_mod, :(macro old_hyg_g_rescope_conflict_new4(); :(let global bad local bad end) end)) run(:(import ..test_mod.@old_hyg_g_rescope_conflict_new4)) Core.@latestworld @test_throws "conflicts with an existing local variable" run(:(@old_hyg_g_rescope_conflict_new4())) context=ctx end # A reference in the same scope as the declaration: flisp keeps the # reference hygienic (hitting the undefined macro-module global), but # JuliaLowering deliberately resolves it to the global the declaration just # created in the calling module, which is more consistent. @testset "references in the declaring scope" for (ctx, mod, run, ref_resolves) in [ ("flisp reference (delete if fail)", fl_mod, x->fl_eval(fl_mod, x), false), ("jl expr_compat_mode=true", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=true), false), ("jl expr_compat_mode=false", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=false), false)] fl_eval(test_mod, :(macro old_hyg_g_ref_top(); quote global old_hyg_g_ref_top_G = 4 old_hyg_g_ref_top_G + 10 end end)) run(:(import ..test_mod.@old_hyg_g_ref_top)) Core.@latestworld if ref_resolves @test run(:(@old_hyg_g_ref_top)) == 14 context=ctx else @test undef_in_test_mod(()->run(:(@old_hyg_g_ref_top)), :old_hyg_g_ref_top_G) context=ctx end Core.@latestworld @test getglobal(mod, :old_hyg_g_ref_top_G) == 4 context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_g_ref_top_G) context=ctx Base.delete_binding(mod, :old_hyg_g_ref_top_G) Base.delete_binding(test_mod, :old_hyg_g_ref_top_G) fl_eval(test_mod, :(macro old_hyg_g_ref_let(); quote let global old_hyg_g_ref_let_G = 5 old_hyg_g_ref_let_G + 10 end end end)) run(:(import ..test_mod.@old_hyg_g_ref_let)) Core.@latestworld if ref_resolves @test run(:(@old_hyg_g_ref_let)) == 15 context=ctx else @test undef_in_test_mod(()->run(:(@old_hyg_g_ref_let)), :old_hyg_g_ref_let_G) context=ctx end Core.@latestworld @test getglobal(mod, :old_hyg_g_ref_let_G) == 5 context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_g_ref_let_G) context=ctx Base.delete_binding(mod, :old_hyg_g_ref_let_G) Base.delete_binding(test_mod, :old_hyg_g_ref_let_G) # repeated `global` declarations of the same name in one scope fl_eval(test_mod, :(macro old_hyg_g_dup(); quote global old_hyg_g_dup_G global old_hyg_g_dup_G = 6 old_hyg_g_dup_G end end)) run(:(import ..test_mod.@old_hyg_g_dup)) Core.@latestworld if ref_resolves @test run(:(@old_hyg_g_dup)) == 6 context=ctx else @test undef_in_test_mod(()->run(:(@old_hyg_g_dup)), :old_hyg_g_dup_G) context=ctx end Core.@latestworld @test getglobal(mod, :old_hyg_g_dup_G) == 6 context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_g_dup_G) context=ctx Base.delete_binding(mod, :old_hyg_g_dup_G) Base.delete_binding(test_mod, :old_hyg_g_dup_G) end # The soft scope exemption (assignment to an existing global from a # top-level loop) is for globals visible to the user at the macrocall site; # a hygienic assignment in an expansion must not hit an existing global of # the same name in the macro's module. @testset "soft scope assignments stay hygienic" for (ctx, mod, run) in [ ("flisp reference (delete if fail)", fl_mod, x->fl_eval(fl_mod, x)), ("jl expr_compat_mode=true", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=true)), ("jl expr_compat_mode=false", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=false))] fl_eval(test_mod, :(macro old_hyg_soft(); quote for i = 1:1 old_hyg_soft_G = 99 end end end)) Core.eval(test_mod, :(global old_hyg_soft_G = 0)) run(:(import ..test_mod.@old_hyg_soft)) Core.@latestworld @test run(enable_softscope(:(@old_hyg_soft))) === nothing context=ctx Core.@latestworld @test getglobal(test_mod, :old_hyg_soft_G) == 0 context=ctx @test !Base.isdefinedglobal(mod, :old_hyg_soft_G) context=ctx Base.delete_binding(mod, :old_hyg_soft_G) Base.delete_binding(test_mod, :old_hyg_soft_G) # escaped version assigns the caller's global as usual fl_eval(test_mod, :(macro old_esc_soft(); Expr(:escape, quote for i = 1:1 old_esc_soft_G = 99 end end) end)) Core.eval(mod, :(global old_esc_soft_G = 0)) run(:(import ..test_mod.@old_esc_soft)) Core.@latestworld @test run(enable_softscope(:(@old_esc_soft))) === nothing context=ctx Core.@latestworld @test getglobal(mod, :old_esc_soft_G) == 99 context=ctx @test !Base.isdefinedglobal(test_mod, :old_esc_soft_G) context=ctx Base.delete_binding(mod, :old_esc_soft_G) Base.delete_binding(test_mod, :old_esc_soft_G) end # Structs defined in the body of an old macro are unhygienic. Constructor # lowering (inner constructors and the runtime default constructors) # references the global struct name, which must resolve to the rescoped # global from within the struct's scope. @testset "struct from the macro body" for (ctx, mod, run) in [ ("flisp reference (delete if fail)", fl_mod, x->fl_eval(fl_mod, x)), ("jl expr_compat_mode=true", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=true)), ("jl expr_compat_mode=false", jl_mod, x->jl_eval(jl_mod, x; expr_compat_mode=false))] fl_eval(test_mod, :(macro old_hyg_struct_ctor(); quote struct old_hyg_struct_ctor_G x::Int old_hyg_struct_ctor_G(x) = new(x) end end end)) run(:(import ..test_mod.@old_hyg_struct_ctor)) Core.@latestworld @test run(:(@old_hyg_struct_ctor)) === nothing context=ctx Core.@latestworld @test run(:(old_hyg_struct_ctor_G(42).x)) == 42 context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_struct_ctor_G) context=ctx Base.delete_binding(mod, :old_hyg_struct_ctor_G) Base.delete_binding(test_mod, :old_hyg_struct_ctor_G) fl_eval(test_mod, :(macro old_hyg_struct_tv(); quote struct old_hyg_struct_tv_G{T} x::T end end end)) run(:(import ..test_mod.@old_hyg_struct_tv)) Core.@latestworld @test run(:(@old_hyg_struct_tv)) === nothing context=ctx Core.@latestworld @test run(:(old_hyg_struct_tv_G(42).x)) == 42 context=ctx @test !Base.isdefinedglobal(test_mod, :old_hyg_struct_tv_G) context=ctx Base.delete_binding(mod, :old_hyg_struct_tv_G) Base.delete_binding(test_mod, :old_hyg_struct_tv_G) end end @testset "@isdefined sees imported globals" begin # implicit Core/Base visibility and `using`-provided names count as defined # at module scope m = Module(:IsdefM) @test JuliaLowering.include_string(m, "@isdefined Core") === true @test JuliaLowering.include_string(m, "@isdefined Base") === true @test JuliaLowering.include_string(m, "@isdefined sin") === true @test JuliaLowering.include_string(m, "@isdefined not_a_thing_anywhere") === false @test JuliaLowering.include_string(m, "f() = @isdefined(Core); f()") === true end