diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index dadc755..08db111 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -2,13 +2,44 @@ name: CompatHelper on: schedule: - cron: 0 0 * * * + workflow_dispatch: +permissions: + contents: write + pull-requests: write jobs: - build: + CompatHelper: runs-on: ubuntu-latest steps: - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() - run: julia -e 'using CompatHelper; CompatHelper.main()' + - name: Check if Julia is already available in the PATH + id: julia_in_path + run: which julia + continue-on-error: true + - name: Install Julia, but only if it is not already available in the PATH + uses: julia-actions/setup-julia@v1 + with: + version: '1' + # arch: ${{ runner.arch }} + if: steps.julia_in_path.outcome != 'success' + - name: "Add the General registry via Git" + run: | + import Pkg + ENV["JULIA_PKG_SERVER"] = "" + Pkg.Registry.add("General") + shell: julia --color=yes {0} + - name: "Install CompatHelper" + run: | + import Pkg + name = "CompatHelper" + uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" + version = "3" + Pkg.add(; name, uuid, version) + shell: julia --color=yes {0} + - name: "Run CompatHelper" + run: | + import CompatHelper + CompatHelper.main() + shell: julia --color=yes {0} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} + # COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6df74..618353c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.1] - 2022-07-01 +### Added +- in case of a MethodError, a `WhereTraitsMethodError` is thrown instead which adds + detailed information about the available traits. +- `UndefVarError` is thrown if variables are used within the traits definition which are + obvious typos. +- added documentation fo WhereTraitsMethodError + ## [1.1.0] - 2022-04-09 ### Added diff --git a/Project.toml b/Project.toml index 1db862e..0979baa 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "WhereTraits" uuid = "c9d4e05b-6318-49cb-9b56-e0e2b0ceadd8" authors = ["Sahm Stephan "] -version = "1.1.0" +version = "1.1.1" [deps] Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" @@ -17,7 +17,6 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" SimpleMatch = "a3ae8450-d22f-11e9-3fe0-77240e25996f" StructEquality = "6ec83bb0-ed9f-11e9-3b4c-2b04cb4e219c" -Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" [compat] Compat = "2.1, 3" @@ -25,21 +24,11 @@ DataTypesBasic = "1.0, 2" ExprParsers = "1.2" Graphs = "1.6" IterTools = "1" +MetaGraphs = "0.7" Pipe = "1.3" ProxyInterfaces = "1.0" -MetaGraphs = "0.7" Reexport = "1.2" Setfield = "0.7, 0.8" SimpleMatch = "1.0" StructEquality = "2.0" -Suppressor = "0.2" julia = "1.6" - -[extras] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -StringDistances = "88034a9c-02f8-509d-84a9-84ec65e18404" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Documenter", "Test", "StringDistances", "InteractiveUtils"] diff --git a/README.md b/README.md index 62d9756..43c9614 100644 --- a/README.md +++ b/README.md @@ -121,24 +121,17 @@ julia> conflict([1,2,3,4]) For more details, take a look at the [documentation](https://jolin-io.github.io/WhereTraits.jl/dev). -## Limitations & Future Plans +## Limitations ### Optimal Code *Warning: While the dispatch works for dynamic functions, it will only be able to create optimal code if your traits function supports proper type-inference. E.g. you can use `Base.isempty`, however type-inference cannot see whether it will return true or false by static inspection. Hence it will use slower dynamic code.* -### MethodError -Currently if you made a mistake in your traits definition, you get a pretty incomprehensible error. What you see is merely an implementation detail and it is on the roadmap to give a nice user-friendly error-handling instead. - ### Keyword arguments Keyword arguments are at the moment not support for WhereTraits dispatch. They are just passed through. -It is planned to support Keyword arguments in some future release. - ### Symbol Level -The extended where syntax is currently implemented on **symbol level**, which is why traits functions like `Base.IteratorSize` and the non-qualified `IteratorSize` (assuming you imported `import Base:IteratorSize`) are treated as two different functions, despite being the same. So for now try to only use the one style or the other. - -There are plans to evaluate the symbols to functions beforehand. Still in evaluation phase. +The extended where syntax is currently implemented on **symbol level**, which is why traits functions like `Base.IteratorSize` and the non-qualified `IteratorSize` (assuming you imported `import Base: IteratorSize`) are treated as two different functions, despite being the same. So for now try to only use the one style or the other. ### Top Level Only Currently **only top-level functions** are supported, as the syntax stores and needs information about previous function definitions, which it stores globally. If macros would get informed about whether they are defined within another function, WhereTraits could also support innerfunctions. @@ -149,4 +142,11 @@ The `@traits` macro currently does not work well within the `Test.@testset` macr Nevertheless there is a workaround. WhereTraits.jl exports a `@traits_test` macro variant which works better, but still might have cases where it fails. This needs to be investigated further, and maybe needs a change on `Test.@testset`. +## Other traits packages + +There are many different attempts to add traits to Julia. +Everyone puts a different emphasis on different aspects of traits interfaces. +- SimpleTraits.jl +- BinaryTraits.jl +- CanonicalTraits.jl diff --git a/docs/jupyter/traits-in-julia.ipynb b/docs/jupyter/traits-in-julia.ipynb index 85eb8f7..4c1033b 100644 --- a/docs/jupyter/traits-in-julia.ipynb +++ b/docs/jupyter/traits-in-julia.ipynb @@ -518,7 +518,7 @@ "", "Stacktrace:", " [1] error(::String) at ./error.jl:33", - " [2] move3(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Animal}}, ::Whale, ::WhereTraits.Syntax.Rendering._BetweenArgsAndTypeVars, ::Type{T} where T, ::WhereTraits.Syntax.Rendering._BetweenTypeVarsAndTraits, ::Val{true}, ::Val{false}) at ./In[46]:2", + " [2] move3(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Animal}}, ::Whale, ::WhereTraits.InternalState.ArgsHelpers_BetweenArgsAndTypeVars, ::Type{T} where T, ::WhereTraits.InternalState.ArgsHelpers_BetweenTypeVarsAndTraits, ::Val{true}, ::Val{false}) at ./In[46]:2", " [3] move3(::Whale; kwargs::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}) at ./In[15]:2", " [4] move3(::Whale) at ./In[15]:2", " [5] top-level scope at In[48]:3", diff --git a/docs/make.jl b/docs/make.jl index fe76e42..23458bc 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -24,7 +24,7 @@ makedocs(; "WhereTraits Usage`" => "usage.md", "WhereTraits Details`" => "details.md", "WhereTraits.BasicTraits" => "basictraits.md", - "Combination with `isdef`" => "isdef.md", + # "Combination with `isdef`" => "isdef.md", ], "Library" => "library.md", ], diff --git a/docs/src/details.md b/docs/src/details.md index 4c3aa2d..a6d270a 100644 --- a/docs/src/details.md +++ b/docs/src/details.md @@ -45,12 +45,260 @@ help?> h ## MethodError handling -TODO +Like with normal function dispatch you can also get MethodErrors when using traits. +While the pure MethodError can be helpful for the experienced WhereTraits user, for most +it will be rather incomprehensible. This is why WhereTraits catches MethodErrors and +rephrases them in easier to understand `TraitsMethodError`. +Here one example +```julia +julia> @traits myfunc(a) where {Base.IteratorSize(a)::Base.HasShape} = 2 + +julia> myfunc([1]) +2 + +julia> myfunc(Iterators.countfrom()) +ERROR: TraitsMethodError: no method matching `myfunc(Base.Iterators.Count{Int64}(1, 1))`. + +It corresponds to the normal julia-dispatch (aka "outerfunction") + +```julia +myfunc(a1::T1; kwargs...) where T1 +``` + +and the traits (traits are normalized to <:) + +```julia +Core.Typeof(Base.IteratorSize(a1)) <: Base.IsInfinite +``` + +however, the only available traits definitions are: + + * * * * * * * * * * * * * * + +```julia +myfunc(a) where Base.IteratorSize(a)::Base.HasShape +``` + +which defines the traits (traits are normalized to <:) -## Implementation Details (partly outdated, as disambiguation is not yet described) +```julia +Core.Typeof(Base.IteratorSize(a1)) <: Base.HasShape +``` + + * * * * * * * * * * * * * * + +Note the following `<:` standardization of traits: + +| WhereTraits | Example | `<:` standardization | +| ------------------:| -------------------------------------:| ------------------------------------------------------:| +| bool trait | `iseven(a)` | `WhereTraits.BoolType(iseven(a)) <: WhereTraits.True` | +| negated bool trait | `!iseven(a)` | `WhereTraits.BoolType(iseven(a)) <: WhereTraits.False` | +| `isa` trait | `Base.IteratorSize(a)::Base.HasShape` | `Core.Typeof(Base.IteratorSize(a)) <: Base.HasShape` | +| `<:` trait | `Base.eltype(a) <: Number` | `Base.eltype(a) <: Number` | + +Stacktrace: + [1] #myfunc#4 + @ ~/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:215 [inlined] + [2] myfunc(a1::Base.Iterators.Count{Int64}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}) + @ Main ./REPL[10]:0 + [3] myfunc + @ ~/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:211 [inlined] + [4] myfunc(a1::Base.Iterators.Count{Int64}) + @ Main ./REPL[10]:0 + [5] top-level scope + @ REPL[12]:1 + +caused by: MethodError: no method matching myfunc(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{, T1} where T1}, ::Base.Iterators.Count{Int64}, ::, ::Type{Base.Iterators.Count{Int64}}, ::, ::Type{Base.IsInfinite})┌ Error: Error showing method candidates, aborted +│ exception = +│ could not determine location of method definition +│ Stacktrace: +│ [1] error(s::String) +│ @ Base ./error.jl:33 +│ [2] functionloc +│ @ ./methodshow.jl:164 [inlined] +│ [3] show_method_candidates(io::IOContext{Base.TTY}, ex::MethodError, kwargs::Any) +│ @ Base ./errorshow.jl:499 +│ [4] showerror(io::IOContext{Base.TTY}, ex::MethodError) +│ @ Base ./errorshow.jl:318 +│ [5] showerror(io::IOContext{Base.TTY}, ex::MethodError, bt::Vector{Base.StackTraces.StackFrame}; backtrace::Bool) +│ @ Base ./errorshow.jl:88 +│ [6] show_exception_stack(io::IOContext{Base.TTY}, stack::Vector{Any}) +│ @ Base ./errorshow.jl:866 +│ [7] display_error(io::IOContext{Base.TTY}, stack::Base.ExceptionStack) +│ @ Base ./client.jl:104 +│ [8] #invokelatest#2 +│ @ ./essentials.jl:716 [inlined] +│ [9] invokelatest +│ @ ./essentials.jl:714 [inlined] +│ [10] print_response(errio::IO, response::Any, show_value::Bool, have_color::Bool, specialdisplay::Union{Nothing, AbstractDisplay}) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:286 +│ [11] (::REPL.var"#45#46"{REPL.LineEditREPL, Pair{Any, Bool}, Bool, Bool})(io::Any) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:275 +│ [12] with_repl_linfo(f::Any, repl::REPL.LineEditREPL) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:508 +│ [13] print_response(repl::REPL.AbstractREPL, response::Any, show_value::Bool, have_color::Bool) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:273 +│ [14] (::REPL.var"#do_respond#66"{Bool, Bool, REPL.var"#77#87"{REPL.LineEditREPL, REPL.REPLHistoryProvider}, REPL.LineEditREPL, REPL.LineEdit.Prompt})(s::REPL.LineEdit.MIState, buf::Any, ok::Bool) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:844 +│ [15] #invokelatest#2 +│ @ ./essentials.jl:716 [inlined] +│ [16] invokelatest +│ @ ./essentials.jl:714 [inlined] +│ [17] run_interface(terminal::REPL.Terminals.TextTerminal, m::REPL.LineEdit.ModalInterface, s::REPL.LineEdit.MIState) +│ @ REPL.LineEdit /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/LineEdit.jl:2493 +│ [18] run_frontend(repl::REPL.LineEditREPL, backend::REPL.REPLBackendRef) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:1230 +│ [19] (::REPL.var"#49#54"{REPL.LineEditREPL, REPL.REPLBackendRef})() +│ @ REPL ./task.jl:423 +└ @ Base errorshow.jl:320 + +Stacktrace: + [1] myfunc(::WhereTraits.InternalState.TraitsDisambiguationSingleton, ::Type{Tuple{, T1} where T1}, a1::Base.Iterators.Count{Int64}, ::, T1::Type, ::, 'trait_1_Base.IteratorSize(a1)'::Type; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}) + @ Main ./none:0 + [2] myfunc(::WhereTraits.InternalState.TraitsDisambiguationSingleton, ::Type{Tuple{, T1} where T1}, a1::Base.Iterators.Count{Int64}, ::, T1::Type, ::, 'trait_1_Base.IteratorSize(a1)'::Type) + @ Main ./none:0 + [3] #myfunc#4 + @ ~/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:212 [inlined] + [4] myfunc(a1::Base.Iterators.Count{Int64}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}) + @ Main ./REPL[10]:0 + [5] myfunc + @ ~/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:211 [inlined] + [6] myfunc(a1::Base.Iterators.Count{Int64}) + @ Main ./REPL[10]:0 + [7] top-level scope + @ REPL[12]:1 +``` + +The error now states clearly that the demanded trait `Core.Typeof(Base.IteratorSize(a1)) <: Base.IsInfinite` is not defined yet. +Internally within WhereTraits all traits (bool traits, `::`-traits and `<:`-traits) are normalized to `<:`-traits. -TODO add disambiguation details +To fix this implementation you would add a definition with the trait `Base.IteratorSize(a1)::Base.IsInfinite`. + +------------------ + +A second example is about a typo: Here the value-level `::` trait is confused with the type-level `<:` Trait. + +```julia +julia> @traits myfunc2(a) where {Base.IteratorSize(a) <: Base.HasShape} = 2 # wrong!! it should be `Base.IteratorSize(a)::Base.HasShape` + +julia> myfunc2([1]) +ERROR: TraitsMethodError: no method matching `myfunc2([1])`. + +It corresponds to the normal julia-dispatch (aka "outerfunction") + +```julia +myfunc2(a1::T1; kwargs...) where T1 +``` + +and the traits (traits are normalized to <:) + +```julia +Base.IteratorSize(a1) <: Base.HasShape{1}() +``` + +however, the only available traits definitions are: + + * * * * * * * * * * * * * * + +```julia +myfunc2(a) where Base.IteratorSize(a) <: Base.HasShape +``` + +which defines the traits (traits are normalized to <:) + +```julia +Base.IteratorSize(a1) <: Base.HasShape +``` + + * * * * * * * * * * * * * * + +Note the following `<:` standardization of traits: + +| WhereTraits | Example | `<:` standardization | +| ------------------:| -------------------------------------:| ------------------------------------------------------:| +| bool trait | `iseven(a)` | `WhereTraits.BoolType(iseven(a)) <: WhereTraits.True` | +| negated bool trait | `!iseven(a)` | `WhereTraits.BoolType(iseven(a)) <: WhereTraits.False` | +| `isa` trait | `Base.IteratorSize(a)::Base.HasShape` | `Core.Typeof(Base.IteratorSize(a)) <: Base.HasShape` | +| `<:` trait | `Base.eltype(a) <: Number` | `Base.eltype(a) <: Number` | + +Stacktrace: + [1] #myfunc2#6 + @ ~/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:215 [inlined] + [2] myfunc2(a1::Vector{Int64}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}) + @ Main ./REPL[13]:0 + [3] myfunc2 + @ ~/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:211 [inlined] + [4] myfunc2(a1::Vector{Int64}) + @ Main ./REPL[13]:0 + [5] top-level scope + @ REPL[14]:1 + +caused by: MethodError: no method matching myfunc2(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{, T1} where T1}, ::Vector{Int64}, ::, ::Type{Vector{Int64}}, ::, ::Base.HasShape{1})┌ Error: Error showing method candidates, aborted +│ exception = +│ could not determine location of method definition +│ Stacktrace: +│ [1] error(s::String) +│ @ Base ./error.jl:33 +│ [2] functionloc +│ @ ./methodshow.jl:164 [inlined] +│ [3] show_method_candidates(io::IOContext{Base.TTY}, ex::MethodError, kwargs::Any) +│ @ Base ./errorshow.jl:499 +│ [4] showerror(io::IOContext{Base.TTY}, ex::MethodError) +│ @ Base ./errorshow.jl:318 +│ [5] showerror(io::IOContext{Base.TTY}, ex::MethodError, bt::Vector{Base.StackTraces.StackFrame}; backtrace::Bool) +│ @ Base ./errorshow.jl:88 +│ [6] show_exception_stack(io::IOContext{Base.TTY}, stack::Vector{Any}) +│ @ Base ./errorshow.jl:866 +│ [7] display_error(io::IOContext{Base.TTY}, stack::Base.ExceptionStack) +│ @ Base ./client.jl:104 +│ [8] #invokelatest#2 +│ @ ./essentials.jl:716 [inlined] +│ [9] invokelatest +│ @ ./essentials.jl:714 [inlined] +│ [10] print_response(errio::IO, response::Any, show_value::Bool, have_color::Bool, specialdisplay::Union{Nothing, AbstractDisplay}) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:286 +│ [11] (::REPL.var"#45#46"{REPL.LineEditREPL, Pair{Any, Bool}, Bool, Bool})(io::Any) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:275 +│ [12] with_repl_linfo(f::Any, repl::REPL.LineEditREPL) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:508 +│ [13] print_response(repl::REPL.AbstractREPL, response::Any, show_value::Bool, have_color::Bool) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:273 +│ [14] (::REPL.var"#do_respond#66"{Bool, Bool, REPL.var"#77#87"{REPL.LineEditREPL, REPL.REPLHistoryProvider}, REPL.LineEditREPL, REPL.LineEdit.Prompt})(s::REPL.LineEdit.MIState, buf::Any, ok::Bool) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:844 +│ [15] #invokelatest#2 +│ @ ./essentials.jl:716 [inlined] +│ [16] invokelatest +│ @ ./essentials.jl:714 [inlined] +│ [17] run_interface(terminal::REPL.Terminals.TextTerminal, m::REPL.LineEdit.ModalInterface, s::REPL.LineEdit.MIState) +│ @ REPL.LineEdit /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/LineEdit.jl:2493 +│ [18] run_frontend(repl::REPL.LineEditREPL, backend::REPL.REPLBackendRef) +│ @ REPL /nix/store/qmlbjq46nhn1v08nnb23y2myrvra8hm7-julia-bin-1.7.1/share/julia/stdlib/v1.7/REPL/src/REPL.jl:1230 +│ [19] (::REPL.var"#49#54"{REPL.LineEditREPL, REPL.REPLBackendRef})() +│ @ REPL ./task.jl:423 +└ @ Base errorshow.jl:320 + +Stacktrace: + [1] myfunc2(::WhereTraits.InternalState.TraitsDisambiguationSingleton, ::Type{Tuple{, T1} where T1}, a1::Vector{Int64}, ::, T1::Type, ::, 'trait_1_Base.IteratorSize(a1)'::Base.HasShape{1}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}) + @ Main ./none:0 + [2] myfunc2(::WhereTraits.InternalState.TraitsDisambiguationSingleton, ::Type{Tuple{, T1} where T1}, a1::Vector{Int64}, ::, T1::Type, ::, 'trait_1_Base.IteratorSize(a1)'::Base.HasShape{1}) + @ Main ./none:0 + [3] #myfunc2#6 + @ ~/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:212 [inlined] + [4] myfunc2(a1::Vector{Int64}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}) + @ Main ./REPL[13]:0 + [5] myfunc2 + @ ~/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:211 [inlined] + [6] myfunc2(a1::Vector{Int64}) + @ Main ./REPL[13]:0 + [7] top-level scope + @ REPL[14]:1 +``` + + + + +## Implementation Details The implementations uses only code-rewrite, creating two nested functions out of the one `@traits` function. The outer function dispatches as normal, the inner function dispatches on the added traits functionality. @@ -66,38 +314,36 @@ After this the macroexpand is simpler to understand which gives the following code ```julia function foo(a1::T1; kwargs...) where T1 - #= none:1 =# - (Main).foo( - WhereTraits.InternalState.TraitsDefSingleton(), - Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}, - a1, - WhereTraits.Syntax.Rendering._BetweenArgsAndTypeVars(), - T1, - WhereTraits.Syntax.Rendering._BetweenTypeVarsAndTraits(), - Val{isodd(a1)}(); - kwargs...) + #= REPL[6]:1 =# + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:211 =# + try + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:212 =# + (Main).foo(WhereTraits.InternalState.TraitsDisambiguationSingleton(), Tuple{, T1} where T1, a1, (), T1, (), (WhereTraits.Utils.BoolTypes.BoolType)(isodd(a1)); kwargs...) + catch exc + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:214 =# + (WhereTraits.InternalState.isWhereTraitsMethodError)(exc) || rethrow() + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:215 =# + throw((WhereTraitsMethodError)(exc)) + end end - -function foo(::WhereTraits.InternalState.TraitsDefSingleton, - ::Type{Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}}, - a, - ::WhereTraits.Syntax.Rendering._BetweenArgsAndTypeVars, - var"'T1'"::Any, - ::WhereTraits.Syntax.Rendering._BetweenTypeVarsAndTraits, - var"'Val{isodd(a1)}()'"::Val{true}) - #= none:1 =# +function foo(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{, T1} where T1}, a, ::, var"'T1'"::Any, ::, var"'isodd(a1)'"::Type{<:WhereTraits.Utils.BoolTypes.True}) + #= REPL[6]:1 =# (a + 1) / 2 end - -function foo(::WhereTraits.InternalState.TraitsDefSingleton) - #= /Users/s.sahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:111 =# - WhereTraits.InternalState.TraitsStore(WhereTraits.InternalState.Reference(Main, :foo), WhereTraits.Utils.TypeDict{WhereTraits.InternalState.DefTraitsFunction}(Tuple{Type,WhereTraits.InternalState.DefTraitsFunction}[(Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}, WhereTraits.InternalState.DefTraitsFunction{Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}}(WhereTraits.InternalState.DefOuterFunc{Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}}(WhereTraits.InternalState.DefOuterFuncFixedPart{Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}}(Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}, :foo, Union{Expr, Symbol}[], ExprParsers.Arg_Parsed[EP.Arg_Parsed(name=:a1, type=:T1, default=ExprParsers.NoDefault())], ExprParsers.TypeRange_Parsed[EP.TypeRange_Parsed(lb=Union{}, name=:T1, ub=Any)], Symbol[:a1], Symbol[:T1]), WhereTraits.InternalState.DefOuterFuncNonFixedPart(Union{Expr, Symbol}[:(Val{isodd(a1)}())])), Dict(WhereTraits.InternalState.DefInnerFuncFixedPart(Dict(:a1 => :a), Dict{Symbol,Symbol}(), Dict{Union{Expr, Symbol},Union{Expr, Symbol}}(:(Val{isodd(a1)}()) => :(var"'Val{isodd(a1)}()'"::Val{true}))) => WhereTraits.InternalState.DefInnerFuncNonFixedPart(Expr[], quote -#= none:1 =# -(a + 1) / 2 -end, :((foo(a) where isodd(a)) = begin - #= none:1 =# - (a + 1) / 2 - end)))))])) +function foo(::WhereTraits.InternalState.TraitsDisambiguationSingleton, ::Type{Tuple{, T1} where T1}, a1, ::, T1, ::, var"'trait_1_isodd(a1)'"::Any; kwargs...) + (Main).foo(WhereTraits.InternalState.TraitsDefSingleton(), Tuple{, T1} where T1, a1, (), T1, (), var"'trait_1_isodd(a1)'"; kwargs...) +end +nothing +function foo(::WhereTraits.InternalState.TraitsStoreSingleton, ::Type{Tuple{, T1} where T1}) + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:76 =# + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:77 =# + WhereTraits.InternalState.TraitsStore{Tuple{, Any}}(WhereTraits.InternalState.DefOuterFunc{Tuple{, Any}}(WhereTraits.InternalState.DefOuterFuncFixedPart{Tuple{, Any}}(Tuple{, T1} where T1, Main, :foo, Union{Expr, Symbol}[], ExprParsers.Arg_Parsed[EP.Arg_Parsed(name=:a1, type=:T1, default=ExprParsers.NoDefault())], ExprParsers.TypeRange_Parsed[EP.TypeRange_Parsed(lb=Union{}, name=:T1, ub=Any)], [:a1], [:T1]), WhereTraits.InternalState.DefOuterFuncNonFixedPart(Union{Expr, Symbol}[:(isodd(a1))], Dict{Union{Expr, Symbol}, Union{Expr, Symbol}}(:(isodd(a1)) => :((WhereTraits.Utils.BoolTypes.BoolType)(isodd(a1)))))), Dict{WhereTraits.InternalState.DefInnerFuncFixedPart, WhereTraits.InternalState.DefInnerFuncNonFixedPart}(WhereTraits.InternalState.DefInnerFuncFixedPart(Dict(:a1 => :a), Dict{Symbol, Symbol}(), Dict{Union{Expr, Symbol}, Union{Expr, Symbol}}(:(isodd(a1)) => :(::Type{<:WhereTraits.Utils.BoolTypes.True}))) => WhereTraits.InternalState.DefInnerFuncNonFixedPart(Main, Expr[], quote + #= REPL[6]:1 =# + (a + 1) / 2 + end, :((foo(a) where isodd(a)) = begin + #= REPL[6]:1 =# + (a + 1) / 2 + end))), WhereTraits.InternalState.DefDisambiguation({1, 0} directed Int64 metagraph with Float64 weights defined by :weight (default weight 1.0))) end nothing ``` @@ -105,49 +351,51 @@ It is actually easy to understand on a high level: 1. The first function `function foo(a1::T1; kwargs...) where T1` is the so called "outer" function which does all the normal standard Julia dispatch. It is the necessary initial entry point in order to then perform a subsequent call to further dispatch on traits. - In the function body you see that this outer function extracts extra information according to the extended where-syntax. Lets go through the arguments one by one + In the function body, within a try-catch, you see that this outer function extracts extra information according to the extended where-syntax. Lets go through the arguments one by one - 1. `WhereTraits.InternalState.TraitsDefSingleton()` is a helper type indicating that this is a call to a traits inner function - 2. `Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}` is the complete function signature of the outer function, with an additional helper `_BetweenCurliesAndArgs` to deal with TypeParameters of UnionAll types (whereupon which you can also define function calls and hence `@traits`) + 1. `WhereTraits.InternalState.TraitsDisambiguationSingleton()` is a helper type indicating that this is a call to the traits disambiguation layer + 2. `Tuple{, T1} where T1` is the complete function signature of the outer function, with an additional helper ``, which is actually a `WhereTraits.Parsing.Normalize._BetweenCurliesAndArgs` to deal with TypeParameters of UnionAll types (whereupon which you can also define function calls and hence `@traits`) 3. `a1` is the first actual argument (after this `a2`, `a3` and etc. could follow in principle) - 4. `WhereTraits.Syntax.Rendering._BetweenArgsAndTypeVars()` is again a helper type to distinguish args from typevariables + 4. `()` is actually a `WhereTraits.InternalState.ArgsHelpers_BetweenArgsAndTypeVars()`, which is again a helper type to distinguish args from typevariables 5. `T1` is a type parameter (again here `T2`, `T3`, etc. would follow if there are more typeparameters) - 6. `WhereTraits.Syntax.Rendering._BetweenTypeVarsAndTraits()` is another helper, now separating the traits definitions - 7. `Val{isodd(a1)}()` here comes our first actual trait definition (if you define more traits, they would follow here) + 6. `()` is actually a `WhereTraits.InternalState.ArgsHelpers_BetweenTypeVarsAndTraits()`, which is another helper, now separating the traits definitions + 7. `(WhereTraits.Utils.BoolTypes.BoolType)(isodd(a1))` here comes our first actual trait definition (if you define more traits, they would follow here), it is a bool definition and uses the special function BoolType to bring bool values to BoolTypes. 8. `; kwargs...` at last all kwargs are just passed through (dispatch on kwargs is not yet supported) - All these arguments are passed on to the inner function, which is defined next. + All these arguments are finally passed on to the inner function, which is defined next. (For completeness, there is an intermediate disambiguation layer, which is sitting between outer function and inner function. It will be described shortly after.) -2. The second function is this inner function `function foo(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{WhereTraits.Syntax._BetweenCurliesAndArgs,Any}}, a, ::WhereTraits.Syntax._BetweenArgsAndTypeVars, var"'T1'"::Any, ::WhereTraits.Syntax._BetweenTypeVarsAndTraits, var"'Val{isodd(a1)}()'"::Val{true})`. +2. The second function is this inner function `function foo(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{, T1} where T1}, a, ::, var"'T1'"::Any, ::, var"'isodd(a1)'"::Type{<:WhereTraits.Utils.BoolTypes.True})`. - Here we do the actual full traits dispatch, specifying a dispatch type for each of the arguments we just put into the inner functions. Let's again go through each single argument: + Here we define the actual full traits dispatch which was just called in the outer function (we ignore the disambiguation layer for now). Let's again go through each single argument: - 1. `::WhereTraits.InternalState.TraitsDefSingleton` dispatches on the singleton type to make sure this does not conflict with any other methods defined for this function - 2. `::Type{Tuple{WhereTraits.Syntax._BetweenCurliesAndArgs,Any}}` dispatches on the signature of the outer function, again adding support for types with Type-parameters which is why you see this extra type `WhereTraits.Syntax._BetweenCurliesAndArgs`. If you would have dispatch for say `function MyType{Int, Bool}(a::Any, b::Any)` this would look like `::Type{Tuple{Int, Bool, WhereTraits.Syntax._BetweenCurliesAndArgs,Any, Any}}` respectively + 1. `::WhereTraits.InternalState.TraitsDefSingleton` dispatches on the singleton type for an inner-function-definition. + 2. `::Type{Tuple{, T1} where T1}` dispatches on the signature of the outer function, again adding support for types with Type-parameters which is why you see this extra type `` (which is actually a `WhereTraits.Syntax.Parsing.Normalize._BetweenCurliesAndArgs`). If you would have dispatch for say `function MyType{Int, Bool}(a::Any, b::Any)` this would look like `::Type{Tuple{Int, Bool, , T1, T2} where {T1, T2}}` respectively 3. `a` is just the standard argument, which was of type `Any`. Hereafter, other arguments would follow. - 4. `::WhereTraits.Syntax._BetweenArgsAndTypeVars` is again a helper for dispatch separation + 4. `::` (which is actually a `::WhereTraits.InternalState.ArgsHelper_BetweenArgsAndTypeVars`) is again a helper for dispatch separation 5. `var"'T1'"::Any` corresponds to the normal standard TypeParameter, here of type Any. It was renamed into `var"'T1'"`, because it is actually nowhere used in the function body. If you would have used the TypeVariable `T1`, it is named plainly `T1`. - This was implemented because the syntax actually may have to add extra typeparameters, which then for sure are not used by the code. Hence we distinguish used/unused typeparameters for better debugging/inspecting. + This was implemented because the syntax actually may have to add extra typeparameters, which then for sure are not used by the code. We distinguish used/unused typeparameters for better debugging/inspecting. Hereafter, other standard type parameters would follow. - 6. `::WhereTraits.Syntax._BetweenTypeVarsAndTraits` is the last helper for dispatch separation - 7. `var"'Val{isodd(a1)}()'"::Val{true})` is our extended where-dispatch for Bool function + 6. `::` (actually `::WhereTraits.InternalState.ArgsHelper_BetweenTypeVarsAndTraits`) is the last helper for dispatch separation + 7. `var"'isodd(a1)'"::Type{<:WhereTraits.Utils.BoolTypes.True}` is our extended where-dispatch for Bool function - You see that the syntax automatically wrapped the function into a `Val` and here we dispatch on `Val{true}`. The name is extra descriptive and refers to the precise function call which happens in the outer function. This can be helpful for debugging and inspecting. + As the argument was wrapped into BoolTypes.BoolType, we can now dispatch on `BoolTypes.True`. The name is extra descriptive and refers to the precise function call which happens in the outer function. This can be helpful for debugging and inspecting. 8. This function does not define any keyword arguments. -3. The last complex looking function is `function foo(::WhereTraits.InternalState.TraitsDefSingleton)`. It again uses the `TraitsDefSingleton` to indicate that this is an internal detail of the traits syntax, however does not take any further arguments. Concretely, it defines the hidden state which is needed to correctly construct the outer and inner functions required to realise the extended dispatch of `@traits`. You don't have to understand it, still you hopefully get the feeling that everything is there. +3. The third function is the disambiguation, which sits between outer function and inner function. In our case it only forwards the arguments to the inner function, but in general this layer implements the functionality of `@traits_order`. -4. Finally there is `nothing` in order to prevent printing possibly confusing internal details. +4. The last complex looking function is `function foo(::WhereTraits.InternalState.TraitsStoreSingleton)`. Concretely, it defines the hidden state which is needed to correctly construct the outer and inner functions required to realise the extended dispatch of `@traits`. You don't have to understand it, still you hopefully get the feeling that everything is there. -If you try `@macroexpand @traits foo(a) where {!isodd(a)} = a/2` instead, you will see that it is very similar, but dispatching on `::Val{false}` instead. This is part of the special support for bool function. +5. Finally there is `nothing` in order to prevent printing possibly confusing internal details. + +If you try `@macroexpand @traits foo(a) where {!isodd(a)} = a/2` instead, you will see that it is very similar, but dispatching on `::BoolTypes.False` instead. This is part of the special support for bool function. Also try `@macroexpand @traits foo(a) where {iseven(a)} = a/2` and see what the syntax does differently. @@ -176,32 +424,57 @@ For instance if you finally defined ```juliarepl julia> @traits_show_implementation foo - Outer function for signature Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any} + Outer function for signature Tuple{, T1} where T1 function foo(a1::T1; kwargs...) where T1 - #= none:1 =# - (Main).foo(WhereTraits.InternalState.TraitsDefSingleton(), Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}, a1, WhereTraits.Syntax.Rendering._BetweenArgsAndTypeVars(), T1, WhereTraits.Syntax.Rendering._BetweenTypeVarsAndTraits(), Val{isodd(a1)}(); kwargs...) + #= REPL[9]:1 =# + begin + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:211 =# + try + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:212 =# + (Main).foo(WhereTraits.InternalState.TraitsDisambiguationSingleton(), Tuple{, T1} where T1, a1, (), T1, (), (WhereTraits.Utils.BoolTypes.BoolType)(isodd(a1)); kwargs...) + catch exc + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:214 =# + (WhereTraits.InternalState.isWhereTraitsMethodError)(exc) || rethrow() + #= /home/ssahm/.julia/dev/WhereTraits/src/Syntax/Rendering.jl:215 =# + throw((WhereTraitsMethodError)(exc)) + end + end end) - • • • + • • • - Inner functions for signature Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any} + Inner functions for signature Tuple{, T1} where T1 - function foo(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}}, a, ::WhereTraits.Syntax.Rendering._BetweenArgsAndTypeVars, var"'T1'"::Any, ::WhereTraits.Syntax.Rendering._BetweenTypeVarsAndTraits, var"'Val{isodd(a1)}()'"::Val{true}) - #= none:1 =# + function foo(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{, T1} where T1}, a, ::, var"'T1'"::Any, ::, var"'isodd(a1)'"::Type{<:WhereTraits.Utils.BoolTypes.True}) + #= REPL[7]:1 =# (a + 1) / 2 end - • • • + • • • - function foo(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{WhereTraits.Syntax.Parsing._BetweenCurliesAndArgs,Any}}, a, ::WhereTraits.Syntax.Rendering._BetweenArgsAndTypeVars, var"'T1'"::Any, ::WhereTraits.Syntax.Rendering._BetweenTypeVarsAndTraits, var"'Val{isodd(a1)}()'"::Val{false}) - #= none:1 =# + function foo(::WhereTraits.InternalState.TraitsDefSingleton, ::Type{Tuple{, T1} where T1}, a, ::, var"'T1'"::Any, ::, var"'isodd(a1)'"::Type{<:WhereTraits.Utils.BoolTypes.False}) + #= REPL[8]:1 =# a / 2 end - • • • + • • • + + • • • + + Disambiguation functions for signature Tuple{, T1} where T1 + + Base.delete_method(foo(::WhereTraits.InternalState.TraitsDisambiguationSingleton, ::Type{Tuple{, T1} where T1}, a1, ::, T1, ::, var"'trait_1_isodd(a1)'"; kwargs...) in Main) + + • • • + + function foo(::WhereTraits.InternalState.TraitsDisambiguationSingleton, ::Type{Tuple{, T1} where T1}, a1, ::, T1, ::, var"'trait_1_isodd(a1)'"::Any; kwargs...) + (Main).foo(WhereTraits.InternalState.TraitsDefSingleton(), Tuple{, T1} where T1, a1, (), T1, (), var"'trait_1_isodd(a1)'"; kwargs...) + end + + • • • - ──────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────────────── ``` diff --git a/docs/src/usage.md b/docs/src/usage.md index 22b932a..1fefaca 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -98,24 +98,17 @@ julia> conflict([1,2,3,4]) For more details, take a look at the [documentation](https://jolin-io.github.io/WhereTraits.jl/dev). -## Limitations & Future Plans +## Limitations ### Optimal Code *Warning: While the dispatch works for dynamic functions, it will only be able to create optimal code if your traits function supports proper type-inference. E.g. you can use `Base.isempty`, however type-inference cannot see whether it will return true or false by static inspection. Hence it will use slower dynamic code.* -### MethodError -Currently if you made a mistake in your traits definition, you get a pretty incomprehensible error. What you see is merely an implementation detail and it is on the roadmap to give a nice user-friendly error-handling instead. - ### Keyword arguments Keyword arguments are at the moment not support for WhereTraits dispatch. They are just passed through. -It is planned to support Keyword arguments in some future release. - ### Symbol Level -The extended where syntax is currently implemented on **symbol level**, which is why traits functions like `Base.IteratorSize` and the non-qualified `IteratorSize` (assuming you imported `import Base:IteratorSize`) are treated as two different functions, despite being the same. So for now try to only use the one style or the other. - -There are plans to evaluate the symbols to functions beforehand. Still in evaluation phase. +The extended where syntax is currently implemented on **symbol level**, which is why traits functions like `Base.IteratorSize` and the non-qualified `IteratorSize` (assuming you imported `import Base: IteratorSize`) are treated as two different functions, despite being the same. So for now try to only use the one style or the other. ### Top Level Only Currently **only top-level functions** are supported, as the syntax stores and needs information about previous function definitions, which it stores globally. If macros would get informed about whether they are defined within another function, WhereTraits could also support innerfunctions. @@ -124,6 +117,3 @@ Currently **only top-level functions** are supported, as the syntax stores and n The `@traits` macro currently does not work well within the `Test.@testset` macro. Usually you won't encounter this, as standard dispatch is probably enough for your tests. Nevertheless there is a workaround. WhereTraits.jl exports a `@traits_test` macro variant which works better, but still might have cases where it fails. This needs to be investigated further, and maybe needs a change on `Test.@testset`. - - - diff --git a/src/ExtraHelpers.jl b/src/ExtraHelpers.jl index cf7c4de..85b3f1d 100644 --- a/src/ExtraHelpers.jl +++ b/src/ExtraHelpers.jl @@ -14,15 +14,15 @@ like @traits, and works within Test.@testset, but cannot be doc-stringed needed because of https://github.com/JuliaLang/julia/issues/34263 """ -macro traits_test(expr_original) - expr = macroexpand(__module__, expr_original) - expr_traits = WhereTraits.Syntax._traits(@MacroEnv, expr, expr_original) +macro traits_test(expr) + expr_traits = WhereTraits.Syntax.traits_impl(@MacroEnv, expr) + if isa(expr_traits, Expr) && expr_traits.head == :escape + # we need to unwrap the escape + expr_traits = expr_traits.args[1] + end # :(eval($(QuoteNode(...))) is a workaround for @testset, see https://github.com/JuliaLang/julia/issues/34263 expr_traits = :(eval($(QuoteNode(expr_traits)))) expr_traits = esc(expr_traits) - if CONFIG.suppress_on_traits_definitions - expr_traits = :(@suppress $expr_traits) - end expr_traits end diff --git a/src/InternalState.jl b/src/InternalState.jl index d24c27c..150c3f0 100644 --- a/src/InternalState.jl +++ b/src/InternalState.jl @@ -1,9 +1,7 @@ -# Internal State of the syntax -# ============================ - module InternalState export get_traitsstore, get_traitsstores, TraitsStore -export WhereTraitsException, WhereTraitsAmbiguityError +export WhereTraitsException, WhereTraitsAmbiguityError, WhereTraitsMethodError +export isWhereTraitsMethodError using WhereTraits: InternalState using WhereTraits.Utils: normalize_mod_and_name @@ -13,6 +11,11 @@ using ProxyInterfaces using Graphs using MetaGraphs +using Markdown + + +# Internal State of the syntax +# ============================ @struct_hash_equal Base.@kwdef struct DefOuterFuncFixedPart{Signature} # everything under fixed should be identifiable via signature @@ -111,6 +114,10 @@ const DefInnerFuncs = Dict{DefInnerFuncFixedPart, DefInnerFuncNonFixedPart} disambiguation::DefDisambiguation end + +# Storing and Accessing Traits Information +# ======================================== + """ returns TraitsStore or nothing if no TraitsStore could be found """ @@ -186,6 +193,19 @@ struct TraitsDocSingleton <: TraitsSingleton end const traitsdocsingleton = TraitsDocSingleton() +# Rendering specific +# ================== + +# we use special Singletons as separators to distinguish different kinds of parameters +struct ArgsHelpers_BetweenTypeVarsAndTraits end +Base.show(io::IO, x::Type{<:ArgsHelpers_BetweenTypeVarsAndTraits}) = print(io, "") + +struct ArgsHelpers_BetweenArgsAndTypeVars end +Base.show(io::IO, x::Type{<:ArgsHelpers_BetweenArgsAndTypeVars}) = print(io, "") + + +# Error Handling +# ============== abstract type WhereTraitsException <: Exception end struct WhereTraitsAmbiguityError <: WhereTraitsException @@ -206,4 +226,104 @@ function Base.showerror(io::IO, e::WhereTraitsAmbiguityError) """) end + +# MethodError support +# ------------------- + +Base.@kwdef struct WhereTraitsMethodError{F, Signature, Args, Typevars, Traits} <: Exception + f::F + signature::Signature + args::Args + typevars::Typevars + traits::Traits +end + +function Base.showerror(io::IO, ex::WhereTraitsMethodError) + traitsstore = ex.f(traitsstoresingleton, ex.signature) + + innerargs_traits_mapping = traitsstore.outerfunc.nonfixed.innerargs_traits_mapping + traitsdef = map(traitsstore.outerfunc.nonfixed.traits, ex.traits) do traitname, trait_arg + :($(innerargs_traits_mapping[traitname]) <: $trait_arg) + end + + outerfunc_signature = to_expr(EP.Signature_Parsed( + name = traitsstore.outerfunc.fixed.name, + curlies = traitsstore.outerfunc.fixed.curlies, + args = traitsstore.outerfunc.fixed.args, + kwargs = [:(kwargs...)], + wheres = traitsstore.outerfunc.fixed.wheres, + )) + + show(io, MIME"text/markdown"(), Markdown.parse(""" + TraitsMethodError: no method matching `$(ex.f)($(join(ex.args, ",")))`. + + It corresponds to the normal julia-dispatch (aka "outerfunction") + ```julia + $outerfunc_signature + ``` + and the traits (traits are normalized to <:) + ```julia + $(join(traitsdef, "\n")) + ``` + however, the only available traits definitions are: + - - - - - - - - - - - - - - + $(join([ + """ + ```julia + $(nonfixed.expr_original.args[1]) + ``` + which defines the traits (traits are normalized to <:) + ```julia + $(join([ + :($(innerargs_traits_mapping[traitname]) <: $(dispatch.args[1].args[2].args[1])) + # dispatch looks like :(::Type{<:B}) + for (traitname, dispatch) in fixed.traits_mapping + # fixed.traits_mapping looks like + # Dict{Union{Expr, Symbol}, Union{Expr, Symbol}} with 1 entry: + # :(Base.IteratorSize(a1)) => :(::Type{<:Base.HasShape}) + ], "\n")) + ``` + - - - - - - - - - - - - - - + """ + for (fixed, nonfixed) in traitsstore.innerfuncs + ], + "\n")) + + Note the following `<:` standardization of traits: + + | WhereTraits | Example | `<:` standardization | + | ------------------ | ------------------------------------- | ------------------------------------------------------ | + | bool trait | `iseven(a)` | `WhereTraits.BoolType(iseven(a)) <: WhereTraits.True` | + | negated bool trait | `!iseven(a)` | `WhereTraits.BoolType(iseven(a)) <: WhereTraits.False` | + | `isa` trait | `Base.IteratorSize(a)::Base.HasShape` | `Core.Typeof(Base.IteratorSize(a)) <: Base.HasShape` | + | `<:` trait | `Base.eltype(a) <: Number` | `Base.eltype(a) <: Number` | + """)) +end + +function WhereTraitsMethodError(exc::MethodError) + @assert (length(exc.args) > 0 + && isa(exc.args[1], TraitsDefSingleton)) + + betweenArgsAndTypeVars = findfirst(exc.args) do arg + isa(arg, ArgsHelpers_BetweenArgsAndTypeVars) + end + betweenTypeVarsAndTraits = findfirst(exc.args) do arg + isa(arg, ArgsHelpers_BetweenTypeVarsAndTraits) + end + + WhereTraitsMethodError( + f = exc.f, + signature = exc.args[2], + args = exc.args[3:betweenArgsAndTypeVars-1], + typevars = exc.args[betweenArgsAndTypeVars+1:betweenTypeVarsAndTraits-1], + traits = exc.args[betweenTypeVarsAndTraits+1:end] + ) +end + +isWhereTraitsMethodError(other_err) = false +function isWhereTraitsMethodError(exc::MethodError) + (length(exc.args) > 0 + && isa(exc.args[1], TraitsDefSingleton)) +end + end # module diff --git a/src/Syntax/Parsing/Extract.jl b/src/Syntax/Parsing/Extract.jl new file mode 100644 index 0000000..30c900e --- /dev/null +++ b/src/Syntax/Parsing/Extract.jl @@ -0,0 +1,41 @@ +module Extract +export extract_vars, extract_functionnames, extract_var_from_qualified + +using ExprParsers + +# TODO these could be improved by using ExprParsers and going into keyword arguments +# and further special cases + +""" +if . is used, it returns the qualified name, i.e. the full 'Base.iseven' +""" +extract_vars(expr::Expr) = extract_vars(Val(expr.head), expr.args...) +extract_vars(::Val{:<:}, upperbound) = extract_vars(x) +extract_vars(::Val{:<:}, x, upperbound) = [extract_vars(x); extract_vars(upperbound)] +extract_vars(::Val{:(::)}, x) = extract_vars(x) +extract_vars(::Val{:(::)}, x, type) = [extract_vars(x); extract_vars(type)] +extract_vars(::Val{:call}, _functionname, args...) = extract_vars(args) +extract_vars(::Val{:macro}, _macroname, _linenumbernode, args...) = extract_vars(args) +extract_vars(::Val{:.}, args...) = [Expr(:., args...)] # qualified_symbol +extract_vars(args::Union{Tuple, Vector}) = [var for arg ∈ args for var in extract_vars(arg)] +extract_vars(symbol::Symbol) = [symbol] +extract_vars(_any...) = [] + +"""includes macro names + +if . is used, it returns the qualified name, i.e. the full 'Base.iseven' +""" +extract_functionnames(expr::Expr) = extract_functionnames(Val(expr.head), expr.args...) +extract_functionnames(::Val{:<:}, upperbound) = extract_functionnames(x) +extract_functionnames(::Val{:<:}, x, upperbound) = [extract_functionnames(x); extract_functionnames(upperbound)] +extract_functionnames(::Val{:(::)}, x) = extract_functionnames(x) +extract_functionnames(::Val{:(::)}, x, type) = [extract_functionnames(x); extract_functionnames(type)] +extract_functionnames(::Val{:call}, _functionname, args...) = [_functionname, extract_functionnames(args)...] +extract_functionnames(::Val{:macro}, _macroname, _linenumbernode, args...) = [_macroname, extract_functionnames(args)...] +extract_functionnames(args::Union{Tuple, Vector}) = [var for arg ∈ args for var in extract_functionnames(arg)] +extract_functionnames(_any...) = [] + + +extract_var_from_qualified(symbol::Symbol) = symbol +extract_var_from_qualified(expr::Expr) = parse_expr(EP.NestedDot(), expr).properties[end] +end \ No newline at end of file diff --git a/src/Syntax/Parsing/Parsing.jl b/src/Syntax/Parsing/Parsing.jl index f167cb5..51fdb92 100644 --- a/src/Syntax/Parsing/Parsing.jl +++ b/src/Syntax/Parsing/Parsing.jl @@ -3,12 +3,16 @@ export parse_traitsfunction, parse_traitsorder using WhereTraits: InternalState using WhereTraits.Utils +using WhereTraits.Utils: isdefined_generalized using SimpleMatch using ExprParsers include("Normalize.jl") using .Normalize +include("Extract.jl") +using .Extract + using Graphs using MetaGraphs @@ -58,47 +62,80 @@ function parse_traitsfunction(env, func_parsed::EP.Function_Parsed, expr_origina Found $(intersect(keys(innerfunc_fixed.args_mapping), keys(innerfunc_fixed.typevars_mapping))) in both.") - # prepare extra wheres - # -------------------- - - traits_names = map(traitname_from_parsedtrait, extra_wheres) - traits_args = map(traitarg_from_parsedtrait, extra_wheres) - traits_upperbounds = map(traitupperbound_from_parsedtrait, extra_wheres) - - # deal with dropped typevariables # ------------------------------- - filtered = [!depends_on(t, typevars_dropped) for t in traits_names] - traits_names_filtered = traits_names[filtered] - traits_args_filtered = traits_args[filtered] - traits_upperbounds_filtered = traits_upperbounds[filtered] + filtered = [!depends_on(to_expr(expr), typevars_dropped) for expr in extra_wheres] + extra_wheres_filtered = extra_wheres[filtered] + + if length(extra_wheres) != length(extra_wheres_filtered) + extra_wheres_dropped = extra_wheres[.!filtered] + on_traits_dropped("Given traits depend on droppable typeparameters ($typevars_dropped). WhereTraits: $extra_wheres_dropped") + end - if length(traits_names) != length(traits_names_filtered) - traits_dropped = traits_names[.!filtered] - on_traits_dropped("Given traits depend on droppable typeparameters ($typevars_dropped). WhereTraits: $traits_dropped") + + # Check for UndefVarError within extra_wheres + # ------------------------------------------- + + _args = values(innerfunc_fixed.args_mapping) + _typevars = values(innerfunc_fixed.typevars_mapping) + _local_scope = [_args...; _typevars...] + for extra_where in extra_wheres_filtered + expr = to_expr(extra_where) + for var in extract_vars(expr) + var ∉ _local_scope || continue + isdefined_generalized(env.mod, var) || throw( + MacroError(UndefVarError(extract_var_from_qualified(var))) + ) + isa(var, Symbol) || continue + !isdefined(Base, var) || continue + !isdefined(Core, var) || continue + # we only warn if the var is a non-qualified global constant + @warn( + "Variable `$var` refers to a global constant." + * " It is used within the trait `$expr`." + * " Could be accidental.", + _file=string(env.source.file), + _line=env.source.line, + _module=env.mod) + end + for var in extract_functionnames(expr) + var ∉ _local_scope || continue + isdefined_generalized(env.mod, var) || throw( + MacroError(UndefVarError(extract_var_from_qualified(var))) + ) + # do nothing, it is normal that a function is defined in the outer scope + end end + # prepare extra_wheres + # -------------------- + + traits_names = map(traitname_from_parsedtrait, extra_wheres_filtered) + traits_args = map(traitarg_from_parsedtrait, extra_wheres_filtered) + traits_upperbounds = map(traitupperbound_from_parsedtrait, extra_wheres_filtered) + + # normalized naming also for innerfunc body # ----------------------------------------- old_to_new = Dict(v => k for (k, v) in merge(innerfunc_fixed.args_mapping, innerfunc_fixed.typevars_mapping)) # TODO we currently do not normalize the traits function names # TODO e.g. using both `Base.IteratorSize(a)` and `IteratorSize(a)` result in two different traits - traits_names_filtered_normalized = change_symbols(old_to_new, traits_names_filtered) - traits_args_filtered_normalized = change_symbols(old_to_new, traits_args_filtered) + traits_names_normalized = change_symbols(old_to_new, traits_names) + traits_args_normalized = change_symbols(old_to_new, traits_args) traits_mapping = Dict(k => :(::Type{<:$v}) - for (k, v) in zip(traits_names_filtered_normalized, traits_upperbounds_filtered)) + for (k, v) in zip(traits_names_normalized, traits_upperbounds)) innerargs_traits_mapping = Dict(k => v - for (k, v) in zip(traits_names_filtered_normalized, traits_args_filtered_normalized)) + for (k, v) in zip(traits_names_normalized, traits_args_normalized)) - @assert length(traits_mapping) == length(traits_names_filtered_normalized) "FATAL. Found duplicate traits in $(traits_names_filtered_normalized)" + @assert length(traits_mapping) == length(traits_names_normalized) "FATAL. Found duplicate traits in $(traits_names_normalized)" # we may encounter no traits at all, namely in the case where a default clause is defined - traits = sortexpr(traits_names_filtered_normalized) + traits = sortexpr(traits_names_normalized) # bring everything together @@ -159,11 +196,13 @@ function create_where_parser(args_names) ) end + function traitname_from_parsedtrait(expr) @match(expr) do f # Bool values are lifted to typelevel BoolType # plain arguments are interpreted as bool f(x::EP.Named{:arg, Symbol}) = to_expr(x.value) + # plain calls are assumed to refer to boolean expressions function f(x::EP.Named{:func, EP.Call_Parsed}) if x.value.name == :! @@ -174,8 +213,10 @@ function traitname_from_parsedtrait(expr) to_expr(x.value) end end + # lifting :: dispatch to typelevel <: dispatch f(x::EP.Named{<:Any, EP.TypeAnnotation_Parsed}) = to_expr(x.value.name) + # standard typelevel <: dispatch f(x::EP.Named{<:Any, EP.TypeRange_Parsed}) = to_expr(x.value.name) end @@ -187,6 +228,7 @@ function traitarg_from_parsedtrait(expr) # Bool values are lifted to typelevel BoolType # plain arguments are interpreted as bool f(x::EP.Named{:arg, Symbol}) = :($BoolType($(to_expr(x.value)))) + # plain calls are assumed to refer to boolean expressions function f(x::EP.Named{:func, EP.Call_Parsed}) if x.value.name == :! @@ -197,8 +239,10 @@ function traitarg_from_parsedtrait(expr) :($BoolType($(to_expr(x.value)))) end end + # lifting :: dispatch to typelevel <: dispatch f(x::EP.Named{<:Any, EP.TypeAnnotation_Parsed}) = :(Core.Typeof($(to_expr(x.value.name)))) + # standard typelevel <: dispatch f(x::EP.Named{<:Any, EP.TypeRange_Parsed}) = to_expr(x.value.name) end @@ -207,9 +251,14 @@ end function traitupperbound_from_parsedtrait(expr) @match(expr) do f - f(x::EP.Named{:arg, Symbol}) = True # plain arguments are interpreted as bool - f(x::EP.Named{:func, EP.Call_Parsed}) = (x.value.name == :!) ? False : True # plain calls are assumed to refer to boolean expressions + # plain arguments are interpreted as bool + f(x::EP.Named{:arg, Symbol}) = True + + # plain calls are assumed to refer to boolean expressions + f(x::EP.Named{:func, EP.Call_Parsed}) = (x.value.name == :!) ? False : True + f(x::EP.Named{<:Any, EP.TypeAnnotation_Parsed}) = to_expr(x.value.type) + function f(x::EP.Named{<:Any, EP.TypeRange_Parsed}) tr = x.value @assert !(tr.lb === Union{} && tr.ub == Any) "should have at least an upperbound or a lowerbound" diff --git a/src/Syntax/Rendering.jl b/src/Syntax/Rendering.jl index 8b12a80..777b6ff 100644 --- a/src/Syntax/Rendering.jl +++ b/src/Syntax/Rendering.jl @@ -60,14 +60,6 @@ end # Render # ====== -# we use special Singletons as separators to distinguish different kinds of parameters -struct _BetweenTypeVarsAndTraits end -Base.show(io::IO, x::Type{<:_BetweenTypeVarsAndTraits}) = print(io, "") - -struct _BetweenArgsAndTypeVars end -Base.show(io::IO, x::Type{<:_BetweenArgsAndTypeVars}) = print(io, "") - - function render(env::MacroEnv, torender::RenderTraitsStore) outerfunc = torender.store.outerfunc @@ -143,9 +135,9 @@ function render(env::MacroEnv, torender::RenderInnerFunc) # overwrite each other's innerfunc :(::$(Type{outerfunc.fixed.signature})); _map_args(innerfunc.fixed.args_mapping, outerfunc.fixed.innerargs_args); - :(::$_BetweenArgsAndTypeVars); + :(::$(InternalState.ArgsHelpers_BetweenArgsAndTypeVars)); _map_args(innerfunc.fixed.typevars_mapping, outerfunc.fixed.innerargs_typevars); - :(::$_BetweenTypeVarsAndTraits); + :(::$(InternalState.ArgsHelpers_BetweenTypeVarsAndTraits)); _map_traits(innerfunc.fixed.traits_mapping, outerfunc.nonfixed.traits); ] @@ -203,9 +195,9 @@ function render(env::MacroEnv, torender::RenderOuterFunc) # overwrite each other's innerfunc outerfunc.fixed.signature; outerfunc.fixed.innerargs_args; - _BetweenArgsAndTypeVars(); + InternalState.ArgsHelpers_BetweenArgsAndTypeVars(); outerfunc.fixed.innerargs_typevars; - _BetweenTypeVarsAndTraits(); + InternalState.ArgsHelpers_BetweenTypeVarsAndTraits(); map(t -> outerfunc.nonfixed.innerargs_traits_mapping[t], outerfunc.nonfixed.traits); ] innerfunc_call = EP.Call_Parsed( @@ -214,8 +206,18 @@ function render(env::MacroEnv, torender::RenderOuterFunc) args = innerargs, kwargs = [:(kwargs...)], ) + # Rewrite Traits MethodErrors + innerfunc_call_wrapped = quote + try + $innerfunc_call + catch exc + $(InternalState.isWhereTraitsMethodError)(exc) || rethrow() + throw($(InternalState.WhereTraitsMethodError)(exc)) + end + end + # add LineNumberNode for debugging purposes - body = Expr(:block, env.source, innerfunc_call) + body = Expr(:block, env.source, innerfunc_call_wrapped) # if we are rendering code for the same module, we need to drop the module information # this is needed for defining the function the very first time as `MyModule.func(...) = ...` is invalid syntax @@ -288,9 +290,9 @@ function render(env::MacroEnv, torender::RenderDisambiguation) # overwrite each other's innerfunc outerfunc.fixed.signature; outerfunc.fixed.innerargs_args; - _BetweenArgsAndTypeVars(); + InternalState.ArgsHelpers_BetweenArgsAndTypeVars(); outerfunc.fixed.innerargs_typevars; - _BetweenTypeVarsAndTraits(); + InternalState.ArgsHelpers_BetweenTypeVarsAndTraits(); resolution_traits; ] @@ -326,9 +328,9 @@ function render(env::MacroEnv, torender::RenderDisambiguation) # overwrite each other's innerfunc :(::$(Type{outerfunc.fixed.signature})); outerfunc.fixed.innerargs_args; - :(::$_BetweenArgsAndTypeVars); + :(::$(InternalState.ArgsHelpers_BetweenArgsAndTypeVars)); outerfunc.fixed.innerargs_typevars; - :(::$_BetweenTypeVarsAndTraits); + :(::$(InternalState.ArgsHelpers_BetweenTypeVarsAndTraits)); outerargs_traits; ] @@ -358,9 +360,9 @@ function render(env::MacroEnv, torender::RenderDisambiguation) # overwrite each other's innerfunc outerfunc.fixed.signature; outerfunc.fixed.innerargs_args; - _BetweenArgsAndTypeVars(); + InternalState.ArgsHelpers_BetweenArgsAndTypeVars(); outerfunc.fixed.innerargs_typevars; - _BetweenTypeVarsAndTraits(); + InternalState.ArgsHelpers_BetweenTypeVarsAndTraits(); innerargs_traits; ] @@ -377,9 +379,9 @@ function render(env::MacroEnv, torender::RenderDisambiguation) # overwrite each other's innerfunc :(::$(Type{outerfunc.fixed.signature})); outerfunc.fixed.innerargs_args; - :(::$_BetweenArgsAndTypeVars); + :(::$(InternalState.ArgsHelpers_BetweenArgsAndTypeVars)); outerfunc.fixed.innerargs_typevars; - :(::$_BetweenTypeVarsAndTraits); + :(::$(InternalState.ArgsHelpers_BetweenTypeVarsAndTraits)); outerargs_traits; ] @@ -441,18 +443,21 @@ function render(env::MacroEnv, torender::RenderDoc) # start documentation with autosignature of outer function header = Markdown.parse(""" - ``` - $signature - ``` - ------ Original @traits definitions follow ------ + ``` + $signature + ``` + ------ Original @traits definitions follow ------ - """) + """) separator = Markdown.parse("- - -\n") doc_exprs = Any[header] for (fixed, nonfixed) in innerfuncs # automatic signature string of inner function - signature_original = Markdown.parse("```julia\n$(nonfixed.expr_original.args[1])\n```") # TODO this assumes that expr_original is a function, can we do this? + signature_original = Markdown.parse(""" + ```julia + $(nonfixed.expr_original.args[1]) + ```""") # TODO this assumes that expr_original is a function, can we do this? push!(doc_exprs, signature_original) # manual doc string of respective inner function push!(doc_exprs, :($(WhereTraits.Utils.DocsHelpers).mygetdoc( @@ -462,7 +467,11 @@ function render(env::MacroEnv, torender::RenderDoc) Type{$(innerfunc_fixed_to_doctype(fixed))}} ))) # automatic doc string of inner function definition - expr_original = Markdown.parse("Original @traits definition:\n```julia\n$(nonfixed.expr_original)\n```") + expr_original = Markdown.parse(""" + Original @traits definition: + ```julia + $(nonfixed.expr_original) + ```""") push!(doc_exprs, expr_original) # better visual separation push!(doc_exprs, separator) @@ -508,5 +517,4 @@ function _Dict_to_normalizedType(d::AbstractDict) Tuple{rows...} end - end # module diff --git a/src/Syntax/Syntax.jl b/src/Syntax/Syntax.jl index 0172ee4..6c8a119 100644 --- a/src/Syntax/Syntax.jl +++ b/src/Syntax/Syntax.jl @@ -6,7 +6,6 @@ import WhereTraits using WhereTraits: CONFIG using WhereTraits.Utils using WhereTraits.InternalState -using Suppressor include("Lowering.jl") using .Lowering @@ -28,14 +27,19 @@ using .Merging """ @traits f(a, b) where {!isempty(a), !isempty(b)} = (a[1], b[1]) """ -macro traits(expr_original) - expr_expanded = macroexpand(__module__, expr_original) - expr_traits = _traits(@MacroEnv, expr_expanded, expr_original) - expr_traits = esc(expr_traits) - if CONFIG.suppress_on_traits_definitions - expr_traits = :(@suppress $expr_traits) +macro traits(expr) + traits_impl(@MacroEnv, expr) +end + +function traits_impl(env::MacroEnv, expr_original) + expr_expanded = macroexpand(env.mod, expr_original) + expr_traits = try + _traits(env, expr_expanded, expr_original) + catch exc + isa(exc, MacroError) || rethrow() + return :($throw($(exc.exception))) end - expr_traits + esc(expr_traits) end function _traits(env, expr_expanded::Expr, expr_original::Expr) @@ -51,7 +55,7 @@ function _traits_parsed(env, func_parsed::EP.Function_Parsed, expr_original::Exp exprs = [render(env, basefunc_to_be_rendered)] for lowering in lowerings - # As lowering dropped variables, also traits may need to be dropped. Do this silently. + # As the lowering-process dropped variables, also traits may need to be dropped. Do this silently. lowered_outer, lowered_inner = parse_traitsfunction(env, lowering, expr_original, on_traits_dropped = msg -> nothing) # we don't document lowerings # lowerings have another distinct signature, hence we do not reuse basefunc_store_new @@ -86,9 +90,6 @@ macro traits_order(functioncall, body) expr_disambiguation = _traits_order(@MacroEnv, expr_expanded, expr_original) expr_disambiguation = esc(expr_disambiguation) - if CONFIG.suppress_on_traits_definitions - expr_disambiguation = :(@suppress $expr_disambiguation) - end expr_disambiguation end diff --git a/src/Utils/Normalize/Normalize.jl b/src/Utils/Normalize/Normalize.jl index 0077292..8d9aa1c 100644 --- a/src/Utils/Normalize/Normalize.jl +++ b/src/Utils/Normalize/Normalize.jl @@ -1,5 +1,5 @@ module Normalize -export normalize_mod_and_name, normalized_arg_by_position +export isdefined_generalized, normalize_mod_and_name, normalized_arg_by_position using Reexport @@ -12,8 +12,54 @@ using ExprParsers normalized_arg_by_position(position::Int) = Symbol("a", position) +""" + isdefined_generalized(mod, name) +like `Base.isdefined`, however can handle nested, qualified names (i.e. containing dots) +Examples +-------- + +```jldoctest +julia> using WhereTraits.Utils: isdefined_generalized + +julia> isdefined_generalized(Main, :(Base.IteratorSize)) +true + +julia> isdefined_generalized(Main, :(Base.HasShape2)) +false +``` +""" +function isdefined_generalized(mod, name) + try + mod, name = normalize_mod_and_name(mod, name) + catch ex + isa(ex, UndefVarError) || rethrow() + return false + end + isdefined(mod, name) +end + + +""" + normalize_mod_and_name(mod, name) + +Does nothing if `name` is a Symbol. +If `name` is a qualified name (i.e. containing a reference to a submodule), +the final submodule and simple Symbol name are returned. + +Examples +-------- +```jldoctest +julia> using WhereTraits.Utils: normalize_mod_and_name + +julia> normalize_mod_and_name(Base, :iseven) +(Base, :iseven) + +julia> normalize_mod_and_name(Base, :(Iterators.CountFrom)) +(Base.Iterators, :CountFrom) +``` +""" function normalize_mod_and_name(mod, name) parser = EP.AnyOf(EP.anysymbol, EP.NestedDot()) normalize_mod_and_name(mod, parse_expr(parser, name)) diff --git a/src/Utils/UtilsExprs/MacroEnvs.jl b/src/Utils/UtilsExprs/MacroEnvs.jl index 36e39e9..94dad1e 100644 --- a/src/Utils/UtilsExprs/MacroEnvs.jl +++ b/src/Utils/UtilsExprs/MacroEnvs.jl @@ -7,9 +7,10 @@ export MacroEnv, @MacroEnv end macro MacroEnv() - quote - MacroEnv($(esc(:__source__)), $(esc(:__module__))) - end + esc(quote + $MacroEnv(__source__, __module__) + end) end + end \ No newline at end of file diff --git a/src/Utils/UtilsExprs/MacroErrors.jl b/src/Utils/UtilsExprs/MacroErrors.jl new file mode 100644 index 0000000..c144dd6 --- /dev/null +++ b/src/Utils/UtilsExprs/MacroErrors.jl @@ -0,0 +1,16 @@ +module MacroErrors +export MacroError + +struct MacroError{E} + exception::E +end + +function Base.showerror(io::IO, exc::MacroError) + print(io, """ + THIS SHOULD NOT BE SEEN. + MacroError should not be thrown directly, + but unpacked and the inner error should be + thrown as a result of the macro call.""") +end + +end \ No newline at end of file diff --git a/src/Utils/UtilsExprs/UtilsExprs.jl b/src/Utils/UtilsExprs/UtilsExprs.jl index 2afc887..cd3d9ee 100644 --- a/src/Utils/UtilsExprs/UtilsExprs.jl +++ b/src/Utils/UtilsExprs/UtilsExprs.jl @@ -7,6 +7,9 @@ include("SortExprs.jl") include("MacroEnvs.jl") @reexport using .MacroEnvs +include("MacroErrors.jl") +@reexport using .MacroErrors + export depends_on, flatten_blocks, change_symbols using ExprParsers diff --git a/src/WhereTraits.jl b/src/WhereTraits.jl index 8da5634..943c917 100644 --- a/src/WhereTraits.jl +++ b/src/WhereTraits.jl @@ -1,24 +1,14 @@ module WhereTraits export @traits, @traits_order, @traits_test, @traits_store, @traits_show_implementation -export WhereTraitsException, WhereTraitsAmbiguityError +export WhereTraitsException, WhereTraitsAmbiguityError, WhereTraitsMethodError using Compat @Base.kwdef mutable struct _Config - # set to false because `@suppress` does currently not work with function definitions - # for updates see https://github.com/JuliaIO/Suppressor.jl/issues/29 - suppress_on_traits_definitions::Bool = false auto_documentation::Bool = true end # TODO documentation of struct fields does not seem to work - hence we document the constant instead -""" -suppress_on_traits_definitions::Bool = false - if true, all warnings, e.g. overwrite warnings are suppressed within @traits definitions - - this is useful, because in the current Julia, @traits is inherintly state-ful and edge-cases - may need to overwrite previous definitions of the same @traits -""" const CONFIG = _Config() include("Utils/Utils.jl") diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..737e873 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,7 @@ +[deps] +DataTypesBasic = "83eed652-29e8-11e9-12da-a7c29d64ffc9" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +StringDistances = "88034a9c-02f8-509d-84a9-84ec65e18404" +Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/runtests.jl b/test/runtests.jl index 4e719da..830f012 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,6 +16,10 @@ end include("syntax_traitsorder.jl") end +@testset "syntax errors" begin + include("syntax_errors.jl") +end + @testset "README" begin include("README.jl") end diff --git a/test/syntax_errors.jl b/test/syntax_errors.jl new file mode 100644 index 0000000..6dfc732 --- /dev/null +++ b/test/syntax_errors.jl @@ -0,0 +1,35 @@ +using WhereTraits +using Test + + +# DONE in case a trait is illegally defined (e.g. referring variable `b` while there is no b, every call to the traits functions fails). There should be an immediate error in best case. + +@testset "UndefVarError" begin + + @traits_test syntax_errors_f(a) where {iseven(a)} = :works + @test syntax_errors_f(2) == :works + + @test_throws UndefVarError(:iseventypo) @traits_test syntax_errors_f(a) where {iseventypo(a)} = 2a + @test_throws UndefVarError(:b) @traits_test syntax_errors_f(a) where {isodd(b)} = 2a + + # TODO this fails because c cannot be found in the definition of @traits_test. + # @eval c = 5 + # # raises a warning which should be testable with @test_logs, but I could not make it work + # @traits_test syntax_errors_g(a) where {isodd(c)} = :workstoo + # @test syntax_errors_g(1) == :workstoo + + @test_throws UndefVarError(:HasShape2) @traits_test syntax_errors_h(a) where {Base.IteratorSize(a)::Base.HasShape2} = 2 + +end + + +@testset "WhereTraitsMethodError" begin + + @traits_test syntax_errors_conflict(a) where {Base.IteratorSize(a) <: Base.HasShape} = 2 + @test_throws WhereTraitsMethodError syntax_errors_conflict([1]) # TraitsMethodError + + @traits_test syntax_errors_conflict2(a) where {Base.IteratorSize(a)::Base.HasShape} = 2 + @test syntax_errors_conflict2([1]) == 2 + @test_throws WhereTraitsMethodError syntax_errors_conflict2(Iterators.countfrom()) # TraitsMethodError + +end diff --git a/test/syntax_traits.jl b/test/syntax_traits.jl index 93e6600..a7c8342 100644 --- a/test/syntax_traits.jl +++ b/test/syntax_traits.jl @@ -67,19 +67,19 @@ end @test fcc(1:4) == (4,) @test fcc("hallo") == 5 - @test_throws MethodError fcc(Base.Iterators.repeated(2)) # IteratorSize == Infinite + @test_throws WhereTraitsMethodError fcc(Base.Iterators.repeated(2)) # IteratorSize == Infinite @traits_test fcc2(a) where {eltype(a)::Type{Int}} = 5 + sum(a) @traits_test fcc2(a) where {eltype(a)::Type{String}} = "[$(join(a, ","))]" @test fcc2([1,2,3,4]) == 15 @test fcc2(["a", "b"]) == "[a,b]" - @test_throws MethodError fcc2([2.0, 5.0]) + @test_throws WhereTraitsMethodError fcc2([2.0, 5.0]) @traits_test fcc3(a::Val{T}) where {T, T::Int} = T + 4 @traits_test fcc3(a::Val{T}) where {T, T::Symbol} = T @test fcc3(Val(1)) == 5 @test fcc3(Val(:hi)) == :hi - @test_throws MethodError fcc3(Val(true)) + @test_throws WhereTraitsMethodError fcc3(Val(true)) end @@ -92,21 +92,21 @@ end @traits_test fsc(a::A) where {A, eltype(A)<:String} = "[$(join(a, ","))]" @test fsc([1,2,3,4]) == 15 @test fsc(["a", "b"]) == "[a,b]" - @test_throws MethodError fsc([2.0, 5.0]) + @test_throws WhereTraitsMethodError fsc([2.0, 5.0]) @traits_test fsc2(a) where {eltype(a)<:Int} = 5 + sum(a) @traits_test fsc2(a) where {eltype(a)<:String} = "[$(join(a, ","))]" @test fsc2([1,2,3,4]) == 15 @test fsc2(["a", "b"]) == "[a,b]" @traits_show_implementation fsc2 - @test_throws MethodError fsc2([2.0, 5.0]) + @test_throws WhereTraitsMethodError fsc2([2.0, 5.0]) @traits_test fsc2(a) where {eltype(a)<:Int} = 5 + sum(a) @traits_test fsc2(a) where {eltype(a)<:String} = "[$(join(a, ","))]" @test fsc2([1,2,3,4]) == 15 @test fsc2(["a", "b"]) == "[a,b]" @traits_show_implementation fsc2 - @test_throws MethodError fsc2([2.0, 5.0]) + @test_throws WhereTraitsMethodError fsc2([2.0, 5.0]) end # Test default @@ -140,8 +140,8 @@ end @traits_test fda2(a::A, b::B=50) where {A<:Number, B<:Number, eltype(A) == Int, A == B, eltype(b) <: Number} = a + b + 999 @traits_show_implementation fda2 @test fda2(1) == 1050 # should not throw "UndefVarError: B not defined" - @test_throws MethodError fda2(1.0) - @test_throws MethodError fda2(1.0, 4) + @test_throws WhereTraitsMethodError fda2(1.0) + @test_throws WhereTraitsMethodError fda2(1.0, 4) end # Test kwargs