test_mod = @newmod(misc)

# Blocks
@test JuliaLowering.include_string(test_mod, """
begin
end
""") == nothing

# Placeholders
@test JuliaLowering.include_string(test_mod, """_ = 10""") == 10

# GC.@preserve
@test JuliaLowering.include_string(test_mod, """
let x = [1,2]
    GC.@preserve x begin
        x
    end
end
""") == [1,2]

@test JuliaLowering.include_string(test_mod, raw"""
let
    x = 10
    @eval $x + 2
end
""") == 12

@test JuliaLowering.include_string(test_mod, raw"""
module EvalTest
    _some_var = 1
end
let
    x = 10
    @eval EvalTest $x + _some_var
end
""") == 11
@test JuliaLowering.include_string(test_mod, raw"""
module EvalTest2
    _some_var = 2
end
let
    x = 10
    @eval EvalTest2 $x + _some_var
end
"""; expr_compat_mode=true) == 12

@test JuliaLowering.include_string(test_mod, """
let x=11
    20x
end
""") == 220

@testset "syntactic --> <: >:" begin
    @test jl_eval(test_mod, Expr(:<:, Int, Number))
    @test jl_eval(test_mod, Expr(:<:, Expr(:..., Expr(:tuple, Int, Number))))
    @test jl_eval(test_mod, Expr(:>:, Number, Int))
    @test jl_eval(test_mod, Expr(:>:, Expr(:..., Expr(:tuple, Number, Int))))

    JuliaLowering.include_string(test_mod, """
    function var"-->"(args...; kws...)
        (args, values(kws))
    end
    """)
    @test jl_eval(test_mod, Expr(:-->, 1, 2)) == ((1,2),(;))
    @test jl_eval(test_mod, Expr(:-->, 1, Expr(:kw, :foo, 2))) == ((1,),(;foo=2))
    @test jl_eval(test_mod, Expr(:-->, Expr(:..., (1,)))) == ((1,),(;))
end

@testset "empty symbol" begin
    @test JuliaLowering.include_string(test_mod, """
    let var\"\"=1; 1; end
    """) == 1

    # Note function name fails in flisp
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block),
                     Expr(:block,
                          Expr(:function, Expr(:call, Symbol(""), :x),
                               Expr(:block, :x)),
                          Expr(:call, Symbol(""), 1)))) == 1

    # function arg
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block),
                     Expr(:block,
                          Expr(:function, Expr(:call, :func, Symbol("")),
                               Expr(:block, Symbol(""))),
                          Expr(:call, :func, 2)))) == 2
    # kwarg
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block),
                     Expr(:block,
                          Expr(:function, Expr(:call, :func, Expr(:parameters, Symbol(""))),
                               Expr(:block, Symbol(""))),
                          Expr(:call, :func, Expr(:kw, Symbol(""), 2))))) == 2

    # empty label
    @test jl_eval(test_mod,
                Expr(:symbolicblock, Symbol(""),
                     Expr(:break, Symbol(""), 1))) == 1

    # read empty local
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block),
                     Expr(:block, Expr(:local, Symbol("")),
                          Expr(:(=), Symbol(""), 17),
                          Symbol("")))) == 17

    # read empty global
    @test jl_eval(test_mod,
                Expr(:block, Expr(:global, Symbol("")),
                     Expr(:(=), Symbol(""), 7),
                     Symbol(""))) == 7

    # typed read of empty
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block, Expr(:(=), Symbol(""), 5)),
                     Expr(:block, Expr(:(::), Symbol(""), :Int)))) == 5

    # empty in curly (read position)
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block, Expr(:(=), Symbol(""), Int)),
                     Expr(:block, Expr(:curly, :Vector, Symbol(""))))) == Vector{Int}

    # empty in tuple (read position)
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block, Expr(:(=), Symbol(""), 99)),
                     Expr(:block, Expr(:tuple, Symbol(""))))) == (99,)

    # isdefined on empty
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block, Expr(:(=), Symbol(""), 1)),
                     Expr(:block, Expr(:isdefined, Symbol("")))))

    # for-loop empty iter var referenced in body
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block, Expr(:(=), :s, 0)),
                     Expr(:block,
                          Expr(:for, Expr(:(=), Symbol(""), Expr(:tuple, 10, 20, 30)),
                               Expr(:block, Expr(:(=), :s, Expr(:call, :+, :s, Symbol(""))))),
                          :s))) == 60

    # tuple destructure with empty lhs
    @test jl_eval(test_mod,
                Expr(:let, Expr(:block),
                     Expr(:block, Expr(:local, Symbol("")), Expr(:local, :a),
                          Expr(:(=), Expr(:tuple, Symbol(""), :a), Expr(:tuple, 1, 2)),
                          Expr(:tuple, Symbol(""), :a)))) == (1, 2)

    # quote of empty
    @test jl_eval(test_mod, Expr(:quote, Symbol(""));
                  expr_compat_mode=true) === Symbol("")
