# This file generates builtins.jl.
# Should be run on the latest Julia nightly
using InteractiveUtils

# Builtins not present in 1.10 (the lowest supported version)
const RECENTLY_ADDED = Core.Builtin[
    Core.current_scope,
    isdefinedglobal,
    Core.memorynew,
    Core.memoryref_isassigned,
    Core.memoryrefget,
    Core.memoryrefmodify!,
    Core.memoryrefnew,
    Core.memoryrefoffset,
    Core.memoryrefreplace!,
    Core.memoryrefset!,
    Core.memoryrefsetonce!,
    Core.memoryrefswap!,
    Core.throw_methoderror,
    modifyglobal!,
    replaceglobal!,
    setfieldonce!,
    setglobalonce!,
    swapglobal!,
    Core._defaultctors,
    Core._import,
    Core._using,
    # Recently became builtins
    Base.invokelatest,
    Base.invoke_in_world,
]
# Builtins present from 1.10, not builtins (potentially still normal functions) anymore
const RECENTLY_REMOVED = GlobalRef.(Ref(Core), [
    :arrayref, :arrayset, :arrayset, :const_arrayref, :memoryref, :set_binding_type!,
    :_apply_pure, :_call_in_world, :_call_latest,
])
const kwinvoke = Core.kwfunc(Core.invoke)
const REQUIRES_WORLD = Core.Builtin[
    setglobal!,
    Core.get_binding_type,
    swapglobal!,
    modifyglobal!,
    replaceglobal!,
    setglobalonce!,
]
const CALL_LATEST = """args = getargs(interp, args, frame)
        if !expand
            return Some{Any}(Core._call_latest(args...))
        end
        new_expr = Expr(:call, args[1])
        popfirst!(args)
        for x in args
            push!(new_expr.args, QuoteNode(x))
        end
        return maybe_recurse_expanded_builtin(interp, frame, new_expr)
"""

function scopedname(f)
    io = IOBuffer()
    show(io, f)
    fstr = String(take!(io))
    occursin('.', fstr) && return fstr
    tn = typeof(f).name
    Base.isexported(tn.module, Symbol(fstr)) && return fstr
    fsym = Symbol(fstr)
    isdefinedglobal(tn.module, fsym) && return string(tn.module) * '.' * fstr
    return "Base." * fstr
end

function nargs(f, table, id)
    # Look up the expected number of arguments in Core.Compiler.tfunc data
    if id !== nothing
        minarg, maxarg, tfunc = table[id]
    else
        minarg = 0
        maxarg = typemax(Int)
    end
    # Specialize ~arrayref and arrayset~ memoryrefnew for small numbers of arguments
    # TODO: how about other memory intrinsics?
    if (@static isdefinedglobal(Core, :memoryrefnew) ? f == Core.memoryrefnew : f == Core.memoryref)
        maxarg = 5
    end
    return minarg, maxarg
end

function generate_fcall_nargs(fname, minarg, maxarg; requires_world::Bool=false)
    # Generate a separate call for each number of arguments
    maxarg < typemax(Int) || error("call this only for constrained number of arguments")
    annotation = fname == "fieldtype" ? "::Type" : ""
    wrapper = "if nargs == "
    for nargs = minarg:maxarg
        wrapper *= "$nargs\n            "
        argcall = ""
        for i = 1:nargs
            argcall *= "lookup(interp, frame, args[$(i+1)])"
            if i < nargs
                argcall *= ", "
            end
        end
        wrapper *= requires_world ? "return Some{Any}(Base.invoke_in_world(frame.world, $fname, $argcall)$annotation)" :
                                    "return Some{Any}($fname($argcall)$annotation)"
        if nargs < maxarg
            wrapper *= "\n        elseif nargs == "
        end
    end
    wrapper *= "\n        else"
    # To throw the correct error
    if requires_world
        wrapper *= "\n            return Some{Any}(Base.invoke_in_world(frame.world, $fname, getargs(interp, args, frame)...)$annotation)"
    else
        wrapper *= "\n            return Some{Any}($fname(getargs(interp, args, frame)...)$annotation)"
    end
    wrapper *= "\n        end"
    return wrapper
end

function generate_fcall(f, table, id)
    minarg, maxarg = nargs(f, table, id)
    fname = scopedname(f)
    requires_world = f ∈ REQUIRES_WORLD
    if maxarg < typemax(Int)
        return generate_fcall_nargs(fname, minarg, maxarg; requires_world)
    end
    # A built-in with arbitrary or unknown number of arguments.
    # This will (unfortunately) use dynamic dispatch.
    if requires_world
        return "return Some{Any}(Base.invoke_in_world(frame.world, $fname, getargs(interp, args, frame)...))"
    end
    return "return Some{Any}($fname(getargs(interp, args, frame)...))"
end

