Revision f442e4230a41d0d40f81346c1707f946f743cbcf authored by Jeff Bezanson on 09 June 2021, 02:51:34 UTC, committed by GitHub on 09 June 2021, 02:51:34 UTC
This PR implements a way to keep tables of methods that are
not part of the internal method table, but still participate
in the special support we have for keeping tables of methods,
in particular unification through precompilation and efficient
lookup. The intended design use case is to allow for method overlay
tables for various non-CPU backends (e.g. GPU and TPU). These
backends would like to modify basic function like `sin` to
perform better on the device in question (or in the case of TPU
to define them over non-LLVM intrinsics).

It is worth noting that this PR only gives the ability to keep
these tables of methods. It assigns no particular meaning to them
and the runtime (and regular inference) do not look at them.
They are designed as an implementation detail for external
compilers and similar tools.

 # Demo

```julia
julia> using Base.Experimental: @overlay, @MethodTable

julia> @MethodTable(mt)
 # 0 methods:

julia> @overlay mt function sin(x::Float64)
           1
       end

julia> @overlay mt function cos(x::Float64)
           1
       end

julia> mt
 # 2 methods:
[1] cos(x::Float64) in Main at REPL[5]:1
[2] sin(x::Float64) in Main at REPL[4]:1

julia> Base._methods_by_ftype(Tuple{typeof(sin), Float64}, mt, 1, typemax(UInt))
1-element Vector{Any}:
 Core.MethodMatch(Tuple{typeof(sin), Float64}, svec(), sin(x::Float64) in Main at REPL[4]:1, true)

julia> Base._methods_by_ftype(Tuple{typeof(sin), Float64}, 1, typemax(UInt))
1-element Vector{Any}:
 Core.MethodMatch(Tuple{typeof(sin), Float64}, svec(Float64), sin(x::T) where T<:Union{Float32, Float64} in Base.Math at special/trig.jl:29, true)
```

Co-authored-by: Tim Besard <tim.besard@gmail.com>
Co-authored-by: Julian P Samaroo <jpsamaroo@jpsamaroo.me>
Co-authored-by: Keno Fischer <keno@juliacomputing.com>
1 parent 0e3276c
Raw File
exceptions.jl
# This file is a part of Julia. License is MIT: https://julialang.org/license

using Test

@testset "Basic exception stack handling" begin
    # Exiting the catch block normally pops the exception
    try
        error("A")
    catch
        @test length(current_exceptions()) == 1
    end
    @test length(current_exceptions()) == 0
    # Exiting via a finally block does not pop the exception
    try
        try
            error("A")
        finally
            @test length(current_exceptions()) == 1
        end
    catch
        @test length(current_exceptions()) == 1
    end
    # The combined try-catch-finally form obeys the same rules as above
    try
        error("A")
    catch
        @test length(current_exceptions()) == 1
    finally
        @test length(current_exceptions()) == 0
    end
    @test length(current_exceptions()) == 0
    # Errors are pushed onto the stack according to catch block nesting
    try
        error("RootCause")
    catch
        @test length(current_exceptions()) == 1
        try
            error("B")
        catch
            stack = current_exceptions()
            @test length(stack) == 2
            @test stack[1].exception.msg == "RootCause"
            @test stack[2].exception.msg == "B"
        end
        # Stack pops correctly
        stack = current_exceptions()
        @test length(stack) == 1
        @test stack[1].exception.msg == "RootCause"
    end
end

@testset "Exception stack lowering special cases" begin
    # try block in value position
    val = try
        error("A")
    catch
        @test length(current_exceptions()) == 1
        1
    end
    @test val == 1
    function test_exc_stack_tailpos()
        # try block in tail position
        try
            error("A")
        catch
            length(current_exceptions())
        end
    end
    @test test_exc_stack_tailpos() == 1
    @test length(current_exceptions()) == 0
end

@testset "Exception stacks - early exit from try or catch" begin
    # Exiting a catch block early with normal control flow — break, continue,
    # return, goto — will result in popping of the exception stack.
    function test_exc_stack_catch_return()
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            return
        end
    end
    test_exc_stack_catch_return()

    for i=1:1
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            break
        end
    end
    # Also test try-break-finally forms here. See #31766
    for i=1:1
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            break
        finally
            @test length(current_exceptions()) == 0
        end
    end
    @test length(current_exceptions()) == 0

    for i=1:1
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            continue
        end
    end
    for i=1:1
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            continue
        finally
            @test length(current_exceptions()) == 0
        end
    end
    @test length(current_exceptions()) == 0

    try
        error("A")
    catch
        @test length(current_exceptions()) == 1
        @goto outofcatch
    end
    @label outofcatch
    try
        error("A")
    catch
        @test length(current_exceptions()) == 1
        @goto outofcatch2
    finally
        @test length(current_exceptions()) == 0
    end
    @label outofcatch2
    @test length(current_exceptions()) == 0

    # Exiting from a try block in various ways should not affect the exception
    # stack state.
    try
        error("ExceptionInOuterTry")
    catch
        @test length(current_exceptions()) == 1
        function test_exc_stack_try_return()
            try
                return
            catch
            end
        end
        test_exc_stack_try_return()
        for i=1:1
            try
                break
            catch
            end
        end
        for i=1:1
            try
                continue
            catch
            end
        end
        try
            @goto outoftry
        catch
        end
        @label outoftry
        @test length(current_exceptions()) == 1
        @test current_exceptions()[1].exception == ErrorException("ExceptionInOuterTry")
    end