end

@eval test_mod libccalltest_var = "libccalltest"

@testset "cglobal" begin
    cg = JuliaLowering.include_string(test_mod, """
        cglobal(:jl_, Any)
    """)
    @test cg isa Ptr{Any}
    @test cg !== C_NULL

    cg = JuliaLowering.include_string(test_mod, """
        cglobal((:global_var, libccalltest_var), Cint)
    """)
    @test cg isa Ptr{Cint}
    @test cg !== C_NULL
    @test unsafe_load(cg) == 1

    # the pointer-vs-name choice is syntactic, not value-based: a runtime
    # variable holding a tuple takes the pointer form, which errors
    @eval test_mod global cglobal_tuple = (:global_var, libccalltest_var)
    @test_throws TypeError JuliaLowering.include_string(test_mod, """
        cglobal(cglobal_tuple, Cint)
    """)
    @test_throws TypeError JuliaLowering.include_string(test_mod, """
        let local_tuple = (:global_var, libccalltest_var)
            cglobal(local_tuple, Cint)
        end
    """)

    # unlike the argtypes / rettype of a ccall, cglobal(name, T) should allow
    # rettype T to be any runtime expression
    cg = JuliaLowering.include_string(test_mod, """
        function cglobal_runtime_type(T)
            cglobal((:global_var, libccalltest_var), T)
        end
        cglobal_runtime_type(Cint)
    """)
    @test cg isa Ptr{Cint}
    @test unsafe_load(cg) == 1

    # invalid foreignsymbol (tuple) forms should error for cglobal
    @test_throws ErrorException JuliaLowering.include_string(test_mod, "cglobal((:a, :b, :c))")
    @test_throws ErrorException JuliaLowering.include_string(test_mod, "cglobal(())")
    @test_throws TypeError JuliaLowering.include_string(test_mod, "cglobal((1,))")

    # cglobal(name) with a non-static name errors, just like ccall
    @test_throws TypeError JuliaLowering.include_string(test_mod, """
        function cglobal_non_static1()
            sym = (:global_var, libccalltest_var)
            cglobal(sym)
        end
        cglobal_non_static1()
    """)
    @eval test_mod global the_sym = (:global_var, libccalltest_var)
    @test_throws TypeError JuliaLowering.include_string(test_mod, """
        cglobal_non_static2() = cglobal(the_sym)
        cglobal_non_static2()
    """)
end

# ccall
@test JuliaLowering.include_string(test_mod, """
ccall(:strlen, Csize_t, (Cstring,), "asdfg")
""") == 5
@test JuliaLowering.include_string(test_mod, """
function cvarargs_0()
    strp = Ref{Ptr{Cchar}}(0)
    fmt = "hi"
    len = ccall(:asprintf, Cint, (Ptr{Ptr{Cchar}}, Cstring, Cfloat...), strp, fmt)
    str = unsafe_string(strp[], len)
    Libc.free(strp[])
    return str
end
""") isa Function
@test test_mod.cvarargs_0() == "hi"
@test JuliaLowering.include_string(test_mod, """
function cvarargs_2(arg1::Float64, arg2::Float64)
    strp = Ref{Ptr{Cchar}}(0)
    fmt = "%3.1f %3.1f"
    len = ccall(:asprintf, Cint, (Ptr{Ptr{Cchar}}, Cstring, Cfloat...), strp, fmt, arg1, arg2)
    str = unsafe_string(strp[], len)
    Libc.free(strp[])
    return str
end
""") isa Function
@test test_mod.cvarargs_2(1.1, 2.2) == "1.1 2.2"
# (function, library) syntax
@test JuliaLowering.include_string(test_mod, """
    ccall((:ctest, :libccalltest), Complex{Int}, (Complex{Int},), 10 + 20im)
""") === 11 + 18im
# (function, library): library is a global
@test JuliaLowering.include_string(test_mod, """
    ccall((:ctest, libccalltest_var), Complex{Int}, (Complex{Int},), 10 + 20im)
""") === 11 + 18im

