text:
@testset "syntax error reports" begin
    let
        s = """
        begin
            c = rand(Bool)
        end

        end
        """

        res = report_text(s).res

        @test !isempty(res.toplevel_error_reports)
        @test first(res.toplevel_error_reports) isa SyntaxErrorReport
    end
end

@testset "virtualize module context" begin
    # the context of the original module should be virtualized
    let
        actual = gen_virtual_module(@__MODULE__)
        Core.eval(actual, quote
            struct X end
            const Y = nothing
            Z() = return
        end)

        virtual = virtualize_module_context(actual)
        @test isdefined(virtual, :X)
        @test isdefined(virtual, :Y)
        @test isdefined(virtual, :Z)
    end

    # in the virtualized context, we can define a name that is already defined in the original module
    let
        actual = gen_virtual_module(@__MODULE__)
        Core.eval(actual, quote
            struct X end
            const Y = nothing
            Z() = return
        end)

        virtual = virtualize_module_context(actual)
        @test (Core.eval(virtual, quote
            struct X end
            const Y = nothing
            Z() = return
        end); true)
    end

    # end to end
    let
        orig = gen_virtual_module(@__MODULE__)
        Core.eval(orig, Expr(:toplevel, (quote
            module Foo
            bar() = return :baz
            export bar
            end
        end).args...))

        res = @analyze_toplevel context = orig begin
            module Foo
            bar() = return :baz
            export bar
            end
        end
        # "cannot assign a value to variable orig.Foo from module orig" shouldn't be reported
        @test isempty(res.toplevel_error_reports)
    end

    # don't error if there is undefined export
    let
        actual = gen_virtual_module(@__MODULE__)
        Core.eval(actual, :(export undefined))

        @test (virtualize_module_context(actual); true)

        res = @analyze_toplevel context = actual begin
            println(undefined)
        end
        @test isempty(res.toplevel_error_reports)
        @test any(res.inference_error_reports) do err
            isa(err, GlobalUndefVarErrorReport) &&
            err.name === :undefined
        end
    end
end

@testset "fix self-reference of virtual module" begin
    let
        res = @analyze_toplevel begin
            const foo = sum
            Main.foo("julia") # `Main.sum` should be resolved as constant
        end
        test_sum_over_string(res)
    end

    let
        res = @analyze_toplevel begin
            let
                Main = "julia" # local `Main` should not be resolved to virtual module
                sum(Main)
            end
        end
        test_sum_over_string(res)
    end

    # https://github.com/aviatesk/JET.jl/issues/151
    let
        res = @analyze_toplevel begin
            struct X end

            module A
            using ..Main: X
            end
        end
        @test isempty(res.toplevel_error_reports)
    end

    let
        res = @analyze_toplevel begin
            module A
            struct X end
            export X
            end

            module B
            import ..Main.A
            println(A.X)
            end

            module C
            import ..Main.A: X
            println(X)
            end

            module D
            using ..Main.A
            println(X)
            end

            module E
            using ..Main.A: X
            println(X)
            end
        end
        @test isempty(res.toplevel_error_reports)
    end
end

@testset "fix toplevel global `Symbol`" begin
    # this case otherwise will throw an error in `CC.typ_for_val` in optimization
    let
        res = @analyze_toplevel begin
            v = '1'
            v = if rand(Bool)
                rand(Int)
            else
                v
            end
            sin(v)
        end
        @test true
    end

    # `c` wrapped in `GotoIfNot` node should be transformed into `GlobalRef(vmod, :c)`,
    # otherwise `NonBooleanCondErrorReport` (and `UncaughtExceptionReport`) will be reported
    let
        res = @analyze_toplevel begin
            c = false
            if c
                throw("should be ignored")
            end
        end
        @test isempty(res.inference_error_reports)
    end
end

@testset "\"toplevel definitions\"" begin
    let
        vmod, = @analyze_toplevel2 begin
            # function
            foo() = nothing

            # local function, shouldn't be evaluated
            let
                c = rand(Bool)
                foo′(a) = a || c
            end

            # macro
            macro foo(ex) ex end

            # abstract type
            abstract type Foo end

            # struct
            struct Foo1 <: Foo
                val::Int
            end
            mutable struct Foo2 <: Foo
                val::Int
            end

            # primitive type
            primitive type Foo3 32 end

            # import, using
            using Base: Fix1
            import Base: getproperty

            function getproperty(foo::Foo, sym::Symbol)
                return if sym === :val
                    getfield(foo, sym)::Int
                else
                    getfield(foo, sym)
                end
            end
        end

        @test is_concrete(vmod, :foo)
        @test !is_concrete(vmod, :foo′)
        @test is_concrete(vmod, Symbol("@foo"))
        @test is_concrete(vmod, :Foo)
        @test is_concrete(vmod, :Foo1)
        @test is_concrete(vmod, :Foo2)
        @test is_concrete(vmod, :Foo3)
        @test is_concrete(vmod, :Fix1)
        @test !isempty(methodswith(getfield(vmod, :Foo), getproperty))
    end

    # basic profiling with user-defined types
    let
        vmod, res = @analyze_toplevel2 begin
            gb = rand(Bool)

            struct Foo
                b::Bool
                Foo(b = gb) = new(b)
            end

            foo = Foo(gb)
        end

        # global variables aren't evaluated but kept in `analyzer` instead
        @test is_abstract(vmod, :gb)
        @test isa_abstract(vmod.gb, Bool)
        @test is_concrete(vmod, :Foo)
        @test is_abstract(vmod, :foo)
        @test isa_abstract(vmod.foo, vmod.Foo)
        @test isempty(res.toplevel_error_reports)
        @test isempty(res.inference_error_reports)
    end

    # definitions using type aliases
    let
        vmod, = @analyze_toplevel2 begin
            const BT = Bool

            struct Foo
                b::BT
            end

            const c = Foo(rand(Bool))
            uc = Foo(rand(Bool))
        end
        @test is_concrete(vmod, :BT)
        @test is_concrete(vmod, :Foo)
        @test is_abstract(vmod, :c)
        @test is_abstract(vmod, :uc)
    end

    # a toplevel definition within a block
    let
        vmod, res = @analyze_toplevel2 begin
            begin
                struct Foo
                    bar
                end

                foo = Foo(:bar)
                println(foo)
            end
        end
        @test is_concrete(vmod, :Foo)
        @test is_analyzed(vmod, :foo)
        @test isa_analyzed(vmod.foo, vmod.Foo)
    end

    # toplevel definitions within a block
    # somewhat related upstream issue: https://github.com/JuliaDebug/LoweredCodeUtils.jl/issues/47
    # well, the actual error here is world age error ...
    let
        vmod, res = @analyze_toplevel2 begin
            begin
                abstract type Foo end
                struct Foo1 <: Foo
                    foo
                end

                foo = Foo1(:foo)
                println(foo)
            end
        end
        @test isempty(res.toplevel_error_reports)
        @test is_analyzed(vmod, :foo)
        @test isa_analyzed(vmod.foo, vmod.Foo1)
    end
end

@testset "macro expansions" begin
    let
        vmod, res = @analyze_toplevel2 begin
            @inline foo(a) = identity(a)
            foo(10)
        end

        @test is_concrete(vmod, :foo)
        @test isempty(res.toplevel_error_reports)
        @test isempty(res.inference_error_reports)
    end

    let
        vmod, res = @analyze_toplevel2 begin
            macro foo(ex)
                @assert Meta.isexpr(ex, :call)
                push!(ex.args, 1)
                return ex
            end

            @foo sin() # otherwise NoMethodError
        end

        @test is_concrete(vmod, Symbol("@foo"))
        @test isempty(res.toplevel_error_reports)
        @test isempty(res.inference_error_reports)
    end

    # macro expansions with access to global variables will fail
    let
        vmod, res = @analyze_toplevel2 begin
            const arg = rand(Bool)

            macro foo(ex)
                @assert Meta.isexpr(ex, :call)
                arg && push!(ex.args, 1)
                return ex
            end

            @foo sin()
        end

        @test is_concrete(vmod, Symbol("@foo"))
        @test length(res.toplevel_error_reports) == 1
        let r = first(res.toplevel_error_reports)
            @test isa(r, MissingConcretization) # this error should be considered as missing concretization
        end
        @test isempty(res.inference_error_reports)
    end

    # macros that can general :module or :toplevel expressions
    let
        # if we don't expand macros before we check `:toplevel` or `:module` expressions,
        # we may pass `:toplevel` or `:module` expressions to `partially_interpret!` and
        # eventually we will fail to concretize them and their toplevel definitions
        vmod, res = @analyze_toplevel2 begin
            macro wrap_in_mod(blk)
                ex = Expr(:module, true, esc(:foo), esc(blk))
                return Expr(:toplevel, ex)
            end

            @wrap_in_mod begin
                bar() = nothing
            end
        end
        @test isdefined(vmod, :foo) && isdefined(vmod.foo, :bar)

        vmod, res = @analyze_toplevel2 begin
            """
                foo

            Docstring for module `foo`
            """
            module foo
            bar() = nothing
            end
        end
        @test isdefined(vmod, :foo) && isdefined(vmod.foo, :bar)
    end
end

@testset "remove `const`" begin
    let
        res = @analyze_toplevel begin
            const s = "julia"
            sum(s)
        end

        @test isempty(res.toplevel_error_reports)
        test_sum_over_string(res)
    end

    let
        res = @analyze_toplevel begin
            let
                const s = "julia"
                sum(s)
            end
        end

        @test !isempty(res.toplevel_error_reports)
        @test first(res.toplevel_error_reports) isa SyntaxErrorReport
    end
end

@testset "expression flattening" begin
    let
        @test (begin
            # internal error shouldn't occur
            res = @analyze_toplevel begin
                r = rand(); s = sin(a); c = cos(b); tan(c)
            end
        end; true)
    end
end

@testset "handle `include`" begin
    let
        f1 = normpath(FIXTURE_DIR, "include1.jl")
        f2 = normpath(FIXTURE_DIR, "include1.jl")

        context = gen_virtual_module(@__MODULE__)
        res = report_file2(f1; context, virtualize = false).res

        @test f1 in res.included_files
        @test f2 in res.included_files
        @test is_concrete(context, :foo)
        @test isempty(res.toplevel_error_reports)
        @test isempty(res.inference_error_reports)
    end

    let
        f = normpath(FIXTURE_DIR, "nonexistinclude.jl")
        res = report_file2(f).res

        @test f in res.included_files
        @test !isempty(res.toplevel_error_reports)
        @test first(res.toplevel_error_reports) isa ActualErrorWrapped
        @test !isempty(res.inference_error_reports)
        @test first(res.inference_error_reports) isa GlobalUndefVarErrorReport
    end

    let
        f = normpath(FIXTURE_DIR, "selfrecursiveinclude.jl")
        res = report_file2(f).res

        @test f in res.included_files
        @test !isempty(res.toplevel_error_reports)
        @test first(res.toplevel_error_reports) isa RecursiveIncludeErrorReport
    end

    let
        f1 = normpath(FIXTURE_DIR, "chainrecursiveinclude1.jl")
        f2 = normpath(FIXTURE_DIR, "chainrecursiveinclude2.jl")
        res = report_file2(f1).res

        @test f1 in res.included_files
        @test f2 in res.included_files
        @test !isempty(res.toplevel_error_reports)
        let
            report = first(res.toplevel_error_reports)
            @test report isa RecursiveIncludeErrorReport
            @test report.duplicated_file == f1
            @test f1 in report.files
            @test f2 in report.files
            @test report.file == f2
            @test report.line == 1
        end
    end
end

@testset "module definition" for s in ("""
                                       module foo end
                                       """,
                                       """
                                       baremodule foo end
                                       """,
                                       """
                                       module foo
                                       module bar end
                                       end
                                       """
                                       )
    res = report_text(s).res
    @test isempty(res.toplevel_error_reports)
end

