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