@testset "(robot-generated) ccall (sym, lib) tuple: globals and hygiene" for expr_compat_mode in [true, false]
    # library is a module-qualified global
    JuliaLowering.include_string(test_mod, """
    module CCallLibMod
        const the_lib = "libccalltest"
    end
    """; expr_compat_mode)
    Core.@latestworld
    @test JuliaLowering.include_string(test_mod, """
        ccall((:ctest, CCallLibMod.the_lib), Complex{Int}, (Complex{Int},), 10 + 20im)
    """) === 11 + 18im

    # macro in a nested module produces ccall with lib from that module (hygiene)
    JuliaLowering.include_string(test_mod, raw"""
    module CCallHygieneMod
        const mylib = "libccalltest"
        import ..JuliaLowering.@legacy_quote_to_syntax
        macro do_ccall()
            @legacy_quote_to_syntax(
                :(ccall((:ctest, mylib), Complex{Int}, (Complex{Int},), 10 + 20im)))
        end
    end
    """; expr_compat_mode)
    Core.@latestworld
    @test JuliaLowering.include_string(test_mod, """
        CCallHygieneMod.@do_ccall()
    """) === 11 + 18im

    # hygiene: `mylib` in the macro body should resolve in CCallHygieneMod, not
    # the caller, even when the caller defines a different `mylib`
    @test JuliaLowering.include_string(test_mod, """
        mylib = "this_lib_does_not_exist"
        CCallHygieneMod.@do_ccall()
    """; expr_compat_mode) === 11 + 18im

    # macro that interpolates the lib value at expansion time
    JuliaLowering.include_string(test_mod, raw"""
    module CCallHygieneMod2
        import ..JuliaLowering.@legacy_quote_to_syntax
        const mylib2 = "libccalltest"
        macro do_ccall_interp()
            lib = mylib2
            @legacy_quote_to_syntax(
                :(ccall((:ctest, $lib), Complex{Int}, (Complex{Int},), 10 + 20im)))
        end
    end
    """; expr_compat_mode)
    Core.@latestworld
    @test JuliaLowering.include_string(test_mod, """
        CCallHygieneMod2.@do_ccall_interp()
    """; expr_compat_mode) === 11 + 18im

    # ccall with plain symbol name still works inside a function
    @test JuliaLowering.include_string(test_mod, """
        function ccall_plain_sym()
            ccall(:strlen, Csize_t, (Cstring,), "abc")
        end
    """; expr_compat_mode) isa Function
    Core.@latestworld
    @test test_mod.ccall_plain_sym() == 3

    # ccall with (sym, lib) tuple where lib is a global, inside a function
    @test JuliaLowering.include_string(test_mod, """
        function ccall_global_lib()
            ccall((:ctest, libccalltest_var), Complex{Int}, (Complex{Int},), 10 + 20im)
        end
    """; expr_compat_mode) isa Function
    Core.@latestworld
    @test test_mod.ccall_global_lib() === 11 + 18im

    # ccall with module-qualified lib inside a function
    @test JuliaLowering.include_string(test_mod, """
        function ccall_qualified_lib()
            ccall((:ctest, CCallLibMod.the_lib), Complex{Int}, (Complex{Int},), 10 + 20im)
        end
    """; expr_compat_mode) isa Function
    Core.@latestworld
    @test test_mod.ccall_qualified_lib() === 11 + 18im
end

# cfunction
JuliaLowering.include_string(test_mod, """
function f_ccallable(x, y)
    x + y * 10
end
""")
cf_int = JuliaLowering.include_string(test_mod, """
@cfunction(f_ccallable, Int, (Int,Int))
""")
@test @ccall($cf_int(2::Int, 3::Int)::Int) == 32
cf_float = JuliaLowering.include_string(test_mod, """
@cfunction(f_ccallable, Float64, (Float64,Float64))
""")
@test @ccall($cf_float(2::Float64, 3::Float64)::Float64) == 32.0

# Test that hygiene works with @ccallable function names
JuliaLowering.include_string(test_mod, raw"""
f_ccallable_hygiene() = 1

module Nested
    import ..JuliaLowering.@legacy_quote_to_syntax
    f_ccallable_hygiene() = 2
    macro cfunction_hygiene()
        @legacy_quote_to_syntax :(@cfunction($f_ccallable_hygiene, Int, ()))
    end
end
""")
cf_hygiene = JuliaLowering.include_string(test_mod, """
Nested.@cfunction_hygiene
""")
@test @ccall($cf_hygiene()::Int) == 2
# Same as above, but non-interpolated symbol.  Arguably this could return 20,
# but if it should, this is a bug in the macro implementation, not lowering.
# Match Base for now.
JuliaLowering.include_string(test_mod, raw"""
f_ccallable_hygiene() = 10

module Nested
    import ..JuliaLowering.@legacy_quote_to_syntax
    f_ccallable_hygiene() = 20
    macro cfunction_hygiene()
        @legacy_quote_to_syntax :(@cfunction(f_ccallable_hygiene, Int, ()))
    end
end
""")
cf_hygiene = JuliaLowering.include_string(test_mod, """
Nested.@cfunction_hygiene
""")
@test @ccall($cf_hygiene()::Int) == 10