@testset "module usage" begin
    # using
    let
        res = @analyze_toplevel begin
            module foo

            using Base.Meta: isexpr

            isexpr(:(foo(bar)), :call)
            isexpr2(:(foo(bar)), :call)

            end
        end

        @test isempty(res.toplevel_error_reports)
        @test !isempty(res.inference_error_reports)
        @test first(res.inference_error_reports) isa GlobalUndefVarErrorReport
        @test occursin("isexpr2", first(res.inference_error_reports).msg)
    end

    # error handling for module usages
    let
        res = @analyze_toplevel begin
            using Base: foo
        end

        @test !isempty(res.toplevel_error_reports)
        er = first(res.toplevel_error_reports)
        @test er isa ActualErrorWrapped
        @test er.err == UndefVarError(:foo)
        @test er.file == (@__FILE__) && er.line == (@__LINE__) - 7
    end

    # sequential usage
    let
        res = @analyze_toplevel begin
            module foo

            bar(s) = sum(s)

            module baz

            using ..foo

            bar("julia") # -> GlobalUndefVarErrorReport

            end # module bar

            end # module foo
        end

        @test isempty(res.toplevel_error_reports)
        @test !isempty(res.inference_error_reports)
        @test first(res.inference_error_reports) isa GlobalUndefVarErrorReport
    end

    # usage of global objects
    let
        res = @analyze_toplevel begin
            module foo

            bar(s) = sum(s)

            module baz

            using ..foo

            foo.bar("julia") # -> NoMethodErrorReports

            end # module bar

            end # module foo
        end

        @test isempty(res.toplevel_error_reports)
        test_sum_over_string(res)
    end

    let
        res = @analyze_toplevel begin
            module foo

            bar(s) = sum(s)

            module baz

            using ..foo: bar

            bar("julia") # -> NoMethodErrorReports

            end # module bar

            end # module foo
        end

        @test isempty(res.toplevel_error_reports)
        test_sum_over_string(res)
    end

    # module usage within a block
    let
        res = @analyze_toplevel begin
            module foo

            bar(s) = sum(s)

            module baz

            begin
                using ..foo: bar
                bar("julia") # -> NoMethodErrorReports
            end

            end # module bar

            end # module foo
        end

        @test isempty(res.toplevel_error_reports)
        test_sum_over_string(res)
    end

    @testset "module usage of abstract global variable" begin
        let
            res = @analyze_toplevel begin
                module foo

                const bar = sum

                module baz

                using ..foo: bar

                bar("julia")

                end # module bar

                end # module foo
            end

            @test isempty(res.toplevel_error_reports)
            test_sum_over_string(res)
        end

        let
            res = @analyze_toplevel begin
                module foo

                const bar = "julia"

                module baz

                using ..foo: bar

                sum(bar)

                end # module bar

                end # module foo
            end

            @test isempty(res.toplevel_error_reports)
            test_sum_over_string(res)
        end

        let
            res = @analyze_toplevel begin
                module foo

                const bar = "julia"

                export bar

                end # module foo

                using .foo
                sum(bar)
            end

            @test isempty(res.toplevel_error_reports)
            test_sum_over_string(res)
        end
    end

    # export
    let
        res = @analyze_toplevel begin
            module foo

            bar(s) = sum(s)

            export bar

            end

            using .foo

            bar("julia") # -> NoMethodErrorReports
        end

        @test isempty(res.toplevel_error_reports)
        test_sum_over_string(res)
    end
end

@testset "sequential" begin
    let
        res = @analyze_toplevel begin
            foo(1:1000)

            foo(a) = length(a)
        end
        @test length(res.inference_error_reports) === 1
        @test first(res.inference_error_reports) isa GlobalUndefVarErrorReport
    end

    let
        res = @analyze_toplevel begin
            foo(a) = length(a)
            foo("julia") # should not error

            foo(a) = sum(a)
            foo(1:1000)  # should not error too
        end
        @test isempty(res.inference_error_reports)
    end
end

@testset "abstract global variables" begin
    let
        vmod, res = @analyze_toplevel2 begin
            var = rand(Bool)
            const constvar = rand(Bool)
        end

        @test is_abstract(vmod, :var)
        @test isa_abstract(vmod.var, Bool)
        @test is_abstract(vmod, :constvar)
        @test isa_abstract(vmod.constvar, Bool)
    end

    @testset "scope" begin
        # function
        # --------

        let
            vmod, res = @analyze_toplevel2 begin
                begin
                    local localvar = rand(Bool)
                    global globalvar = rand(Bool)
                end
            end

            @test !isdefined(vmod, :localvar)
            @test is_abstract(vmod, :globalvar)
            @test isa_abstract(vmod.globalvar, Bool)
        end

        # blocks
        # ------

        let
            vmod, res = @analyze_toplevel2 begin
                begin
                    globalvar = rand(Bool)
                end
            end

            @test is_abstract(vmod, :globalvar)
            @test isa_abstract(vmod.globalvar, Bool)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                begin
                    local localvar = rand(Bool)
                    globalvar = localvar
                end
            end

            @test !isdefined(vmod, :localvar)
            @test is_abstract(vmod, :globalvar)
            @test isa_abstract(vmod.globalvar, Bool)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                begin
                    local localvar
                    localvar = rand(Bool) # this shouldn't be annotated as `global`
                    globalvar = localvar
                end
            end

            @test !isdefined(vmod, :localvar)
            @test is_abstract(vmod, :globalvar)
            @test isa_abstract(vmod.globalvar, Bool)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                globalvar2 = begin
                    local localvar = rand(Bool)
                    globalvar1 = localvar
                end
            end

            @test !isdefined(vmod, :locbalvar)
            @test is_abstract(vmod, :globalvar1)
            @test isa_abstract(vmod.globalvar1, Bool)
            @test is_abstract(vmod, :globalvar2)
            @test isa_abstract(vmod.globalvar2, Bool)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                let
                    localvar = rand(Bool)
                end
            end

            @test !isdefined(vmod, :localvar)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                globalvar = let
                    localvar = rand(Bool)
                    localvar
                end
            end

            @test !isdefined(vmod, :localvar)
            @test is_abstract(vmod, :globalvar)
            @test isa_abstract(vmod.globalvar, Bool)
        end

        # loops
        # -----

        let
            vmod, res = @analyze_toplevel2 begin
                for i = 1:100
                    localvar = i
                end
            end

            @test !isdefined(vmod, :localvar)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                for i in 1:10
                    localvar = rand(Bool)
                    global globalvar = localvar
                end
            end

            @test !isdefined(vmod, :localvar)
            @test is_abstract(vmod, :globalvar)
            @test isa_abstract(vmod.globalvar, Bool)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                while true
                    localvar = rand(Bool)
                end
            end

            @test !isdefined(vmod, :localvar)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                while true
                    localvar = rand(Bool)
                    global globalvar = localvar
                end
            end

            @test !isdefined(vmod, :localvar)
            @test is_abstract(vmod, :globalvar)
            @test isa_abstract(vmod.globalvar, Bool)
        end
    end

    @testset "multiple declaration/assignment" begin
        let
            vmod, res = @analyze_toplevel2 begin
                r1, r2 = rand(2)
            end

            @test is_abstract(vmod, :r1)
            @test isa_abstract(vmod.r1, Float64)
            @test is_abstract(vmod, :r2)
            @test isa_abstract(vmod.r2, Float64)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                begin
                    local r1, r2
                    r1, r2 = rand(2)
                end
            end

            @test !isdefined(vmod, :r1)
            @test !isdefined(vmod, :r2)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                let
                    global r1, r2
                    r1, r2 = rand(2)
                end
            end

            @test is_abstract(vmod, :r1)
            @test isa_abstract(vmod.r1, Float64)
            @test is_abstract(vmod, :r2)
            @test isa_abstract(vmod.r2, Float64)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                ro1, ro2 = let
                    ri1, ri2 = rand(2)
                end
            end

            @test !isdefined(vmod, :ri1)
            @test !isdefined(vmod, :ri2)
            @test is_abstract(vmod, :ro1)
            @test isa_abstract(vmod.ro1, Float64)
            @test is_abstract(vmod, :ro2)
            @test isa_abstract(vmod.ro2, Float64)
        end

        let
            vmod, res = @analyze_toplevel2 begin
                begin
                    local l
                    l, g = rand(2)
                end
            end
            @test !isdefined(vmod, :l)
            @test is_abstract(vmod, :g)
            @test isa_abstract(vmod.g, Float64)
        end
    end

    @testset "concretize statically constant variables" begin
        let
            m, = @analyze_toplevel2 begin
                const a = 0
            end
            @test is_concrete(m, :a) && m.a == 0
        end

        # try to concretize even if it's not declared as constant
        let
            m, = @analyze_toplevel2 begin
                a = 0
            end
            @test is_concrete(m, :a) && m.a == 0
        end

        let
            m, = @analyze_toplevel2 begin
                const a = :jetzero # should be quoted, otherwise undef var error
            end
            @test is_concrete(m, :a) && m.a === :jetzero
        end

        # sequential
        let
            m, = @analyze_toplevel2 begin
                a = rand(Int)
                a = 0
            end
            @test is_concrete(m, :a)
            @test m.a == 0
        end
        let
            m, = @analyze_toplevel2 begin
                a = 0
                a = rand(Int)
            end
            @test is_abstract(m, :a)
            @test isa_abstract(m.a, Int)
        end
    end

    @testset "https://github.com/aviatesk/JET.jl/issues/142" begin
        res = @analyze_toplevel begin
            Circle = @NamedTuple begin
                radius::Float64
            end

            function area(c::Circle)
                pi * c.radius^2
            end

            area(Circle(2))
        end

        @test isempty(res.toplevel_error_reports)
    end
end

let # global declaration
    res = @analyze_toplevel begin
        global var
    end
    @test isempty(res.toplevel_error_reports)
end

@testset "toplevel throw" begin
    res = @analyze_toplevel begin
        throw("throw me")
    end
    @test length(res.inference_error_reports) == 1
    @test first(res.inference_error_reports) isa UncaughtExceptionReport
end

@testset "error handling within ConcreteInterpreter" begin
    let
        res = @analyze_toplevel begin
            struct A <: B end # UndefVarError(:B) should be handled into `res.toplevel_error_reports`
        end

        @test !isempty(res.toplevel_error_reports)
        er = first(res.toplevel_error_reports)
        @test er isa ActualErrorWrapped
        @test er.err == UndefVarError(:B)
        @test er.file == (@__FILE__) && er.line == (@__LINE__) - 7
    end

    @testset "stacktrace scrubbing" begin
        # NOTE some of the tests below are line-number-sensitive

        # scrub internal frames until (errored) user macro
        let
            res = @analyze_toplevel begin
                macro badmacro(s) throw(s) end # L1
                @badmacro "hi"                 # L2
            end

            @test !isempty(res.toplevel_error_reports)
            er = first(res.toplevel_error_reports)
            @test er isa ActualErrorWrapped
            @test er.file == (@__FILE__) && er.line == (@__LINE__) - 6 # L2
            @test length(er.st) == 1
            sf = first(er.st)
            @test sf.file === Symbol(@__FILE__) && sf.line == (@__LINE__) - 10 # L1
            @test sf.func === Symbol("@badmacro")
        end

        # TODO add test for `eval_with_err_handling` and `lower_with_err_handling`

        # scrub all the internal frame when errors happens in `maybe_evaluate_builtin`
        let
            res = @analyze_toplevel begin
                struct A
                    fld::UndefinedType
                end
            end

            @test !isempty(res.toplevel_error_reports)
            er = first(res.toplevel_error_reports)
            @test er isa ActualErrorWrapped
            @test er.err == UndefVarError(:UndefinedType)
            @test er.file == (@__FILE__) && er.line == (@__LINE__) - 9
            @test isempty(er.st)
        end

        # errors from user functions (i.e. those from `@invokelatest f(fargs...)` in the overload
        # `JuliaInterpreter.evaluate_call_recurse!(interp::ConcreteInterpreter, frame::Frame, call_expr::Expr; enter_generated::Bool=false)`)
        let
            res = @analyze_toplevel begin
                foo() = throw("don't call me, pal")
                struct A <: foo() end
            end

            @test !isempty(res.toplevel_error_reports)
            er = first(res.toplevel_error_reports)
            @test er isa ActualErrorWrapped
            @test er.err == "don't call me, pal"
            @test er.file == (@__FILE__) && er.line == (@__LINE__) - 7
            @test length(er.st) == 1
            sf = first(er.st)
            @test sf.file === Symbol(@__FILE__) && sf.line == (@__LINE__) - 11
        end
    end
