# This file is a part of Julia. License is MIT: https://julialang.org/license

# setup
# -----

include("setup_Compiler.jl")
include("irutils.jl")

using Test

struct InvalidationTesterToken end

struct InvalidationTester <: Compiler.AbstractInterpreter
    world::UInt
    inf_params::Compiler.InferenceParams
    opt_params::Compiler.OptimizationParams
    inf_cache::Compiler.InferenceCache
    function InvalidationTester(;
                                world::UInt = Base.get_world_counter(),
                                inf_params::Compiler.InferenceParams = Compiler.InferenceParams(),
                                opt_params::Compiler.OptimizationParams = Compiler.OptimizationParams(),
                                inf_cache::Compiler.InferenceCache = Compiler.InferenceCache())
        return new(world, inf_params, opt_params, inf_cache)
    end
end

Compiler.InferenceParams(interp::InvalidationTester) = interp.inf_params
Compiler.OptimizationParams(interp::InvalidationTester) = interp.opt_params
Compiler.get_inference_world(interp::InvalidationTester) = interp.world
Compiler.get_inference_cache(interp::InvalidationTester) = interp.inf_cache
Compiler.cache_owner(::InvalidationTester) = InvalidationTesterToken()

# Local constprop proofs expose same-module binding dependencies on the published caller.
module LocalProofBindingInvalidation61752
    _getproperty(M::Module, s::Symbol) = getglobal(M, s)

    const VALUE = "v1"
    probe() = _getproperty(LocalProofBindingInvalidation61752, :VALUE)::String
end

let interp = InvalidationTester()
    @test Base.infer_return_type(LocalProofBindingInvalidation61752.probe, (); interp) === String

    mi = Base.method_instance(LocalProofBindingInvalidation61752.probe, ())
    ci = mi.cache
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world === typemax(UInt)

    binding = convert(Core.Binding,
        GlobalRef(LocalProofBindingInvalidation61752, :VALUE))
    @test any(edge -> edge === binding, ci.edges)
    callee_mi = Base.method_instance(
        LocalProofBindingInvalidation61752._getproperty, (Module, Symbol))
    @test any(edge -> edge isa Core.CodeInstance && edge.def === callee_mi, ci.edges)

    world_before = Base.get_world_counter()
    @eval LocalProofBindingInvalidation61752 const VALUE = "v2"
    @test ci.max_world != typemax(UInt)
    @test ci.max_world <= world_before
end

# Eliding a finalizer registration based on inferred effects must keep the proof
# for those effects on the caller's CodeInstance.
module FinalizerEffectInvalidation62338
    mutable struct Target end
    callback(::Target) = nothing
    register(x::Target) = finalizer(callback, x)
end

let
    inf_params = Compiler.InferenceParams(
        ; cache_owner=FinalizerEffectInvalidation62338)
    interp = Compiler.NativeInterpreter(Base.get_world_counter(); inf_params)
    mi = Base.method_instance(FinalizerEffectInvalidation62338.register,
        (FinalizerEffectInvalidation62338.Target,))
    ci = Compiler.typeinf_ext(interp, mi, Compiler.SOURCE_MODE_GET_SOURCE)
    src = Compiler.ci_get_source(interp, ci)
    @test !any(iscall((src, Core.finalizer)), src.code)
    @test ci.max_world == typemax(UInt)

    world_before = Base.get_world_counter()
    @eval FinalizerEffectInvalidation62338 callback(::Target) =
        (global callback_ran = true; nothing)
    @test world_before < Base.get_world_counter()
    @test ci.max_world < Base.get_world_counter()
end

# basic functionality test
# ------------------------

basic_callee(x) = x
basic_caller(x) = basic_callee(x)

# run inference and check that cache exist
@test Base.return_types((Float64,); interp=InvalidationTester()) do x
    basic_caller(x)
end |> only === Float64

let mi = Base.method_instance(basic_callee, (Float64,))
    ci = mi.cache
    @test !isdefined(ci, :next)
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world == typemax(UInt)
end

let mi = Base.method_instance(basic_caller, (Float64,))
    ci = mi.cache
    @test !isdefined(ci, :next)
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world == typemax(UInt)
end

# this redefinition below should invalidate the cache
const BASIC_CALLER_WORLD = Base.get_world_counter()+1
basic_callee(x) = x, x
@test !isdefined(Base.method_instance(basic_callee, (Float64,)), :cache)
let mi = Base.method_instance(basic_caller, (Float64,))
    ci = mi.cache
    @test !isdefined(ci, :next)
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world == BASIC_CALLER_WORLD
end