@test JuliaLowering.include_string(test_mod, """
cfunction_ignores_locals() = Int32(1)
function getptr()
    cfunction_ignores_locals = @cfunction(cfunction_ignores_locals, Int32, ())
    cfunction_ignores_locals
end
getptr() isa Ptr{Cvoid}
""")

# quoted function in cfunction
quoted_cfn_anon = JuliaLowering.include_string(test_mod, raw"""
    @cfunction((function(x); x; end), Int, (Int,))
""")
@test ccall(quoted_cfn_anon, Int, (Int,), 1) == 1

quoted_cfn_named = JuliaLowering.include_string(test_mod, raw"""
    @cfunction((function fname_unused(x); x; end), Int, (Int,))
""")
@test ccall(quoted_cfn_named, Int, (Int,), 1) == 1

# flisp-expanded ccall, cfunction should be lowerable by us
let fl_ex = macroexpand(
    test_mod,
    :(cfun_flisp_thunk() = @cfunction(+, Cint, (Cint, Cint))))

    fl_st = JuliaLowering.expr_to_est(fl_ex)
    fl_fn = JuliaLowering.eval(test_mod, fl_st)
    fl_cfn = @invokelatest fl_fn()
    @test fl_fn isa Function
    @test fl_cfn isa Ptr
    @test ccall(fl_cfn, Int, (Int,Int), 1, 2) == 3
end
let fl_ex = macroexpand(
    test_mod,
    :(cfun_flisp_thunk() = @cfunction(function some_cfunc_add(x,y); x+y; end,
                                      Cint, (Cint, Cint))))

    fl_st = JuliaLowering.expr_to_est(fl_ex)
    fl_fn = JuliaLowering.eval(test_mod, fl_st)
    fl_cfn = @invokelatest fl_fn()
    @test fl_fn isa Function
    @test fl_cfn isa Ptr
    @test ccall(fl_cfn, Int, (Int,Int), 1, 2) == 3
end
let fl_ex = macroexpand(
    test_mod,
    :(@ccall(libccalltest_var.ctest((10+20im)::Complex{Int})::Complex{Int})))
    fl_st = JuliaLowering.expr_to_est(fl_ex)
    @test JuliaLowering.eval(test_mod, fl_st) == 11 + 18im
end

# raw :foreigncall should also be lowerable
let raw_foreigncall_ex = Expr(
    :foreigncall, Expr(
        :tuple, QuoteNode(:ctest), "libccalltest"),
    :(Complex{Int}),
    :(Core.svec(Complex{Int})),
    0,
    QuoteNode((:ccall, 0x0000, false)),
    10+20im,
    Complex{Int})

    # test flisp does this: it's unclear how much desugaring the user is
    # responsible for here
    @test fl_eval(test_mod, raw_foreigncall_ex) == 11 + 18im

    @test JuliaLowering.eval(test_mod, JuliaLowering.expr_to_est(raw_foreigncall_ex)) == 11 + 18im
end

# Test that ccall can be passed static parameters in type signatures.
#
# Note that the cases where this works are extremely limited and tend to look
# like `Ptr{T}` or `Ref{T}` (`T` doesn't work!?) because of the compilation
# order in which the runtime inspects the arguments to ccall (`Ptr{T}` has a
# well defined C ABI even when `T` is not yet determined). See also
# https://github.com/JuliaLang/julia/issues/29400
# https://github.com/JuliaLang/julia/pull/40947
JuliaLowering.include_string(test_mod, raw"""
function sparam_ccallable(x::Ptr{T}) where {T}
    unsafe_store!(x, one(T))
    nothing
end

function ccall_with_sparams(::Type{T}) where {T}
    x = T[zero(T)]
    cf = @cfunction(sparam_ccallable, Cvoid, (Ptr{T},))
    @ccall $cf(x::Ptr{T})::Cvoid
    x[1]
end
""")
@test test_mod.ccall_with_sparams(Int) === 1
@test test_mod.ccall_with_sparams(Float64) === 1.0

# (AI) has_fcall: sparam-dependent @cfunction must not inline into a caller
# knowing T only abstractly. The abstract field keeps T non-constant there.
@test JuliaLowering.include_string(test_mod, """
cfunc_has_fcall(buf::Ptr{UInt8}, len::UInt32) = Int32(0)
get_cf(::Type{T}) where {T} = @cfunction cfunc_has_fcall Int32 (Ref{T}, Ptr{UInt8}, UInt32)
do_cf(stream::T) where {T <: IO} = get_cf(T)
mutable struct Box3
    x::IO
end
call_it(b::Box3) = do_cf(b.x)
call_it(Box3(IOBuffer()))
""") isa Ptr{Cvoid}