end

@testset "invalid constant redefinition/declaration" begin
    # for abstract global assignment
    let
        vmod = gen_virtual_module()
        res = @test_logs (:warn,) @analyze_toplevel context = vmod virtualize = false begin
            fib(n) = n≤2 ? n : fib(n-1)+fib(n-1)
            const foo = fib(1000000000000) # ::Int
            foo = fib(1000000000000.) # ::Float64
        end

        @test is_abstract(vmod, :foo)
        @test length(res.inference_error_reports) == 1
        er = first(res.inference_error_reports)
        @test er isa InvalidConstantRedefinition
        @test er.name === :foo
    end
    let
        vmod = gen_virtual_module()
        res = @test_logs (:warn,) @analyze_toplevel context = vmod virtualize = false begin
            fib(n) = n≤2 ? n : fib(n-1)+fib(n-1)
            foo = fib(1000000000000.) # ::Float64
            const foo = fib(1000000000000) # ::Int
        end

        @test is_abstract(vmod, :foo)
        @test length(res.inference_error_reports) == 1
        er = first(res.inference_error_reports)
        @test er isa InvalidConstantDeclaration
        @test er.name === :foo
    end

    # for concretized constants
    let
        vmod = gen_virtual_module()
        res = @test_logs (:warn,) @analyze_toplevel context = vmod virtualize = false begin
            fib(n) = n≤2 ? n : fib(n-1)+fib(n-1)
            const T = typeof(fib(1000000000000)) # never terminates
            T = Nothing
        end

        @test is_concrete(vmod, :T) # wao, this is concretized
        @test length(res.inference_error_reports) == 1
        er = first(res.inference_error_reports)
        @test er isa InvalidConstantRedefinition
        @test er.name === :T
    end
    let
        vmod = gen_virtual_module()
        res = @test_logs (:warn,) @analyze_toplevel context = vmod virtualize = false begin
            fib(n) = n≤2 ? n : fib(n-1)+fib(n-1)
            T = Nothing
            const T = typeof(fib(1000000000000)) # never terminates
        end

        @test is_concrete(vmod, :T) # wao, this is concretized
        @test length(res.inference_error_reports) == 1
        er = first(res.inference_error_reports)
        @test er isa InvalidConstantDeclaration
        @test er.name === :T
    end

    let
        vmod = gen_virtual_module()
        res = @test_logs (:warn,) @analyze_toplevel context = vmod virtualize = false begin
            a = 0
            const a = 1
        end
        @test length(res.inference_error_reports) == 1
        let er = first(res.inference_error_reports)
            @test er isa InvalidConstantDeclaration
            @test er.name === :a
        end
    end
end

@testset "non-deterministic abstract global assignments" begin
    let
        vmod, res = @analyze_toplevel2 begin
            v = '0'
            v = 0 # deterministic
        end
        @test is_analyzed(vmod, :v)
        @test isa_analyzed(vmod.v, Int)
    end
    let
        vmod, res = @analyze_toplevel2 begin
            v = '0'
            v = rand(Int) # deterministic
        end
        @test is_analyzed(vmod, :v)
        @test isa_analyzed(vmod.v, Int)
    end
    let
        vmod, res = @analyze_toplevel2 begin
            v = '0'
            rand(Bool) && (v = 0) # non-deterministic
        end
        @test is_analyzed(vmod, :v)
        @test isa_analyzed(vmod.v, Union{Char,Int})
    end
    let
        vmod, res = @analyze_toplevel2 begin
            v = '0'
            rand(Bool) && (v = rand(Int)) # non-deterministic
        end
        @test is_analyzed(vmod, :v)
        @test isa_analyzed(vmod.v, Union{Char,Int})
    end

    # end to end
    let
        vmod, res = @analyze_toplevel2 begin
            v = '0'
            v = rand(Int) # deterministic

            f(::Number) = :ok
            f(v) # no method error should NOT happen here
        end

        @test isempty(res.inference_error_reports)
    end
    let
        vmod, res = @analyze_toplevel2 begin
            v = '0'
            rand(Bool) && (v = rand(Int)) # non-deterministic

            f(::Number) = :ok
            f(v) # union-split no method error should happen here
        end

        @test length(res.inference_error_reports) === 1
        er = first(res.inference_error_reports)
        @test er isa NoMethodErrorReport
        @test isa(er.t, Vector)
    end
end

@testset "docstrings" begin
    # can find errors within docstring generation
    let
        vmod, res = @analyze_toplevel2 begin
            """
                foo

            This is a invalid docstring for `$(foo)`,
            becuase `foo` is undefined at this point.
            """
            :(foo)
        end
        @test any(res.inference_error_reports) do r
            r isa GlobalUndefVarErrorReport &&
            r.name === :foo
        end
    end
end

@testset "world age" begin
    # https://github.com/aviatesk/JET.jl/issues/104
    res = @analyze_toplevel begin
        using Test
        @testset begin
            a = @inferred(ones(Int,ntuple(d->1,1)), ntuple(x->x+1,1))
        end
    end
    @test true

    res = @analyze_toplevel begin
        using Test

        @test_warn "foo" println(stderr, "foo")
    end
    @test isempty(res.toplevel_error_reports)
end

@testset "avoid too much bail out from `_virtual_process!`" begin
    let
        res = @analyze_toplevel begin
            sin′
        end;
        @test !isempty(res.inference_error_reports)
    end

    let
        res = @analyze_toplevel begin
            s = nothing
            sin′
        end;
        @test !isempty(res.inference_error_reports)
    end
end

# will be used in the following two testsets
let
    fixtures_dir = normpath(@__DIR__, "..", "fixtures")
    global const CONCRETIZATION_PATTERNS_FILE   = normpath(fixtures_dir, "concretization_patterns.jl")
    global const CONCRETIZATION_PATTERNS_CONFIG = normpath(fixtures_dir, "..JET.toml")
end

@testset "custom concretization pattern" begin
    # custom concretization pattern should work on AST level
    let
        vmod, res = @analyze_toplevel2 begin
            const foo = Dict() # won't be concretized by default
        end
        @test !is_concrete(vmod, :foo)
    end
    let
        vmod, res = @analyze_toplevel2 begin
            const foo = Dict() # now this will be forcibly concretized
        end concretization_patterns = [:(const foo = Dict())]
        @test is_concrete(vmod, :foo)
    end

    # the analysis on `test/fixtures/concretization_patterns.jl` will produce inappropriate
    # top-level error report because of missing concretization
    let
        res = report_file2(CONCRETIZATION_PATTERNS_FILE).res
        @test length(res.toplevel_error_reports) == 1
        let r = first(res.toplevel_error_reports)
            @test isa(r, MissingConcretization)
        end
    end

    # we can circumvent the issue by using the `concretization_patterns` configuration !
    let
        res = report_file2(CONCRETIZATION_PATTERNS_FILE;
                           concretization_patterns = [:(const GLOBAL_CODE_STORE = Dict())],
                           ).res
        @test isempty(res.toplevel_error_reports)
    end

    # we can specify whatever pattern `@capture` can accept
    let
        vmod, res = @analyze_toplevel2 begin
            foo() = rand((1,2,3))
            a = foo()
            b = foo()
        end concretization_patterns = [:x_] # means "concretize everything"
        @test is_concrete(vmod, :a)
        @test is_concrete(vmod, :b)
    end

    # `concretization_patterns` should "intuitively" work for code with documentations attached
    let
        vmod, res = @analyze_toplevel2 begin
            """
                foo

            This is a documentation for `foo`
            """
            const foo = Dict()
        end concretization_patterns = [:(const foo = Dict())]
        @test is_concrete(vmod, :foo)
    end
end

# NOTE better to be in test_misc.jl, but here it is since this test is only valid when the testset above gets passed
@testset "configuration file" begin
    dir = mktempdir()
    analysis_target = normpath(dir, "concretization_patterns.jl")
    config_target   = normpath(dir, JET.CONFIG_FILE_NAME)

    back = pwd()
    try
        # in order to check the functionality, fixtures/..JET.toml logs toplevel analysis
        # into toplevel.txt (relative to the current working directory)
        # and so let's `cd` into the temporary directory to not pollute this directory
        cd(dir)

        open(CONCRETIZATION_PATTERNS_FILE) do f
            write(analysis_target, f)
        end

        # no configuration, thus top-level analysis should fail
        let
            res = report_file2(analysis_target)
            nreported = print_reports(IOBuffer(), res)
            @test !iszero(nreported) # error reported
        end

        # setup a configuration file
        open(CONCRETIZATION_PATTERNS_CONFIG) do f
            write(config_target, f)
        end

        # now any top-level analysis failure shouldn't happen
        let
            res = report_file2(analysis_target)
            nreported = print_reports(IOBuffer(), res)
            @test iszero(nreported) # no error happened
            @test isfile("toplevel.txt") # not closed yet
        end
    catch err
        rethrow(err)
    finally
        cd(back)
    end
end

@testset "`collect_toplevel_signature!`" begin
    res = @analyze_toplevel analyze_from_definitions=false begin
        foo() = return
        bar(a) = return a
        baz(a) = return a
        baz(a::Int) = return a
        qux(a::T) where T<:Integer = return a
    end
    @test isempty(res.toplevel_signatures)

    vmod, res = @analyze_toplevel2 analyze_from_definitions=true begin
        foo() = return
        bar(a) = return a
        baz(a) = return a
        baz(a::Int) = return a
        qux(a::T) where T<:Integer = return a
    end
    @test !isempty(res.toplevel_signatures)
    @test any(res.toplevel_signatures) do sig
        sig === Tuple{typeof(vmod.foo)}
    end
    @test any(res.toplevel_signatures) do sig
        sig === Tuple{typeof(vmod.bar),Any}
    end
    @test any(res.toplevel_signatures) do sig
        sig === Tuple{typeof(vmod.baz),Any}
    end
    @test any(res.toplevel_signatures) do sig
        sig === Tuple{typeof(vmod.baz),Int}
    end
    @test any(res.toplevel_signatures) do sig
        sig <: Tuple{typeof(vmod.qux),Integer} # `Tuple{typeof(vmod.qux),TypeVar}`
    end
end

@testset "analyze from definitions" begin
    let
        res = @analyze_toplevel analyze_from_definitions=false begin
            foo() = return undefvar
        end
        @test isempty(res.inference_error_reports)

        res = @analyze_toplevel analyze_from_definitions=true begin
            foo() = return undefvar
        end
        @test !isempty(res.inference_error_reports)
        @test any(res.inference_error_reports) do err
            isa(err, GlobalUndefVarErrorReport) &&
            err.name === :undefvar
        end
    end

    let # COMBAK use different aggregation policy for "user-script" analysis ?
        res = @analyze_toplevel analyze_from_definitions=true begin
            foo(a) = b # typo
            bar() = foo("julia")
        end
        @test length(res.inference_error_reports) ≥ 1
        # @test_broken any(res.inference_error_reports) do err # report analyzed from `foo`
        #     isa(err, GlobalUndefVarErrorReport) &&
        #     err.name === :b &&
        #     length(err.vst) == 1
        # end &&       any(res.inference_error_reports) do err # report analyzed from `bar`
        #     isa(err, GlobalUndefVarErrorReport) &&
        #     err.name === :b &&
        #     length(err.vst) == 2
        # end
    end

    let
        res = @analyze_toplevel analyze_from_definitions=true begin
            foo(a) = sum(a)
            bar() = foo("julia")
        end
        test_sum_over_string(res)
    end
end