# `io` is for the generated source file
# `intrinsicsfile` is the path to Julia's `src/intrinsics.h` file
function generate_builtins(file::String)
    open(file, "w") do io
        generate_builtins(io::IO)
    end
end
function generate_builtins(io::IO)
    pat = r"(ADD_I|ALIAS)\((\w*),"
    print(io,
"""
# This file is generated by `generate_builtins.jl`. Do not edit by hand.

function getargs(interp::Interpreter, args::Vector{Any}, frame::Frame)
    nargs = length(args)-1  # skip f
    callargs = resize!(frame.framedata.callargs, nargs)
    for i = 1:nargs
        callargs[i] = lookup(interp, frame, args[i+1])
    end
    return callargs
end

const kwinvoke = Core.kwfunc(Core.invoke)

function maybe_recurse_expanded_builtin(interp::Interpreter, frame::Frame, new_expr::Expr)
    f = new_expr.args[1]
    if isa(f, Core.Builtin) || isa(f, Core.IntrinsicFunction)
        return maybe_evaluate_builtin(interp, frame, new_expr, true)
    else
        return new_expr
    end
end

\"\"\"
    ret = maybe_evaluate_builtin(interp::Interpreter, frame::Frame, call_expr::Expr, expand::Bool)

If `call_expr` is to a builtin function, evaluate it, returning the result inside a `Some` wrapper.
Otherwise, return `call_expr`.

If `expand` is true, `Core._apply_iterate` calls will be resolved as a call to the applied function.
\"\"\"
function maybe_evaluate_builtin(interp::Interpreter, frame::Frame, call_expr::Expr, expand::Bool)
    args = call_expr.args
    nargs = length(args) - 1
    fex = args[1]
    if isa(fex, QuoteNode)
        f = fex.value
    else
        f = lookup(interp, frame, fex)
    end

    if f isa Core.OpaqueClosure
        if expand
            if !Base.uncompressed_ir(f.source).inferred
                return Expr(:call, f, args[2:end]...)
            else
                @debug "not interpreting opaque closure \$f since it contains inferred code"
            end
        end
        return Some{Any}(f(args...))
    end
    if !(isa(f, Core.Builtin) || isa(f, Core.IntrinsicFunction))
        return call_expr
    end
    # By having each call appearing statically in the "switch" block below,
    # each gets call-site optimized.
""")
    firstcall = true
    for ft in subtypes(Core.Builtin)
        ft === Core.IntrinsicFunction && continue
        ft === typeof(kwinvoke) && continue  # handle this one later
        head = firstcall ? "if" : "elseif"
        firstcall = false
        f = ft.instance
        fname = scopedname(f)
        # Tuple is common, especially for returned values from calls. It's worth avoiding
        # dynamic dispatch through a call to `ntuple`.
        if f === tuple
            print(io,
"""
    $head f === tuple
        return Some{Any}(ntupleany(i::Int->lookup(interp, frame, args[i+1]), length(args)-1))
""")
            continue
        elseif f === Core._apply_iterate
            # Resolve varargs calls
            print(io,
"""
    $head f === Core._apply_iterate
        argswrapped = getargs(interp, args, frame)
        if !expand
            return Some{Any}(Core._apply_iterate(argswrapped...))
        end
        aw1 = argswrapped[1]::Function
        @assert aw1 === Core.iterate || aw1 === Core.Compiler.iterate || aw1 === Base.iterate "cannot handle `_apply_iterate` with non iterate as first argument, got \$(aw1), \$(typeof(aw1))"
        new_expr = Expr(:call, argswrapped[2])
        popfirst!(argswrapped) # pop the iterate
        popfirst!(argswrapped) # pop the function
        argsflat = append_any(argswrapped...)
        for x in argsflat
            push!(new_expr.args, QuoteNode(x))
        end
        return maybe_recurse_expanded_builtin(interp, frame, new_expr)
""")
            continue
        elseif f === Core.invoke
            fstr = scopedname(f)
            print(io,
"""
    $head f === $fstr
        if !expand
            argswrapped = getargs(interp, args, frame)
            return Some{Any}($fstr(argswrapped...))
        end
        # This uses the original arguments to avoid looking them up twice
        # See #442
        return Expr(:call, invoke, args[2:end]...)
""")
            continue
        elseif f === Core.current_scope
            print(io,
"""
    elseif @static isdefinedglobal(Core, :current_scope) && f === Core.current_scope
        if nargs == 0
            currscope = Core.current_scope()
            for scope in frame.framedata.current_scopes
                currscope = Scope(currscope, scope.values...)
            end
            return Some{Any}(currscope)
        else
            return Some{Any}(Core.current_scope(getargs(interp, args, frame)...))
        end
""")
            continue
        end

        id = findfirst(isequal(f), Core.Compiler.T_FFUNC_KEY)
        fcall = generate_fcall(f, Core.Compiler.T_FFUNC_VAL, id)
        if f in RECENTLY_ADDED
            if f === Core.invokelatest
                fcall = replace(CALL_LATEST, "_call_latest" => "invokelatest")
            end
            print(io,
"""
    $head @static isdefinedglobal($(ft.name.module), $(repr(nameof(f)))) && f === $fname
        $fcall
""")
        else
            print(io,
"""
    $head f === $fname
        $fcall
""")
        end
        firstcall = false
    end
    print(io,
"""
    # Intrinsics
""")
    print(io,
"""
    elseif f === Base.cglobal
        if nargs == 1
            call_expr = copy(call_expr)
            args2 = args[2]
            call_expr.args[2] = isa(args2, QuoteNode) ? args2 : lookup(interp, frame, args2)
            return Some{Any}(Core.eval(moduleof(frame), call_expr))
        elseif nargs == 2
            call_expr = copy(call_expr)
            args2 = args[2]
            call_expr.args[2] = isa(args2, QuoteNode) ? args2 : lookup(interp, frame, args2)
            call_expr.args[3] = lookup(interp, frame, args[3])
            return Some{Any}(Core.eval(moduleof(frame), call_expr))
        end
""")
    # recently removed builtins
    for (; mod, name) in RECENTLY_REMOVED
        minarg = 1
        if name in (:arrayref, :const_arrayref, :memoryref)
            maxarg = 5
        elseif name === :arrayset
            maxarg = 6
        elseif name === :arraysize
            maxarg = 2
        elseif name === :set_binding_type!
            minarg = 2
            maxarg = 3
        elseif name in (:_apply_pure, :_call_in_world, :_call_latest)
            minarg = 0
            maxarg = typemax(Int)
        end
        _scopedname = "$mod.$name"
        fcall = name === :_call_latest ? CALL_LATEST :
                maxarg < typemax(Int) ? generate_fcall_nargs(_scopedname, minarg, maxarg) :
                                        "return Some{Any}($_scopedname(getargs(interp, args, frame)...))"
        rname = repr(name)
        print(io,
"""
    elseif @static (isdefinedglobal($mod, $rname) && $_scopedname isa Core.Builtin) && f === $_scopedname
        $fcall
""")
    end
    # Extract any intrinsics that support varargs
    fva = []
    minmin, maxmax = typemax(Int), 0
    for fsym in names(Core.Intrinsics)
        fsym === :Intrinsics && continue
        isdefinedglobal(Base, fsym) || continue
        f = getfield(Base, fsym)
        id = reinterpret(Int32, f) + 1
        minarg, maxarg = nargs(f, Core.Compiler.T_IFUNC, id)
        if maxarg == typemax(Int)
            push!(fva, f)
        else
            minmin = min(minmin, minarg)
            maxmax = max(maxmax, maxarg)
        end
    end
    for f in fva
        id = reinterpret(Int32, f) + 1
        fname = scopedname(f)
        fcall = generate_fcall(f, Core.Compiler.T_IFUNC, id)
        print(io,
"""
    elseif f === $fname
        $fcall
    end
""")
    end
    # Now handle calls with bounded numbers of args
    print(io,
"""
    if isa(f, Core.IntrinsicFunction)
        cargs = getargs(interp, args, frame)
        if f === Core.Intrinsics.have_fma && length(cargs) == 1
            cargs1 = cargs[1]
            if cargs1 == Float64
                return Some{Any}(FMA_FLOAT64[])
            elseif cargs1 == Float32
                return Some{Any}(FMA_FLOAT32[])
            elseif cargs1 == Float16
                return Some{Any}(FMA_FLOAT16[])
            end
        end
        if f === Core.Intrinsics.muladd_float && length(cargs) == 3
            a, b, c = cargs
            Ta, Tb, Tc = typeof(a), typeof(b), typeof(c)
            if !(Ta == Tb == Tc)
                error("muladd_float: types of a, b, and c must match")
            end
            if Ta == Float64 && FMA_FLOAT64[]
                f = Core.Intrinsics.fma_float
            elseif Ta == Float32 && FMA_FLOAT32[]
                f = Core.Intrinsics.fma_float
            elseif Ta == Float16 && FMA_FLOAT16[]
                f = Core.Intrinsics.fma_float
            end
        end
        return Some{Any}(ccall(:jl_f_intrinsic_call, Any, (Any, Ptr{Any}, UInt32), f, cargs, length(cargs)))
""")
    print(io,
"""
    end
    if isa(f, typeof(kwinvoke))
        return Some{Any}(kwinvoke(getargs(interp, args, frame)...))
    end
    return call_expr
end
""")
end

builtins_dir = get(ENV, "JULIAINTERPRETER_BUILTINS_DIR", joinpath(@__DIR__, "..", "src"))
generate_builtins(joinpath(builtins_dir, "builtins.jl"))