end

@testset "Finally handling with exception stacks" begin
    # The lowering of finally is quite subtle when combined with break or
    # return because each finally block may be entered via multiple code paths
    # (eg, different occurrences of return), and these code paths must diverge
    # again once the finally block has completed. To complicate matters
    # further, the return code path must thread through every nested finally
    # block before actually returning, all the while preserving the information
    # about which variable to return.

    # Issue #34579
    (()-> begin
        try
            throw("err")
        catch
            # Explicit return => exception should be popped before finally block
            return
        finally
            @test length(Base.current_exceptions()) == 0
        end
    end)()
    @test length(Base.current_exceptions()) == 0

    while true
        try
            error("err1")
        catch
            try
                # Break target is outside catch block, but finally is inside =>
                # exception should not be popped inside finally block
                break
            finally
                @test length(Base.current_exceptions()) == 1
            end
        end
    end
    @test length(Base.current_exceptions()) == 0

    # Nested finally handling with `return`: each finally block should observe
    # only the active exceptions as according to its nesting depth.
    (() -> begin
        try
            try
                error("err1")
            catch
                try
                    try
                        error("err2")
                    catch
                        # This return needs to thread control flow through
                        # multiple finally blocks in the linearized IR.
                        return
                    end
                finally
                    # At this point err2 is dealt with
                    @test length(Base.current_exceptions()) == 1
                    @test Base.current_exceptions()[1].exception == ErrorException("err1")
                end
            end
        finally
            # At this point err1 is dealt with
            @test length(Base.current_exceptions()) == 0
        end
    end)()
    @test length(Base.current_exceptions()) == 0
end

@testset "Deep exception stacks" begin
    # Generate deep exception stack with recursive handlers.
    #
    # (Note that if you let this overflow the program stack (not the exception
    # stack) julia will crash. See #28577.)
    function test_exc_stack_deep(n)
        n != 1 || error("RootCause")
        try
            test_exc_stack_deep(n-1)
        catch
            error("n==$n")
        end
    end
    @test try
        test_exc_stack_deep(100)
    catch
        @test current_exceptions()[1].exception == ErrorException("RootCause")
        length(current_exceptions())
    end == 100
    @test length(current_exceptions()) == 0
end

@testset "Exception stacks and Tasks" begin
    # Task switching should not affect exception state. See #12485.
    try
        error("A")
    catch
        t = @task try
            error("B")
        catch exc
            exc
        end
        yield(t)
        @test t.state == :done
        @test t.result == ErrorException("B")
        # Task exception state is preserved around task switches
        @test length(current_exceptions()) == 1
        @test current_exceptions()[1].exception == ErrorException("A")
    end
    @test length(current_exceptions()) == 0
    # test rethrow() rethrows correct state
    bt = []
    try
        try
            error("A")
        catch
            bt = catch_backtrace()
            t = @task try
                error("B")
            catch exc
                exc
            end
            yield(t)
            @test t.state == :done
            @test t.result == ErrorException("B")
            @test bt == catch_backtrace()
            rethrow()
        end
    catch exc
        @test exc == ErrorException("A")
        @test bt == catch_backtrace()
    end
    @test length(current_exceptions()) == 0
    # test rethrow with argument
    bt = []
    try
        try
            error("A")
        catch
            t = @task try
                error("B")
            catch exc
                exc
            end
            yield(t)
            @test t.state == :done
            @test t.result == ErrorException("B")
            bt = catch_backtrace()
            rethrow(ErrorException("C"))
        end
    catch exc
        @test exc == ErrorException("C")
        @test bt == catch_backtrace()
    end
    @test length(current_exceptions()) == 0
    # Exception stacks on other tasks
    t = @task try
        error("A")
    catch
        error("B")
    end
    yield(t)
    @test t.state == :failed
    @test t.result == ErrorException("B")
    @test current_exceptions(t, backtrace=false) == [
        (exception=ErrorException("A"),backtrace=nothing),
        (exception=ErrorException("B"),backtrace=nothing)
    ]
    # Exception stacks for tasks which never get the chance to start
    t = @task nothing
    @test (try
        @async Base.throwto(t, ErrorException("expected"))
        wait(t)
    catch e
        e
    end).task.exception == ErrorException("expected")
    @test length(current_exceptions(t)) == 1
    @test length(current_exceptions(t)[1].backtrace) > 0 # backtrace is nonempty
    # Exception stacks should not be accessed on concurrently running tasks
    t = @task ()->nothing
    @test_throws ErrorException("Inspecting the exception stack of a task which might "*
                                "be running concurrently isn't allowed.") current_exceptions(t)
end

@testset "rethrow" begin
    @test try
        rethrow()
    catch exc
        exc
    end == ErrorException("rethrow() not allowed outside a catch block")
    @test try
        rethrow(ErrorException("A"))
    catch exc
        exc
    end == ErrorException("rethrow(exc) not allowed outside a catch block")
end

# issue #36527
function f36527()
    caught = false
    🏡 = Core.eval(Main, :(module asdf36527 end))
    try
        Core.eval(🏡, :(include_string($🏡, "@assert z36527 == 10")))
    catch ex
        GC.gc()
        catch_backtrace()
        caught = true
    end
    return caught
end

@test f36527()

# accessing an undefined var in tail position in a catch block
function undef_var_in_catch()
    try
        error("first error")
    catch
        __probably_n0t_defined__
    end
end
@test length(try
    undef_var_in_catch()
    []
catch
    current_exceptions()
end) == 2
back to top