@testset "top-level statement selection" begin
    # simplest example
    let
        # global function
        vmod, res = @analyze_toplevel2 begin
            foo() = return # should be concretized
        end
        @test is_concrete(vmod, :foo)

        # inner function
        vmod, res = @analyze_toplevel2 analyze_from_definitions=true let
            foo(a) = a # should be concretized, and its signature should have been collected
        end
        @test length(res.toplevel_signatures) == 1
    end

    # simple negative case test, which checks we do NOT select statements not involved with any definition
    # this particular example is adapted from https://en.wikipedia.org/wiki/Program_slicing
    let
        src = JET.@src let
            sum = 0
            product = 1 # should NOT be selected
            w = 7
            for i in 1:N
                sum += i + w
                product *= i # should NOT be selected
            end
            @eval global getsum() = $sum # concretization is forced
            write(product) # should NOT be selected
        end
        slice = JET.select_statements(src)

        for (i, stmt) in enumerate(src.code)
            if JET.@isexpr(stmt, :(=))
                lhs, rhs = stmt.args
                if isa(lhs, Core.SlotNumber)
                    if src.slotnames[lhs.id] === :w || src.slotnames[lhs.id] === :sum
                        @test slice[i]
                    elseif src.slotnames[lhs.id] === :product
                        @test !slice[i]
                    end
                end
            elseif JET.@capture(stmt, write(x_))
                @test !slice[i]
            end
        end
    end

    @testset "captured variables" begin
        let
            vmod, res = @analyze_toplevel2 begin
                begin
                    s = join(rand(Char, 100))
                    foo() = return s
                end
            end
            @test is_concrete(vmod, :foo)
            @test is_abstract(vmod, :s)
        end

        # captured variables for global functions
        let
            # XXX `s` below aren't necessarily concretized, but concretization of `foo` requires
            # it (since `s` will be embedded into `foo`'s body wrapped in `QuoteNode`)
            # and thus we can't abstract it away as far as we depend on JuliaInterpreter ...
            vmod, res = @analyze_toplevel2 let
                s = "julia"
                global foo() = return s
            end
            @test is_concrete(vmod, :foo)

            vmod, res = @analyze_toplevel2 let
                s = undefvar # actual top-level error is better not to happen here
                global foo() = return s
            end
            @test_broken isempty(res.toplevel_error_reports)

            # more realistic example
            vmod, res = @analyze_toplevel2 let s = sprint(showerror, DivideError())
                global errmsg(s = s) = string("error: ", s)
            end
            @test is_concrete(vmod, :errmsg)
            @test isempty(res.toplevel_error_reports)
        end
    end

    @testset "conditional control flow" begin
        # ignore last statement of a definition block if possible
        let
            vmod, res = @analyze_toplevel2 if true # force multiple blocks
                foo() = return
                throw("foo")
            end
            @test is_concrete(vmod, :foo)
            @test isempty(res.toplevel_signatures)
        end
    end

    # NOTE here we test "try/catch"-involved control flow in a fine-grained level
    # we want to enrich this test suite as much as possible since this control flow often
    # appears in test files with the test macros, while we will do the e2e level tests
    # with those macros
    @testset "try/catch control flow" begin
        # https://github.com/aviatesk/JET.jl/issues/150
        let
            res = @analyze_toplevel analyze_from_definitions=true try
                foo(a) = sum(a) # essentially same as inner function, should be concretized
                foo("julia") # shouldn't be concretized
            catch err
                err
            end

            @test isempty(res.toplevel_error_reports)
            @test length(res.toplevel_signatures) == 1
            test_sum_over_string(res)
        end

        # captured variables within a try clause
        let
            res = @analyze_toplevel analyze_from_definitions=true try
                s = "julia"
                foo(f) = f(s) # should be concretized
                foo(sum) # shouldn't be concretized
            catch err
                err
            end

            @test isempty(res.toplevel_error_reports)
            @test length(res.toplevel_signatures) == 1
            test_sum_over_string(res)
        end

        # captured variables within a catch clause
        let
            res = @analyze_toplevel analyze_from_definitions=true try
                s = "julia"
                foo(f) = f(s) # should be concretized
                foo(sum) # shouldn't be concretized
            catch err
                function shows() # should be concretized
                    io = stderr::IO
                    showerror(io, err)
                    showerror(io, err2)
                end
                shows() # shouldn't be concretized
            end

            @test isempty(res.toplevel_error_reports)
            @test length(res.toplevel_signatures) == 2
            test_sum_over_string(res)
            @test any(res.inference_error_reports) do r
                isa(r, GlobalUndefVarErrorReport) &&
                r.name === :err2
            end
        end

        # XXX similar to "captured variables for global functions" cases, but such patterns
        # can be even worse when used within a catch clause; it will just fail into a hell.
        # I include this case just for the reference and won't expect this to work robustly
        # since it heavily depends on Julia's AST lowering and the implementation detail of
        # JuliaInterpreter.jl
        let
            vmod, res = @analyze_toplevel2 try
                s = "julia"
                foo(f) = f(s) # should be concretized
                foo(sum) # shouldn't be concretized
            catch err # will be `MethodError` in actual execution
                global geterr() = return err # should be concretized
            end

            # thanks to JuliaInterpreter's implementation detail, the analysis above won't
            # report top-level errors and can concretize `geterr` even if the actual `err`
            # is not thrown and thus these first two test cases will pass
            @test isempty(res.toplevel_error_reports)
            @test is_concrete(vmod, :geterr) && length(methods(vmod.geterr)) == 1
            # yet we still need to make `geterr` over-approximate an actual execution soundly;
            # currently JET's abstract interpretation special-cases `_INACTIVE_EXCEPTION`
            # and fix it to `Any`, and we test it here in the last test case
            result = report_call(vmod.geterr)
            @test MethodError ⊑ get_result(result)
        end
    end

    @testset "top-level `eval` statement" begin
        let
            vmod, res = @analyze_toplevel2 let
                sym = :foo
                @eval $sym() = :foo # should be concretized
            end
            @test is_concrete(vmod, :foo)
        end
    end

    @testset "toplevel definitions involved within a loop" begin
        let
            vmod, res = @analyze_toplevel2 begin
                # these definitions should be concretized
                for fname in (:foo, :bar, :baz)
                    @eval begin
                        @inline ($(Symbol("is", fname)))(a) = a === $(QuoteNode(fname))
                    end
                end
            end

            @test is_concrete(vmod, :isfoo)
            @test is_concrete(vmod, :isbar)
            @test is_concrete(vmod, :isbaz)
        end

        let
            vmod, res = @analyze_toplevel2 let
                # these definitions should be concretized
                stack = [:foo, :bar, :baz]
                while !isempty(stack)
                    fname = popfirst!(stack)
                    @eval begin
                        @inline ($(Symbol("is", fname)))(a) = a === $(QuoteNode(fname))
                    end
                end
            end

            @test is_concrete(vmod, :isfoo)
            @test is_concrete(vmod, :isbar)
            @test is_concrete(vmod, :isbaz)
        end
    end

    # code expanded by `@enum` is fairly complex and it couldn't be handled well by JET's
    # general statement selection logic; so currently JET special case it and concretize
    # the whole macro context by pre-defined `concretization_patterns` configurations
    # (xref: originally reported from https://github.com/aviatesk/JET.jl/issues/175)
    @testset "test with @enum macro" begin
        res = @analyze_toplevel begin
            @enum Fruit apple orange
        end
        @test isempty(res.toplevel_error_reports)
    end
end

# in this e2e test suite, we just check usage of test macros doesn't lead to (false positive)
# top-level errors (e.g. world age, etc.)
# TODO
# - leave where these sample code are extracted from
# - add test for `@inferred`
@testset "@test macros" begin
    vmod = Module()
    Core.eval(vmod, :(using Test))

    let # @test
        res = @analyze_toplevel context = vmod virtualize = false begin
            @test true
            @test [1, 2] + [2, 1] == [3, 3]
            @test π ≈ 3.14 atol=0.01
            @test 2 + 2 ≈ 6 atol=1 broken=true
            @test 2 + 2 ≈ 5 atol=1 broken=false
            @test 2 + 2 == 5 skip=true
            @test 2 + 2 == 4 skip=false
        end
        @test isempty(res.toplevel_error_reports)
    end

    let # @test_skip
        res = @analyze_toplevel context = vmod virtualize = false begin
            @test_skip 1 == 2
            @test_skip 1 == 2 atol=0.1
        end
        @test isempty(res.toplevel_error_reports)
    end

    let # @test_broken
        res = @analyze_toplevel context = vmod virtualize = false begin
            @test_broken 1 == 2
            @test_broken 1 == 2 atol=0.1
        end
        @test isempty(res.toplevel_error_reports)
    end

    let # @test_throws
        res = @analyze_toplevel context = vmod virtualize = false begin
            @test_throws BoundsError [1, 2, 3][4]
            @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
        end
        @test isempty(res.toplevel_error_reports)
    end

    let # @test_logs
        res = @analyze_toplevel context = vmod virtualize = false begin
            using Logging: Warn, Debug

            function foo(n)
                @info "Doing foo with n=$n"
                for i=1:n
                    @debug "Iteration $i"
                end
                42
            end
            @test_logs (:info, "Doing foo with n=2") foo(2)
            @test_logs (:info,"Doing foo with n=2") (:debug,"Iteration 1") (:debug,"Iteration 2") min_level=Debug foo(2)
            @test_logs (:info,) (:debug,"Iteration 42") min_level=Debug match_mode=:any foo(100)
            @test (@test_logs (:info,"Doing foo with n=2") foo(2)) == 42

            f() = return
            @test_logs min_level=Warn f()  # test `f` logs no messages when the logger level is warn.
        end
        @test isempty(res.toplevel_error_reports)
    end

    let # @test_warn, @test_nowarn
        res = @analyze_toplevel context = vmod virtualize = false begin
            @test 1234 === @test_nowarn(1234)
            @test 5678 === @test_warn("WARNING: foo", begin println(stderr, "WARNING: foo"); 5678; end)
            let a
                @test_throws UndefVarError(:a) a
                @test_nowarn a = 1
                @test a === 1
            end
        end
        @test isempty(res.toplevel_error_reports)
    end

    let # @test_deprecated
        res = @analyze_toplevel context = vmod virtualize = false begin
            a() = 1
            b() = 2
            @test_deprecated a()
        end
        @test isempty(res.toplevel_error_reports)
    end

    let # @testset
        res = @analyze_toplevel context = vmod virtualize = false begin
            @testset "trigonometric identities" begin
                θ = 2/3*π
                @test sin(-θ) ≈ -sin(θ)
                @test cos(-θ) ≈ cos(θ)
                @test sin(2θ) ≈ 2*sin(θ)*cos(θ)
                @test cos(2θ) ≈ cos(θ)^2 - sin(θ)^2
            end
        end
        @test isempty(res.toplevel_error_reports)
    end

    let # @testset with actual type-level errors
        res = @analyze_toplevel context = vmod virtualize = false begin
            @testset "JET example" begin
                @test sum("julia") == "julia" # actual errors
            end
        end
        @test isempty(res.toplevel_error_reports)
        test_sum_over_string(res)
    end

    let
        # adapted from https://github.com/JuliaLang/julia/blob/ddeba0ba8aa452cb2064eb2d03bea47fe6b0ebbe/test/dict.jl#L469-L478
        res = @analyze_toplevel begin
            using Test
            @testset "IdDict{Any,Any} and partial inference" begin
                a = IdDict{Any,Any}()
                a[1] = a
                a[a] = 2

                ca = copy(a)
                ca = empty!(ca)
                @assert length(a) == 2 # FIXME this statement should be abstracted

                d = @inferred IdDict{Any,Any}(i=>i for i=1:3)
            end
        end
        @test isempty(res.toplevel_error_reports)
    end
end

# in this e2e test suite, we run JET against a selection of Julia's base test suite
# (i.e. from https://github.com/JuliaLang/julia/tree/master/test) and check that it won't
# produce (false positive) top-level errors (e.g., by actually running test code, etc.)
# NOTE this could be very fragile ...
@testset "test file targets" begin
    TARGET_DIR = normpath(FIXTURE_DIR, "targets")

    let
        res = report_file2(normpath(TARGET_DIR, "error.jl")).res
        @test isempty(res.toplevel_error_reports)
    end

    let
        res = report_file2(normpath(TARGET_DIR, "dict.jl")).res
        @test isempty(res.toplevel_error_reports)
    end
end