# Test that ccall can be passed static parameters in the function name
# Note that this only works with `@generated` functions from 1.13 onwards,
# where the function name can be evaluated at code generation time.
JuliaLowering.include_string(test_mod, raw"""
# In principle, may add other strlen-like functions here for different string
# types
ccallable_sptest_name(::Type{String}) = :strlen

@generated function ccall_with_sparams_in_name(s::T) where {T}
    name = QuoteNode(ccallable_sptest_name(T))
    @legacy_quote_to_syntax :(ccall($name, Csize_t, (Cstring,), s))
end
""")
@test test_mod.ccall_with_sparams_in_name("hii") == 3

# Where local variables may appear in ccall / cfunction type and name positions
@testset "(AI) local variables in ccall/cfunction type/name positions" begin
    # fresh module per case: many of these define global methods
    cc(code) = JuliaLowering.include_string(Module(:cc_locals), code)

    # ---- ALLOWED: a top-level local interpolated into a global method ----
    # return type
    @test cc("""
        begin
            local t = Csize_t
            g() = ccall(:strlen, t, (Cstring,), "asdfg")
            g()
        end""") == 5
    # argument type
    @test cc("""
        begin
            local a = Cstring
            g(s) = ccall(:strlen, Csize_t, (a,), s)
            g("asdfg")
        end""") == 5
    # library (2nd element of the name tuple): a constant string is fine
    @test cc("""
        begin
            local lib = "libccalltest"
            g() = ccall((:ctest, lib), Complex{Int}, (Complex{Int},), 10+20im)
            g()
        end""") == 11 + 18im
    # still a global method (its capture still top-level) when nested in a `let`
    @test cc("""
        begin
            local t = Csize_t
            let
                global g
                g() = ccall(:strlen, t, (Cstring,), "asdfg")
            end
            g()
        end""") == 5
    # cfunction mirrors ccall for the return and argument types
    @test cc("""fcb(x) = x + 1; @cfunction(fcb, Int, (Int,)) isa Ptr""")
    @test cc("""
        begin
            local RT = Int
            fcb(x) = x + 1
            g() = @cfunction(fcb, RT, (Int,))
            g() isa Ptr
        end""")
    @test cc("""
        begin
            local AT = Int
            fcb(x) = x + 1
            g() = @cfunction(fcb, Int, (AT,))
            g() isa Ptr
        end""")

    # ---- REJECTED (`static_eval_disallowed_binding`) ----
    @test_broken false == """
    Note these should ideally all be LoweringError, but that's just so the error
    prints nicely.  flisp doesn't do much validation, and leaves it to the ccall
    machinery to reject everything it can't handle.
    """

    # A same-frame local (declared in the method that runs the ccall) is a slot.
    @test_throws LoweringError cc("""
        function g()
            local rt = Csize_t
            ccall(:strlen, rt, (Cstring,), "asdfg")
        end
        g()""")
    # A genuine (local-name / escaping) closure captures the type as a *field*.
    @test_throws ErrorException cc("""
        let t = Csize_t
            g() = ccall(:strlen, t, (Cstring,), "asdfg")
            g()
        end""")
    @test_throws ErrorException cc("""
        let a = Cstring
            g(s) = ccall(:strlen, Csize_t, (a,), s)
            g("asdfg")
        end""")
    # A *reassigned* (boxed) top-level local can't be baked in as a constant.
    @test_throws LoweringError cc("""
        begin
            local t = Csize_t
            t = Csize_t
            g() = ccall(:strlen, t, (Cstring,), "asdfg")
            g()
        end""")
    # cfunction rejects the same shapes.
    @test_throws LoweringError cc("""
        function g()
            fcb(x) = x + 1
            local RT = Int
            @cfunction(fcb, RT, (Int,))
        end
        g()""")
    @test_throws UndefVarError cc("""
        let RT = Int
            fcb(x) = x + 1
            g() = @cfunction(fcb, RT, (Int,))
            g()
        end""")
    # ...and so does the library position.
    @test_throws LoweringError cc("""
        function g()
            local lib = "libccalltest"
            ccall((:ctest, lib), Complex{Int}, (Complex{Int},), 10+20im)
        end
        g()""")
    @test_throws TypeError cc("""
        let lib = "libccalltest"
            g() = ccall((:ctest, lib), Complex{Int}, (Complex{Int},), 10+20im)
            g()
        end""")

    # A top-level ccall (no enclosing method) referencing a same-frame local: the
    # static_eval can't be evaluated with no locals available.
    @test_throws LoweringError cc("""
        let rt = Csize_t
            ccall(:strlen, rt, (Cstring,), "asdfg")
        end""")
    # The function *name* must be a literal symbol/string; a local holding a
    # Symbol is only rejected later, at codegen.
    @test_throws TypeError cc("""
        function g()
            local nm = :strlen
            ccall(nm, Csize_t, (Cstring,), "asdfg")
        end
        g()""")
    # A top-level local *library* likewise slips past lowering and fails later.
    @test_throws LoweringError cc("""
        let lib = "libccalltest"
            ccall((:ctest, lib), Complex{Int}, (Complex{Int},), 10+20im)
        end""")

    @test_throws TypeError cc("""
    begin
        local nm = :strlen
        g() = ccall(nm, Csize_t, (Cstring,), "asdfg")
        g()
    end""")
