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

using Base.ScopedValues

include(joinpath(@__DIR__,"../Compiler/test/irutils.jl"))

@testset "errors" begin
    @test ScopedValue{Float64}(1)[] == 1.0
    @test_throws InexactError ScopedValue{Int}(1.5)
    let val = ScopedValue(1)
        @test_throws MethodError val[] = 2
        with() do
            @test_throws MethodError val[] = 2
        end
    end
    let val = ScopedValue{String}()
        @test_throws KeyError val[]
    end
    let val = ScopedValue{Int}()
        @test_throws KeyError val[]
    end
    @test_throws MethodError ScopedValue()
end

const sval = ScopedValue(1)
@testset "inheritance" begin
    @test sval[] == 1
    with() do
        @test sval[] == 1
        with() do
            @test sval[] == 1
        end
        with(sval => 2) do
            @test sval[] == 2
        end
        @test sval[] == 1
    end
    @test sval[] == 1
end

const sval_float = ScopedValue(1.0)

@testset "multiple scoped values" begin
    with(sval => 2, sval_float => 2.0) do
        @test sval[] == 2
        @test sval_float[] == 2.0
    end
    with(sval => 2, sval => 3) do
        @test sval[] == 3
    end
end

emptyf() = nothing

@testset "conversion" begin
    with(emptyf, sval_float=>2)
    @test_throws MethodError with(emptyf, sval_float=>"hello")
    a = ScopedValue(1)
    with(a => 2.0) do
        @test a[] == 2
        @test a[] isa Int
    end
    a = ScopedValue(1.0)
    with(a => 2) do
        @test a[] == 2.0
        @test a[] isa Float64
    end
end

import Base.Threads: @spawn
@testset "tasks" begin
    @test fetch(@spawn begin
        sval[]
    end) == 1
    with(sval => 2) do
        @test fetch(@spawn begin
            sval[]
        end) == 2
    end
end

@testset "show" begin
    @test sprint(show, ScopedValue{Int}(), context=(:module=>Core,)) == "Base.ScopedValues.ScopedValue{$Int}()"
    @test sprint(show, sval, context=(:module=>Core,)) == "Base.ScopedValues.ScopedValue{$Int}(1)"
    with(sval => 2.0) do
        @test sprint(show, sval, context=(:module=>Core,)) == "Base.ScopedValues.ScopedValue{$Int}(2)"
        objid = sprint(show, Base.objectid(sval))
        let str = sprint(show, Core.current_scope(), context=(:module=>Core,))
            @test startswith(str, "Base.Scope")
            @test contains(str, "Base.ScopedValues.ScopedValue{$Int}@$objid => 2")
        end
    end
end

const depth = ScopedValue(0)
function nth_with(f, n)
    if n <= 0
        f()
    else
        with(depth => n) do
            nth_with(f, n-1)
        end
    end
end


@testset "nested with" begin
    @testset for depth in 1:16
        nth_with(depth) do
            @test sval_float[] == 1.0
        end
        with(sval_float=>2.0) do
            nth_with(depth) do
                @test sval_float[] == 2.0
            end
        end
        nth_with(depth) do
            with(sval_float=>2.0) do
                @test sval_float[] == 2.0
            end
        end
    end
    with(sval_float=>2.0) do
        nth_with(15) do
            @test sval_float[] == 2.0
            with(sval_float => 3.0) do
                @test sval_float[] == 3.0
            end
        end
    end
end

@testset "macro" begin
    @with sval=>2 sval_float=>2.0 begin
        @test sval[] == 2
        @test sval_float[] == 2.0
    end
    # Doesn't do much...
    @with begin
        @test sval[] == 1
        @test sval_float[] == 1.0
    end
    @with sval=>2 sval_float=>2.0 begin
        @with begin
            @test sval[] == 2
            @test sval_float[] == 2.0
        end
    end
end