# re-run inference and check the result is updated (and new cache exists)
@test Base.return_types((Float64,); interp=InvalidationTester()) do x
    basic_caller(x)
end |> only === Tuple{Float64,Float64}
let mi = Base.method_instance(basic_callee, (Float64,))
    ci = mi.cache
    @test !isdefined(ci, :next)
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world == typemax(UInt)
end

let mi = Base.method_instance(basic_caller, (Float64,))
    ci = mi.cache
    @test isdefined(ci, :next)
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world == typemax(UInt)
    ci = ci.next
    @test !isdefined(ci, :next)
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world != typemax(UInt)
end


# backedge optimization
# ---------------------

const GLOBAL_BUFFER = IOBuffer()

# test backedge optimization when the callee's type and effects information are maximized
begin
    take!(GLOBAL_BUFFER)

    pr48932_callee(x) = (print(GLOBAL_BUFFER, x); Base.inferencebarrier(x))
    pr48932_caller(x) = pr48932_callee(Base.inferencebarrier(x))

    # assert that type and effects information inferred from `pr48932_callee(::Any)` are the top
    let rt = only(Base.return_types(pr48932_callee, (Any,)))
        @test rt === Any
        effects = Base.infer_effects(pr48932_callee, (Any,))
        @test effects == Compiler.Effects()
    end

    # run inference on both `pr48932_caller` and `pr48932_callee`
    let (src, rt) = code_typed((Int,); interp=InvalidationTester()) do x
            @inline pr48932_caller(x)
        end |> only
        @test rt === Any
        @test any(iscall((src, pr48932_callee)), src.code)
    end

    let mi = only(Base.method_instances(pr48932_callee, Tuple, Base.get_world_counter()))
        ci = mi.cache
        @test isdefined(ci, :next)
        @test ci.owner === nothing
        @test ci.max_world == typemax(UInt)

        # In cache due to Base.return_types(pr48932_callee, (Any,))
        ci = ci.next
        @test !isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test ci.max_world == typemax(UInt)
    end
    let mi = Base.method_instance(pr48932_caller, (Int,))
        ci = mi.cache
        @test !isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test ci.max_world == typemax(UInt)
    end

    @test 42 == pr48932_caller(42)
    @test "42" == String(take!(GLOBAL_BUFFER))

    # test that we didn't add the backedge from `pr48932_callee` to `pr48932_caller`:
    # this redefinition below should invalidate the cache of `pr48932_callee` but not that of `pr48932_caller`
    pr48932_callee(x) = (print(GLOBAL_BUFFER, x); nothing)

    @test length(Base.methods(pr48932_callee)) == 1
    @test Base.only(Base.methods(pr48932_callee, Tuple{Any})) === only(Base.methods(pr48932_callee))
    @test isempty(Base.specializations(Base.only(Base.methods(pr48932_callee, Tuple{Any}))))
    let mi = only(Base.specializations(Base.only(Base.methods(pr48932_caller))))
        # Base.method_instance(pr48932_callee, (Any,))
        ci = mi.cache
        @test isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test_broken ci.max_world == typemax(UInt)
        ci = ci.next
        @test !isdefined(ci, :next)
        @test ci.owner === nothing
        @test_broken ci.max_world == typemax(UInt)
    end

    @test isnothing(pr48932_caller(42))
    @test "42" == String(take!(GLOBAL_BUFFER))
end

begin
    deduped_callee(x::Int) = @noinline rand(Int)
    deduped_caller1(x::Int) = @noinline deduped_callee(x)
    deduped_caller2(x::Int) = @noinline deduped_callee(x)

    # run inference on both `deduped_callerx` and `deduped_callee`
    let (src, rt) = code_typed((Int,); interp=InvalidationTester()) do x
            @inline deduped_caller1(x)
            @inline deduped_caller2(x)
        end |> only
        @test rt === Int
        @test any(isinvoke(:deduped_callee), src.code)
    end

    # Verify that adding the backedge again does not actually add a new backedge
    let mi = Base.method_instance(deduped_caller1, (Int,)),
        ci = mi.cache

        callee_mi = Base.method_instance(deduped_callee, (Int,))

        # Inference should have added the callers to the callee's backedges
        @test ci in callee_mi.backedges

        # In practice, inference will never end up calling `store_backedges`
        # twice on the same CodeInstance like this - we only need to check
        # that de-duplication works for a single invocation
        N = length(callee_mi.backedges)
        Core.Compiler.store_backedges(ci, Core.svec(callee_mi, callee_mi))
        N′ = length(callee_mi.backedges)

        # A single `store_backedges` invocation should de-duplicate any of the
        # edges it is adding.
        @test N′ - N == 1
    end