end

@testset "CodeInfo: has_image_globalref" begin
    @test lower_str(test_mod, "x + y").args[1].has_image_globalref === false
    @test lower_str(Main, "x + y").args[1].has_image_globalref === true
end

baremodule baremod
macro int128_str(x);  error("baremod macro; expected call to Core macro"); end
macro uint128_str(x); error("baremod macro; expected call to Core macro"); end
macro big_str(x);     error("baremod macro; expected call to Core macro"); end
macro cmd(x);         error("baremod macro; expected call to Core macro"); end
macro doc(x, y);      error("baremod macro; expected call to Core macro"); end
global nothing = "baremod.nothing; expected core nothing"
end
@testset "globalrefs inserted by parsing" begin
    local jl_s_eval = x->JuliaLowering.include_string(baremod, x; expr_compat_mode=true)
    local fl_s_eval = x->fl_eval(baremod, JuliaSyntax.parsestmt(Expr, x; filename="file"))

    let s = "100000000000000000000000000000"
        @test jl_s_eval(s) == fl_s_eval(s)
    end
    let s = "0x100000000000000000000000000000"
        @test jl_s_eval(s) == fl_s_eval(s)
    end
    let s = "10000000000000000000000000000000000000000000000000000000000000000"
        @test jl_s_eval(s) == fl_s_eval(s)
    end
    let s = "`ls`"
        @test jl_s_eval(s) == fl_s_eval(s)
    end

    let s = """
            "foo" function fl_documented_function(); fl_documented_function; end
            """
        @test fl_s_eval(s) isa Function
    end
    @test baremod.fl_documented_function() == baremod.fl_documented_function
    let s = """
            "foo" function jl_documented_function(); jl_documented_function; end
            """
        @test jl_s_eval(s) isa Function
    end
    @test baremod.jl_documented_function() == baremod.jl_documented_function

    let s = """
            function fl_ret_nothing(); return; end
            """
        @test fl_s_eval(s) isa Function
    end
    @test baremod.fl_ret_nothing() == Core.nothing
    let s = """
            function jl_ret_nothing(); return; end
            """
        @test fl_s_eval(s) isa Function
    end
    @test baremod.jl_ret_nothing() == Core.nothing
end

