Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add static_methods function #8

Merged
merged 11 commits into from
Jan 23, 2020
Merged

Conversation

MasonProtter
Copy link
Contributor

@MasonProtter MasonProtter commented Jan 17, 2020

This is the result of some explorations I took in finding all the methods of a function at compile time, with back-edges built in.

julia> using Tricks

julia> f(x) = x + 1
f (generic function with 1 method)

julia> static_methods(f)
# 1 method for generic function "f":
[1] f(x) in Main at REPL[2]:1

julia> @btime static_methods(f);
  43.324 ns (0 allocations: 0 bytes)

julia> @btime methods(f);
  2.250 μs (12 allocations: 720 bytes)

As you can see it happens at compile time, but also gets recompiled when new methods are added:

julia> f(::Int) = 1
f (generic function with 2 methods)

julia> static_methods(f)
# 2 methods for generic function "f":
[1] f(::Int64) in Main at REPL[6]:1
[2] f(x) in Main at REPL[2]:1

julia> f(x, y) = x / y
f (generic function with 3 methods)

julia> static_methods(f, Tuple{Int, Float64})
# 1 method for generic function "f":
[1] f(x, y) in Main at REPL[9]:1

I messed with the indentation to reflect regular module organization, but if you don't like that I can revert it.

src/Tricks.jl Outdated Show resolved Hide resolved
src/Tricks.jl Outdated Show resolved Hide resolved
@oxinabox
Copy link
Owner

What if rather than dooing all this stuff to create a CodeInfo for a function.
we just made a function that does what we want:

julia> @generated function _neomethods(f)
       methods(f.instance)
       end
_neomethods (generic function with 1 method)

julia> _neomethods(identity)
# 1 method for generic function "identity":
[1] identity(x) in Base at operators.jl:502

julia> @code_typed _neomethods(identity)
CodeInfo(
1 ─     return # 1 method for generic function "identity":
[1] identity(x) in Base at operators.jl:502
) => Base.MethodList

julia> Base.identity(::Float64) = 3

julia> _neomethods(identity)
# 1 method for generic function "identity":
[1] identity(x) in Base at operators.jl:502

So that, as you see does indeed compute the methods at compile time, but it is missing the hooks.

But then we do the same thing we do in static_hasmethod, and copy its code info
(we make it have the same signature as what ever outer method has)
and then attached hooks.

Probably extract out some common functionality between the two into helpers.

src/Tricks.jl Outdated Show resolved Hide resolved
@NHDaly
Copy link
Collaborator

NHDaly commented Jan 18, 2020

So what happened with your investigation into whether the optimizer is already doing this?

@oxinabox
Copy link
Owner

So what happened with your investigation into whether the optimizer is already doing this?

The optimizer won't do this, it might do the thing that was Mason's original use case for this though

@MasonProtter
Copy link
Contributor Author

I'm not sure I understand exactly what you mean here

But then we do the same thing we do in static_hasmethod, and copy its code info
(we make it have the same signature as what ever outer method has)
and then attached hooks.

or how to achieve it.

@oxinabox
Copy link
Owner

Like in my example we define at the start :

@generated _neomethods(f, t) = methods(f,t)

Then in our static_methods we copy it's codeinfo, and add edges to it before returning it.

This we don't need to do the fiddly expr to codeinfo thing

@NHDaly
Copy link
Collaborator

NHDaly commented Jan 19, 2020

Yes, exactly!

Then your whole function would basically just be like:

@generated function static_methods(@nospecialize(m::Module), @nospecialize(f) , @nospecialize(_T::Type{T})) where {T <: Tuple}
        # Re-use the body of _neomethods:
        ci_orig = uncompressed_ast(typeof(_neomethods).name.mt.defs.func)
        ci = ccall(:jl_copy_code_info, Ref{CodeInfo}, (Any,), ci_orig)

        # And just add the edges so if a method is added to `f`, this recompiles
        mt = f.name.mt
        ci.edges = Core.Compiler.vect(mt, Tuple{Vararg{Any}})
        return ci
end

Or something else simple, more or less like that. :)


I'm actually not even sure if _neomethods needs to be @generated like you have it, @oxinabox

@MasonProtter
Copy link
Contributor Author

MasonProtter commented Jan 19, 2020

Unfortunately, if _neomethods is not @generated, then static_methods will run at runtime, not compiletime.

If _neomethods is @generated, then uncompressed_ast doesn't work and julia suggests to use code_lowered instead, but when I switch to code_lowered(_neomethods, Tuple{f, T}) I get

julia> begin
       using Tricks
       f(::Int) = 1
       static_methods(f, Tuple{Any})
       end