@testset "@generated function" begin
    genex = :(@generated function foo(a)
        if a <: Integer
            return :(a)
        elseif a <: Number
            return :(undefvar) # report me this case
        end
        throw("invalid argument")
    end)

    let
        res = @eval @analyze_toplevel begin
            $genex

            foo(100) # generate valid code
        end
        @test isempty(res.inference_error_reports)
    end

    let
        res = @eval @analyze_toplevel begin
            $genex

            foo(100.0) # generate invalid code
        end
        @test length(res.inference_error_reports) == 1
        r = first(res.inference_error_reports)
        @test isa(r, GlobalUndefVarErrorReport)
    end

    let
        res = @eval @analyze_toplevel begin
            $genex

            foo("julia") # unsuccessful code generation
        end
        @test length(res.inference_error_reports) == 1
        r = first(res.inference_error_reports)
        @test isa(r, GeneratorErrorReport) && r.err == "invalid argument"
    end

    # when `analyze_from_definitions = true`, code generation will likely fail
    # (because we can't expect method signatures to be concrete for `@generated` function),
    # but we just ignore that
    let
        res = @eval @analyze_toplevel analyze_from_definitions=true begin
            $genex
        end
        @test isempty(res.inference_error_reports)
    end

    # when `analyze_from_definitions = true`, we can analyze generator itself
    let
        res = @analyze_toplevel analyze_from_definitions = true begin
            @generated function foo(a)
                if a <: Integer
                    return :(a)
                elseif t <: Number # `t` is undefined
                    return :(2a)
                end
                throw("invalid argument")
            end
        end
        @test length(res.inference_error_reports) == 1
        r = first(res.inference_error_reports)
        @test isa(r, GlobalUndefVarErrorReport) && r.name === :t
    end
end


text:
module WindSites

using Proj4, XLSX, CSV, DataFrames, Dates, GDAL_jll, Pkg.TOML, Plots, MAT,
        Loess, SmoothingSplines, Plots.PlotMeasures, CategoricalArrays

export openmap, readusa, readdk, readuk, readse, readde, readturbinedata, shapefile2csv,
    scatterplots_model, plotdist, fill_missing_rotordiams!, closestturbine, turbines2parks,
    analyze_protected, analyze_natura2000, analyze_landtype, grouppopulationdensity, groupwindspeeds

include("regional_level_GIS.jl")
include("turbine_level_GIS.jl")

function openmap(df::DataFrame, turbinenumber::Int)
    openmap(df, turbinenumber, :bing)
    openmap(df, turbinenumber, :google)
end

function openmap(df::DataFrame, turbinenumber::Int, source::Symbol)
    lon, lat = df[turbinenumber, :lon], df[turbinenumber, :lat]
    if source == :google
        url = "http://maps.google.com/maps?t=k\"&\"q=loc:$lat+$lon"
    else
        url = "https://bing.com/maps/default.aspx?cp=$lat~$lon\"&\"lvl=18\"&\"style=a\"&\"sp=point.$(lat)_$(lon)_"
    end
    c = Cmd(`cmd /c start \"\" $url`, windows_verbatim=true)
    run(c)
    return df[turbinenumber, :]
end

function openmap(lon::Real, lat::Real)
    url = "http://maps.google.com/maps?t=k\"&\"q=loc:$lat+$lon"
    # Extra quotes to avoid errors with special chars ? and &:
    # https://superuser.com/questions/36728/can-i-launch-urls-from-command-line-in-windows
    # https://discourse.julialang.org/t/quoting-special-characters-of-a-url-in-cmd-objects-on-windows/44324
    c = Cmd(`cmd /c start \"\" $url`, windows_verbatim=true)
    run(c)
end

function openmapchrome(lon::Real, lat::Real)
    command = "C:/Program Files (x86)/Google/Chrome/Application/chrome"
    url = "http://maps.google.com/maps?t=k&q=loc:$lat+$lon"
    println("Opening $url...")
    run(`$command $url`)
end

function readusa()
    # US Wind Turbine Database
    # https://eerscmap.usgs.gov/uswtdb/
    datadir = joinpath(@__DIR__, "..", "data")
    df = DataFrame(CSV.File("$datadir/USA_uswtdb_v3_1_20200717.csv"))
    select!(df, [:t_cap, :xlong, :ylat, :p_year, :t_model, :t_hh, :t_rd])
    rename!(df, [:capac, :lon, :lat, :year, :model, :hubheight, :rotordiam])
    delete!(df, ismissing.(df[:, :capac]))
    delete!(df, ismissing.(df[:, :year]))
    df[!, :capac] = convert.(Int, df[!, :capac])
    return df
end

parse_not_missing(T, x) = ismissing(x) ? missing : parse(T, x)
convert_not_missing(T, x) = ismissing(x) ? missing : convert(T, x)

function readuk()
    # UK Renewable Energy Planning Database (REPD)
    # https://www.gov.uk/government/publications/renewable-energy-planning-database-monthly-extract
    datadir = joinpath(@__DIR__, "..", "data")
    cols = [6,9,14,15,16,19,25,26,47]
    df = DataFrame(CSV.File("$datadir/UK_REPD-June-2020-update.csv"))[:, cols]
    rename!(df, [:type, :capac_park, :capac, :nturbines, :height, :status, :lon, :lat, :year])
    delete!(df, .!startswith.(df[!, :type], "Wind"))
    delete!(df, df[:, :status] .!= "Operational")
    select!(df, Not(:status))
    df[!, :type] = replace.(df[!, :type], "Wind " => "")
    df[!, :nturbines] = parse.(Int, df[!, :nturbines])
    df[!, :height] = parse_not_missing.(Int, df[!, :height])
    df[!, :capac] = parse.(Float64, df[!, :capac])
    df[!, :capac_park] = parse.(Float64, replace.(df[!, :capac_park], "," => ""))
    df[!, :lon] = parse.(Int, replace.(df[!, :lon], "," => ""))
    df[!, :lat] = parse.(Int, replace.(df[!, :lat], "," => ""))
    df[!, :year] = Date.(df[!, :year], dateformat"d/m/y")

    x = df[!, :lon] # BNG: British National Grid = EPSG:27700
    y = df[!, :lat] # https://en.wikipedia.org/wiki/Ordnance_Survey_National_Grid#General
    source = Projection("+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs")
    dest = Projection("+proj=longlat +datum=WGS84 +no_defs")
    len = size(df,1)
    lon, lat = zeros(len), zeros(len)
    for i = 1:len
        lon[i], lat[i], _ = Proj4.transform(source, dest, [x[i] y[i] 0])
    end
    df[!, :lon] = lon
    df[!, :lat] = lat
    return df
end

function readdk()
    # DK Energistyrelsen: Stamdataregister for vindkraftanlæg
    # https://ens.dk/service/statistik-data-noegletal-og-kort/data-oversigt-over-energisektoren
    datadir = joinpath(@__DIR__, "..", "data")
    cols = [3,13,14,2,7,5,4,10,58]
    df = DataFrame(XLSX.readtable("$datadir/DK_anlaegprodtilnettet_0.xlsx", "IkkeAfmeldte-Existing turbines",
        "A:BM", first_row=19, header=false, infer_eltypes=false)...)[!, cols]
    rename!(df, [:capac, :lon, :lat, :year, :model, :hubheight, :rotordiam, :onshore, :elec2018])
    df[!, :capac] = convert.(Int, round.(df[!, :capac]))
    df[!, :model] = string.(df[:, :model])
    df[!, :hubheight] = convert.(Float64, df[!, :hubheight])
    df[!, :rotordiam] = convert.(Float64, df[!, :rotordiam])
    df[!, :year] = Dates.year.(df[!, :year])
    df[!, :onshore] = uppercase.(df[!, :onshore]) .== "LAND"    # no missings here, no need to check
    df[!, :elec2018] = df[!, :elec2018] / 1000
    delete!(df, ismissing.(df[:, :lon]))

    x = df[!, :lon] # European Terrestrial Reference System 1989: ETRS89/UTM zone 32N = EPSG:25832
    y = df[!, :lat] # https://en.wikipedia.org/wiki/European_Terrestrial_Reference_System_1989
    source = Projection("+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs")
    dest = Projection("+proj=longlat +datum=WGS84 +no_defs")
    len = size(df,1)
    lon, lat = zeros(len), zeros(len)
    for i = 1:len
        lon[i], lat[i], _ = Proj4.transform(source, dest, [x[i] y[i] 0])
    end
    df[!, :lon] = lon
    df[!, :lat] = lat
    return df
end

function xlsx2csv()
    datadir = joinpath(@__DIR__, "..", "data")
    df = DataFrame(XLSX.readtable("$datadir/SE_Vindbrukskollen_export_allman_Prod.xlsx", "Vindkraftverk",
        "A:AN", first_row=1, header=true, infer_eltypes=true)...)
    CSV.write("$datadir/SE_Vindbrukskollen_export_allman_Prod.csv", df)
    return df
end

function readse()
    # Länsstyrelsen: Vindbrukskollen
    # https://vbk.lansstyrelsen.se/  (click "Excel-export")
    # shapefile: https://ext-dokument.lansstyrelsen.se/gemensamt/geodata/ShapeExport/LST.VKOLLEN_VINDKRAFTVERK.zip
    # shapefile2csv("LST.VKOLLEN_VINDKRAFTVERK/LST_VKOLLEN_VVERK.shp")
    # but actually used xlsx2csv() above to convert the Excel file
    datadir = joinpath(@__DIR__, "..", "data")
    df = DataFrame(CSV.File("$datadir/SE_Vindbrukskollen_export_allman_Prod.csv"))
    select!(df, ["Status", "Placering", "E-Koordinat", "N-Koordinat", "Totalhöjd (m)", "Navhöjd (m)",
        "Rotordiameter (m)", "Maxeffekt (MW)", "Uppfört", "Fabrikat", "Modell"])
    rename!(df, [:status, :type, :lon, :lat, :height, :hubheight, :rotordiam, :capac, :year, :brand, :model])
    delete!(df, df[:, :status] .!= "Uppfört")
    select!(df, Not(:status))
    twoturbines = findall(ismissing.(df[!, :type]))
    if twoturbines == [1196, 1197]     # hard code a fix for two turbines
        df[twoturbines, :type] .= "Land"
    end
    df[!, :onshore] = startswith.(df[!, :type], "Land")     # all turbines are marked "Land" or "Vatten"
    select!(df, Not(:type))
    delete!(df, ismissing.(df[:, :capac]))
    delete!(df, df[:, :capac] .== 0)
    df[!, :capac] = Int.(df[!, :capac] * 1000)
    # df[!, :year] = [d == "Jan 01, 1900" ? missing : Date(d, dateformat"u d, y") for d in df[!, :year]]
    df[df[!, :year] .< Date(1980), :year] .= missing
    df[!, :model] = strip.(coalesce.(df[!, :brand], "")) .* " " .* strip.(coalesce.(df[!, :model], ""))
    df[!, :model] = [m == " " ? missing : m for m in df[!, :model]]
    select!(df, Not(:brand))

    x = df[!, :lon] # SWEREF99 TM = EPSG:3006
    y = df[!, :lat] # https://www.lantmateriet.se/sv/Kartor-och-geografisk-information/gps-geodesi-och-swepos/referenssystem/tvadimensionella-system/sweref-99-projektioner/
    source = Projection("+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs")
    dest = Projection("+proj=longlat +datum=WGS84 +no_defs")
    len = size(df,1)
    lon, lat = zeros(len), zeros(len)
    for i = 1:len
        lon[i], lat[i], _ = Proj4.transform(source, dest, [x[i] y[i] 0])
    end
    df[!, :lon] = lon
    df[!, :lat] = lat
    return df
end

function readturbinedata(; rotordiamplots=false)
    df_dk = readdk()
    df_usa = readusa()
    df_uk = readuk()
    df_se = readse()
    df_de = readde()

    fill_missing_rotordiams!(df_dk, df_usa, df_uk, df_se, df_de; rotordiamplots=rotordiamplots)

    CSV.write(in_datafolder("turbines_DK.csv"), df_dk)
    CSV.write(in_datafolder("turbines_USA.csv"), df_usa)
    CSV.write(in_datafolder("turbines_UK.csv"), df_uk)
    CSV.write(in_datafolder("turbines_SE.csv"), df_se)
    CSV.write(in_datafolder("turbines_DE.csv"), df_de)

    return df_dk, df_usa, df_uk, df_se, df_de
