diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..2d02f4f --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,37 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1.4' + - '1.5' + - 'nightly' + os: + - ubuntu-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 \ No newline at end of file diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..c9272ff --- /dev/null +++ b/Project.toml @@ -0,0 +1,18 @@ +name = "Traitor" +uuid = "51d8d3fb-42fa-4eee-8664-c9fdd13ad6d2" +authors = ["Andy Ferris", "Chris Foster"] +version = "0.1.0" + +[deps] +Cassette = "7057c7e9-c182-5462-911a-8362d720325c" +MethodAnalysis = "85b6ec6f-f7df-4429-9514-a64bcd9ee824" + +[compat] +julia = "1.2" + +[extras] +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test", "InteractiveUtils"] diff --git a/README.md b/README.md index 23ae5d0..0c5f814 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,17 @@ Currently the package has basic functionality, supporting the features discussed in the following sections. Some obvious oversights include lack of support for default values and keyword arguments. +One major difference between Traitor.jl and other trait packages that utilize the +so called "Holy trait pattern", is that Traitor.jl has it's own internal trait +based dispatch mechanism separate from the usual Julia multiple dispatch machinery. + +This difference increases the complexity of Traitor, but comes with some advantages +such as allowing multiple people who are not coordinating together to add traits to +a type and not collide with eachother. Put another way, similarly to how different +people can create their own subtypes of an abstract type and share functions, +Traitor allows multiple people to add traits and share trait functions. + + **Warning: please have some fun using this package, but it might not yet be suitable for production code.** ### Our expectations of a traits type system @@ -29,11 +40,11 @@ trait class, while subtypes of them define examples of that trait. For example: ```julia -abstract Size +abstract type Size end -immutable Big <: Size; end -immutable Medium <: Size; end -immutable Small <: Size; end +struct Big <: Size end +struct Medium <: Size end +struct Small <: Size end ``` Types are annotated with traits *post-hoc* by returning the appropriate trait @@ -43,8 +54,8 @@ question. So we could have: ```julia # Note: both input and output are types Size(::Union{Type{Int32},Type{Int64}}) = Small -Size(::Type{BigInt}) = Big -Size(::Type{Int128}) = Medium +Size(::Type{BigInt}) = Big +Size(::Type{Int128}) = Medium ``` ### The `@traitor` macro @@ -69,13 +80,30 @@ end "So-so" end -# One can combine standard dispatch and traits-based dispatch. In this case, -# standard multiple-dispatch occurs first, and `Traitor` then selects the most -# appropriate trait-based submethod. +# One can combine standard dispatch and traits-based dispatch. @traitor function howbig(x::Integer::Small) "Teensy..." end ``` +Since standard dispatch happens before trait dispatch, the above method `howbig(x::Integer::Small)` has higher precendence than `howbig(x::::Medium)` and `howbig(x::Any::Big)`, so we need to define more specific versions of those methods: +```julia +@traitor howbig(x::Integer::Big) = "Huge!" +@traitor howbig(x::Integer::Medium) = "So-so" +``` +Now, +```julia +julia> howbig(1) +"Teensy..." + +julia> howbig(Int128(1)) +"So-so" + +julia> howbig(BigInt(1)) +"Huge!" + +``` + + ### Unions of traits via `Union` @@ -94,8 +122,8 @@ Traits of *different trait classes* can be combined using a `Tuple{...}`. This represents a *smaller* set of Julia objects - those which satisfy all of the traits simultaneously. For example, ```julia -abstract Odor -immutable Smelly <: Odor; end +abstract type Odor end +struct Smelly <: Odor end @traitor describeyourself(::::Tuple{Big,Smelly}) = "I'm big and smelly" ``` @@ -114,9 +142,9 @@ this slippery code below: module Treason using Traitor -abstract Mutability -immutable Immutable <: Mutability; end -immutable Mutable <: Mutability; end +abstract type Mutability end +struct Immutable <: Mutability end +struct Mutable <: Mutability end @pure Mutability(T) = T.mutable ? Mutable : Immutable @@ -135,24 +163,43 @@ end for i = 1:length(x) # Mutate it on the heap out[i] = f(x[i]) # Elsewhere define setindex!(::RefValue{Tuple}), etc end - return out.x # Copy it back to the stack + return out[] # Copy it back to the stack end -function setindex!{T <: Tuple}(x::RefValue{T}, val, i::Integer) +function setindex!(x::RefValue{T}, val, i::Integer) where {T <: Tuple} # grab a pointer and set some memory (safely, please...) end end # module ``` -### Help wanted: the `@betray` macro +### `betray!`ing functions -We would like to have a macro, say `@betray`, that would be able to "steal" +Et tu? + +The `betray!` function allows one to effectively "steal" pre-existing method definitions and make them compatible with `@traitor` methods -as a default fallback. The current roadblock is that we don't know how to -take the code corresponding to a `Method` (or `LambdaInfo`) and insert it into -a new method definition (since the `LambdaInfo` is lowered IR and it is normal -to define methods with top-level expression syntax). +as a default fallback. + +```julia +module SomeoneElsesCode +f(x) = x + 1 +end + +import .SomeoneElsesCode: f + +betray!(f, Tuple{Any}) # Inputs are provided similarly to the `methods` of `code_lowered` functions + +@traitor f(x::::Big) = x - 1 +``` + +```julia +julia> f(1) +2 + +julia> f(BigInt(1)) +0 +``` ## Acknowledgements diff --git a/REQUIRE b/REQUIRE deleted file mode 100644 index 70e314a..0000000 --- a/REQUIRE +++ /dev/null @@ -1 +0,0 @@ -julia 0.5- diff --git a/src/Traitor.jl b/src/Traitor.jl index 3e97274..272b1b3 100644 --- a/src/Traitor.jl +++ b/src/Traitor.jl @@ -54,56 +54,78 @@ previous definition. """ module Traitor -import Base: @pure -export @traitor, @betray, supertrait +using Base: @pure, uncompressed_ast, unwrap_unionall, tuple_type_cons +using Core: SimpleVector, svec, CodeInfo + +export @traitor, supertrait, betray! + + +using Cassette + +# A context for doing nothing +Cassette.@context DoNothingCtx """ - @betray f + betray!(f, ::Type{TT}) where {TT <: Tuple} -The goal of the `@betray` macro is to take over the methods of `f` and ready -them to be compatible with traits-based dispatch, so that new submethods -defined by `@traitor` do not overwrite existing default methods (and used where -no more specific trait match is found). +Et tu? -WARNING: Please note this macro has NOT been implemented yet! (If anyone knows -how to turn a `Method` object into a new method definition, please let us know). +Take over the methods of `f` and readythem to be compatible with traits-based dispatch, +so that new submethods defined by `@traitor` do not overwrite existing default methods +(and used where no more specific trait match is found). -(See also `Traitor` and `@traitor`.) +Betraying functions is not a very kind thing to do, and can cause unintended side effects. """ -macro betray(ex) - if !isa(ex, Symbol) - error("Use @betray functionname") +function betray!(f, ::Type{TT}) where {TT<:Tuple} + whereparams = TT isa UnionAll ? typevars(TT.body) : tuple() + if !isempty(whereparams) + throw("Where parameters not currently supported") end + # TODO: make this work with TT of the form (Tuple{T, T} where T) + + m = (last ∘ collect ∘ methods)(f, TT) + mod = (m.module) + sig = tuple_type_cons(typeof(f), TT) + @assert m.sig == sig "the provided signature does not exactly match a method. Given $sig, expected $(m.sig) " + ci = (last ∘ code_lowered)(f, TT) + _fname = gensym(Symbol(f)) + + N = length((TT).parameters) + argnames = [gensym(Symbol(:arg, i)) for i ∈ 1:N] + argstyped = map(1:N) do i + T = unwrap_unionall(TT).parameters[i] + :($(argnames[i]) :: $T :: $Tuple{}) + end + fsym = Symbol(f) - return quote - if !isdefined(ex) - eval(:(function $ex end)) - else - # TODO - warn("""The @betray macro isn't currently implemented. The idea is to - take the methods of a generic function and put them into a - trait-dispatch table, so the user may then specialize these - methods further with traits while not losing all the - pre-existing definitions.""") + @eval mod begin + @generated function $_fname($(argnames...)) + return $ci end + $Traitor.@traitor $fsym($(argstyped...)) = $_fname($(argnames...)) end end + """ supertrait(trait) For a simple trait type, returns `supertype(t)`, while for a `Union` of traits is returns their common supertype (or else throws an error). """ -supertrait(t::DataType) = supertype(t) -supertrait(t::Union) = supertrait_union(t) -Base.@pure function supertrait_union(t) - traitclass = supertype(t.types[1]) - for j = 2:length(t.types) - if traitclass != supertype(t.types[j]) - error("Unions of traits must be in the same class. Got $t") +@generated function supertrait(::Type{T}) where {T} + !(T isa Union) && return supertype(T) + traitclass = supertype(T.a) + while true + Tb = T.b + if traitclass != supertype(Tb) + error("Unions of traits must be in the same class. Got $T") end + (Tb isa Union) || break + end + if traitclass == Any + error("$T does not have a supertrait.") end return traitclass end @@ -170,10 +192,15 @@ number of trait-based submethods. This generated function returns (and creates, if necessary, the first time it is called) the table joining the traits to the submethods """ -@generated function get_trait_table{Signature<:Tuple}(f::Function, ::Type{Signature}) +@generated function get_trait_table(f::Function, ::Type{Signature}) where {Signature <: Tuple} + # This is a questionable usage of a generated function. The idea is that it's an 'elegant' way + # to create an independant global dictionary for each method specification which can later be mutated. + # The problem is that if this generated function gets recompiled for some reason (which the compiler + # is allowed to do!) then the dictionary will be replaced with a new empty dict, deleting our trait + # table. d = Dict{Any, Function}() return quote - #$(Expr(:meta,:inline)) + $(Expr(:meta, :inline)) ($d) end end @@ -232,8 +259,8 @@ macro traitor(ex) push!(traits, trait) end - argnames = (argnames...) # I'm conused why $((argnames...)) doesn't work in the quote below - quotednames = (quotednames...) + argnames = (argnames...,) # I'm conused why $((argnames...)) doesn't work in the quote below + quotednames = (quotednames...,) # Make a new name for this specialized function internalname = "_"*string(funcname)*"{" @@ -246,23 +273,87 @@ macro traitor(ex) internalname = internalname * "}" internalname = Symbol(internalname) - # It's hard to get all of this right with nest quote blocks, AND it's hard - # to get this right with Expr() objects... grrr... - esc(Expr(:block, - Expr(:stagedfunction, Expr(:call, funcname, args...), Expr(:block, - :( dict = Traitor.get_trait_table($funcname, $(Expr(:curly, :Tuple, argtypes...))) ), - :( f = Traitor.trait_dispatch(dict, $(Expr(:curly, :Tuple, argnames...))) ), - Expr(:quote, Expr(:block, - Expr(:meta, :inline), - Expr(:call, Expr(:$, :f), argnames...) - )) - )), - Expr(:function, Expr(:call, internalname, args...), body), - :( d = Traitor.get_trait_table($funcname, $(Expr(:curly, :Tuple, argtypes...))) ), - :( d[$(Expr(:curly, :Tuple, traits...))] = $internalname ), - )) + dispatchname = gensym(Symbol(funcname, :_dispatched)) + + ex = quote + $Traitor.@generated function $funcname($(args...)) + dict = Traitor.get_trait_table($funcname, $(Expr(:curly, :Tuple, argtypes...))) + thunk = () -> Traitor.trait_dispatch(dict, $(Expr(:curly, :Tuple, argnames...))) + + # This cassette overdub pass literally does nothing other than normal evaluation. + # However, if I don't use it, the backedge attachment later on doesn't appear + # to have the desired effect. + $dispatchname = $Cassette.overdub($DoNothingCtx(), thunk) + #$dispatchname = thunk() + ex = (Expr(:call, $dispatchname, $(QuoteNode.(argnames)...))) + + # Create a codeinfo + ci = $expr_to_codeinfo($(__module__), [Symbol("#self#"), $(quotednames...)], [], (), ex) + + # Attached edges from MethodInstrances of the `supertrait` function to to this CodeInfo. + # This should make it so that adding members to a trait relevant to this function + # triggers recompilation, fixing the #265 equivalent for trait methods. + ci.edges = $Core.MethodInstance[] + for TT in [$(traits...)] + for T in TT.parameters + for mi in $Core.Compiler.method_instances($supertrait, Tuple{Type{T}}) + push!(ci.edges, mi) + end + end + end + + return ci + end + $internalname($(args...)) = $body + local d = $Traitor.get_trait_table($funcname, $(Expr(:curly, :Tuple, argtypes...))) + d[Tuple{$(traits...)}] = $internalname + $funcname + end + esc(ex) end +""" + expr_to_codeinfo(m::Module, argnames, spnames, sp, e::Expr) + +Take an expr (usually a generated function generator) and convert it into a CodeInfo object +(Julia's internal, linear representation of code). + +`m` is the module that the CodeInfo should be generated from (used for name resolution) + +`argnames` must be an iterable container of symbols describing the CodeInfo's input arguments. +NOTE: the first argument should be given as `Symbol("#self#")`. So if the function is `f(x) = x + 1`, +then `argnames = [Symbol("#self#"), :x]` + +`spnames` should be an iterable container of the names of the static parameters to the CodeInfo body +(e.g.) in `f(x::T) where {T <: Int} = ...`, `T` is a static parameter, so `spnames` should be `[:T]` + +`sp` should be an iterable container of the static parameters to the CodeInfo body themselves (as +opposed to their names) (e.g.) in `f(x::T) where {T <: Int} = ...`, `T` is a static parameter, +so `sp` should be `[T]` + +`e` is the actual expression to lower to CodeInfo. This must be 'pure' in the same sense as generated +function bodies. +""" +function expr_to_codeinfo(m::Module, argnames, spnames, sp, e::Expr) + lam = Expr(:lambda, argnames, + Expr(Symbol("scope-block"), + Expr(:block, + Expr(:return, + Expr(:block, + e, + ))))) + ex = if spnames === nothing || isempty(spnames) + lam + else + Expr(Symbol("with-static-parameters"), lam, spnames...) + end + + # Get the code-info for the generatorbody in order to use it for generating a dummy + # code info object. + ci = ccall(:jl_expand_and_resolve, Any, (Any, Any, Core.SimpleVector), ex, m, Core.svec(sp...)) + @assert ci isa Core.CodeInfo "Failed to create a CodeInfo from the given expression. This might mean it contains a closure or comprehension?\n Offending expression: $e" + ci +end """ @@ -271,9 +362,8 @@ this generated function is specialized on the (standard) signature of the inputs, so the only task remaining is to find the most specific matching "trait signature". """ -function trait_dispatch{Sig <: Tuple}(trait_dictionary::Dict{Any, Function}, ::Type{Sig}) +function trait_dispatch(trait_dictionary::Dict{Any, Function}, ::Type{Sig}) where {Sig <: Tuple} n_args = length(Sig.parameters) - # First check which (if any) of our trait conditions is satisfied by Sig matching_traits = Vector{Any}() for traits ∈ keys(trait_dictionary) @@ -311,7 +401,6 @@ function trait_dispatch{Sig <: Tuple}(trait_dictionary::Dict{Any, Function}, ::T if i == j continue end - if is_more_specific_traitsig(matching_traits[j], matching_traits[i]) most_specific = false break @@ -327,7 +416,6 @@ end function is_more_specific_traitsig(traits, traits2) n = length(traits.parameters) - more_specific = true for i = 1:n more_specific = more_specific & is_more_specific_traitarg(traits.parameters[i], traits2.parameters[i]) @@ -360,7 +448,6 @@ function is_more_specific_traitarg(traits, traits2) # For each trait, calculate what is going on more_specific = true - for traitclass in union(keys(d), keys(d2)) if haskey(d, traitclass) if haskey(d2, traitclass) diff --git a/test/runtests.jl b/test/runtests.jl index 5a9c73e..7a4f344 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,11 +1,11 @@ using Traitor -using Base.Test +using Test -abstract Size +abstract type Size end -immutable Big <: Size; end -immutable Medium <: Size; end -immutable Small <: Size; end +struct Big <: Size end +struct Medium <: Size end +struct Small <: Size end Size(::Union{Type{Int32},Type{Int64}}) = Small Size(::Type{BigInt}) = Big @@ -14,6 +14,7 @@ Size(::Type{Int128}) = Medium @traitor function howbig(x::Any::Big) "Huge!" end + @traitor function howbig(x::::Medium) "So-so" end @@ -39,12 +40,29 @@ end @test @inferred(supertrait(Small)) == Size @test @inferred(supertrait(Union{Small,Medium})) == Size -abstract Fooness +abstract type Fooness end -immutable FooA <: Fooness ; end -immutable FooB <: Fooness ; end +struct FooA <: Fooness end +struct FooB <: Fooness end Fooness(::Type{Any}) = FooA Fooness(::Type{Int16}) = FooB @test_throws ErrorException supertrait(Union{Small,FooB}) + + +module SomeoneElsesCode +f(x) = x + 1 +end + +import .SomeoneElsesCode: f +betray!(f, Tuple{Any}) + +@traitor f(x::::Big) = x - 1 + +@test f(1) == 2 +@test f(BigInt(1)) == 0 + + +Size(::Type{Bool}) = Small +@test howbig(true) == "Teensy..."