ERROR: Could not expand generator for `@generated` method MethodInstance for _neomethods(::typeof(f), ::Tuple{Any}). This can happen if the provided argument types (Tuple{typeof(f),Tuple{Any}}) are not leaf types, but the `generated` argument is `true`.
Stacktrace:
 [1] error(::String, ::Core.MethodInstance, ::String, ::String, ::Type, ::String, ::String) at ./error.jl:42
 [2] (::Base.var"#12#13"{Bool,DataType})(::Core.MethodInstance) at ./reflection.jl:806
 [3] _collect(::Array{Core.MethodInstance,1}, ::Base.Generator{Array{Core.MethodInstance,1},Base.var"#12#13"{Bool,DataType}}, ::Base.EltypeUnknown, ::Base.HasShape{1}) at ./generator.jl:47
 [4] collect_similar(::Array{Core.MethodInstance,1}, ::Base.Generator{Array{Core.MethodInstance,1},Base.var"#12#13"{Bool,DataType}}) at ./array.jl:564
 [5] map(::Function, ::Array{Core.MethodInstance,1}) at ./abstractarray.jl:2073
 [6] #code_lowered#11 at ./reflection.jl:801 [inlined]
 [7] code_lowered at ./reflection.jl:793 [inlined]
 [8] #s15#7 at /Users/mason/.julia/dev/Tricks/src/Tricks.jl:69 [inlined]
 [9] #s15#7(::Any, ::Any, ::Any, ::Any) at none:0
 [10] (::Core.GeneratedFunctionStub)(::Any, ::Vararg{Any,N} where N) at ./boot.jl:524
 [11] top-level scope at REPL[1]:4

where the relevant code from Tricks is

@generated _neomethods(@nospecialize(f), @nospecialize(t)) = methods(f, t)

@generated function static_methods(@nospecialize(f) , @nospecialize(_T::Type{T})) where {T <: Tuple}
    # Re-use the body of _neomethods:
    ci_orig = code_lowered(_neomethods, Tuple{f, T})
    ci = ccall(:jl_copy_code_info, Ref{CodeInfo}, (Any,), ci_orig)

    # And just add the edges so if a method is added to `f`, this recompiles
    mt = f.name.mt
    ci.edges = Core.Compiler.vect(mt, Tuple{Vararg{Any}})
    return ci
end

@NHDaly
Copy link
Collaborator

NHDaly commented Jan 19, 2020 via email

@MasonProtter
Copy link
Contributor Author

That’s what I saw testing it myself. It’s possible I did something incorrect though.

@oxinabox
Copy link
Owner

hmm, I will have a play with this for a big see if i can make it work.

@oxinabox
Copy link
Owner

I couldn't work it out either.
Lets make the change to get rid of the ::Module argument,
and update the readme,
and then we can merge it.

@MasonProtter
Copy link
Contributor Author

Okay, will do.

@MasonProtter
Copy link
Contributor Author

Okay, so I'd updated the docstring, switched to @__MODULE__ in expr_to_code_info and added a test case where I make sure we're capturing the method-table for functions living in other modules.

Tests pass locally.

Maybe we should get Travis set up for this package?

src/Tricks.jl Outdated Show resolved Hide resolved
@oxinabox
Copy link
Owner

I have added Travis. If you rebase off master is should run.

Can you also bump the version in the Project.toml so i can tag a release?
See:
https://white.ucc.asn.au/2019/09/28/Continuous-Delivery-For-Julia-Packages.html#what-if-i-dont-want-to-release-right-now--dev-versions

Copy link
Collaborator

@NHDaly NHDaly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, if _neomethods is not @generated, then static_methods will run at runtime, not compiletime.


Is that right? Even though static_methods itself is a @generated function?

Oh, wait, right, duh, of course that's right. Because now the method you're generating has as its body, a runtime call to methods(), rather than having as its body the result of a call to methods from within the generated function. ✔️


I think the code as you have it is the best way to do what you want: you call methods() from within the body of the generated function, then you create a new CodeInfo that does nothing but return that result, and attach edges to it! ✔️

src/Tricks.jl Show resolved Hide resolved
@MasonProtter
Copy link
Contributor Author

I think the CI failures are unrelated.

@oxinabox
Copy link
Owner

urg, those test failures are unrelated yes.

I need to work out a better way to test these thins

@oxinabox
Copy link
Owner

@MasonProtter can you take a look at the commit I just mad and see that i didn't break anything?

I also note that since sparams is never nothing
we could simplify to

    expr = Expr(Symbol("with-static-parameters"),
        Expr(:lambda,
            argnames,
            Expr(Symbol("scope-block"),
                Expr(:block,
                    Expr(:return, value),
                )
            )
        )
        spnames...
    )

But I think that might make the code less clear

@MasonProtter
Copy link
Contributor Author

Your commit looks good to me and the tests pass locally.

@oxinabox oxinabox merged commit dd68bf2 into oxinabox:master Jan 23, 2020
@oxinabox oxinabox mentioned this pull request Jan 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants