Given a Julia function `f` and its arguments `agrs` return typed Julia source code via a macro

Is there a macro for that? Or is it possible to write one?

It’s always easier to understand via an example:

function f(x,y)
    a = sqrt(x)
    b = x*y
    return a + b
end

Macro call:

@get_typed_source_code f(1,2)
function f(x::Int64, y::Int64)::Float64
    a::Float64 = sqrt(x::Int64)
    b::Int64 = x::Int64 * y::Int64
    return a::Float64 + b::Int64
end

Eg @code_typed?

Kind of, but not that low level. More like in the example above so that one can copy-past the generated julia source code

It may be doable, but quite a few passes are done before the compiler types some code, so it would have to go back somehow to the original form.

I am curious about the use case.

Just exploring the language :slight_smile:
For beginners it’s much easier to read and explore Julia source code instead of the lowered Julia code. And sometimes one might want fully typed code.
(And my boss is used to statically typed languages and what to type everything)

1 Like

I think one might start in macros.jl with this piece of code

function gen_call_with_extracted_types(__module__, fcn, ex0)
    if isa(ex0, Expr)
        if any(a->(Meta.isexpr(a, :kw) || Meta.isexpr(a, :parameters)), ex0.args)
            return quote
                local arg1 = $(esc(ex0.args[1]))
                local args, kwargs = $separate_kwargs($(map(esc, ex0.args[2:end])...))
                $(fcn)(Core.kwfunc(arg1),
                       Tuple{typeof(kwargs), Core.Typeof(arg1), map(Core.Typeof, args)...})
            end
        elseif ex0.head === :call
            return Expr(:call, fcn, esc(ex0.args[1]),
                        Expr(:call, typesof, map(esc, ex0.args[2:end])...))
        elseif ex0.head === :(=) && length(ex0.args) == 2
            lhs, rhs = ex0.args
            if isa(lhs, Expr)
                if lhs.head === :(.)
                    return Expr(:call, fcn, Base.setproperty!,
                                Expr(:call, typesof, map(esc, lhs.args)..., esc(rhs)))
                elseif lhs.head === :ref
                    return Expr(:call, fcn, Base.setindex!,
                                Expr(:call, typesof, esc(lhs.args[1]), esc(rhs), map(esc, lhs.args[2:end])...))
                end
            end
        elseif ex0.head === :vcat || ex0.head === :typed_vcat
            if ex0.head === :vcat
                f, hf = Base.vcat, Base.hvcat
                args = ex0.args
            else
                f, hf = Base.typed_vcat, Base.typed_hvcat
                args = ex0.args[2:end]
            end
            if any(a->isa(a,Expr) && a.head === :row, args)
                rows = Any[ (isa(x,Expr) && x.head === :row ? x.args : Any[x]) for x in args ]
                lens = map(length, rows)
                return Expr(:call, fcn, hf,
                            Expr(:call, typesof,
                                 (ex0.head === :vcat ? [] : Any[esc(ex0.args[1])])...,
                                 Expr(:tuple, lens...),
                                 map(esc, vcat(rows...))...))
            else
                return Expr(:call, fcn, f,
                            Expr(:call, typesof, map(esc, ex0.args)...))
            end
        else
            for (head, f) in (:ref => Base.getindex, :hcat => Base.hcat, :(.) => Base.getproperty, :vect => Base.vect, Symbol("'") => Base.adjoint, :typed_hcat => Base.typed_hcat, :string => string)
                if ex0.head === head
                    return Expr(:call, fcn, f,
                                Expr(:call, typesof, map(esc, ex0.args)...))
                end
            end
        end
    end
    if isa(ex0, Expr) && ex0.head === :macrocall # Make @edit @time 1+2 edit the macro by using the types of the *expressions*
        return Expr(:call, fcn, esc(ex0.args[1]), Tuple{#=__source__=#LineNumberNode, #=__module__=#Module, Any[ Core.Typeof(a) for a in ex0.args[3:end] ]...})
    end

    ex = Meta.lower(__module__, ex0)
    if !isa(ex, Expr)
        return Expr(:call, :error, "expression is not a function call or symbol")
    end

    exret = Expr(:none)
    if ex.head === :call
        if any(e->(isa(e, Expr) && e.head === :(...)), ex0.args) &&
            (ex.args[1] === GlobalRef(Core,:_apply) ||
             ex.args[1] === GlobalRef(Base,:_apply))
            # check for splatting
            exret = Expr(:call, ex.args[1], fcn,
                        Expr(:tuple, esc(ex.args[2]),
                            Expr(:call, typesof, map(esc, ex.args[3:end])...)))
        else
            exret = Expr(:call, fcn, esc(ex.args[1]),
                         Expr(:call, typesof, map(esc, ex.args[2:end])...))
        end
    end
    if ex.head === :thunk || exret.head === :none
        exret = Expr(:call, :error, "expression is not a function call, "
                                  * "or is too complex for @$fcn to analyze; "
                                  * "break it down to simpler parts if possible")
    end
    return exret
end

Unfortunately I’m not able to understand it yet :joy:

No, this is currently not possible, see:

https://github.com/JuliaLang/julia/issues/2625

Edit: this is only regarding extracting the original source code, without any type information.

1 Like

you can use @edit to have the source code pop up in a text editor.

Who do I use it?

julia> using Revise

julia> Revise.@edit
ERROR: LoadError: UndefVarError: @edit not defined
in expression starting at REPL[53]:1

@edit is not in Revise, it’s in InteractiveUtils, so it should already be imported in the REPL.

2 Likes

Make sure you know how to do julia> ?@edit in the REPL to see the documentation. In this case you write

julia> @edit println("hey")

But how would I use this to get typed Julia source code?

Oh, sorry, I might have misunderstood your original question. In Julia, expressions don’t have any types. The way the compiler works, your code is first lowered into an intermediate representation (IR), which turns loops and conditionals into gotos, handles fused broadcasting, etc. This is also where tools like Cassette and IRTools operate at. Type inference is then done only on this IR code and you can see what this results in with @code_typed. This means that you can’t really ask Julia for typed Julia code, because that’s not how the compiler works and may not always be well-posed. I would just try to experiment with @code_typed a bit, since it can be really useful for getting a feeling for where type instabilities occur.

2 Likes

It’s unrelated. And the link in Given a Julia function `f` and its arguments `agrs` return typed Julia source code via a macro - #7 by simeonschaub isn’t related either.

Just don’t…

3 Likes

Haha :joy:

OP I think the real solution here is to accept that Julia has dynamic elements, in that you can write duck-typed code at a high level and it will still be performant. I would suggest do @code_warntype a bunch and fixing you see Any pop up. It’s not perfect but it’s more in the spirit of writing “julian” code where you don’t nail down all the types all the time.

1 Like