@testset "docstrings: doc-only expressions" begin
    local jeval(mod, str) = JuliaLowering.include_string(mod, str; expr_compat_mode=true)
    jeval(test_mod, "function fun_exists(x); x; end")
    jeval(test_mod, "module M end; module M2 end")
    # TODO: return values are to be determined, currently Base.Docs.Binding for
    # both lowering implementations.  We can't return the value of the
    # expression in these special cases.
    jeval(test_mod, "\"docstr1\" sym_noexist")
    jeval(test_mod, "\"docstr2\" fun_noexist()")
    jeval(test_mod, "\"docstr3\" fun_exists(sym_noexist)")
    jeval(test_mod, "\"docstr4\" M.sym_noexist")
    jeval(test_mod, "\"docstr5\" M.fun_noexist()")
    jeval(test_mod, "\"docstr6\" M.fun_exists(sym_noexist)")
    @test jeval(test_mod, "@doc sym_noexist")               |> string === "docstr1\n"
    @test jeval(test_mod, "@doc fun_noexist()")             |> string === "docstr2\n"
    @test jeval(test_mod, "@doc fun_exists(sym_noexist)")   |> string === "docstr3\n"
    @test jeval(test_mod, "@doc M.sym_noexist")             |> string === "docstr4\n"
    @test jeval(test_mod, "@doc M.fun_noexist()")           |> string === "docstr5\n"
    @test jeval(test_mod, "@doc M.fun_exists(sym_noexist)") |> string === "docstr6\n"
    @test jeval(test_mod.M, "@doc M.sym_noexist")             |> string === "docstr4\n"
    @test jeval(test_mod.M, "@doc M.fun_noexist()")           |> string === "docstr5\n"
    @test jeval(test_mod.M, "@doc M.fun_exists(sym_noexist)") |> string === "docstr6\n"

    jeval(test_mod.M2, "\"docstr7\" M2.M2.sym_noexist")
    jeval(test_mod.M2, "\"docstr8\" M2.M2.fun_noexist()")
    jeval(test_mod.M2, "\"docstr9\" M2.M2.fun_exists(sym_noexist)")
    @test jeval(test_mod, "@doc M2.M2.sym_noexist")             |> string === "docstr7\n"
    @test jeval(test_mod, "@doc M2.M2.fun_noexist()")           |> string === "docstr8\n"
    @test jeval(test_mod, "@doc M2.M2.fun_exists(sym_noexist)") |> string === "docstr9\n"
    @test jeval(test_mod.M2, "@doc M2.M2.sym_noexist")             |> string === "docstr7\n"
    @test jeval(test_mod.M2, "@doc M2.M2.fun_noexist()")           |> string === "docstr8\n"
    @test jeval(test_mod.M2, "@doc M2.M2.fun_exists(sym_noexist)") |> string === "docstr9\n"

    # Try with signatures and type variables
    jeval(test_mod, "abstract type T_exists end")

    jeval(test_mod, "\"docstr10\" f10(x::Int, y, z::T_exists)")
    d = jeval(test_mod, "@doc f10")
    @test d |> string === "docstr10\n"

    jeval(test_mod, "\"docstr11\" f11(x::T_exists, y::U, z::T) where {T, U<:Number}")
    d = jeval(test_mod, "@doc f11")
    @test d |> string === "docstr11\n"

    jeval(test_mod, "\"docstr12\" f12(x::Int, y::U, z::T=1) where {T, U<:Number}")
    d = jeval(test_mod, "@doc f12")
    @test d |> string === "docstr12\n"

    # doc-strings on macrocalls (punned on quoted macrocall)
    # TODO: implement and test `doc!` support for this
    @test_broken jeval(test_mod, """
        "doc string"
        :@test
    """) isa Expr
end

# SyntaxTree @eval should pass along expr_compat_mode
@test JuliaLowering.include_string(test_mod, raw"""
    let T = gensym("documented_sym_no_logspam")
        @eval @doc $"This is a $T" $T = 1
    end
"""; expr_compat_mode=true) === 1
@test JuliaLowering.include_string(test_mod, raw"""
    let T = gensym("documented_sym_no_logspam")
        @eval @doc $"This is a $T" $T = 1
    end
"""; expr_compat_mode=false) === 1

@testset "tryfinally with scopedvalues" begin
    @eval test_mod scopedval = Base.ScopedValues.ScopedValue(1)
    @eval test_mod val_history = []
    ex = Expr(:tryfinally,
              :(push!(val_history, scopedval[])),
              :(push!(val_history, scopedval[])),
              :(Base.ScopedValues.Scope(Core.current_scope(),
                                        $test_mod.scopedval => 2)))
    JuliaLowering.eval(test_mod, JuliaLowering.expr_to_est(ex); expr_compat_mode=true)
    # try block uses "inner" dynamic scope, finally does not
    @test test_mod.val_history == [2, 1]
    JuliaLowering.eval(test_mod, JuliaLowering.expr_to_est(ex))
    @test test_mod.val_history == [2, 1, 2, 1]
end

# JuliaLowering/issues/144
emptyblock_result = JuliaLowering.eval(test_mod, Expr(:(=), :emptyblock_144, Expr(:block)))
@test emptyblock_result == nothing

