diff --git a/NEWS.md b/NEWS.md index 3a08ff3..08e84fd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,15 @@ Feature updates for Traits.jl ============================= +14 April 2015 +------------- + +Major overhaul of `istrait` function. Now it uses custom-programmed +checks instead of `method_exists`. This allows parameterized methods +to be used both in the `@traitdef` and in the trait implementation. +This may have pushed Traits.jl into usable terrain. Closes issues #2 +and #8. + 30 March 2015 ------------- diff --git a/README.md b/README.md index 4f831f3..e785935 100644 --- a/README.md +++ b/README.md @@ -26,57 +26,67 @@ mentioned in (1). But Julia does not support (2) or (3) yet. (2) is fairly easy to implement. However, dispatch on a "contract" is not easily possible, but Tim Holy recently came up with [a trick](https://github.com/JuliaLang/julia/issues/2345#issuecomment-54537633). -The cool thing about that trick is that the code for a trait-dispatch -function should be identical to a duck-typed function, i.e. there is -no loss in performance. +The cool thing about that trick is that the generated machine-code for +a trait-dispatch function should be identical to a duck-typed +function, i.e. there is no loss in performance. `Traits.jl` adds those kind of traits to Julia, using Tim's trick -combined with stagedfunctions. See also the Julia-issue +combined with stagedfunctions and extensive facilities to define +traits. See also the Julia-issue [#6975](https://github.com/JuliaLang/julia/issues/6975) concerning interfaces/traits. -Example: +Example `examples/ex1.jl`: ```julia using Traits -# Check Cmp-trait (comparison) which is implemented in src/commontraits.jl -@assert istrait(Cmp{Int,Float64}) -@assert istrait(Cmp{Int,String})==false +# Check Cmp-trait (comparison) which is implemented in Traits.jl/src/commontraits.jl +@assert istrait(Cmp{Int,Float64}) # Int and Float64 can be compared +@assert istrait(Cmp{Int,String})==false # Int and String cannot be compared # make a new trait and add a type to it: @traitdef MyTr{X,Y} begin - foobar(X,Y) -> Bool + foobar(X,Y) -> Bool # All type-tuples for which there is a method foo + # with that signature belong to MyTr end type A a::Int end -foobar(a::A, b::A) = a.a==b.a -@assert istrait(MyTr{A,A}) # true +@assert istrait(MyTr{A,A})==false # foobar not implement yet +foobar(a::A, b::A) = a.a==b.a # implement it +@assert istrait(MyTr{A,A}) # voila! @assert istrait(MyTr{Int,Int})==false # make a function which dispatches on traits: @traitfn ft1{X,Y; Cmp{X,Y}}(x::X,y::Y) = x>y ? 5 : 6 @traitfn ft1{X,Y; MyTr{X,Y}}(x::X,y::Y) = foobar(x,y) ? -99 : -999 -ft1(4,5) # 6 -ft1(A(5), A(6)) # -999 +ft1(4,5) # ==6 i.e. dispatches to first definition +ft1(A(5), A(6)) # ==-999 i.e. dispatches to second definition ft1("asdf", 6) # -> ERROR: TraitException("No matching trait found for function ft1") ``` -This is an experimental package and I will not try to keep backwards -compatibility as I move on. But please give it a try in your code and -give feedback. I will try to document the new features in [NEWS](NEWS.md). +# Package status + +New features are documented in [NEWS](NEWS.md) as they are added. I +keep some notes, musings and plans in [dev_notes.md](docs/dev_notes.md). + +This is a fairly experimental package and I will not try to keep +backwards compatibility as I move on. Please try it out and give me +feedback, issues or pull requests! # Syntax -(source in `examples/ex2.jl`) +The source of below examples is in `examples/ex2.jl`. Most of the +important functions are documented and will respond to `?` in the REPL. -Trait definition: +Trait definition (for details see [traitdef.md](docs/traitdef.md)): ```julia using Traits # simple @traitdef Tr1{X} begin - fun1(X) -> Number + fun1(X) -> Number # this means a method with signature fun1(::X) + # returning a Number end @traitdef Tr2{X,Y} begin fun2(X,Y) -> Number @@ -101,13 +111,12 @@ end end ``` Note that return-type checking is quite experimental. It can be -turned off by defining `Main.Traits_check_return_types=false` before -`using Traits`. +turned off with `check_return_types(false)`. -Trait implementation: +Trait implementation and checking with `istrait`: ```julia -# manual, i.e. just define the functions +# manual definiton, i.e. just define the functions fun1(x::Int) = 5x @assert istrait(Tr1{Int}) @@ -159,20 +168,18 @@ catch e end ``` -Trait functions & dispatch: +Trait functions & dispatch (for details see [traitfns.md](docs/traitfns.md)): ```julia -@traitfn tf1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y) = fun1(a) + fun1(b) -@traitfn tf1{X, Y; Tr2{X,Y}}(a::X, b::Y) = fun2(a,b) +@traitfn tf1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y) = fun1(a) + fun1(b) # I +@traitfn tf1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y, c::Int) = fun1(a) + fun1(b) + c # II +@traitfn tf1{X, Y; Tr2{X,Y}}(a::X, b::Y) = fun2(a,b) # III # Note that all the type-parameters are in the {} and that all # arguments need a type parameter (a limitation of the -# macro-parser). Bad examples are: +# macro-parser). This doesn't work: # # julia> @traitfn ttt1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y, c) = fun1(a) + fun1(b) + c # ERROR: type Symbol has no field args # -# julia> @traitfn ttt1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y, c::Int) = fun1(a) + fun1(b) + c -# ERROR: X3 not defined -# # But this works: # # julia> @traitfn ttt1{X, Y, Z; Tr1{X}, Tr1{Y}}(a::X, b::Y, c::Z) = fun1(a) + fun1(b) + c @@ -180,24 +187,31 @@ Trait functions & dispatch: # tf1 now dispatches on traits -tf1(5.,6.) # -> 77 (Float64 is part of Tr1 but not Tr2) +@assert tf1(5.,6.)==77. # -> 77 ; dispatches to I because istrait(Tr1{Float64}) + # but not istrait(Tr2{Float64,Float64}) +@assert tf1(5.,6.,77)==154. # -> 154. ; dispatches to II because of the extra argument # Errors because of dispatch ambiguity: try - tf1(5,6) # Int is part of Tr1{Int} and Tr2{Int, Int} + tf1(5,6) # istrait(Tr1{Int}) and istrait(Tr2{Int,Int}) are both true! catch e println(e) end -# adding a type to Tr1 will make it work with tf1: +# Implementing Tr1 for a type will make it work with tf1: type MyType a::Int end +try + tf1(MyType(8), 9) # not implemented yet +catch e + println(e) +end @traitimpl Tr1{MyType} begin fun1(x::MyType) = x.a+9 end -tf1(MyType(8), 9) # -> 62 +@assert tf1(MyType(8), 9)==62 # -> 62 ; dispatches to I ``` # Generated code @@ -221,7 +235,7 @@ top: ``` However, for more complicated functions code is not quite the same, -see `test/traitdispatch.jl`. +see `test/perf/perf.jl`. # Inner workings @@ -248,12 +262,12 @@ In Julia dispatch works on types, to extend this to traits I use His trick uses a function to check whether its input types satisfy certain conditions (only dependent on their type) and returns one type or another depending on the outcome. That check-function is then used -for dispatch in another function. Example of Tim's trick: +for dispatch in another function. Example of Tim's trick (`examples/ex_tims_traits.jl`): ```julia type Trait1 end type Trait2 end type Trait3 end -# now define function +# now define function f which should dispatch on those traits f(x,y) = _f(x,y, checkfn(x,y)) _f(x,y,::Type{Trait1}) = x+y _f(x,y,::Type{Trait2}) = x-y @@ -265,12 +279,12 @@ checkfn(::Int, ::Int) = Trait1 checkfn(::Int, ::FloatingPoint) = Trait2 checkfn(::FloatingPoint, ::FloatingPoint) = Trait3 # use -f(3,4) # 7 -f(3,4.) # -1.0 -f(3.,4.) # 12.0 +@assert f(3,4)==7 # Trait1 +@assert f(3,4.)==-1.0 # Trait2 +@assert f(3.,4.)==12.0 # Trait3 # add another type-tuple to Trait3 checkfn(::String, ::String) = Trait3 -f("Lorem ", "Ipsum") # "Lorem Ipsum" +@assert f("Lorem ", "Ipsum")=="Lorem Ipsum" ``` What does this add compared to what we had before with usual dispatch? @@ -334,30 +348,6 @@ trait-hierarchies into account. Although, note that it is easily possible to have unsolvable ambiguities with trait-dispatch as traits do not have a strict hierarchy like types. -# To ponder - -- For many "traits" in Julia, only a few functions need to be - implemented to provide many more. For example for comparison only - `isless` and `==` need to be implemented to automatically get `>`, - `<`, `>=`, `<=`. It would be nice to somehow specify or query those - automatic functions. - -- Are there better ways for trait-dispatch? - -- Sometimes it would be good to get at type parameters, for instance - for Arrays and the like: - ```julia - @traitdef Indexable{X{Y}} begin - getindex(X, Any) -> Y - setindex!(X, Y, Any) -> X - end - ``` - This problem is similar to triangular dispatch and may be solved - by: https://github.com/JuliaLang/julia/issues/6984#issuecomment-49751358 - -# Issues - - # Other trait implementations See the Julia-issue diff --git a/dev_notes.md b/docs/dev_notes.md similarity index 89% rename from dev_notes.md rename to docs/dev_notes.md index 452bbc5..97c9b60 100644 --- a/dev_notes.md +++ b/docs/dev_notes.md @@ -1,6 +1,35 @@ Development notes ================= +Planned work +------------ + +- [ ] Making it easy to specify traits for datatype, see issue #1 +- [ ] improve dispatch of traitfn, see issue #5 + +To ponder +--------- + +- For many "traits" in Julia, only a few functions need to be + implemented to provide many more. For example for comparison only + `isless` and `==` need to be implemented to automatically get `>`, + `<`, `>=`, `<=`. It would be nice to somehow specify or query those + automatic functions. + +- Are there better ways for trait-dispatch? + +- Sometimes it would be good to get at type parameters, for instance + for Arrays and the like: + ```julia + @traitdef Indexable{X{Y}} begin + getindex(X, Any) -> Y + setindex!(X, Y, Any) -> X + end + ``` + This problem is similar to triangular dispatch and may be solved + by: https://github.com/JuliaLang/julia/issues/6984#issuecomment-49751358 + + Road blocks ----------- diff --git a/docs/traitdef.md b/docs/traitdef.md new file mode 100644 index 0000000..4958772 --- /dev/null +++ b/docs/traitdef.md @@ -0,0 +1,112 @@ +About trait definitions +======================= + +It turns out that trait definitions based on methods signatures are a +rather tricky business because Julia allows for quite intricate method +definitions. + +For a generic function `f` and a trait definition +```julia +@traitdef Tr{X} begin + f{...}(...) -> ... +end +``` +what does it mean that `istrait(Tr{T})==true` for some type `T`? +First a slight detour on the meaning of `{...}(...)`, this is +essentially a type tuple with constraints on the actual types in +`(...)` in `{...}`. Inside a `Method` `m` these two parts are stored +in `m.tvars` (the `{}`) and `m.sig` (the `()`), which I will use below. + + +What I implemented (discounting bugs) are the following rules: + +## Method call signature + +The method call signature, the `{...}(...)` <=> `tm.tvars`, `tm.sig` +part of above definition, is satisfied if at least one method `fm` of +generic function `f` satisfies: + +A) `tm.sig<:fm.sig` i.e. just the type tuple of the trait-method is a + subtype of the generic-fn-method. + +B) The parametric constraints parameters on `fm.sig` and `tm.sig` need + to feature in the same argument positions. Except when the + corresponding function parameter is constraint by a concrete type: + then make sure that all the occurrences are the same concrete type. + +So, as long as neither the trait-method nor the generic-fn-method has +any parametric constraints, it's easy. It's just the subtyping +relation between the two. However, once parametric constraints are +use on either or both then it is complicated. + +Examples + +The same constraints on both methods: +```julia +@traitdef Pr0{X} begin + fn75{Y <: Integer}(X, Y) +end +fn75{Y<:Integer}(x::UInt8, y::Y) = y+x +@test istrait(Pr0{UInt8}) +```` + +Only the last constraint is general enough to assure `fn77` will be +callable for all `X` which are `Pr2{X}`: +```julia +@traitdef Pr2{X} begin + fn77{Y<:Number}(X,Y,Y) +end +fn77(a::Array,b::Int, c::Float64) = a[1] +@test !istrait(Pr2{Array}) +fn77{Y<:Real}(a::Array,b::Y, c::Y) = a[1] +@test !istrait(Pr2{Array}) +fn77{Y<:Number}(a::Array,b::Y, c::Y) = a[1] +@test istrait(Pr2{Array}) +``` + +A trait-method with constraints can be implemented with a method +without constraints for a concrete type: +```julia +@traitdef Pr3{X} begin + fn78{T<:X}(T,T) +end +fn78(b::Int, c::Int) = b +# This works because fn78 can be called for all arguments (Int,): +@test istrait(Pr3{Int}) + +fn78(b::Real, c::Real) = b +# This fails because the call fn78(5, 6.) is possible but not allowed +# by Pr3: +@test !istrait(Pr3{Real}) +# Now all good: +fn78{T}(b::T, c::T) = b +@test istrait(Pr3{Real}) +@test istrait(Pr3{Any}) +``` + +The other way around is similar +```julia +@traitdef Pr07{X} begin + fnpr07(X, X, Integer) # no parametric-constraints +end +fnpr07{T<:Integer}(::T, ::T, ::Integer) = 1 +# This is not true as for instance a call fnpr07(8, UInt(8)) would fail: +@test !istrait(Pr07{Integer}) +# This is fine as any call (Int,Int) will succeed: +@test istrait(Pr07{Int}) +``` + +There are a lot more examples in `test/traitdef.jl`. Most of this +functionality is implemented in the `isfitting` function. + +## Method return signature + +The specifed return type in the `@traitdef` (`tret`) and return-type +interfered with `Base.treturn_types` of the generic function has to be +`fret<:tret`. Note that this is backwards to argument types checking +above, which makes sense as the variance of functions arguments and +return types is different. + +Note that currently there is no check that the method which satisfies +the 'method call signature' test is the same method which satisfies +the 'return signature' test. diff --git a/docs/traitfns.md b/docs/traitfns.md new file mode 100644 index 0000000..6ccb87f --- /dev/null +++ b/docs/traitfns.md @@ -0,0 +1,8 @@ +About trait functions +===================== + +ToDo + +Dispatch +-------- +See issue #5 and links diff --git a/examples/ex1.jl b/examples/ex1.jl index ee4630d..d9bf345 100644 --- a/examples/ex1.jl +++ b/examples/ex1.jl @@ -1,35 +1,33 @@ using Traits -using Base.Test - -# check some traits implemented in src/commontraits.jl -@assert istrait(Cmp{Int,Float64}) -@assert istrait(Cmp{Int,String})==false +# Check Cmp-trait (comparison) which is implemented in Traits.jl/src/commontraits.jl +@assert istrait(Cmp{Int,Float64}) # Int and Float64 can be compared +@assert istrait(Cmp{Int,String})==false # Int and String cannot be compared # make a new trait and add a type to it: @traitdef MyTr{X,Y} begin - foobar(X,Y) -> Bool + foobar(X,Y) -> Bool # All type-tuples for which there is a method foo + # with that signature belong to MyTr end type A a::Int end -foobar(a::A, b::A) = a.a==b.a -@assert istrait(MyTr{A,A}) # true +@assert istrait(MyTr{A,A})==false # foobar not implement yet +foobar(a::A, b::A) = a.a==b.a # implement it +@assert istrait(MyTr{A,A}) # voila! @assert istrait(MyTr{Int,Int})==false # make a function which dispatches on traits: @traitfn ft1{X,Y; Cmp{X,Y}}(x::X,y::Y) = x>y ? 5 : 6 @traitfn ft1{X,Y; MyTr{X,Y}}(x::X,y::Y) = foobar(x,y) ? -99 : -999 -ft1(4,5) # 6 -ft1(A(5), A(6)) # -999 - -# # dispatch on traits has its pitfalls: -# @traitfn ft1{X,Y; Arith{X,Y}}(x::X,y::Y) = x+y +ft1(4,5) # ==6 i.e. dispatches to first definition +ft1(A(5), A(6)) # ==-999 i.e. dispatches to second definition -# # now it's impossible to decide which method of ft1 to pick -# @test_throws TraitException ft1(4,5) - -@test_throws TraitException ft1("asdf", 5) +try + ft1("asdf", 5) +catch err + println(err) +end foobar(a::String, b::Int) = length(a)==b ft1("asdf", 5) @@ -56,5 +54,5 @@ bar(a::B2, b::B2) = a.a==b.a # here for Julia to infer the # return type -@test gt1(B1(1), B1(1))=="MyTr" -@test gt1(B2(1), B2(1))=="MyTr2" +@assert gt1(B1(1), B1(1))=="MyTr" +@assert gt1(B2(1), B2(1))=="MyTr2" diff --git a/examples/ex2.jl b/examples/ex2.jl index 586766c..60ba912 100644 --- a/examples/ex2.jl +++ b/examples/ex2.jl @@ -4,7 +4,8 @@ using Traits # simple @traitdef Tr1{X} begin - fun1(X) -> Number + fun1(X) -> Number # this means a method with signature fun1(::X) + # returning a Number end @traitdef Tr2{X,Y} begin fun2(X,Y) -> Number @@ -78,31 +79,49 @@ end ### Trait functions ################### -@traitfn tf1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y) = fun1(a) + fun1(b) -@traitfn tf1{X, Y; Tr2{X,Y}}(a::X, b::Y) = fun2(a,b) +@traitfn tf1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y) = fun1(a) + fun1(b) # I +@traitfn tf1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y, c::Int) = fun1(a) + fun1(b) + c # II +@traitfn tf1{X, Y; Tr2{X,Y}}(a::X, b::Y) = fun2(a,b) # III +# Note that all the type-parameters are in the {} and that all +# arguments need a type parameter (a limitation of the +# macro-parser). This doesn't work: +# +# julia> @traitfn ttt1{X, Y; Tr1{X}, Tr1{Y}}(a::X, b::Y, c) = fun1(a) + fun1(b) + c +# ERROR: type Symbol has no field args +# +# But this works: +# +# julia> @traitfn ttt1{X, Y, Z; Tr1{X}, Tr1{Y}}(a::X, b::Y, c::Z) = fun1(a) + fun1(b) + c +# ttt1 (generic function with 6 methods) # tf1 now dispatches on traits -tf1(5.,6.) # -> 77 +@assert tf1(5.,6.)==77. # -> 77 ; dispatches to I because istrait(Tr1{Float64}) + # but not istrait(Tr2{Float64,Float64}) +@assert tf1(5.,6.,77)==154. # -> 154. ; dispatches to II because of the extra argument # Errors because of dispatch ambiguity: try - tf1(5,6) + tf1(5,6) # istrait(Tr1{Int}) and istrait(Tr2{Int,Int}) are both true! catch e println(e) end -# adding a type to Tr1 will make it work with tf1: +# Implementing Tr1 for a type will make it work with tf1: type MyType a::Int end +try + tf1(MyType(8), 9) # not implemented yet +catch e + println(e) +end @traitimpl Tr1{MyType} begin fun1(x::MyType) = x.a+9 end +@assert tf1(MyType(8), 9)==62 # -> 62 ; dispatches to I -tf1(MyType(8), 9) # -> 62 - -### Generated code -################## +### Generated machine code +########################## f(x,y) = 7x + 7y @code_llvm f(5.,6.) @code_llvm tf1(5.,6.) diff --git a/examples/ex_tims_traits.jl b/examples/ex_tims_traits.jl new file mode 100644 index 0000000..3edddef --- /dev/null +++ b/examples/ex_tims_traits.jl @@ -0,0 +1,21 @@ +type Trait1 end +type Trait2 end +type Trait3 end +# now define function f which should dispatch on those traits +ftim(x,y) = _ftim(x,y, checkfn(x,y)) +_ftim(x,y,::Type{Trait1}) = x+y +_ftim(x,y,::Type{Trait2}) = x-y +_ftim(x,y,::Type{Trait3}) = x*y +# default +checkfn{T,S}(x::T,y::S) = error("Function ftim not implemented for type ($T,$S)") +# associate types-tuples to Trait1, Trait2 or Trait3: +checkfn(::Int, ::Int) = Trait1 +checkfn(::Int, ::FloatingPoint) = Trait2 +checkfn(::FloatingPoint, ::FloatingPoint) = Trait3 +# use +@assert ftim(3,4)==7 # Trait1 +@assert ftim(3,4.)==-1.0 # Trait2 +@assert ftim(3.,4.)==12.0 # Trait3 +# Add another type-tuple to Trait3 +checkfn(::String, ::String) = Trait3 +@assert ftim("Lorem ", "Ipsum")=="Lorem Ipsum" diff --git a/src/Traits.jl b/src/Traits.jl index 6a682df..13ff2bb 100644 --- a/src/Traits.jl +++ b/src/Traits.jl @@ -8,7 +8,7 @@ module Traits - they are structural types: i.e. they needn't be declared explicitly """ -> current_module() -export istrait, istraittype, issubtrait, +export istrait, istraittype, issubtrait, check_return_types, traitgetsuper, traitgetpara, traitmethods, @traitdef, @traitimpl, @traitfn, TraitException, All @@ -16,55 +16,88 @@ if !(VERSION>v"0.4-") error("Traits.jl needs Julia version 0.4.-") end -# Flags: by setting them in Main before using, they can be turned on -# or off. +## patches for bugs in base +include("base_fixes.jl") + +## common helper functions +include("helpers.jl") + +####### +# Flags +####### +# By setting them in Main before using, they can be turned on or off. +# TODO: update to use functions. if isdefined(Main, :Traits_check_return_types) println("Traits.jl: not using return types of @traitdef functions") - flag_check_return_types = Main.Traits_check_return_types + const flag_check_return_types = Main.Traits_check_return_types else - flag_check_return_types = true + const flag_check_return_types = true end @doc "Flag to select whether return types in @traitdef's are checked" flag_check_return_types +@doc "Toggles return type checking. Will issue warning because of const declaration, ignore:"-> +function check_return_types(flg::Bool) + global flag_check_return_types + flag_check_return_types = flg +end + +####### +# Types +####### @doc """`abstract Trait{SUPER}` - All traits are direct decedents of abstract type Trait. The type parameter - SUPER of Trait is needed to specify super-traits (a tuple).""" -> + All traits are direct decedents of abstract type Trait. The type parameter + SUPER of Trait is needed to specify super-traits (a tuple).""" -> abstract Trait{SUPER} +# Type of methods field of concrete traits: +typealias FDict Dict{Union(Function,DataType),Function} + # A concrete trait type has the form ## Tr{X,Y,Z} <: Trait{(ST1{X,Y},ST2{Z})} # -# immutable Tr{X,Y,Z} <: Trait{(ST1{X,Y},ST2{Z})} -# methods -# Tr() = new(methods_made_in_macro) +# immutable Tr1{X1} <: Traits.Trait{()} +# methods::FDict +# constraints::Vector{Bool} +# assoctyps::Vector{Any} +# Tr1() = new(FDict(methods_defined), Bool[], []) # end # -# where methods holds the function signatures, like so: +# where methods field holds the function signatures, like so: # Dict{Function,Any} with 3 entries: -# next => ((Int64,Any),(Any...,)) -# done => ((Int64,Any),(Bool,)) -# start => ((Int64,),(Any...,)) +# start => _start(Int64) = (Any...,) +# next => _next(Int64,Any) = (Any...,) +# done => _done(Int64,Any) = Bool # used to dispatch to helper methods -immutable _TraitDispatch end +immutable _TraitDispatch end immutable _TraitStorage end -@doc """Type All is to denote that any type goes in type signatures in - @traitdef. This is a bit awkward: - - - method_exists(f, s) returns true if there is a method of f with - signature sig such that s<:sig. Thus All<->Union() - - Base.return_types works the other way around, there All<->Any - - See also https://github.com/JuliaLang/julia/issues/8974"""-> -abstract All +# @doc """Type All is to denote that any type goes in type signatures in +# @traitdef. This is a bit awkward: + +# - method_exists(f, s) returns true if there is a method of f with +# signature sig such that s<:sig. Thus All<->Union() +# - Base.return_types works the other way around, there All<->Any + +# See also https://github.com/JuliaLang/julia/issues/8974"""-> +# abstract All # General trait exception type TraitException <: Exception msg::String end +# Helper dummy types used in istrait below +abstract _TestType{T} +immutable _TestTvar{T}<:_TestType{T} end # used for TypeVar testing +#Base.show{T<:_TestType}(io::IO, x::Type{T}) = print(io, string(x.parameters[1])*"_") + +######### +# istrait, one of the core functions +######### + +# Update after PR #10380 @doc """Tests whether a DataType is a trait. (But only istrait checks whether it's actually full-filled)""" -> istraittype(x) = false @@ -72,101 +105,113 @@ istraittype{T<:Trait}(x::Type{T}) = true istraittype(x::Tuple) = mapreduce(istraittype, &, x) @doc """Tests whether a set of types fulfill a trait. - A Trait Tr is defined for some parameters if: + A Trait Tr is defined for some parameters if: - - all the functions of a trait are defined for them - - all the trait constraints are fulfilled + - all the functions of a trait are defined for them + - all the trait constraints are fulfilled - Example: + Example: - `istrait(Tr{Int, Float64})` + `istrait(Tr{Int, Float64})` - or with a tuple of traits: + or with a tuple of traits: - `istrait( (Tr1{Int, Float64}, Tr2{Int}) )` - """ -> + `istrait( (Tr1{Int, Float64}, Tr2{Int}) )` + """ -> function istrait{T<:Trait}(Tr::Type{T}; verbose=false) + if verbose + println_verb(x) = println("**** Checking $(deparameterize_type(Tr)): " * x) + else + println_verb = x->x + end + if !hasparameters(Tr) throw(TraitException("Trait $Tr has no type parameters.")) end # check supertraits !istrait(traitgetsuper(Tr); verbose=verbose) && return false - # check methods definitions - try - Tr() + + # check instantiating + tr = nothing + try + tr = Tr() catch err - if verbose - println("""Could not instantiate instance for type encoding the trait $Tr. - Failed with error: $err""") - end + println_verb("""Could not instantiate instance for type encoding the trait $Tr. + This usually indicates that something is amiss with the @traitdef + or that one of the generic functions is not defined. + The error was: $err""") + return false + end + + # check constraints + if !all(tr.constraints) + println_verb("Not all constraints are satisfied for $T") return false end - out = true - # check call signature of methods: - for (meth,sig) in Tr().methods - # instead of: - ## checks = length(methods(meth, sig[1]))>0 - # Now using method_exists. But see bug - # https://github.com/JuliaLang/julia/issues/8959 - - sigg = map(x->x===All ? Union() : x, sig[1]) - if isa(meth, Function) - if !method_exists(meth, sigg) # I think this does the right thing. - if verbose - println("Method $meth with call signature $(sig[1]) not defined for $T") + + # Check call signature of all methods: + for (gf,_gf) in tr.methods + println_verb("*** Checking function $gf") + # Loop over all methods defined for each function in traitdef + for tm in methods(_gf) + println_verb("** Checking method $tm") + checks = false + # Only loop over methods which have the right number of arguments: + for fm in methods(gf, NTuple{length(tm.sig),Any}) + if isfitting(tm, fm, verbose=verbose) + checks = true + break end - out = false end - elseif isa(meth, DataType) # a constructor, presumably. - # But discard the catch all to convert, i.e. this means - # method_exists(call, (Type{meth}, sigg...))==true for all types - chatch_all = methods(call, (Type{Array},)) - if methods(call, tuple(Type{meth}, sigg...))==chatch_all - if verbose - println("Datatype constructor $meth with call signature $sigg not defined for trait $T") - end - out = false + if !checks # if check==false no fitting method was found + println_verb("""No method of the generic function/call-overloaded `$gf` matched the + trait specification: `$tm`""") + return false end - else - throw(TraitException("Trait $Tr has something funny in its method dictionary: $meth.")) end end - # check return-type - if flag_check_return_types && out # only check if all methods were defined - for (meth,sig) in Tr().methods - # replace All in sig[1] with Any - sigg = map(x->x===All ? Any : x, sig[1]) - tmp = Base.return_types(meth, sigg) - if length(tmp)==0 - rettype = [] - out = false - if verbose - println("Method `$meth` with signature $sigg->$(sig[2]) has an empty return signature!") + + # TODO: only check return-types for methods which passed call-signature checks. + + # check return-type. Specifed return type tret and return-type of + # the methods frets should fret<:tret. This is backwards to + # argument types checking above. + if flag_check_return_types + for (gf,_gf) in tr.methods + println_verb("*** Checking return types of function $gf") + for tm in methods(_gf) # loop over all methods defined for each function in traitdef + println_verb("** Checking return types of method $tm") + tret_typ = Base.return_types(_gf, tm.sig) # trait-defined return type + if length(tret_typ)==0 + continue # this means the signature contains None which is not compatible with return types + # TODO: introduce a special type signaling that no return type was given. + elseif length(tret_typ)>1 + if !allequal(tret_typ) # Ok if all return types are the same. + throw(TraitException("Querying the return type of the trait-method $tm did not return exactly one return type: $tret_typ")) + end end - else#if length(tmp)==1 - rettype = tmp[1] - if !(rettype<:sig[2]) - out = false - if verbose - println("Method `$meth` with signature $sigg->$(sig[2]) has wrong return type: $rettype") + tret_typ = tret_typ[1] + fret_typ = Base.return_types(gf, tm.sig) + # at least one of the return types need to be a subtype of tret_typ + checks = false + for fr in fret_typ + if fr<:tret_typ + checks = true end end - # else - # out = false - # if verbose - # println("Method `$meth` with signature $sigg->$(sig[2]) has more than one return type!") - # end + if !checks + println_verb("""For function $gf: no return types found which are subtypes of the specified return type: + $tret_typ + List of found return types: + $fret_typ + Returning false. + """) + return false + end end end end - # check constraints - if !all(Tr().constraints) - if verbose - println("Not all constraints are satisfied for $T") - end - return false - end - return out + return true end # check a tuple of traits against a signature function istrait(Trs::Tuple; verbose=false) @@ -176,12 +221,298 @@ function istrait(Trs::Tuple; verbose=false) return true end +## Helpers for istrait +immutable FakeMethod + sig::(Any...,) + tvars::(Any...,) + va::Bool +end +@doc """isfitting checks whether the signature of a method `tm` + specified in the trait definition is fulfilled by one method `fm` + of the corresponding generic function. This is the core function + which is called by istraits. + + Checks that tm.sig<:fm.sig and that the parametric constraints on + fm and tm are equal where applicable. Lets call this relation tm<<:fm. + + So, summarizing, for a trait-signature to be satisfied (fitting) + the following condition need to hold: + + A) `tsig<:sig` for just the types themselves (sans parametric + constraints) + + B) The parametric constraints parameters on `sig` and `tsig` need + to feature in the same argument positions. Except when the + corresponding function parameter is constraint by a concrete + type: then make sure that all the occurrences are the same + concrete type. + + Examples, left trait-method, right implementation-method: + {T<:Real, S}(a::T, b::Array{T,1}, c::S, d::S) <<: {T<:Number, S}(a::T, b::AbstractArray{T,1}, c::S, d::S) + -> true + + {T<:Integer}(T, T, Integer) <<: {T<:Integer}(T, T, T) + -> false as parametric constraints are not equal + """ -> +function isfitting(tmm::Method, fmm::Method; verbose=false) # tm=trait-method, fm=function-method + println_verb = verbose ? println : x->x + + # Make a "copy" of tmm & fmm as it may get updated: + tm = FakeMethod(tmm.sig, isa(tmm.tvars,Tuple) ? tmm.tvars : (tmm.tvars,), tmm.va) + fm = FakeMethod(fmm.sig, isa(fmm.tvars,Tuple) ? fmm.tvars : (fmm.tvars,), fmm.va) + # Note the `? : ` is needed because of https://github.com/JuliaLang/julia/issues/10811 + + # Replace type parameters which are constraint by a concrete type + # (because Vector{TypeVar(:V, Int)}<:Vector{Int}==false but we need ==true) + tm = replace_concrete_tvars(tm) + fm = replace_concrete_tvars(fm) + + # Special casing for call-overloading. + if fmm.func.code.name==:call && tmm.func.code.name!=:call # true if only fm is call-overloaded + # prepend ::Type{...} to signature + tm = FakeMethod(tuple(fm.sig[1], tm.sig...), tm.tvars, tm.va) + # check whether there are method parameters too: + for ftv in fm.tvars + flocs = find_tvar(fm.sig, ftv) + if flocs[1] # yep, has a constraint like call{T}(::Type{Array{T}},...) + if sum(flocs)==1 + tm = FakeMethod(tm.sig, tuple(ftv, tm.tvars...) , tm.va) + else + println_verb("This check is not implemented, returning false.") + # Note that none of the 1000 methods of call in + # Base end up here. + return false + end + end + end + # There is a strange bug which is prevented by this never + # executing @show. I'll try and investigate this in branch + # m3/heisenbug + if length(tm.sig)==-10 + @show tm + error("This is not possible") + end + end + + ## Check condition A: + # If there are no function parameters then just compare the + # signatures. + if tm.tvars==() && fm.tvars==() + println_verb("Reason fail/pass: no tvars in trait-method only checking signature. Result: $(tm.sig<:fm.sig)") + return tm.sig<:fm.sig + end + # If !(tm.sig<:fm.sig) then tm<<:fm is false + # but the converse is not true: + if !(tm.sig<:fm.sig) + println_verb("""Reason fail: !(tm.sig<:fm.sig) + tm.sig = $(tm.sig) + fm.sig = $(fm.sig)""") + return false + end + # False if there are not the same number of arguments: (I don't + # think this test is necessary as it is tested above.) + if length(tm.sig)!=length(fm.sig)! + println_verb("Reason fail: not same argument length.") + return false + end + # Getting to here means that that condition (A) is fulfilled. + + ## Check condition B: + # If there is only one argument then we're done as parametric + # constraints play no role: + if length(tm.sig)==1 + println_verb("Reason pass: length(tm.sig)==1") + return true + end + + # First special case if tm.tvars==() && !(fm.tvars==()) + if tm.tvars==() + fm.tvars==() && error("Execution shouldn't get here as this should have been checked above!") + for (i,ftv) in enumerate(fm.tvars) + # If all the types in tm.sig, which correspond to a + # parameter constraint argument of fm.sig, are the same then pass. + typs = tm.sig[find_tvar(fm.sig, ftv)] + if length(typs)==0 + println_verb("Reason fail: this method $fmm is not callable because the static parameter does not occur in signature.") + return false + elseif length(typs)==1 # Necessarily the same + continue + else # length(typs)>1 + if !all(map(isleaftype, typs)) # note isleaftype can have some issues with inner constructors + println_verb("Reason fail: not all parametric-constraints in function-method $fmm are on leaftypes in traitmethod $tmm.") + return false + else + # Now check that all of the tm.sig-types have the same type at the parametric-constraint sites. + if !allequal(find_correponding_type(tm.sig, fm.sig, ftv)) + println_verb("Reason fail: not all parametric-constraints in function-method $fmm correspond to the same type in traitmethod $tmm.") + return false + end + end + end + end + println_verb("""Reason pass: All occurrences of the parametric-constraint in $fmm correspond to the + same type in trait-method $tmm.""") + return true + end + + # Strategy: go through constraints on trait-method and check + # whether they are fulfilled in function-method. + for tv in tm.tvars + # find all occurrences in the signature + locs = find_tvar(tm.sig, tv) + if !any(locs) + throw(TraitException("The parametric-constraint of trait-method $tmm has to feature in at least one argument of the signature.")) + end + # Find the tvar in fm which corresponds to tv. + ftvs = Any[] + for ftv in fm.tvars + flocs = find_tvar(fm.sig, ftv) + if all(flocs[find(locs)]) + push!(ftvs,ftv) + end + end + + if length(ftvs)==0 + ## This should pass, because the trait-parameter is a leaftype: + # @traitdef Tr01{X} begin + # g01{T<:X}(T, T) -> T + # end + # g01(::Int, ::Int) = Int + # @assert istrait(Tr01{Int}, verbose=true) + if isleaftype(tv.ub) # note isleaftype can have some issues with inner constructors + # Check if the method definition of fm has the same + # leaftypes in the same location. + if mapreduce(x -> x==tv.ub, &, true, fm.sig[locs]) + println_verb("Reason pass: parametric constraints only on leaftypes.") + return true + end + end + println_verb("Reason fail: parametric constraints on function method not as severe as on trait-method.") + return false + end + + # Check that they constrain the same thing in each argument. + # E.g. this should fail: {K,V}(::Dict{K,V}, T) <<: {T}(::Dict{V,K}, T). + # Do this by substituting a concrete type into the respective + # TypeVars and check that arg(tv')<:arg(ftv') + checks = false + for ft in ftvs + for i in find(locs) + targ = subs_tvar(tv, tm.sig[i], _TestTvar{i}) + farg = subs_tvar(ft, fm.sig[i], _TestTvar{i}) + checks = checks || (targ<:farg) + end + end + if !checks + println_verb("Reason fail: parametric constraints on args $(tm.sig[i]) and $(fm.sig[i]) on different TypeVar locations!") + return false + end + end + + println_verb("Reason pass: all checks passed") + return true +end + +# helpers for isfitting +function subs_tvar(tv::TypeVar, arg::DataType, TestT::DataType) + # Substitute `TestT` for a particular TypeVar `tv` in an argument `arg`. + # + # Example: + # Array{I<:Int64,N} -> Array{_TestTvar{23},N} + if isleaftype(arg) || length(arg.parameters)==0 # concrete type or abstract type with no parameters + return arg + else # It's a parameterized type: do substitution on all parameters: + pa = [ subs_tvar(tv, arg.parameters[i], TestT) for i=1:length(arg.parameters) ] + typ = deparameterize_type(arg) + return typ{pa...} + end +end +subs_tvar(tv::TypeVar, arg::TypeVar, TestT::DataType) = tv===arg ? TestT : arg # note === this it essential! +subs_tvar(tv::TypeVar, arg, TestT::DataType) = arg # for anything else + +function replace_concrete_tvars(m::FakeMethod) + # Example: + # FakeMethod((T<:Int64,Array{T<:Int64,1},Integer),(T<:Int64,),false) + # -> + # FakeMethod((Int64, Array{Int64,1}, Integer),() ,false) + newtv = [] + newsig = Any[m.sig...] # without the Any I get seg-faults and + # other strange erros! + for tv in m.tvars + if !isleaftype(tv.ub) + push!(newtv, tv) + else + newsig = Any[subs_tvar(tv, arg, tv.ub) for arg in newsig] + end + end + FakeMethod(tuple(newsig...), tuple(newtv...), m.va) +end + +# Finds the types in tmsig which correspond to TypeVar ftv in fmsig +function find_correponding_type(tmsig::Tuple, fmsig::Tuple, ftv::TypeVar) + out = Any[] + for (ta,fa) in zip(tmsig,fmsig) + if isa(fa, TypeVar) + fa===ftv && push!(out, ta) + elseif isa(fa, DataType) || isa(fa, Tuple) + append!(out, find_correponding_type(ta,fa,ftv)) + else + @show ta, fa + error("Not implemented") + end + end + return out +end +function find_correponding_type(ta::DataType, fa::DataType, ftv::TypeVar) + # gets here if fa is not a TypeVar + out = Any[] + if !( deparameterize_type(ta)<:deparameterize_type(fa)) # || + # length(ta.parameters)!=length(fa.parameters) # don't check for length. If not the same length, assume that the first parameters are corresponding... + push!(out, _TestType{:no_match}) # this will lead to a no-match in isfitting + return out + end + for (tp,fp) in zip(ta.parameters,fa.parameters) + if isa(fp, TypeVar) + fp===ftv && push!(out, tp) + elseif isa(fp, DataType) || isa(fa, Tuple) + append!(out, find_correponding_type(tp,fp,ftv)) + end + end + return out +end + +# find_tvar finds index of arguments in a function signature `sig` where a +# particular TypeVar `tv` features. Example: +# +# find_tvar( (T, Int, Array{T}) -> [true, false, true] +function find_tvar(sig::Tuple, tv) + ns = length(sig) + out = falses(ns) + for i = 1:ns + out[i] = any(find_tvar(sig[i], tv)) + end + return out +end +find_tvar(sig::TypeVar, tv) = sig===tv ? [true] : [false] # note ===, this it essential! +function find_tvar(arg::DataType, tv) + ns = length(arg.parameters) + out = false + for i=1:ns + out = out || any(find_tvar(arg.parameters[i], tv)) + end + return [out] +end +find_tvar(sig, tv) = [false] + +###################### +# Sub and supertraits: +###################### @doc """Returns the super traits""" -> traitgetsuper{T<:Trait}(t::Type{T}) = t.super.parameters[1]::Tuple traitgetpara{T<:Trait}(t::Type{T}) = t.parameters @doc """Checks whether a trait, or a tuple of them, is a subtrait of - the second argument.""" -> + the second argument.""" -> function issubtrait{T1<:Trait,T2<:Trait}(t1::Type{T1}, t2::Type{T2}) if t1==t2 return true @@ -201,7 +532,7 @@ function issubtrait{T1<:Trait}(t1::Type{T1}, t2::Tuple) # the empty trait is the super-trait of all traits true else - error("") + throw(TraitException("")) end end @@ -218,12 +549,6 @@ function issubtrait(t1::Tuple, t2::Tuple) return checks end -## patches for bugs in base -include("base_fixes.jl") - -## common helper functions -include("helpers.jl") - ## Trait definition include("traitdef.jl") diff --git a/src/base_fixes.jl b/src/base_fixes.jl index ed09bc5..6979665 100644 --- a/src/base_fixes.jl +++ b/src/base_fixes.jl @@ -11,3 +11,8 @@ function Base.func_for_method(m::Method, tt, env) end end println(" endof ok-warning.") + + +# eltype for dicts +Base.eltype{K}(::Type{Associative{K}}) = (K,Any) +Base.eltype(::Type{Associative}) = (Any,Any) diff --git a/src/commontraits.jl b/src/commontraits.jl index b99fe01..ceeac31 100644 --- a/src/commontraits.jl +++ b/src/commontraits.jl @@ -16,8 +16,8 @@ end @traitdef Iter{X} begin # type-functions based on return_type: - State = Base.return_types(start, (X,))[1] # this is circular but that is ok, as trait needs to be implemented. - Item = Base.return_types(next, (X,State))[1][1] + State = Base.return_types(start, (X,))[1] + Item = Base.return_types(next, (X,State))[1][1] # use eltype instead # interface functions start(X) -> State @@ -44,9 +44,8 @@ end @traitdef Indexable{X} <:Collection{X} begin El = eltype(X) - # TODO issue https://github.com/JuliaLang/julia/issues/9135: - #getindex(X, All) - #setindex!(X, El, All) + getindex(X, None) # when using None no return types can be used... + setindex!(X, El, None) length(X) -> Integer # automatically provided: @@ -56,19 +55,19 @@ end @traitdef Assoc{X} <: Indexable{X} begin K,V = eltype(X) - + # note, ObjectId dict is not part of this interface - haskey(X, All) - get(X, All, All) - get(Function, X, All) - get!(X, All, All) - get!(Function, X, All) - getkey(X, All, All) - delete!(X, All) -> X - pop!(X, All) - pop!(X, All, All) - merge(X, All...) -> X - merge!(X, All...) + haskey(X, Any) + get(X, Any, Any) + get(Function, X, Any) + get!(X, Any, Any) + get!(Function, X, Any) + getkey(X, Any, Any) + delete!(X, Any) -> X + pop!(X, Any) + pop!(X, Any, Any) + # merge(X, Any...) -> X + # merge!(X, Any...) # provieds # keys(X) -> Base.KeyIterator # values(X) -> Base.ValueIterator diff --git a/src/helpers.jl b/src/helpers.jl index 7a1b08c..9d0dac6 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -6,7 +6,8 @@ export deparameterize_type It is often useful to make an associated type with this to match against methods which do not specialize on the type parameters. """ -> -deparameterize_type(A::Type) = eval(A.name.module, A.name.name)::DataType +deparameterize_type(A::DataType) = eval(A.name.module, A.name.name)::DataType +deparameterize_type(A::TypeConstructor) = error("TypeConstructor not supported by deparameterize_type.") # Internal helpers ################## @@ -37,6 +38,8 @@ function Base.done(lns::Lines, nr) end end +@doc "Checks all elements of a collection are equal" -> +allequal(x) = all([x[1]==xx for xx in x]) ## Parsing #### @@ -82,18 +85,6 @@ function tvar2tvar!(exs::Vector{Any}) nothing end -# function return_types_v2(f::Function, typs::ANY) -# # for some reason this function take forever to JIT. (about 4 secs!) -# # see https://github.com/JuliaLang/julia/issues/9131 -# a = code_typed(f, typs) -# if length(a)>1 -# error("several return types") -# elseif length(a)==0 -# error("no return types") -# end -# a[1].args[end].typ -# end - # check whether a type is parameterized isparameterized(t::DataType) = length(t.parameters)==0 ? false : true @@ -109,244 +100,10 @@ function hasparameters(t::DataType) end end -# # check whether a function is parameterized -# function isparameterized(m::Method) -# if isa(m.tvars, Tuple) -# return length(m.tvars)==0 ? false : true -# else -# return true -# end -# end - -# Base.subtypes(x::(ANY...)) = Base.subtypes(Main, x::(ANY...)) -# function Base.subtypes(m::Module, ts::(ANY...); maxout=100_000) -# # Extends subtypes to work with tuple-types. -# # Returns at most maxout (default=100_000) types. -# slots = Array(Tuple, length(ts)) -# allempty = true -# for (i,t) in enumerate(ts) -# st = subtypes(t) -# if length(st)==0 -# slots[i] = (t,) # if a slot is empty use t -# else -# slots[i] = tuple(st...) -# allempty = false -# end -# end -# allempty && return Any[] -# # Make all possible combinations (could be many!) -# out = combos(slots, maxout) -# end - -# function combos{T<:Tuple}(typs::Vector{T}, maxout; verbose=true) -# # makes all possible combinations. See test/helpers.jl -# len = length(typs) -# lens = map(length, typs) -# n = min(prod(lens), maxout) -# if n==maxout && verbose -# println("subtypes: not returning all $(prod(lens)) type tuples, only $maxout") -# end -# inds = ones(Int,len) -# out = Array(Tuple, n) -# for j=1:n -# try -# out[j] = tuple([typs[ii][i] for (ii,i) in enumerate(inds)]...) -# catch e -# @show j, inds -# end -# # updated inds -# inds[1] += 1 -# for i=1:len-1 -# if inds[i]>lens[i] -# inds[i] = 1 -# inds[i+1] += 1 -# end -# end -# end -# return out -# end - -# # Does the same as method_exists except that it also works for -# # abstract/parameterized types by considering whether method_exists -# # for all concrete subtypes. -# # -# # I.e. -# # method_exists_forall_subtypes(f, ts)==true => for all sts<:ts, f(::ts) works -# baremodule Res -# const F = 0 # false -# const T = 1 # true -# const M = 2 # undecided, maybe, i.e. look further -# end - -# # function get_intypes(f, Ts) -# # # based on Jiahao's: http://nbviewer.ipython.org/gist/jiahao/b0d4279cec83b681d95f -# # ct = code_typed(f, Ts) -# # intypes = Array(Any, length(ct)) -# # for (i,method) in enumerate(code_typed(f, Ts)) -# # #Get types of inputs -# # tmp = [x[2] for x in method.args[2][2][1:length(Ts)]] -# # for (j,tm) in enumerate(tmp) -# # if isa(tm,TypeVar) -# # tmp[j] = tm.ub -# # end -# # end -# # intypes[i] = tmp -# # end -# # return intypes -# # end - -# dbg_println(st::String) = verb && println(st) -# const verb = true -# function is_fnparameter_match_inputs(meth_tvars) -# for i=1:2:length(meth_tvars) -# # @show meth_tvars[i+1], meth_tvars[i] -# !( meth_tvars[i+1]<:meth_tvars[i] ) && return false -# end -# return true -# end - -# # function is_type_parameter_match_inputs(TS, meth_sig) -# # for i=1:2:length(meth_tvars) -# # # @show meth_tvars[i+1], meth_tvars[i] -# # !( meth_tvars[i+1]<:meth_tvars[i] ) && return false -# # end -# # return true -# # end - -# function method_exists_forthis_type(f::Function, TS::(ANY...), meths) -# # if the method is defined for TS we're done: -# #@show meths - -# if method_exists(f, TS) # note this does no match parameterized -# # functions when its parameters are not -# # specified. -# dbg_println("A method matches exactly.") -# return Res.T -# end - -# # if there is no method, we're done: -# if length(meths)==0 -# dbg_println("False: No methods for: $TS") -# return Res.F -# end - -# # if there are one or more methods, check them -# for mm in meths -# if is_fnparameter_match_inputs(mm[2]) -# ----------> check this -# @show TS, mm[1], TS<:mm[1], mm[1]<:TS -# if TS<:mm[1] -# dbg_println("A parameterized method matches exactly: $TS") -# return Res.T -# else -# dbg_println("A parameterized method does not match exactly: $TS, looking further") -# end -# else -# dbg_println("A parameterized cannot match: $TS") -# return Res.F -# end - -# end - -# # if length(meths)==1 && isparameterized(meths[1][3]) -# # if is_parameter_match_inputs(meths[1][2]) -# # if Base.typeseq(meths[1][1], TS) -# # dbg_println("A parameterized method matches exactly: $TS") -# # return Res.T -# # else -# # dbg_println("A parameterized method does not match exactly: $TS, looking further") -# # end -# # else -# # dbg_println("A parameterized cannot match: $TS") -# # return Res.F -# # end -# # end - -# # If it is a leaf-type and we're not done by now, there is no hope: -# if all(map(isleaftype,TS)) # todo think about parameterized types -# dbg_println("Leaftype testing false: $TS") -# return Res.F -# else # we don't know -# return Res.M -# end -# end - -# function method_exists_forall_subtypes(f::Function, TS::(ANY...); depth=4, checktop=true) -# # Checks whether a method of f exists for all subtypes of TS, up -# # to a specified search depth (default=4, set to -1 for inf depth). -# # -# # Return: -# # - 1 if true -# # - 0 if false -# # - 2 if undecided -# dbg_println("recusing at depth $depth") - -# if depth==0 -# return Res.M -# end - -# # reflection.jl/_methods. See video "Introduction to Julia" by Jeff -# # Internals-osdeT-tWjzk.mp4, minute 29 -# # inputs: function, type, limit of number of matches -# # outputs: -# # matchtype-signature, values of method parameters, method object -# if checktop -# ms = Base._methods(f, TS, -1) -# if ms!=false -# r = method_exists_forthis_type(f, TS, ms) -# if r==Res.T || r==Res.F -# return r -# end # otherwise check further -# else # no method found -# dbg_println("No methods found, recursing.") -# end -# end - -# # Do a breadth first search in the subtypes: -# checksTS = Any[] -# for sTS in subtypes(TS) -# ms = Base._methods(f, sTS, 2) -# if ms==false -# if isleaftype(sTS) -# dbg_println("False: one of the subtypes test false: $sTS") -# return Res.F -# else -# push!(checksTS, sTS) # needs further checking below -# continue -# end -# end -# #@show 2, ms -# r = method_exists_forthis_type(f, sTS, ms) -# if r==Res.F # if one subtype does not check-out return false -# dbg_println("False: one subtype does not check: $sTS") -# return Res.F -# end -# if r==Res.M -# push!(checksTS, sTS) # needs further checking below -# end -# end -# # recurse into subtypes where above was not -# out = 0 -# for (i,sTS) in enumerate(checksTS) -# r = method_exists_forall_subtypes(f, sTS, depth=(depth-1), checktop=false) -# if r==Res.F # if one subtype does not check-out return false -# dbg_println("False: one lower subtype does not check: $sTS") -# return Res.F -# end -# out += r # accumulate undicided -# end - -# if out==0 -# dbg_println("True: all subtypes test true.") -# return Res.T -# else -# return Res.M -# end -# end - -# # reflection.jl/_methods. See Introduction to Julia Internals-osdeT-tWjzk.mp4 minute 29 -# # 1) inputs: function, type, limit of number of matches -# # 2) inputs: only used in its internal recursion -# # -# # output: -# # matchtype -signature, values of method parameters, method object +# debugging +# import Traits: FakeMethod, replace_concrete_tvars, get_tms_fms, isfitting +function get_tms_fms(Tr, gf::Function, ind=1) + tm = collect(methods(Tr().methods[gf]))[1] + fms = collect(methods(gf, NTuple{length(tm.sig),Any})) + tm, fms +end diff --git a/src/traitdef.jl b/src/traitdef.jl index d0e87a5..3a6c6c3 100644 --- a/src/traitdef.jl +++ b/src/traitdef.jl @@ -6,6 +6,7 @@ # # It looks like # @traitdef Cmp{X,Y} <: Eq{X,Y} begin +# T = eltype(X) # associated type # isless(x,y) -> Bool # @constraints begin # X==Y @@ -95,7 +96,7 @@ function parsebody(body::Expr) isassoc(ex::Expr) = ex.head==:(=) # associated types isconstraints(ex::Expr) = ex.head==:macrocall # constraints - outfns = Expr(:dict) + outfns = :(Traits.FDict()) constr = :(Bool[]) assoc = quote end for ln in Lines(body) @@ -132,23 +133,30 @@ function parseconstraints!(constr, block) end function parsefnstypes!(outfns, ln) + # parse one line containing a function definition function parsefn(def) # Parse to get function signature. # parses f(X,Y), f{X <:T}(X,Y) and X+Y - tvars = Any[] - if isa(def.args[1], Symbol) # f(X,Y) + # into f and _f(...) + + getsymbol(fn) = symbol("__"*string(fn)) + + _fn = deepcopy(def) + if isa(def.args[1], Symbol) # f(X,Y) or X+Y fn = def.args[1] + _fn.args[1] = getsymbol(fn) elseif def.args[1].head==:curly # f{X}(X,Y) fn = def.args[1].args[1] - # get - tvars = def.args[1].args[2:end] + _fn.args[1].args[1] = getsymbol(fn) else throw(TraitException( - "Something went wrong parsing the trait definition body with line:\n$ln")) + "Something went wrong parsing the trait function definition:\n$fn")) end - argtype = :() - append!(argtype.args, def.args[2:end]) - return fn, argtype, tvars + # transform X->::X + for i=2:length(_fn.args) + _fn.args[i] = :(::$(_fn.args[i])) + end + return fn, _fn end function parseret!(rettype, ln) # parse to get return types @@ -156,11 +164,10 @@ function parsefnstypes!(outfns, ln) ln = ln.args[end] end tmp = rettype.args - rettype.args = Any[] - push!(rettype.args, ln.args[end]) + rettype.args = Any[] # per-pend + push!(rettype.args, :($(ln.args[end])())) # e.g. Bool(), the () is for return_types to work append!(rettype.args, tmp) end - rettype = :() tuplereturn = false @@ -168,13 +175,15 @@ function parsefnstypes!(outfns, ln) tuplereturn = true # several ret-types: # f1(X,Y) -> X,Y - append!(rettype.args, ln.args[2:end]) + for r in ln.args[2:end] + push!(rettype.args, :($r())) + end ln = ln.args[1] end if ln.head==:(->) # f1(X,Y) -> x parseret!(rettype, ln) - fn, argtype, tvars = parsefn(ln.args[1]) + fn, _fn = parsefn(ln.args[1]) elseif ln.head==:call # either f1(X,Y) or X + Y -> Z if isa(ln.args[end], Expr) && ln.args[end].head==:(->) # X + Y -> Z def = Expr(:call) @@ -187,37 +196,22 @@ function parsefnstypes!(outfns, ln) parseret!(rettype, ln) else # f1(X,Y) def = ln - rettype = :(Any,) + rettype = :(Any(),) end - fn, argtype, tvars = parsefn(def) + fn, _fn = parsefn(def) else throw(TraitException( "Something went wrong parsing the trait definition body with line:\n$ln")) end - # replace types with constraints by TypeVars - tmp = Any[] - for t in tvars - if isa(t,Symbol) - #error("Having a ") - push!(tmp,t) - else - push!(tmp,t.args[1]) - end - end - # trans = Dict(zip([t.args[1] for t in tvars], tvars)) # this will error if there is a type-var without constraints! - trans = Dict(zip(tmp,tvars)) - translate!(argtype.args, trans) - tvar2tvar!(argtype.args) - subt2tvar!(rettype.args) - translate!(rettype.args, trans) - tvar2tvar!(rettype.args) # if return is not a tuple, ditch the tuple if !tuplereturn rettype = rettype.args[1] end - push!(outfns.args, :($fn => ($argtype, $rettype))) + # make _fn + _fn = :($_fn = $rettype) + push!(outfns.args, :($fn => $_fn)) end # 3) piece it together @@ -274,7 +268,7 @@ macro traitdef(head, body) # make sure a generic function of all associated types exisits traitbody = quote - methods::Dict{Union(Function,DataType), Tuple} + methods::Traits.FDict constraints::Vector{Bool} assoctyps::Vector{Any} function $((name))() diff --git a/src/traitimpl.jl b/src/traitimpl.jl index 241aa87..4ed3c7e 100644 --- a/src/traitimpl.jl +++ b/src/traitimpl.jl @@ -122,7 +122,8 @@ macro traitimpl(head, body) trait = eval_curmod(trait_expr) ## Check supertraits are implemented: - if !(istrait(traitgetsuper(trait); verbose=true)) + if !istrait(traitgetsuper(trait)) + istrait(traitgetsuper(trait); verbose=true) throw(TraitException("""Not all supertraits of $trait are implemented. Implement them first.""")) end @@ -138,6 +139,6 @@ macro traitimpl(head, body) end ## Assert that the implementation went smoothly - push!(out.args, :(@assert istrait($trait_expr, verbose=true))) + push!(out.args, :(istrait($trait_expr) ? nothing : @assert istrait($trait_expr, verbose=true))) return esc(out) end diff --git a/test/manual-traitdef.jl b/test/manual-traitdef.jl index 06bafb6..7704f10 100644 --- a/test/manual-traitdef.jl +++ b/test/manual-traitdef.jl @@ -5,21 +5,23 @@ # the types: @test istrait( () ) - immutable Tr1{X1} <: Traits.Trait{()} - methods - constraints - Tr1() = new(Dict(), []) + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} + Tr1() = new(Traits.FDict(), Bool[], []) end immutable Tr2{X1,X2} <: Traits.Trait{()} - methods - constraints - Tr2() = new(Dict(), []) + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} + Tr2() = new(Traits.FDict(), Bool[], []) end immutable Tr3{X1,X2} <: Traits.Trait{(Tr1{X1}, Tr2{X1,X2})} - methods - constraints - Tr3() = new(Dict(), []) + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} + Tr3() = new(Traits.FDict(), Bool[], []) end @test istraittype(Tr1) @@ -32,35 +34,40 @@ end @test traitgetsuper(Tr3{A1,A2})==(Tr1{A1},Tr2{A1,A2}) # any type is part of a unconstrained trait: -@test istrait(Tr1{Int}) +@test istrait(Tr1{Int}, verbose=verbose) @test istrait(Tr2{DataType,Int}) @test istrait(Tr3{String,DataType}) @test_throws TraitException istrait(Tr3{:a,7}) # maybe this should error? immutable D1{X1} <: Traits.Trait{()} - methods - constraints + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} function D1() - new(Dict( - sin => ((X1,), Float64), - cos => ((X1,), Float64), + new(Traits.FDict( + sin => _sin(::X1) = Float64(), # note 1: _sin could be any symbol; + # note 2: Float64() would throw an error but works with return_types + cos => _cos(::X1) = Float64() ), + Bool[], [] ) end end -@test istrait(D1{Int}) +@test istrait(D1{Int}, verbose=verbose) @test !istrait(D1{String}) immutable D2{X1,X2} <: Traits.Trait{(D1{X1}, D1{X2})} - methods - constraints + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} function D2() - new(Dict( - (+) => ((X1, X2), Any), - (-) => ((X1, X2), Any) + new(Traits.FDict( + (+) => _plus(::X1, ::X2) = Any(), + (-) => _minus(::X1, ::X2) = Any() ), + Bool[], [] ) end @@ -70,32 +77,35 @@ end @test !istrait(D2{Int, String}) immutable D3{X1} <: Traits.Trait{()} - methods - constraints + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} function D3() - new(Dict( - getkey => ((X1,Any,Any), Any), - get! => ((X1, Any, Any), Any) + new(Traits.FDict( + getkey => _getkey(::X1,::Any,::Any) = Any(), + get! => _get!(::X1, ::Any, ::Any) = Any() ), + Bool[], [] ) end end immutable D4{X1,X2} <: Traits.Trait{()} # like D2 but without supertraits - methods - constraints + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} function D4() - new(Dict( - (+) => ((X1, X2), Any), - (-) => ((X1, X2), Any) + new(Traits.FDict( + (+) => _plus(::X1, ::X2) = Any(), + (-) => _minus(::X1, ::X2) = Any() ), + Bool[], [] ) end end - @test istrait(D3{Dict{Int,Int}}) @test !istrait(D3{Int}) @@ -106,18 +116,17 @@ end ### adding other constraints immutable CTr1{X1,X2} <: Traits.Trait{()} - methods::Dict - constraints::Array{Bool,1} # constraints are an array of functions - # which need to evaluate to true. Their - # signature is f(X,Y) = ... - + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} function CTr1() - new(Dict( - (+) => ((X1, X2), Any), + new(Traits.FDict( + (+) => _plus(::X1, ::X2) = Any(), ), Bool[ X1==X2 - ] + ], + [] ) end end @@ -128,18 +137,16 @@ end ### adding other associated types immutable CTrAs{X1,X2} <: Traits.Trait{()} - methods::Dict - constraints::Array{Bool,1} # constraints are an array of functions - # which need to evaluate to true. Their - # signature is f(X,Y) = ... - assoctyps::Array{TypeVar,1} + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} function CTrAs() R = promote_type(X1, X2) D = (X1,X2)<:(Integer,Integer) ? Float64 : promote_type(X1, X2) - assoctyps = [TypeVar(:R, R), TypeVar(:D, D)] - new(Dict( - (+) => ((X1, X2), R), - (/) => ((X1, X2), D), + assoctyps = Any[TypeVar(:R, R), TypeVar(:D, D)] + new(Traits.FDict( + (+) => _plus(::X1, ::X2) = Any(), + (-) => _minus(::X1, ::X2) = Any() ), Bool[], assoctyps @@ -151,3 +158,111 @@ end # @test istrait(CTrAs{Integer, Integer}) # doesn't work because return type of /(Integer, Integer)==Any @test istrait(CTrAs{Int, Int}) @test !istrait(CTrAs{Int, String}) + +# parametric methods +#################### + +# @traitdef Tr01{X} begin +# g01{T<:X}(T, T) -> T +# end +immutable Tr01{X} <: Traits.Trait{()} + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} + function Tr01() + new(Traits.FDict( + g01 => _g01{T<:X}(::T, ::T) = T() + ), + Bool[], + [] + ) + end +end + + +g01(::Int, ::Int) = Int +@test istrait(Tr01{Int}) # == true as constraints Int isleaftype +@test !istrait(Tr01{Integer}) +g01{I<:Integer}(::I, ::I) = I +@test istrait(Tr01{Integer}) # == true + +# @traitdef Tr02{X} begin +# g02{T<:X}(T, T) -> T +# end +immutable Tr02{X} <: Traits.Trait{()} + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} + function Tr02() + new(Traits.FDict( + g02 => _g02{T<:X}(::T, ::T) = T() + ), + Bool[], + [] + ) + end +end + +g02{I<:Integer}(::I, ::I) = Integer +# By using Base.return_types it is not possible to figure out whether +# the returned value is constrained or not by I: +if function_types_bug1 + @test istrait(Tr02{Integer}) + # or throw an error/warning here saying parametric return types + # are only supported for leaftypes +else + @test !istrait(Tr02{Integer}) # if function types get implemented this should be possible to catch +end +@test istrait(Tr02{Int}) # == true + +# @traitdef Tr03{X} begin +# g03{T<:X}(T, Vector{T}) +# end +immutable Tr03{X} <: Traits.Trait{()} + methods::Traits.FDict + constraints::Vector{Bool} + assoctyps::Vector{Any} + function Tr03() + new(Traits.FDict( + g03 => _g03{T<:X}(::T, ::Vector{T}) = T() + ), + Bool[], + [] + ) + end +end + +g03{I<:Integer}(::I, ::Vector{I}) = 1 +@test istrait(Tr03{Integer}) +@test istrait(Tr03{Int}) + + +# ## If I ever need to get to the trait parameters, something like this should work: +# # @traitdef Tr04{X} begin +# # g04{T<:X}(T, Vector{T}) +# # end +# immutable Tr04{X} <: Traits.Trait{()} +# methods::Traits.FDict +# constraints::Vector{Bool} +# assoctyps::Vector{Any} +# function Tr04() +# A1 = getassoc(X) +# new(Traits.FDict( +# g04 => _g04{T<:X}(::T, ::Vector{T}, ::A1) = T() +# ), +# Bool[], +# [A1] +# ) +# end +# function Tr04(::Type{Traits._TestTraitPara}) +# A1 = Traits._TestAssoc{:A1} +# new(Traits.FDict( +# g04 => _g04{T<:X}(::T, ::Vector{T}, ::A1) = T() +# ), +# Bool[], +# [A1] +# ) +# end +# end +# getassoc{T<:Integer}(::Type{T}) = UInt +# g04{I<:Integer}(::I, ::Vector{I}, ::Integer) = 1 diff --git a/test/manual-traitimpl.jl b/test/manual-traitimpl.jl index f2d2ec1..234135f 100644 --- a/test/manual-traitimpl.jl +++ b/test/manual-traitimpl.jl @@ -72,9 +72,9 @@ length(tmp)==length(implfns) || error("Duplicate method definition(s)") # check right number of defs length(D2{T1,T2}().methods)==length(implfns) || error("Not right number of method definitions") # check that the signature of fns agrees with D2{T1,T2}().methods -for (fn,sig) in D2{T1,T2}().methods +for (fn,_fn) in D2{T1,T2}().methods # for now just check length - if length(sig)!=length(get_fnsig(implfns[fn])) + if length(_fn.env.defs.sig)!=length(get_fnsig(implfns[fn])) error("""Method definition: $fn $sig does not match implementation: diff --git a/test/perf/perf.jl b/test/perf/perf.jl index 1c16a0e..4a9194b 100644 --- a/test/perf/perf.jl +++ b/test/perf/perf.jl @@ -48,8 +48,8 @@ function compare_code_native(f1, f2, types, fraction_range=[-Inf,Inf]) out end - df1 = prune_native(Base._dump_function(f1, types, true, false)) - df2 = prune_native(Base._dump_function(f2, types, true, false)) + df1 = prune_native(Base._dump_function(f1, types, true, false, true)) + df2 = prune_native(Base._dump_function(f2, types, true, false, true)) rel_diff = abs(length(df1)-length(df2))/length(df2) if !(fraction_range[1]<=rel_diff<=fraction_range[2]) println("""Warning: length of code native of $(f1.env.name) and $(f2.env.name) differ by $rel_diff, diff --git a/test/runtests.jl b/test/runtests.jl index dc21e58..c00fe70 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,19 +1,44 @@ # tests using Base.Test using Traits +## BUG flags: set to false once fixed to activate tests +# Julia issues: +method_exists_bug1 = false # see https://github.com/JuliaLang/julia/issues/8959 +method_exists_bug2 = false # see https://github.com/JuliaLang/julia/issues/9043 and https://github.com/mauro3/Traits.jl/issues/2 +# these two are not relevant anymore as method_exists is not used anymore + +function_types_bug1 = true # set to false if function types get implemented in Julia +# Traits.jl issues: +dispatch_bug1 = true # in traitdispatch.jl +# how much output to print +verbose=false +# src/Traits.jl tests type A1 end type A2 end - @test !istraittype(A1) -## BUG flags: set to false once fixed to activate tests -# Julia issues: -method_exists_bug1 = true # see https://github.com/JuliaLang/julia/issues/8959 -method_exists_bug2 = true # see https://github.com/JuliaLang/julia/issues/9043 and https://github.com/mauro3/Traits.jl/issues/2 -# Traits.jl issues: -dispatch_bug1 = true # in traitdispatch.jl +# istrait helper function: +I = TypeVar(:I) +T = TypeVar(:T) +@test Traits.subs_tvar(I, Array{TypeVar(:I,Int64)}, Traits._TestTvar{1})==Array{TypeVar(:I,Int64)} +@test Traits.subs_tvar(TypeVar(:I,Int64), Array{TypeVar(:I,Int64)}, Traits._TestTvar{1})==Array{Traits._TestTvar{1}} +@test Traits.subs_tvar(T, Array, Traits._TestTvar{1})==Array{Traits._TestTvar{1}} # this is kinda bad +f8576{T}(a::Array, b::T) = T +other_T = f8576.env.defs.tvars +@test Traits.subs_tvar(other_T, Array, Traits._TestTvar{1})==Array # f8576.env.defs.tvars looks like T but is different! + +@test Traits.find_tvar( (Array, ), T)==[true] +@test Traits.find_tvar( (Array, ), other_T)==[false] +@test Traits.find_tvar( (Int, Array, ), T)==[false,true] + +@test Traits.find_correponding_type(Array{Int,2}, Array{I,2}, I)==Any[Int] +@test Traits.find_correponding_type((Array{Int,2}, Float64, (UInt8, UInt16)), + (Array{I,2}, I, (String, I)) , I) == Any[Int, Float64, Traits._TestType{:no_match}, UInt16] +@test Traits.find_correponding_type((Array{Int,2}, Float64, (UInt8, UInt16)), + (Array{I,2}, I, (UInt8, I)) , I) == Any[Int, Float64, UInt16] + # manual implementations include("manual-traitdef.jl") diff --git a/test/traitdef.jl b/test/traitdef.jl index 172a90f..8f2ac0f 100644 --- a/test/traitdef.jl +++ b/test/traitdef.jl @@ -2,9 +2,15 @@ td = :(@traitdef Cr20{X} begin length(X) end) -a,b = Traits.parsebody(td.args[end]) -@test a==Expr(:dict, :(length=>((X,),Any))) +a,b,c = Traits.parsebody(td.args[end]) +# a is not hard to test because of the random gensym +@test a.head==:call +@test a.args[1]==:(Traits.FDict) +@test a.args[2].head==:(=>) +@test a.args[2].args[1] == :length +@test a.args[2].args[2].args[2] == :(Any()) @test b==:(Bool[]) +@test c.args[1]==:(assoctyps = Any[]) td0 = :(@traitdef Cr20{X} begin length(X) @@ -14,7 +20,6 @@ td0 = :(@traitdef Cr20{X} begin end end) a,b = Traits.parsebody(td0.args[end]) -@test a==Expr(:dict, :(length=>((X,),Any))) @test b==:(Bool[(string(X.name))[1] == 'I']) td1 = :(@traitdef Cr20{X} begin @@ -25,7 +30,6 @@ td1 = :(@traitdef Cr20{X} begin end end) a,b = Traits.parsebody(td1.args[end]) -@test a==Expr(:dict, :(length=>((X,),Int))) @test b==:(Bool[(string(X.name))[1] == 'I']) td2 = :(@traitdef Cr20{X,Y} begin @@ -38,9 +42,6 @@ td2 = :(@traitdef Cr20{X,Y} begin end end) a,b,c = Traits.parsebody(td2.args[end]) -@test a==Expr(:dict, :((+) => ((X,Y),(Int,Float64))), - :((-) => ((X,Y),Int)), - :((/) => ((X,Y),Int))) @test b==:(Bool[(string(X.name))[1] == 'I']) @test c.head==:block @@ -48,20 +49,19 @@ td3 = :(@traitdef Cr20{X,Y} begin fn(X) -> Type{X} end) a,b,c = Traits.parsebody(td3.args[end]) -@test a==Expr(:dict, :((fn) => ((X,),Type{X}))) -td4 = :(@traitdef Cr20{X} begin - fn{Y<:II}(X,Y) -> Type{X} - fn76{K<:FloatingPoint, I<:Integer}(X, Vector{I}, Vector{K}) -> I -end) -a,b,c = Traits.parsebody(td4.args[end]) -v = :(TypeVar(symbol("Y"),II)) -t = :(TypeVar(symbol("I"),Integer)) -k = :(TypeVar(symbol("K"),FloatingPoint)) +# td4 = :(@traitdef Cr20{X} begin +# fn{Y<:II}(X,Y) -> Type{X} +# fn76{K<:FloatingPoint, I<:Integer}(X, Vector{I}, Vector{K}) -> I +# end) +# a,b,c = Traits.parsebody(td4.args[end]) +# v = :(TypeVar(symbol("Y"),II)) +# t = :(TypeVar(symbol("I"),Integer)) +# k = :(TypeVar(symbol("K"),FloatingPoint)) -@test a==Expr(:dict, :(fn=>((X,$v),Type{X})), - :(fn76=>((X,Vector{$t},Vector{$k}),$t)) - ) +# @test a==Expr(:dict, :(fn=>((X,$v),Type{X})), +# :(fn76=>((X,Vector{$t},Vector{$k}),$t)) +# ) ## test making traits @@ -70,55 +70,60 @@ k = :(TypeVar(symbol("K"),FloatingPoint)) start(X) end -## Testing trait definitions +## Testing trait definitions in commontraits.jl @test istrait(Cmp{Int,Int}) @test istrait(Cmp{Int,Float64}) @test !istrait(Cmp{Int,String}) -coll = [Vector{Int}, Dict{Int,Int}, Set{Int}] +coll = [Vector, Vector{Int}, Dict{Int}, Dict{Int,Int}, Set{Int}] iter = [Traits.GenerateTypeVars{:upcase}, Int] #todo: add String, if method_exists_bug1 - assoc = [] #todo add again: Dict{Int,Int}] # , ObjectIdDict] + dicts = [] #todo add again: Dict{Int,Int}] # , ObjectIdDict] else - assoc = [Array{Int,2}, Dict{Int,Int}, StepRange{Int,Int}] + dicts = [Dict{Int}, Dict{Int,Int}] # Dict does not work, ObjectIdDict does not fulfill the trait end index = [Array{Int,2}, StepRange{Int,Int}] - +c =1 for c in coll - @test istrait(Collection{c}, verbose=true) - @test istrait(Iter{c}, verbose=true) - @test istrait(IterColl{c}, verbose=true) +# @show Collection{c}() # heisenbug protection + @test istrait(Collection{c}, verbose=verbose) + @test istrait(Iter{c}, verbose=verbose) + @test istrait(IterColl{c}, verbose=verbose) end -println("""After fix of https://github.com/JuliaLang/julia/issues/9135 - uncomment following line again and in commontraits.jl""") -# @test !istrait(Indexable{Set}) +@test !istrait(Indexable{Set}) for c in iter - @test istrait(Iter{c}, verbose=true) + @test istrait(Iter{c}, verbose=verbose) end -for c in assoc - @test istrait(Assoc{c}, verbose=true) +for c in dicts + @test istrait(Assoc{c}, verbose=verbose) end for c in index - @test istrait(Indexable{c}, verbose=true) + @test istrait(Indexable{c}, verbose=verbose) end -@test istrait(Iter{Array}, verbose=true) -@test istrait(Iter{ASCIIString}, verbose=true) -@test istrait(Iter{Int}, verbose=true) +@test istrait(Iter{Array}, verbose=verbose) +@test istrait(Iter{ASCIIString}, verbose=verbose) +@test istrait(Iter{Int}, verbose=verbose) @test !istrait(Iter{Nothing}) arith = [Int, Float64, Rational{Int}] for a1 in arith for a2 in arith - @test istrait(Arith{a1,a2}, verbose=true) + @test istrait(Arith{a1,a2}, verbose=verbose) end end ## test trait definition +@traitdef FF{X} begin + f948576() +end +@test !istrait(FF{Int}) +f948576() = 1 +@test istrait(FF{Int}) @traitdef Tr20{X} begin length(X) -> Bool @@ -157,19 +162,16 @@ end @test issubtrait(Tr13, Tr20) @test issubtrait((Tr21,), (Tr20,)) -@test issubtrait((Tr21,Tr11), (Tr20,Tr10)) +@test issubtrait((Tr21,Tr11), (Tr20,Tr10)) +@test !issubtrait((Tr21,Tr11), (Tr10,Tr20)) # todo: this should be true, as order shouldn't matter @test issubtrait((Tr11,Tr21), (Tr10,Tr20)) -@test !issubtrait((Tr21,Tr11), (Tr10,Tr20)) # todo: this should be true, I think @test !issubtrait(Tr21{Int}, Tr20{Float64}) @test !issubtrait((Tr21{Int},), (Tr20{Float64},)) -#--> need to be able to do this in terms of type variables. - -# test functions parameterized on non-trait parameters. This isn't currently working: -# https://github.com/mauro3/Traits.jl/issues/2 -# https://github.com/JuliaLang/julia/issues/9043 - +#### +# Test functions parameterized on non-trait parameters. +### @traitdef Pr0{X} begin fn75{Y <: Integer}(X, Y) -> Y end @@ -181,9 +183,6 @@ else end @test !istrait(Pr0{Int8}) -fn75(x::UInt8, y::Int8) = y+x -@test !istrait(Pr0{UInt8}) # this works, not because only for y::Int8 not for all Integers - @traitdef Pr1{X} begin fn76{I<:Integer}(X, Vector{I}) -> I end @@ -193,20 +192,97 @@ if method_exists_bug2 else @test istrait(Pr1{UInt8}) end -@test !istrait(Pr1{UInt8}) @traitdef Pr2{X} begin fn77{Y<:Number}(X,Y,Y) -> Y # fn77{Y}(X) end fn77(a::Array,b::Int, c::Float64) = a[1] -if method_exists_bug2 - @test !istrait(Pr2{Array}) -else - @test istrait(Pr2{Array}) +@test !istrait(Pr2{Array}) +fn77{Y<:Real}(a::Array,b::Y, c::Y) = a[1] +@test !istrait(Pr2{Array}) +fn77{Y<:Number}(a::Array,b::Y, c::Y) = a[1] +@test istrait(Pr2{Array}) + +##### +# Trait functions parameterized on trait parameters +#### +check_return_types(false) +@traitdef Pr3{X} begin + fn78{T<:X}(T,T) end -# test constraints +fn78(b::Int, c::Int) = b +@test istrait(Pr3{Int}) +fn78(b::Real, c::Real) = b +@test !istrait(Pr3{Real}) +fn78{T}(b::T, c::T) = b +@test istrait(Pr3{Real}) +@test istrait(Pr3{Any}) + +@traitdef Pr04{X} begin + fnpr04{T<:X, S<:Integer}(T,T, S, S) +end +fnpr04(b::Int, c::Int, ::Int, ::Int) = b +@test !istrait(Pr04{Int}) +fnpr04{I<:Integer}(b::Int, c::Int, ::I, ::I) = b +@test istrait(Pr04{Int}) + + +@traitdef Pr05{X} begin + fnpr05{T<:X, S<:Integer}(Dict{T,T}, Dict{S,T}) +end +fnpr05{T<:FloatingPoint, S<:Integer}(::Dict{T,T}, ::Dict{S,T}) = 1 +@test istrait(Pr05{Float64}) + +@traitdef Pr06{X} begin + fnpr06{T<:X, S<:Integer}(Dict{T,S}, Dict{S,T}) +end +fnpr06{T<:FloatingPoint, S<:Integer}(::Dict{T,S}, ::Dict{S,T}) = 1 +@test istrait(Pr06{Float64}) + +@traitdef Pr07{X} begin + fnpr07(X, X, Integer) +end +fnpr07{T<:Integer}(::T, ::T, ::Integer) = 1 +@test !istrait(Pr07{Integer}) +@test istrait(Pr07{Int}) + +# function parameters only one of the methods +@traitdef Pr08{X} begin + fnpr08(X, Vector{X}, Integer) +end +fnpr08{T<:Integer}(::T, ::Vector{T}, ::Integer) = 1 +@test !istrait(Pr08{Integer}) +@test istrait(Pr08{Int}) + +@traitdef Pr10{X} begin + fnpr10{T<:X}(T, Vector{T}, Integer) +end +fnpr10(::Int, ::Vector{Int}, ::Integer) = 1 +@test !istrait(Pr10{Integer}) +@test istrait(Pr10{Int}) + +@traitdef Pr11{X} begin + fnpr11(Int, Vector{UInt}, X) +end +fnpr11{T<:Integer}(::T, ::Vector{T}, ::Integer) = 1 +@test !istrait(Pr11{Integer}) +@test !istrait(Pr11{Int}) +@test !istrait(Pr11{UInt}) + +@traitdef Pr12{X} begin + fnpr12(Int, Vector{UInt}, X) +end +fnpr12{T<:Integer}(::T, ::Vector{T}, ::Integer) = 1 +@test !istrait(Pr12{Integer}) +@test !istrait(Pr12{Int}) + +check_return_types(true) + +#### +# Test constraints +#### @traitdef Cr20{X} begin length(X) -> Any @@ -215,8 +291,6 @@ end end end -@test Cr20{Int}().methods==Dict(length => ((Int,),Any)) - @test !istrait(Cr20{Float32}) @test istrait(Cr20{Int}) @@ -248,6 +322,7 @@ end ###### # istrait ##### +check_return_types(false) f12(x::Int) = 1 @traitdef UU{X} begin f12(X) @@ -265,7 +340,7 @@ end @test !istrait(UU13{Any}) @test istrait(UU13{Integer}) @test istrait(UU13{Int8}) - +check_return_types(true) ##### # Associated types #### @@ -297,7 +372,33 @@ Base.getindex(::T3484675, i::Int) = i AssocIsBits{T3484675{Int,4.5,:a}}() @test istrait(AssocIsBits{T3484675{Int,4.5,:a}}) # errors because it is assumed that all # parameters are TypeVars +##### +# Varags +##### +@traitdef TT31{X} begin + foo31(X, Int...) +end +foo31(::String, x::UInt...) = 1 +@test !istrait(TT31{String}) +foo31(::String) = 2 # to avoid ambiguity warnings +foo31(::String, x::Int...) = 2 +@test istrait(TT31{String}) + +@traitdef TT32{X} begin + foo32(X...) +end +foo32(::String) = 1 +@test !istrait(TT32{String}) +foo32(a::String...) = 2 # to avoid ambiguity warnings +@test istrait(TT32{String}) +@traitdef TT33{X} begin + foo33{Y<:X}(X, Y...) +end +foo33(::String) = 1 +@test !istrait(TT33{String}) +foo33{T<:String}(::String, a::T...) = 2 +@test istrait(TT33{String}) #### # DataType constructors @@ -309,20 +410,37 @@ AssocIsBits{T3484675{Int,4.5,:a}}() D() -> D end type A4758 end +type A4759 + a +end @test istrait(TT45{A4758}) +@test !istrait(TT45{A4759}) @test istrait(TT45{Dict{Int,Int}}) @test istrait(TT45{Set{Int}}) @test !istrait(TT45{Int}) @test !istrait(TT45{Array{Int,1}}) + + +@traitdef TT44{D} begin + Array(Type{D},Integer) -> Array # the standard array constructor, should be working for all Types +end +@test istrait(TT44{A4758}) +@test istrait(TT44{A4759}) +@test istrait(TT44{Dict{Int,Int}}) +@test istrait(TT44{Set{Int}}) +@test istrait(TT44{Int}) +@test istrait(TT44{Array{Int,1}}) + + # This is the trait for datatypes with Array like constructors: @traitdef TT46{Ar} begin T = Type{eltype(Ar)} Arnp = deparameterize_type(Ar) # Array stripped of type parameters - #Arnp(T, Int64) -> Ar - Arnp(T, Int...) -> Ar # see issue #8 & https://github.com/JuliaLang/julia/issues/10642 + Arnp(T, Int64) -> Ar + Arnp(T, Int...) -> Ar @constraints begin length(Ar.parameters)>1 # need at least two parameters to be array-like, right? end @@ -331,10 +449,12 @@ end if Traits.flag_check_return_types @test !istrait(TT46{Dict{Int,Int}}) else - @test istrait(TT46{Dict{Int,Int}}, verbose=true) # this is a false positive + @test istrait(TT46{Dict{Int,Int}}, verbose=verbose) # this is a false positive end -# @test istrait(TT46{Set{Int}}, verbose=true) this actually works, but not as expected and gives a deprecation warning +# @test istrait(TT46{Set{Int}}, verbose=verbose) this actually works, but not as expected and gives a deprecation warning @test !istrait(TT46{Int}) -@test istrait(TT46{Array{Int,1}}, verbose=true) -# @test istrait(TT46{Array{Int}}, verbose=true) # this does not pass currently because of https://github.com/JuliaLang/julia/issues/10642 -@test istrait(TT46{Array}, verbose=true) +@test istrait(TT46{Array{Int,1}}, verbose=verbose) +# @test istrait(TT46{Array{Int}}, verbose=verbose) # this does not pass currently because of https://github.com/JuliaLang/julia/issues/10642 +@test istrait(TT46{Array}, verbose=verbose) + +