end

function readde()
    # Eichhorn et al. (2019), Spatial Distribution of Wind Turbines, Photovoltaic Field Systems, Bioenergy,
    # and River Hydro Power Plants in Germany
    # https://www.mdpi.com/2306-5729/4/1/29
    # https://www.ufz.de/record/dmp/archive/5467/en/

    # shapefile2csv("renewable_energy_plants_germany_until_2015/windpower.shp")
    datadir = joinpath(@__DIR__, "..", "data")
    df = DataFrame(CSV.File("$datadir/DE_windpower.csv"))[!, 2:7]
    rename!(df, [:year, :capac, :hubheight, :rotordiam, :lon, :lat])

    x = df[!, :lon] # European Terrestrial Reference System 1989: ETRS89/LAEA Europe = EPSG:3035
    y = df[!, :lat] # https://en.wikipedia.org/wiki/European_Terrestrial_Reference_System_1989
    source = Projection("+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs")
    dest = Projection("+proj=longlat +datum=WGS84 +no_defs")
    len = size(df,1)
    lon, lat = zeros(len), zeros(len)
    for i = 1:len
        lon[i], lat[i], _ = Proj4.transform(source, dest, [x[i] y[i] 0])
    end
    df[!, :lon] = lon
    df[!, :lat] = lat
    return df
end

function shapefile2csv(filename)
    datadir = joinpath(@__DIR__, "..", "data")
    shapefile = "$datadir/$filename"
    layername = splitext(basename(filename))[1]
    # sql = "select FID+1 as FID from \"$layername\""

    ogrinfo_path() do ogrinfo
        @time run(`$ogrinfo -al -so $shapefile`)
    end

    ogr2ogr_path() do ogr2ogr
        # @time run(`$ogr2ogr -f CSV $outfile -dialect SQlite -sql $sql $shapefile`)
        @time run(`$ogr2ogr -f CSV $datadir/$layername.csv $shapefile`)
    end
end

# println("\nProjecting coordinates (Mollweide)...")
# res = 0.01
# res2 = res/2
# lons = (-180+res2:res:180-res2)[lonrange]         # longitude values (pixel center)
# lats = (90-res2:-res:-90+res2)[latrange]          # latitude values (pixel center)
# source = LonLat()
# dest = Projection("+proj=moll +lon_0=$(mean(lons))")
# xs, ys = xygrid(lons, lats)
# Proj4.transform!(source, dest, vec(xs), vec(ys))

in_datafolder(names...) = joinpath(getconfig("datafolder"), names...)

getconfig(key) = getconfig()[key]

function getconfig()
    configfile = joinpath(homedir(), ".GlobalEnergyGIS_config")
    if !isfile(configfile)
        error("Configuration file missing, please run saveconfig(datafolder, uid, api_key) first. See GlobalEnergyGIS README.")
    end
    return TOML.parsefile(configfile)
end

# estimate missing rotor diameters from turbine capacity using smoothing splines
# fit separate splines to turbines below and above 1 MW
function fill_missing_rotordiams!(df_dk, df_usa, df_uk, df_se, df_de; rotordiamplots=false)
    df = df_de[:, [:rotordiam, :capac]]
    badrows = (df[!, :capac] .< 80) .& (df[!, :rotordiam] .> 20)
    df = df[.!badrows, :]
    for dd in [df_se, df_dk, df_usa]
        df = vcat(df, dd[:, [:rotordiam, :capac]])
    end
    rotordiamplots && plotly()
    rotordiamplots && display(histogram(df[ismissing.(df[!, :rotordiam]), :capac]))

    dfd = dropmissing(df, [:rotordiam, :capac])
    dfd = dfd[dfd[!, :rotordiam] .> 5, :]
    x = float.(dfd[:, :capac])
    y = float.(dfd[:, :rotordiam])
    rotordiamplots && scatter(x, y .+ 1*randn(size(dfd,1)), markersize=1, alpha=0.1)
    ss1 = fit(SmoothingSpline, x, y, 0.0001*maximum(x)^3)
    ss2 = fit(SmoothingSpline, x, y, 0.1*maximum(x)^3)
    # ss = loess(x, y, span=0.75, degree=3)
    xx = range(minimum(x), maximum(x), length=1000)
    yy1 = SmoothingSplines.predict(ss1, xx)
    yy2 = SmoothingSplines.predict(ss2, xx)
    rotordiamplots && display(plot!(xx, [yy1, yy2]))

    df_uk[:, :rotordiam] = zeros(Int, size(df_uk,1))
    for dd in [df_se, df_dk, df_de, df_uk, df_usa]
        rows = (dd[!, :capac] .<= 1000)
        is_uk = (size(dd,1) == size(df_uk,1))
        rows = is_uk ? rows : rows .& ismissing.(dd[!, :rotordiam])
        dd[rows, :rotordiam] =
            round.(Int, SmoothingSplines.predict(ss1, float.(dd[rows, :capac])))
        rows = (dd[!, :capac] .> 1000)
        rows = is_uk ? rows : rows .& ismissing.(dd[!, :rotordiam])
        dd[rows, :rotordiam] =
            round.(Int, SmoothingSplines.predict(ss2, float.(dd[rows, :capac])))
        dd[!, :rotordiam] = round.(Int, dd[:, :rotordiam])
    end
end

function minimumpositive(arr::AbstractArray)
    min = typemax(eltype(arr))
    index = 0
    for (i, elem) in enumerate(arr)
        if elem > 0 && elem < min
            min = elem
            index = i
        end
    end
    return index, min
end

function closestturbine(region_abbrev)
    df = DataFrame(CSV.File(in_datafolder("turbines_$region_abbrev.csv")))
    len = size(df, 1)
    m_per_degree = pi*6371*2/360*1000
    closest_dist = zeros(len)
    rotordiams = zeros(Int, len)
    for i = 1:len
        row = df[i, :]
        _, min = minimumpositive((df[:lat] .- row[:lat]).^2 + (df[:lon] .- row[:lon]).^2)
        closest_dist[i] = m_per_degree * sqrt(min)
        rotordiams[i] = row[:rotordiam]
    end
    # display(histogram(closest_dist[closest_dist .< 3000], bins=100))
    rotordist = closest_dist./rotordiams
    display(histogram(rotordist[rotordist .< 25], bins=1000))
    df, closest_dist, rotordiams
end

end # module

ext: jl

max_stars_repo_name: niclasmattsson/WindSites

max_issues_repo_name: niclasmattsson/WindSites

max_forks_repo_name: niclasmattsson/WindSites


text:
"""
A trait for the ways the bin search step can be implemented.

Histograms that operate on real-valued data must implement the following functions, in addition to any trait-specific functions:
- `get_weights(hist)::AbstractArray{Int,N}` Returns the weights (i.e. counts) array for an N-dimensional histogram.

Histograms that operate on text data must implement the following functions, in addition to any trait-specific functions:
- `get_table(hist)::AbstractDict{String,Int}` Returns the table for the histogram.
"""
abstract type BinSearchAlgorithm end

"""
Basic arithmetic to determine the bin to update, compatible only with the FixedWidth bin type.

Requires these functions to be defined:
- `binmin(hist, axis)::Int` Returns the value of the lowest bin edge for the axis. The implementation should use `@propagate_inbounds` for good performance.
- `norm(hist, axis)::Float32` Returns the inverse of the size of the bin range for the axis (`1 / (last_bin - first_bin)`). The implementation should use `@propagate_inbounds` for good performance.
- `nbins(hist, axis)::Int` Returns the number of bins for the axis. The implementation should use `@propagate_inbounds` for good performance.
"""
struct Arithmetic <: BinSearchAlgorithm end

"""
Uses binary search to find the bin to update. Meant to be used with the VariableWidth bin type.

Requires these functions to be defined:
- `bin_edges(hist, axis)::Vector{Int}` Returns a sorted vector of the bin edges for the axis. The implementation should use `@propagate_inbounds` for good performance.
"""
struct BinarySearch <: BinSearchAlgorithm end

"""
Uses a hash function to find the bin to update. Compatible only with the UnboundedWidth bin type.
"""
struct HashFunction <: BinSearchAlgorithm end

BinSearchAlgorithm(t) = error("BinSearchAlgorithm not defined for $(typeof(t))")

"""
    bin_search(h, axis, data)

Returns the index of the bin to increment.
"""
bin_search(h, axis, data) = bin_search(BinSearchAlgorithm(h), h, axis, data)

function bin_search(::Arithmetic, h, axis, data)
    # Using `min(nbins, max(1, ceil(<computed index>)))` here is consistent with StatsBase, but it's 2 μs slower than
    # truncating. Therefore, we add 1 and then truncate to get the same result.
    return @inbounds clamp(trunc(Int, (data - binmin(h, axis)) * norm(h, axis) * nbins(h, axis) + 1), 1, nbins(h, axis))
end

function bin_search(::BinarySearch, h, axis, data)
    @inbounds idxs = searchsorted(bin_edges(h, axis), data)
    return @inbounds clamp(last(idxs), 1, nbins(h, axis))
end


text:
### A Pluto.jl notebook ###
# v0.12.21

using Markdown
using InteractiveUtils

# ╔═╡ a4fd1d7c-91f4-11eb-04b9-ef70a90ddf23
begin
	using PlutoUI
	using NativeSVG
end

# ╔═╡ 50e6776a-91f4-11eb-11c9-f7dd1bfb308b
md"""# Subtyping

In the previous chapter we introduced the multiple dispatch mechanism and polymorphic methods. Not specifying the types of the arguments results in a method that can be called with arguments of any type. Specifying a subset of allowed types in the signature of a method is a logical next step.

In this chapter I demonstrate subtyping using types that represent playing cards, decks of cards, and poker hands.

If you don’t play poker, you can [read about it](https://en.wikipedia.org/wiki/Poker), but you don’t have to; I’ll tell you what you need to know for the exercises.
"""

# ╔═╡ b225017a-91f4-11eb-24c2-758f1579bae0
md"""## Cards

There are 52 cards in a deck, each of which belongs to one of 4 suits and one of 13 ranks. The suits are Spades (♠), Hearts (♥), Diamonds (♦), and Clubs (♣). The ranks are Ace (A), 2, 3, 4, 5, 6, 7, 8, 9, 10, Jack (J), Queen (Q), and King (K). Depending on the game that you are playing, an Ace may be higher than a King or lower than a 2.

If we want to define a new object to represent a playing card, it is obvious what the attributes should be: rank and suit. It is not as obvious what type the attributes should be. One possibility is to use strings containing words like "Spade" for suits and "Queen" for ranks. One problem with this implementation is that it would not be easy to compare cards to see which had a higher rank or suit.

An alternative is to use integers to *encode* the ranks and suits. In this context, “encode” means that we are going to define a mapping between numbers and suits, or between numbers and ranks. This kind of encoding is not meant to be a secret (that would be “encryption”).

For example, we might map the suits to integer codes as follows:

* ♠ ⟼ 4
* ♥ ⟼ 3
* ♦ ⟼ 2
* ♣ ⟼ 1

This makes it easy to compare cards; because higher suits map to higher numbers, we can compare suits by comparing their codes.

I am using the symbol to make it clear that these mappings are not part of the Julia program. They are part of the program design, but they don’t appear explicitly in the code.

The struct definition for `Card` looks like this:

```julia
struct Card
	suit :: Int64
	rank :: Int64
	function Card(suit::Int64, rank::Int64)
		@assert(1 ≤ suit ≤ 4, "suit is not between 1 and 4")
		@assert(1 ≤ rank ≤ 13, "rank is not between 1 and 13")
		new(suit, rank)
	end
end
```

To create a `Card`, you call `Card` with the suit and rank of the card you want:

```julia
julia> queen_of_diamonds = Card(2, 12)
Card(2, 12)
```
"""