@testset "string forms" begin
    @test JuliaLowering.include_string(test_mod, raw"""
        "str"
    """) == "str"
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = 1
        "str$x"
    end
    """) == "str1"
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = 1
        "str$(x)"
    end
    """) == "str1"
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = [1,2,3]
        "str$(x...)"
    end
    """) == "str123"
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = [1,2,3]
        "str$(x)"
    end
    """) == "str[1, 2, 3]"
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = [1,2,3]
        "str$("innerstr$(x...)")"
    end
    """) == "strinnerstr123"
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = [1,2,3]
        "str$(["innerstr$(x...)"]...)"
    end
    """) == "strinnerstr123"

    # cmds
    @test JuliaLowering.include_string(test_mod, raw"""
        `cmdstr`
    """) == `cmdstr`
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = 1
        `cmdstr$x`
    end
    """) == `cmdstr1`
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = 1
        `cmdstr$(x)`
    end
    """) == `cmdstr1`
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = [1,2,3]
        `cmdstr$(x...)`
    end
    """) == `cmdstr123`
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = [1,2,3]
        `cmdstr$(x)`
    end
    """) == `cmdstr1 cmdstr2 cmdstr3`
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = [1,2,3]
        `cmdstr$("innerstr$(x...)")`
    end
    """) == `cmdstrinnerstr123`
    @test JuliaLowering.include_string(test_mod, raw"""
    let x = [1,2,3]
        `cmdstr$(["innerstr$(x...)"]...)`
    end
    """) == `cmdstrinnerstr123`
end

let op_mod = Module(:opmod, false)
    @testset "operators" for run in [
            s->fl_eval(op_mod, JuliaSyntax.parseall(Expr, s)),
            s->JuliaLowering.include_string(op_mod, s; expr_compat_mode=true),
            s->JuliaLowering.include_string(op_mod, s; expr_compat_mode=false)]

        @testset "unary prefix (no parens needed)" for op in String["⋆", "±", "∓", "~", "!", "¬", "√", "∛", "∜"]
            @test run("$(op)x = (x,)") isa Function
            Core.@latestworld
            @test run(op) isa Function
            @test run("$(op)1") == (1,)
            @test run("""
                let $(op)x = (x,x)
                    $(op)1
                end """) == (1,1)
        end
        @testset "prefix" for op in String["..", "+", "-", "⋆", "±", "∓", "~", "!", "¬", "√", "∛", "∜"]
            @test run("$op(a,b,c) = 3") isa Function
            Core.@latestworld
            @test run(op) isa Function
            @test run("$op(1,2,3)") == 3
            @test run("""
                let $op(a,b,c) = (c,b,a)
                    $op(1,2,3)
                end """) == (3,2,1)
        end
        @testset "infix" for op in String["&", "|", "+", "-", ":", ".."]
            @test run("a$(op)b = (a,b)") isa Function
            Core.@latestworld
            @test run(op) isa Function
            @test run("var\"$op\"(1,2)") == (1,2)
            @test run("1$(op)2") == (1,2)
            @test run("""
                let a$(op)b = (b,a)
                    1$(op)2
                end """) == (2,1)
        end
    end
end

@testset "jl_assert" begin
    st = @ast_ [K"function" "foo"::K"Identifier"]
    if JL.DEBUG
        err = try
            JuliaLowering.@jl_assert(1 == 2, (st, "error message 1"), (st, "error message 2"))
            nothing
        catch err
            err
        end
        @test err isa LoweringError
        @test err.internal === true
        @test length(err.sts) == 2
        @test length(err.msgs) == 2
        shown = sprint(show, err)
        @test contains(shown, "error message 1")
        @test contains(shown, "error message 2")
        err = try
            new_st_name = st
            JuliaLowering.@jl_assert(1 == 2, (st, "error message 1"), new_st_name, (st, "error message 2"))
            nothing
        catch err
            err
        end
        @test err isa LoweringError
        @test err.internal === true
        shown = sprint(show, err)
        @test contains(shown, "new_st_name")
    else
        @test nothing !== try
            JuliaLowering.@jl_assert false st
        catch err
            err
        else
            nothing
        end
    end
end

@testset "(static_parameter n) form as lowering input" begin
    # function (::Type{T},) where T; return (sp 1); end
    ex = Expr(:function,
         Expr(:where,
              Expr(:tuple, Expr(:(::), Expr(:curly, :Type, :T))),
              :T),
              Expr(:block, Expr(:return, Expr(:static_parameter, 1))))
    local f
    @test (f = fl_eval(test_mod, ex)) isa Function
    @test f(String) == String
    @test (f = jl_eval(test_mod, ex; expr_compat_mode=true)) isa Function
    @test f(String) == String
    @test (f = jl_eval(test_mod, ex; expr_compat_mode=false)) isa Function
    @test f(String) == String

    # function (x::T, y::U) where {T, U}; (x, y, (sp 1), (sp 2)); end
    ex = Expr(:function,
         Expr(:where,
              Expr(:tuple, Expr(:(::), :x, :T), Expr(:(::), :y, :U)),
              :T, :U),
              Expr(:block,
                   Expr(:return,
                        Expr(:tuple, :x, :y,
                             Expr(:static_parameter, 1),
                             Expr(:static_parameter, 2)))))
    @test (f = fl_eval(test_mod, ex)) isa Function
    @test f(1, 'a') == (1, 'a', Int, Char)
    @test (f = jl_eval(test_mod, ex; expr_compat_mode=true)) isa Function
    @test f(1, 'a') == (1, 'a', Int, Char)
    @test (f = jl_eval(test_mod, ex; expr_compat_mode=false)) isa Function
    @test f(1, 'a') == (1, 'a', Int, Char)
end