end

# we can avoid adding backedge even if the callee's return type is not the top
# when the return value is not used within the caller
begin take!(GLOBAL_BUFFER)
    pr48932_callee_inferable(x) = (print(GLOBAL_BUFFER, x); Base.inferencebarrier(1)::Int)
    pr48932_caller_unuse(x) = (pr48932_callee_inferable(Base.inferencebarrier(x)); nothing)

    # assert that type and effects information inferred from `pr48932_callee(::Any)` are the top
    let rt = only(Base.return_types(pr48932_callee_inferable, (Any,)))
        @test rt === Int
        effects = Base.infer_effects(pr48932_callee_inferable, (Any,))
        @test effects == Compiler.Effects()
    end

    # run inference on both `pr48932_caller` and `pr48932_callee`:
    # we don't need to add backedge to `pr48932_callee` from `pr48932_caller`
    # since the inference result of `pr48932_callee` is maximized and it's not inlined
    let (src, rt) = code_typed((Int,); interp=InvalidationTester()) do x
            @inline pr48932_caller_unuse(x)
        end |> only
        @test rt === Nothing
        @test any(iscall((src, pr48932_callee_inferable)), src.code)
    end

    let mi = only(Base.specializations(Base.only(Base.methods(pr48932_callee_inferable))))
        ci = mi.cache
        @test isdefined(ci, :next)
        @test ci.owner === nothing
        @test ci.max_world == typemax(UInt)
        ci = ci.next
        @test !isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test ci.max_world == typemax(UInt)
    end
    let mi = Base.method_instance(pr48932_caller_unuse, (Int,))
        ci = mi.cache
        @test !isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test ci.max_world == typemax(UInt)
    end

    @test isnothing(pr48932_caller_unuse(42))
    @test "42" == String(take!(GLOBAL_BUFFER))

    # test that we didn't add the backedge from `pr48932_callee_inferable` to `pr48932_caller_unuse`:
    # this redefinition below should invalidate the cache of `pr48932_callee_inferable` but not that of `pr48932_caller_unuse`
    pr48932_callee_inferable(x) = (print(GLOBAL_BUFFER, "foo"); x)

    @test isempty(Base.specializations(Base.only(Base.methods(pr48932_callee_inferable, Tuple{Any}))))
    let mi = Base.method_instance(pr48932_caller_unuse, (Int,))
        ci = mi.cache
        @test isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test_broken ci.max_world == typemax(UInt)
        ci = ci.next
        @test !isdefined(ci, :next)
        @test ci.owner === nothing
        @test_broken ci.max_world == typemax(UInt)
    end
    @test isnothing(pr48932_caller_unuse(42))
    @test "foo" == String(take!(GLOBAL_BUFFER))
end

# we need to add backedge when the callee is inlined
begin take!(GLOBAL_BUFFER)

    @noinline pr48932_callee_inlined(@nospecialize x) = (print(GLOBAL_BUFFER, x); Base.inferencebarrier(x))
    pr48932_caller_inlined(x) = pr48932_callee_inlined(Base.inferencebarrier(x))

    # assert that type and effects information inferred from `pr48932_callee(::Any)` are the top
    let rt = only(Base.return_types(pr48932_callee_inlined, (Any,)))
        @test rt === Any
        effects = Base.infer_effects(pr48932_callee_inlined, (Any,))
        @test effects == Compiler.Effects()
    end

    # run inference on `pr48932_caller_inlined` and `pr48932_callee_inlined`
    let (src, rt) = code_typed((Int,); interp=InvalidationTester()) do x
            @inline pr48932_caller_inlined(x)
        end |> only
        @test rt === Any
        @test any(isinvoke(:pr48932_callee_inlined), src.code)
    end

    let mi = only(Base.method_instances(pr48932_callee_inlined, (Any,), Base.get_world_counter()))
        ci = mi.cache
        @test isdefined(ci, :next)
        @test ci.owner === nothing
        @test ci.max_world == typemax(UInt)
        ci = ci.next
        @test !isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test ci.max_world == typemax(UInt)
    end
    let mi = only(Base.method_instances(pr48932_caller_inlined, (Int,), Base.get_world_counter()))
        ci = mi.cache
        @test !isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test ci.max_world == typemax(UInt)
    end

    @test 42 == pr48932_caller_inlined(42)
    @test "42" == String(take!(GLOBAL_BUFFER))

    # test that we added the backedge from `pr48932_callee_inlined` to `pr48932_caller_inlined`:
    # this redefinition below should invalidate the cache of both `pr48932_callee_inlined` and `pr48932_caller_inlined`
    @noinline pr48932_callee_inlined(@nospecialize x) = (print(GLOBAL_BUFFER, x); nothing)

    @test isempty(Base.specializations(Base.only(Base.methods(pr48932_callee_inlined, Tuple{Any}))))
    let mi = Base.method_instance(pr48932_caller_inlined, (Int,))
        ci = mi.cache
        @test isdefined(ci, :next)
        @test ci.owner === InvalidationTesterToken()
        @test ci.max_world != typemax(UInt)
        ci = ci.next
        @test !isdefined(ci, :next)
        @test ci.owner === nothing
        @test ci.max_world != typemax(UInt)
    end

    @test isnothing(pr48932_caller_inlined(42))
    @test "42" == String(take!(GLOBAL_BUFFER))
end

# Issue #57696
# This test checks for invalidation of recursive backedges. However, unfortunately, the original failure
# manifestation was an unreliable segfault or an assertion failure, so we don't have a more compact test.
@test success(`$(Base.julia_cmd()) -e 'Base.typejoin(x, ::Type) = 0; exit()'`)

# Test drop_all_caches functionality
@testset "drop_all_caches" begin
    # Run in subprocess to avoid disrupting the main test process
    script = """
        # Define test functions
        drop_cache_test_f(x) = x + 1
        drop_cache_test_g(x) = drop_cache_test_f(x) * 2

        # Compile the functions and capture stderr
        drop_cache_test_g(5) == 12 || error("failure")

        println(stderr, "==DROPPING ALL CACHES==")

        # Drop all caches
        Base.drop_all_caches()

        # Functions should still work (but will be recompiled on next call)
        drop_cache_test_g(5) == 12 || error("failure")

        println(stderr, "SUCCESS: drop_all_caches test passed")
        exit(0)
    """

    io = Pipe()
    # Run the test in a subprocess because Base.drop_all_caches() is extreme.
    # Drain stderr concurrently: the trace-compile output can exceed the pipe
    # buffer, and the child blocks in its atexit uv loop until it is read.
    result = run(pipeline(`$(Base.julia_cmd()[1]) --startup-file=no --trace-compile=stderr -e "$script"`, stderr=io), wait=false)
    close(io.in)
    reader = @async read(io, String)
    @test success(result)
    err = fetch(reader)::String
    # println(err)
    err_before, err_after = split(err, "==DROPPING ALL CACHES==")
    @test occursin("SUCCESS: drop_all_caches test passed", err_after)
    @test occursin("precompile(Tuple{typeof(Main.drop_cache_test_g), $Int})", err_before)
    @test occursin("precompile(Tuple{typeof(Main.drop_cache_test_g), $Int}) # recompile", err_after)
end

# Test that backedge compaction clears mi.backedges when all backedges are removed
begin
    pr61102_callee(x) = 2x
    pr61102_caller(x) = pr61102_callee(x)
    pr61102_caller(0)
    callee_mi = Base.method_instance(pr61102_callee, (Int,))
    @test isdefined(callee_mi, :backedges)
    pr61102_callee(x::Int) = 3x
    @test !isdefined(callee_mi, :backedges)
end

# `Core.TypeName.concrete_only`: inference records no backedge at call sites with
# non-concrete argument types, so adding a more-specific method later does not
# invalidate the caller's compiled code
abstract type COStyle end
struct CODefStyle <: COStyle end
struct COFill end
function co_callee end
typeof(co_callee).name.concrete_only = true
co_callee(::COStyle, op, x) = 1
struct COBox
    s::COStyle
    x::Any
end
co_caller(b::COBox) = co_callee(b.s, zero, b.x)

# the non-concrete call site gives up to `Any`
@test Base.return_types((COBox,); interp=InvalidationTester()) do b
    co_caller(b)
end |> only === Any

let mi = Base.method_instance(co_caller, (COBox,))
    ci = mi.cache
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world == typemax(UInt)
end

# add a more-specific method; the caller must remain valid
co_callee(::CODefStyle, op, x::COFill) = 2
let mi = Base.method_instance(co_caller, (COBox,))
    ci = mi.cache
    @test ci.owner === InvalidationTesterToken()
    @test ci.max_world == typemax(UInt)
end