# ╔═╡ 3a19ed60-91f5-11eb-0614-a550fdd98d4b
md"""## Global Variables

In order to print `Card` objects in a way that people can easily read, we need a mapping from the integer codes to the corresponding ranks and suits. A natural way to do that is with arrays of strings:

```julia
const suit_names = ["♣", "♦", "♥", "♠"]
const rank_names = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J",
                    "Q", "K"]
```

The variables `suit_names` and `rank_names` are global variables. The `const` declaration means that the variable can only be assigned once. This solves the performance problem of global variables.

Now we can implement an appropriate show method:

```julia
function Base.show(io::IO, card::Card)
	print(io, rank_names[card.rank], suit_names[card.suit])
end
```

The expression `rank_names[card.rank]` means “use the field rank from the object card as an index into the array `rank_names`, and select the appropriate string.”

With the methods we have so far, we can create and print `Card`:

```julia
julia> Card(3, 11)
J♥
```
"""

# ╔═╡ a9f58e82-91f5-11eb-3863-53f39944a784
md"""## Comparing Cards

For built-in types, there are relational operators (`<`, `>`, `==`, etc.) that compare values and determine when one is greater than, less than, or equal to another. For programmer-defined types, we can override the behavior of the built-in operators by providing a method named `<`.

The correct ordering for cards is not obvious. For example, which is better, the 3 of Clubs or the 2 of Diamonds? One has a higher rank, but the other has a higher suit. In order to compare cards, you have to decide whether rank or suit is more important.

The answer might depend on what game you are playing, but to keep things simple, we’ll make the arbitrary choice that suit is more important, so all of the Spades outrank all of the Diamonds and so on.

With that decided, we can write `<`:

```julia
Base.isless(c1::Card, c2::Card) = (c1.suit, c1.rank) < (c2.suit, c2.rank)
```
"""

# ╔═╡ eb053c18-91f5-11eb-0fae-c762d9d17018
md"""#### Exercise 18-1

Write a `<` method for `MyTime` objects. You can use tuple comparison, but you also might consider comparing integers.
"""

# ╔═╡ 0261965c-91f6-11eb-384c-d77046f0e6ab
md"""## Unit Testing

*Unit testing* allows you to verify the correctness of your code by comparing the results of your code to what you expect. This can be useful to be sure that your code is still correct after modifications, and it is also a way to predefine the correct behavior of your code during development.

Simple unit testing can be performed with the `@test` macro:

```julia
julia> using Test

julia> @test Card(1, 4) < Card(2, 4)
Test Passed
julia> @test Card(1, 3) < Card(1, 4)
Test Passed
```

`@test` returns a "Test Passed" if the expression following it is `true`, a "Test Failed" if it is `false`, and an "Error Result" if it could not be evaluated.
"""

# ╔═╡ 3c0aeeaa-91f6-11eb-2fdf-77ec7c2b14fa
md"""## Decks

Now that we have `Cards`, the next step is to define Decks. Since a deck is made up of cards, it is natural for each `Deck` to contain an array of `Card`s as an attribute.
The following is a struct definition for `Deck`. The constructor creates the field `cards` and generates the standard set of 52 `Card`s:

```julia
struct Deck
	cards :: Array{Card, 1}
end

function Deck()
	deck = Deck(Card[])
	for suit in 1:4
		for rank in 1:13
			push!(deck.cards, Card(suit, rank))
		end
	end
	deck
end
```

The easiest way to populate the deck is with a nested loop. The outer loop enumerates the suits from 1 to 4. The inner loop enumerates the ranks from 1 to 13. Each iteration creates a new `Card` with the current suit and rank, and pushes it to `deck.cards`.

Here is a `show` method for `Deck`:

```julia
function Base.show(io::IO, deck::Deck)
	for card in deck.cards
		print(io, card, " ")
	end
	println()
end
```

Here’s what the result looks like:

```julia
julia> Deck()
A♣ 2♣ 3♣ 4♣ 5♣ 6♣ 7♣ 8♣ 9♣ 10♣ J♣ Q♣ K♣ A♦ 2♦ 3♦ 4♦ 5♦ 6♦ 7♦ 8♦ 9♦ 10♦ J♦ Q♦ K♦
A♥ 2♥ 3♥ 4♥ 5♥ 6♥ 7♥ 8♥ 9♥ 10♥ J♥ Q♥ K♥ A♠ 2♠ 3♠ 4♠ 5♠ 6♠ 7♠ 8♠ 9♠ 10♠ J♠ Q♠ K♠
```
"""

# ╔═╡ 7f423f9a-9212-11eb-21f7-23211e7383ca
md"""## Add, Remove, Shuffle, and Sort

To deal cards, we would like a function that removes a `Card` from the `Deck` and returns it. The function `pop!` provides a convenient way to do that:

```julia
Base.pop!(deck::Deck) = pop!(deck.cards)
```

Since `pop!` removes the last `Card` in the array, we are dealing from the bottom of the `Deck`.

To add a `Card`, we can use the function `push!`:

```julia
function Base.push!(deck::Deck, card::Card)
	push!(deck.cards, card)
	deck
end
```

A method like this that uses another method without doing much work is sometimes called a *veneer*. The metaphor comes from woodworking, where a veneer is a thin layer of good-quality wood glued to the surface of a cheaper piece of wood to improve the appearance.

In this case `push!` is a “thin” method that expresses an array operation in terms appropriate for decks. It improves the appearance, or interface, of the implementation.

As another example, we can write a method named `shuffle!` using the function `Random.shuffle!`:

```julia
using Random

function Random.shuffle!(deck::Deck)
	shuffle!(deck.cards)
	deck
end
```
"""

# ╔═╡ 7c970f54-9213-11eb-3d29-c38b10f3cf1c
md"""#### Exercise 18-2

Write a function named `sort!` that uses the function `sort!` to sort the `cards` in a `Deck`. `sort!` uses the `<` method we defined to determine the order.
"""

# ╔═╡ a40c70e4-9213-11eb-3054-d381a4db08ec
md"""## Abstract Types and Subtyping

We want a type to represent a “hand”; that is, the cards held by one player. A hand is similar to a deck: both are made up of a collection of cards, and both require operations like adding and removing cards.

A hand is also different from a deck; there are operations we want for hands that don’t make sense for a deck. For example, in poker we might compare two hands to see which one wins. In bridge, we might compute a score for a hand in order to make a bid.

So, we need a way to group related *concrete types*. In Julia this is done by defining an *abstract type* that serves as a parent for both `Deck` and `Hand`. This is called *subtyping*.

Let’s call this abstract type `CardSet`:

```julia
abstract type CardSet end
```

A new abstract type is created with the `abstract type` keyword. An optional “par‐ ent” type can be specified by putting `<:` after the name, followed by the name of an already existing abstract type.

When no *supertype* is given, the default supertype is `Any`—a predefined abstract type that all objects are instances of and all types are subtypes of.

We can now express that `Deck` is a descendant of `CardSet`:

```julia
struct Deck <: CardSet
	cards :: Array{Card, 1}
end

function Deck()
	deck = Deck(Card[])
	for suit in 1:4
		for rank in 1:13
			push!(deck.cards, Card(suit, rank))
		end
	end
	deck
end
```

The operator `isa` checks whether an object is of a given type:

```julia
julia> deck = Deck();

julia> deck isa CardSet
true
```

A `Hand` is also a kind of `CardSet`:

```julia
struct Hand <: CardSet cards :: Array{Card, 1}
	label :: String
end

Hand(label::String="") = Hand(Card[], label)
```

Instead of populating the hand with 52 new Cards, the constructor for `Hand` initializes cards with an empty array. An optional argument can be passed to the constructor giving a label to the `Hand`:

```julia
julia> hand = Hand("new hand")
Hand(Card[], "new hand")
```
"""

# ╔═╡ 902972b6-9216-11eb-3bc2-0b6c29094443
md"""## Abstract Types and Functions

We can now express the common operations between `Deck` and `Hand` as functions having a `CardSet` as an argument:

```julia
function Base.show(io::IO, cs::CardSet)
	for card in cs.cards
		print(io, card, " ")
	end
end

Base.pop!(cs::CardSet) = pop!(cs.cards)

function Base.push!(cs::CardSet, card::Card)
	push!(cs.cards, card)
	nothing
end
```

We can use `pop!` and `push!` to deal a card:

```julia
julia> deck = Deck()
A♣ 2♣ 3♣ 4♣ 5♣ 6♣ 7♣ 8♣ 9♣ 10♣ J♣ Q♣ K♣ A♦ 2♦ 3♦ 4♦ 5♦ 6♦ 7♦ 8♦ 9♦ 10♦ J♦ Q♦ K♦
A♥ 2♥ 3♥ 4♥ 5♥ 6♥ 7♥ 8♥ 9♥ 10♥ J♥ Q♥ K♥ A♠ 2♠ 3♠ 4♠ 5♠ 6♠ 7♠ 8♠ 9♠ 10♠ J♠ Q♠ K♠
julia> shuffle!(deck)
4♠ Q♦ A♣ 9♦ Q♠ 6♣ 10♣ Q♥ A♦ 8♥ 9♥ Q♣ 4♦ 5♥ 9♠ 10♥ A♠ 7♣ 2♠ 5♠ 2♦ K♣ J♠ 10♠ 7♦ 2♥
3♦ 7♠ 8♦ A♥ K♥ 7♥ J♥ 6♦ J♦ 6♥ K♦ 8♠ 5♦ 4♥ 8♣ J♣ 9♣ 3♠ 2♣ K♠ 3♥ 5♣ 6♠ 10♦ 4♣ 3♣
julia> card = pop!(deck)
3♣
julia> push!(hand, card)
```

A natural next step is to encapsulate this code in a function called `move!`:

```julia
function move!(cs1::CardSet, cs2::CardSet, n::Int)
	@assert 1 ≤ n ≤ length(cs1.cards)
	for _ in 1:n
		card = pop!(cs1)
		push!(cs2, card)
	end
	nothing
end
```

`move!` takes three arguments, two `CardSet` objects and the number of cards to deal. It modifies both `CardSet` objects, and returns `nothing`.

In some games, cards are moved from one hand to another, or from a hand back to the deck. You can use `move!` for any of these operations: `cs1` and `cs2` can be either a `Deck` or a `Hand`.
"""

# ╔═╡ eb991f36-925a-11eb-2507-9548f77fe360
md"""## Type Diagrams

So far we have seen stack diagrams, which show the state of a program, and object diagrams, which show the attributes of an object and their values. These diagrams represent a snapshot of the execution of a program, so they change as the program runs.

They are also highly detailed—for some purposes, too detailed. A *type diagram* is a more abstract representation of the structure of a program. Instead of showing individual objects, it shows types and the relationships between them.

There are several kinds of relationships between types:

* Objects of a concrete type might contain references to objects of another type. For example, each `Rectangle` contains a reference to a `Point`, and each `Deck` contains references to an array of `Card`s. This kind of relationship is called *has-a*, as in, “a `Rectangle` has a `Point`.”

* A concrete type can have an abstract type as a supertype. This relationship is called *is-a*, as in, “a `Hand` is a kind of a `CardSet`.”

* One type might depend on another, in the sense that objects of one type take objects of the second type as parameters, or use objects of the second type as part of a computation. This kind of relationship is called a *dependency*.
"""