@testset "isassigned" begin
    sv = ScopedValue(1)
    @test isassigned(sv)
    sv = ScopedValue{Int}()
    @test !isassigned(sv)
    with(sv => 2) do
        @test isassigned(sv)
    end
end

# Test that the `@with` macro doesn't introduce unnecessary PhiC nodes
# (which can be hard for the optimizer to remove).
function with_macro_slot_cross()
    a = 1
    @with sval=>1 begin
        a = sval_float[]
    end
    return a
end

let code = code_typed(with_macro_slot_cross)[1][1].code
    @test !any(x->isa(x, Core.PhiCNode), code)
end

# inline constant scoped values
const inlineable_const_sv = ScopedValue(1)
@test fully_eliminated(; retval=(inlineable_const_sv => 1)) do
    inlineable_const_sv => 1
end

# Handle nothrow scope bodies correctly (#56609)
@eval @noinline function nothrow_scope(@nospecialize(scope_at_entry))
    $(Expr(:tryfinally, :(), nothing, 1))
    @test Core.current_scope() === scope_at_entry
end
nothrow_scope(Core.current_scope())

# https://github.com/JuliaLang/julia/issues/56062
@testset "issue #56062" begin
    ts = Int[]
    try
        @with begin
            return
        end
    catch err
    finally
        push!(ts, 2)
    end
end

# LazyScopedValue
global lsv_ncalled = 0
const lsv = LazyScopedValue{Int}(OncePerProcess(() -> (global lsv_ncalled; lsv_ncalled += 1; 1)))
@testset "LazyScopedValue" begin
    @test (@with lsv=>2 lsv[]) == 2
    @test lsv_ncalled == 0
    @test lsv[] == 1
    @test lsv_ncalled == 1
    @test lsv[] == 1
    @test lsv_ncalled == 1
end

@testset "ScopedThunk" begin
    function check_svals()
        @test sval[] == 8
        @test sval_float[] == 8.0
    end
    sf = nothing
    @with sval=>8 sval_float=>8.0 begin
        sf = ScopedThunk(check_svals)
    end
    sf()
    @with sval=>8 sval_float=>8.0 begin
        sf2 = ScopedThunk{Function}(check_svals)
    end
    sf2()
end

using Base.ScopedValues: ScopedValue, with
@noinline function test_59483()
    sv = ScopedValue([])
    ch = Channel{Bool}()

    # Spawn a child task, which inherits the parent's Scope
    @noinline function inner_function()
        # Block until the parent task has left the scope.
        take!(ch)
        # Now, per issue 59483, this task's scope is not rooted, except by the task itself.

        # Now switch to an inner scope, leaving the current scope possibly unrooted.
        val = with(sv=>Any[2]) do
            # Inside this new scope, when we perform GC, the parent scope can be freed.
            # The fix for this issue made sure that the first scope in this task remains
            # rooted.
            GC.gc()
            GC.gc()
            sv[]
        end
        @test val == Any[2]
        # Finally, we've returned to the original scope, but that could be a dangling
        # pointer if the scope itself was freed by the above GCs. So these GCs could crash:
        GC.gc()
        GC.gc()
    end
    @noinline function spawn_inner()
        # Set a new Scope in the parent task - this is the scope that could be freed.
        with(sv=>Any[1]) do
            return @async inner_function()
        end
    end

    # RUN THE TEST:
    t = spawn_inner()
    # Exit the scope, and let the child task proceed
    put!(ch, true)
    wait(t)
end
@testset "issue 59483" begin
    test_59483()
end

# issue #53584 - ScopedValue should not allocate when accessed
const sv_53584 = ScopedValue([0])
@noinline function access_scoped_53584()
    v = sv_53584[]
    v[1] += 1
    return nothing
end
function run_53584()
    @with sv_53584=>[0] for _ in 1:100_000
        access_scoped_53584()
    end
end
@testset "issue #53584" begin
    run_53584() # warmup
    @test (@allocated run_53584()) < 10_000
end