# ╔═╡ 3a94d66e-925b-11eb-385b-79e764664fad
Drawing(width=720, height=150) do
	defs() do
        marker(id="arrow", markerWidth="10", markerHeight="10", refX="0", refY="3", orient="auto", markerUnits="strokeWidth") do
      		path(d="M0,0 L0,6 L9,3 z", fill="black")
		end
		marker(id="hollowarrow", markerWidth="10", markerHeight="10", refX="0", refY="3", orient="auto", markerUnits="strokeWidth") do
      		path(d="M0,0 L0,6 L9,3 z", fill="none", stroke="black")
		end
	end
	rect(x=310, y=10, width=100, height=30, fill="rgb(242, 242, 242)", stroke="black")
	text(x=335, y=30, font_family="JuliaMono, monospace", font_size="0.85rem", font_weight=600) do
		str("CardSet")
	end
	rect(x=150, y=60, width=100, height=30, fill="rgb(242, 242, 242)", stroke="black")
	text(x=185, y=80, font_family="JuliaMono, monospace", font_size="0.85rem", font_weight=600) do
		str("Deck")
	end
	line(x1=250, y1=60, x2=301, y2=43, stroke="black", marker_end="url(#hollowarrow)")
	rect(x=470, y=60, width=100, height=30, fill="rgb(242, 242, 242)", stroke="black")
	text(x=505, y=80, font_family="JuliaMono, monospace", font_size="0.85rem", font_weight=600) do
		str("Hand")
	end
	line(x1=470, y1=60, x2=419, y2=43, stroke="black", marker_end="url(#hollowarrow)")
	rect(x=310, y=110, width=100, height=30, fill="rgb(242, 242, 242)", stroke="black")
	text(x=345, y=130, font_family="JuliaMono, monospace", font_size="0.85rem", font_weight=600) do
		str("Card")
	end
	line(x1=250, y1=90, x2=301, y2=107, stroke="black", marker_end="url(#arrow)")
	line(x1=470, y1=90, x2=419, y2=107, stroke="black", marker_end="url(#arrow)")
	text(x=305, y=105, font_family="JuliaMono, monospace", font_size="0.85rem", font_weight=600) do
		str("*")
	end
	text(x=405, y=105, font_family="JuliaMono, monospace", font_size="0.85rem", font_weight=600) do
		str("*")
	end
end

# ╔═╡ ebce8202-925d-11eb-3f97-c988f85bf57b
md"*Figure 18-1. Type diagram.*"

# ╔═╡ f5385824-925d-11eb-2879-1f88fbd6b330
md"""The arrow with a hollow triangle head represents an is-a relationship; in this case it indicates that `Hand` has `CardSet` as a supertype.

The standard arrowhead represents a has-a relationship; in this case a `Deck` has references to `Card` objects.

The star (`*`) near the arrowhead is a multiplicity; it indicates how many `Card`s a `Deck` has. A multiplicity can be a simple number, like `52`; a range, like `5:7`; or a star, which indicates that a `Deck` can have any number of `Card`s.

There are no dependencies in this diagram. They would normally be shown with a dashed arrow. Or if there are a lot of dependencies, they are sometimes omitted.

A more detailed diagram might show that a `Deck` actually contains an array of `Card`s, but built-in types like array and dictionaries are usually not included in type diagrams.
"""

# ╔═╡ 39794bee-925e-11eb-1617-195f9c786a02
md"""## Debugging

Subtyping can make debugging difficult because when you call a function with an object as an argument, it might be hard to figure out which method will be invoked.

Suppose you are writing a function that works with `Hand` objects. You would like it to work with all kinds of `Hand`s, like `PokerHand`s, `BridgeHand`s, etc. If you invoke a method like `sort!`, you might get the version defined for an abstract type `Hand`, but if a method `sort!` with any of the subtypes as an argument exists, you’ll get that version instead. This behavior is usually a good thing, but it can be confusing:

```julia
Base.sort!(hand::Hand) = sort!(hand.cards)
```

Any time you are unsure about the flow of execution through your program, the simplest solution is to add print statements at the beginning of the relevant methods. If `shuffle!` prints a message that says something like `"Running shuffle! Deck"`, then as the program runs it traces the flow of execution.

As a better alternative, you can use the `@which` macro:

```julia
julia> @which sort!(hand)
sort!(hand::Hand) in Main at REPL[5]:1
```

This tells us that the `sort!` method for `hand` is the one having as an argument an object of type `Hand`.

Here’s a design suggestion: when you override a method, the interface of the new method should be the same as the old one. It should take the same parameters, return the same type, and obey the same preconditions and postconditions. If you follow this rule, you will find that any function designed to work with an instance of a supertype, like a `CardSet`, will also work with instances of its subtypes (`Deck` and `Hand`).

If you violate this rule, which is called the “Liskov substitution principle,” your code will collapse like (sorry) a house of cards.

The function `supertype` can be used to find the direct supertype of a type:

```julia
julia> supertype(Deck)
CardSet
```
"""

# ╔═╡ b790e4a4-925e-11eb-3497-3f0389e2767f
md"""## Data Encapsulation

The previous chapters demonstrated a development plan we might call “type-oriented design.” We identified objects we needed—like `Point`, `Rectangle`, and `MyTime`—and defined structs to represent them. In each case there was an obvious correspondence between the object and some entity in the real world (or at least a mathematical world).

But sometimes it is less obvious what objects you need and how they should interact. In that case you need a different development plan. In the same way that we discovered function interfaces by encapsulation and generalization, we can discover type interfaces by *data encapsulation*.

Markov analysis, described in “Markov Analysis”, provides a good example. If you look at my solution to “Exercise 13-8”, you’ll see that it uses two global variables—`suffixes` and `prefix`—that are read and written from several functions:

```julia
const suffixes = Dict()
const prefix = []
```

Because these variables are global, we can only run one analysis at a time. If we read two texts, their prefixes and suffixes would be added to the same data structures (which makes for some interesting generated text).

To run multiple analyses, and keep them separate, we can encapsulate the state of each analysis in an object. Here’s what that looks like:

```julia
struct Markov
	order :: Int64
	suffixes :: Dict{Tuple{String,Vararg{String}}, Array{String, 1}}
	prefix :: Array{String, 1}
	function Markov(order::Int64=2)
		new(order, Dict{Tuple{String,Vararg{String}}, Array{String, 1}}(), Array{String, 1}())
	end
end
```

Next, we transform the functions into methods. For example, here’s `processword`:

```julia
function processword(markov::Markov, word::String)
	if length(markov.prefix) < markov.order
		push!(markov.prefix, word)
		return
	end
	get!(markov.suffixes, (markov.prefix...,), Array{String, 1}())
	push!(markov.suffixes[(markov.prefix...,)], word)
	popfirst!(markov.prefix)
	push!(markov.prefix, word)
end
```

Transforming a program like this—changing the design without changing the behavior—is another example of refactoring (see “Refactoring”).

This example suggests a development plan for designing types:

* Start by writing functions that read and write global variables (when necessary).
* Once you get the program working, look for associations between global variables and the functions that use them.
* Encapsulate related variables as fields of a struct.
* Transform the associated functions into methods with as argument objects of the new type.
"""

# ╔═╡ ccb90fe0-925f-11eb-3786-15147a772503
md"""#### Exercise 18-3

Download my Markov code from GitHub, and follow the steps described here to encapsulate the global variables as attributes of a new struct called `Markov`.
"""

# ╔═╡ dd89d250-925f-11eb-18ba-35b060cddaa3
md"""## Glossary

*encode*:
To represent one set of values using another set of values by constructing a mapping between them.

*unit testing*:
A standardized way to test the correctness of code.

*veneer*:
A method or function that provides a different interface to another function without doing much computation.

*concrete type*:
A type that can be constructed.

*abstract type*:
A type that can act as a parent for another type.

*subtyping*:
The ability to define a hierarchy of related types.

*supertype*:
An abstract type that is the parent of another type.

*subtype*:
A type that has as its parent an abstract type.

*type diagram*:
A diagram that shows the types in a program and the relationships between them.

*has-a relationship*:
A relationship between two types where instances of one type contain references to instances of the other.

*is-a relationship*:
A relationship between a subtype and its supertype.

*dependency*:
A relationship between two types where instances of one type use instances of the other type, but do not store them as fields.

*multiplicity*:
A notation in a type diagram that shows, for a has-a relationship, how many references there are to instances of another class.

*data encapsulation*:
A program development plan that involves a prototype using global variables and a final version that makes the global variables into instance fields.
"""

# ╔═╡ 266416f4-9260-11eb-226b-4b8e377b7e18
md"""## Exercises

#### Exercise 18-4

For the following program, draw a type diagram that shows these types and the relationships among them.

```julia
abstract type PingPongParent end

struct Ping <: PingPongParent
	pong :: PingPongParent
end

struct Pong <: PingPongParent
	pings :: Array{Ping, 1}
	function Pong(pings=Array{Ping, 1}())
		new(pings)
	end
end

function addping(pong::Pong, ping::Ping)
	push!(pong.pings, ping)
	nothing
end

pong = Pong()
ping = Ping(pong)
addping(pong, ping)
```
"""

# ╔═╡ 5f0e371c-9260-11eb-2edf-9b5dd31992d8
md"""#### Exercise 18-5

Write a method called `deal!` that takes three parameters: a `Deck`, the number of `Hand`s, and the number of `Card`s per `Hand`. It should create the appropriate number of `Hand` objects, deal the appropriate number of `Card`s per `Hand`, and return an array of `Hand`s.
"""

# ╔═╡ 950c43d4-9260-11eb-0c8e-fdf5c93d8eae
md"""## Exercise 18-6

The following are the possible hands in poker, in increasing order of value and decreasing order of probability:

*Pair*:
Two cards with the same rank

*Two pair*:
Two pairs of cards with the same rank

*Three of a kind*:
Three cards with the same rank

*Straight*:
Five cards with ranks in sequence (Aces can be high or low, so Ace-2-3-4-5 is a straight and so is 10-Jack-Queen-King-Ace, but Queen-King-Ace-2-3 is not)

*Flush*:
Five cards with the same suit

*Full house*:
Three cards with one rank, two cards with another

*Four of a kind*:
Four cards with the same rank

*Straight flush*:
Five cards in sequence (as defined previously) and with the same suit

The goal of this exercise is to estimate the probability of drawing these various hands.

1. Add methods named `haspair`, `hastwopair`, etc., that return `true` or `false` according to whether or not the hand meets the relevant criteria. Your code should work correctly for hands that contain any number of cards (although five and seven are the most common sizes).
2. Write a method named `classify` that figures out the highest-value classification for a hand and sets the label field accordingly. For example, a seven-card hand might contain a flush and a pair; it should be labeled flush.
3. When you are convinced that your classification methods are working, the next step is to estimate the probabilities of the various hands. Write a function that shuffles a deck of cards, divides it into hands, classifies the hands, and counts the number of times various classifications appear.
4. Print a table of the classifications and their probabilities. Run your program with larger and larger numbers of hands until the output values converge to a reason‐ able degree of accuracy. Compare your results to [these hand ranks](http://www.codethrowdown.com/5CardSingleDeckHands.txt).
"""

# ╔═╡ Cell order:
# ╟─a4fd1d7c-91f4-11eb-04b9-ef70a90ddf23
# ╟─50e6776a-91f4-11eb-11c9-f7dd1bfb308b
# ╟─b225017a-91f4-11eb-24c2-758f1579bae0
# ╟─3a19ed60-91f5-11eb-0614-a550fdd98d4b
# ╟─a9f58e82-91f5-11eb-3863-53f39944a784
# ╟─eb053c18-91f5-11eb-0fae-c762d9d17018
# ╟─0261965c-91f6-11eb-384c-d77046f0e6ab
# ╟─3c0aeeaa-91f6-11eb-2fdf-77ec7c2b14fa
# ╟─7f423f9a-9212-11eb-21f7-23211e7383ca
# ╟─7c970f54-9213-11eb-3d29-c38b10f3cf1c
# ╟─a40c70e4-9213-11eb-3054-d381a4db08ec
# ╟─902972b6-9216-11eb-3bc2-0b6c29094443
# ╟─eb991f36-925a-11eb-2507-9548f77fe360
# ╟─3a94d66e-925b-11eb-385b-79e764664fad
# ╟─ebce8202-925d-11eb-3f97-c988f85bf57b
# ╟─f5385824-925d-11eb-2879-1f88fbd6b330
# ╟─39794bee-925e-11eb-1617-195f9c786a02
# ╟─b790e4a4-925e-11eb-3497-3f0389e2767f
# ╟─ccb90fe0-925f-11eb-3786-15147a772503
# ╟─dd89d250-925f-11eb-18ba-35b060cddaa3
# ╟─266416f4-9260-11eb-226b-4b8e377b7e18
# ╟─5f0e371c-9260-11eb-2edf-9b5dd31992d8
# ╟─950c43d4-9260-11eb-0c8e-fdf5c93d8eae