├── .gitignore ├── docs ├── Project.toml ├── src │ ├── api.md │ └── index.md └── make.jl ├── test ├── runtests.jl ├── def_tools.jl ├── type_utils.jl ├── method.jl └── function.jl ├── src ├── ExprTools.jl ├── type_utils.jl ├── def_tools.jl ├── function.jl └── method.jl ├── Project.toml ├── .github └── workflows │ ├── TagBot.yml │ ├── CompatHelper.yml │ ├── blue_style_formatter.yml │ ├── JuliaNightly.yml │ └── CI.yml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.jl.*.cov 2 | *.jl.cov 3 | *.jl.mem 4 | /docs/build/ 5 | Manifest.toml 6 | -------------------------------------------------------------------------------- /docs/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 3 | ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" 4 | -------------------------------------------------------------------------------- /docs/src/api.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = ExprTools 3 | ``` 4 | 5 | # API 6 | 7 | ```@index 8 | ``` 9 | 10 | ```@docs 11 | splitdef 12 | combinedef 13 | signature 14 | ``` 15 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using ExprTools 2 | using Test 3 | 4 | @testset "ExprTools.jl" begin 5 | include("function.jl") 6 | include("method.jl") 7 | include("def_tools.jl") 8 | include("type_utils.jl") 9 | end 10 | -------------------------------------------------------------------------------- /src/ExprTools.jl: -------------------------------------------------------------------------------- 1 | module ExprTools 2 | 3 | export args_tuple_expr, combinedef, parameters, signature, splitdef 4 | 5 | include("function.jl") 6 | include("method.jl") 7 | include("type_utils.jl") 8 | include("def_tools.jl") 9 | 10 | end # module -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "ExprTools" 2 | uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" 3 | authors = ["Invenia Technical Computing"] 4 | version = "0.1.10" 5 | 6 | [compat] 7 | julia = "1" 8 | 9 | [extras] 10 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 11 | 12 | [targets] 13 | test = ["Test"] 14 | -------------------------------------------------------------------------------- /src/type_utils.jl: -------------------------------------------------------------------------------- 1 | """ 2 | parameters(type) 3 | 4 | Extracts the type-parameters of the `type`. 5 | 6 | e.g. `parameters(Foo{A, B, C}) == [A, B, C]` 7 | """ 8 | parameters(sig::UnionAll) = parameters(sig.body) 9 | parameters(sig::DataType) = sig.parameters 10 | parameters(sig::Union) = Base.uniontypes(sig) 11 | parameters(sig::TypeVar) = [sig] 12 | -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | workflow_dispatch: 7 | jobs: 8 | TagBot: 9 | if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: JuliaRegistries/TagBot@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | ssh: ${{ secrets.DOCUMENTER_KEY }} 16 | -------------------------------------------------------------------------------- /.github/workflows/CompatHelper.yml: -------------------------------------------------------------------------------- 1 | name: CompatHelper 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' # Everyday at midnight 5 | workflow_dispatch: 6 | jobs: 7 | CompatHelper: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Pkg.add("CompatHelper") 11 | run: julia -e 'using Pkg; Pkg.add("CompatHelper")' 12 | - name: CompatHelper.main() 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} 16 | run: julia -e 'using CompatHelper; CompatHelper.main()' 17 | -------------------------------------------------------------------------------- /.github/workflows/blue_style_formatter.yml: -------------------------------------------------------------------------------- 1 | name: Format suggestions 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | format: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: julia-actions/setup-julia@latest 12 | with: 13 | version: 1 14 | - run: | 15 | julia -e 'using Pkg; Pkg.add("JuliaFormatter")' 16 | julia -e 'using JuliaFormatter; format(".", BlueStyle(); verbose=true)' 17 | - uses: reviewdog/action-suggester@v1 18 | with: 19 | tool_name: JuliaFormatter 20 | fail_on_error: true 21 | -------------------------------------------------------------------------------- /docs/make.jl: -------------------------------------------------------------------------------- 1 | using ExprTools 2 | using Documenter 3 | 4 | makedocs(; 5 | modules=[ExprTools], 6 | authors="Curtis Vogt ", 7 | repo="https://github.com/JuliaTesting/ExprTools.jl/blob/{commit}{path}#L{line}", 8 | sitename="ExprTools.jl", 9 | format=Documenter.HTML(; 10 | prettyurls=get(ENV, "CI", "false") == "true", 11 | canonical="https://JuliaTesting.github.io/ExprTools.jl", 12 | assets=String[], 13 | ), 14 | pages=[ 15 | "Home" => "index.md", 16 | "API" => "api.md" 17 | ], 18 | ) 19 | 20 | deploydocs(; 21 | repo="github.com/JuliaTesting/ExprTools.jl", 22 | ) 23 | -------------------------------------------------------------------------------- /test/def_tools.jl: -------------------------------------------------------------------------------- 1 | @testset "def_tools.jl" begin 2 | @testset "args_tuple_expr" begin 3 | @test args_tuple_expr(splitdef(:(f(x, y)=1))) == :((x, y)) 4 | @test args_tuple_expr(splitdef(:(f(x::Int, y::Float64)=1))) == :((x, y)) 5 | @test args_tuple_expr(splitdef(:(f(x::Vector{T}) where T=1))) == :((x,)) 6 | @test args_tuple_expr(splitdef(:(f(x::Vararg)=1))) == :((x...,)) 7 | @test args_tuple_expr(splitdef(:(f(x::Vararg{Int})=1))) == :((x...,)) 8 | @test args_tuple_expr(splitdef(:(f(x...)=1))) == :((x...,)) 9 | @test args_tuple_expr(splitdef(:(f(x::Int...)=1))) == :((x...,)) 10 | @test args_tuple_expr(splitdef(:(f(x::(Vector{T} where T)...)=1))) == :((x...,)) 11 | end 12 | end -------------------------------------------------------------------------------- /.github/workflows/JuliaNightly.yml: -------------------------------------------------------------------------------- 1 | name: JuliaNightly 2 | # Nightly Scheduled Julia Nightly Run 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST) 6 | jobs: 7 | test: 8 | name: Julia Nightly - Ubuntu - x64 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: julia-actions/setup-julia@v1 13 | with: 14 | version: nightly 15 | arch: x64 16 | - uses: actions/cache@v2 17 | env: 18 | cache-name: julianightly-cache-artifacts 19 | with: 20 | path: ~/.julia/artifacts 21 | key: ${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 22 | restore-keys: | 23 | ${{ env.cache-name }}- 24 | - uses: julia-actions/julia-buildpkg@latest 25 | - uses: julia-actions/julia-runtest@latest 26 | - uses: julia-actions/julia-processcoverage@v1 27 | - uses: codecov/codecov-action@v1 28 | with: 29 | file: lcov.info 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Curtis Vogt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | # ExprTools 2 | 3 | ExprTools provides tooling for working with Julia expressions during [metaprogramming](https://docs.julialang.org/en/v1/manual/metaprogramming/). 4 | This package aims to provide light-weight performant tooling without requiring additional package dependencies. 5 | 6 | Alternatively see the [MacroTools](https://github.com/MikeInnes/MacroTools.jl) package for more powerful set of tools. 7 | 8 | Currently, this package provides the `splitdef`, `signature` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions. 9 | - [`splitdef`](@ref) works on a function definition expression and returns a `Dict` of its parts. 10 | - [`combinedef`](@ref) takes `Dict` from `splitdef` and builds it into an expression. 11 | - [`signature`](@ref) works on a `Method` returning a similar `Dict` that holds the parts of the expressions that would form its signature. 12 | 13 | e.g. 14 | ```jldoctest 15 | julia> using ExprTools 16 | 17 | julia> ex = :( 18 | function Base.f(x::T, y::T) where T 19 | x + y 20 | end 21 | ) 22 | :(function Base.f(x::T, y::T) where T 23 | #= none:3 =# 24 | x + y 25 | end) 26 | 27 | julia> def = splitdef(ex) 28 | Dict{Symbol,Any} with 5 entries: 29 | :args => Any[:(x::T), :(y::T)] 30 | :body => quote… 31 | :name => :(Base.f) 32 | :head => :function 33 | :whereparams => Any[:T] 34 | 35 | julia> def[:name] = :g; 36 | 37 | julia> def[:head] = :(=); 38 | 39 | julia> def[:body] = :(x * y); 40 | 41 | julia> g_expr = combinedef(def) 42 | :((g(x::T, y::T) where T) = x * y) 43 | 44 | julia> eval(g_expr) 45 | g (generic function with 1 method) 46 | 47 | julia> g_method = first(methods(g)) 48 | g(x::T, y::T) where T in Main 49 | 50 | julia> signature(g_method) 51 | Dict{Symbol,Any} with 3 entries: 52 | :name => :g 53 | :args => Expr[:(x::T), :(y::T)] 54 | :whereparams => Any[:T] 55 | ``` 56 | -------------------------------------------------------------------------------- /test/type_utils.jl: -------------------------------------------------------------------------------- 1 | @testset "type_utils.jl" begin 2 | @testset "parameters" begin 3 | # basic case 4 | @test collect(parameters(AbstractArray{Float32,3})) == [Float32, 3] 5 | # Type-alias 6 | @test collect(parameters(Vector{Float64})) == [Float64, 1] 7 | 8 | # Tuple 9 | @test collect(parameters(Tuple{Int8,Bool})) == [Int8, Bool] 10 | # Tuple with fixed count Vararg 11 | @test collect(parameters(Tuple{Int8,Vararg{Bool,3}})) == [Int8, Bool, Bool, Bool] 12 | 13 | # Tuple with varadic Vararg 14 | a, b = collect(parameters(Tuple{Int8,Vararg{Bool}})) 15 | @test a == Int8 16 | @test b == Vararg{Bool} 17 | 18 | # TypeVar 19 | tvar1 = parameters(Tuple{T} where {T<:Number})[1] 20 | @test tvar1 isa TypeVar 21 | @test tvar1.name == :T 22 | @test tvar1.lb == Union{} 23 | @test tvar1.ub == Number 24 | 25 | # Shared TypeVar 26 | tvar2, tvar3 = parameters(Tuple{X,X} where X<:Integer) 27 | @test tvar2 === tvar3 28 | @test tvar2.name == :X 29 | @test tvar2.lb == Union{} 30 | @test tvar2.ub == Integer 31 | 32 | # Shared TypeVar in different parameter 33 | tvar4, part = parameters(Tuple{Y,Tuple{Y}} where Integer <: Y <: Real) 34 | @test part <: Tuple 35 | tvar5 = parameters(part)[1] 36 | @test tvar4 === tvar5 37 | @test tvar4.name == :Y 38 | @test tvar4.lb == Integer 39 | @test tvar4.ub == Real 40 | 41 | # Union 42 | @test Set(parameters(Union{Int8,Bool})) == Set([Int8, Bool]) 43 | @test Set(parameters(Union{Int8,Bool,Set})) == Set([Int8, Bool, Set]) 44 | # Partially collapsing Union 45 | @test Set(parameters(Union{Int8,Real,Set})) == Set([Real, Set]) 46 | 47 | # Unions with type-vars 48 | umem1, umem2 = parameters(Union{Tuple{Z},Set{Z}} where {Z}) 49 | utvar1 = parameters(umem1)[1] 50 | utvar2 = parameters(umem2)[1] 51 | @test utvar1 == utvar2 52 | @test utvar1 isa TypeVar 53 | @test utvar1.name == :Z 54 | @test utvar1.lb == Union{} 55 | @test utvar1.ub == Any 56 | 57 | # Non-parametric type 58 | @test isempty(parameters(Bool)) 59 | 60 | # type-vars in signatures 61 | s = only(parameters(TypeVar(:T))) 62 | @test s.name == :T 63 | @test s.lb == Union{} 64 | @test s.ub == Any 65 | 66 | # https://github.com/JuliaTesting/ExprTools.jl/issues/39 67 | @testset "#39" begin 68 | s = signature(Tuple{Type{T},T} where {T<:Number}) 69 | @test only(s[:whereparams]).args[1] == :T 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /src/def_tools.jl: -------------------------------------------------------------------------------- 1 | # These utilities are for working with the signature_def `Dict` that comes out of 2 | # `signature`/`splitdef` 3 | 4 | 5 | 6 | """ 7 | args_tuple_expr(signature_def::Dict{Symbol}) 8 | args_tuple_expr(arg_exprs) 9 | 10 | For `arg_exprs` being a list of positional argument expressions from a signature, of a form 11 | such as `[:(x::Int), :(y::Float64), :(z::Vararg)]`, or being a whole `signature_def` `Dict` 12 | containing a `signature_def[:args]` value of that form. 13 | 14 | This returns a tuple expression containing all of the args by name. It correctly handles 15 | splatting for things that are `Vararg` typed, e.g for the prior example `:((x, y, z...))` 16 | 17 | This is useful for modifying the `signature_def[:body]`. 18 | For example, one could printout all the arguments via 19 | 20 | ```julia 21 | signature_def[:body] = quote 22 | args = \$(args_tuple_expr(signature_def)) 23 | println("args = ",args) 24 | \$(signature_def[:body]) # insert old body 25 | end 26 | ``` 27 | 28 | A more realistic use case is if you want to insert a call to another function 29 | that accepts the same arguments as the original function. 30 | """ 31 | function args_tuple_expr end 32 | 33 | args_tuple_expr(signature_def::Dict{Symbol}) = args_tuple_expr(signature_def[:args]) 34 | 35 | function args_tuple_expr(arg_exprs) 36 | ret = Expr(:tuple) 37 | ret.args = map(arg_exprs) do arg 38 | 39 | # remove splatting (will put back on at end) 40 | was_splatted = Meta.isexpr(arg, :(...), 1) 41 | if was_splatted 42 | arg = arg.args[1] 43 | end 44 | 45 | # handle presence or absence of type constraints 46 | if Meta.isexpr(arg, :(::), 2) 47 | arg_name, Texpr = arg.args 48 | elseif arg isa Symbol 49 | arg_name = arg 50 | Texpr = nothing 51 | else 52 | error("unexpected form of argument: $arg") 53 | end 54 | 55 | # Clean up types so we can recognise if it is `Vararg` 56 | # remove where clauses (because they interfere with recognizing Vararg) 57 | if Meta.isexpr(Texpr, :where) 58 | Texpr = Texpr.args[1] 59 | end 60 | # remove curlies from type constraints Needs to be after removing `where` 61 | # important because we want to make Vararg{T,N}` into just `Vararg` 62 | if Meta.isexpr(Texpr, :curly) 63 | Texpr = Texpr.args[1] 64 | end 65 | # now can detect if should be splatted because of using Vararg in some form 66 | was_splatted |= Texpr == :Vararg 67 | 68 | #Finally apply splatting if required. 69 | if was_splatted 70 | return :($arg_name...) 71 | else 72 | return arg_name 73 | end 74 | end 75 | return ret 76 | end 77 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | # Run on master, tags, or any pull request 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST) 6 | push: 7 | branches: [master] 8 | tags: ["*"] 9 | pull_request: 10 | jobs: 11 | test: 12 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | version: 18 | - "1.6" # LTS 19 | - "1" # Latest Release 20 | os: 21 | - ubuntu-latest 22 | - macOS-latest 23 | - windows-latest 24 | arch: 25 | - x64 26 | - x86 27 | exclude: 28 | # Test 32-bit only on Linux 29 | - os: macOS-latest 30 | arch: x86 31 | - os: windows-latest 32 | arch: x86 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: julia-actions/setup-julia@v1 36 | with: 37 | version: ${{ matrix.version }} 38 | arch: ${{ matrix.arch }} 39 | - uses: actions/cache@v2 40 | env: 41 | cache-name: cache-artifacts 42 | with: 43 | path: ~/.julia/artifacts 44 | key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 45 | restore-keys: | 46 | ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}- 47 | ${{ runner.os }}-${{ matrix.arch }}-test- 48 | ${{ runner.os }}-${{ matrix.arch }}- 49 | ${{ runner.os }}- 50 | - uses: julia-actions/julia-buildpkg@latest 51 | - uses: julia-actions/julia-runtest@latest 52 | - uses: julia-actions/julia-processcoverage@v1 53 | - uses: codecov/codecov-action@v1 54 | with: 55 | file: lcov.info 56 | 57 | slack: 58 | name: Notify Slack Failure 59 | needs: test 60 | runs-on: ubuntu-latest 61 | if: always() && github.event_name == 'schedule' 62 | steps: 63 | - uses: technote-space/workflow-conclusion-action@v2 64 | - uses: voxmedia/github-action-slack-notify-build@v1 65 | if: env.WORKFLOW_CONCLUSION == 'failure' 66 | with: 67 | channel: nightly-dev 68 | status: FAILED 69 | color: danger 70 | env: 71 | SLACK_BOT_TOKEN: ${{ secrets.DEV_SLACK_BOT_TOKEN }} 72 | 73 | docs: 74 | name: Documentation 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v2 78 | - uses: julia-actions/setup-julia@v1 79 | with: 80 | version: '1' 81 | - run: | 82 | julia --project=docs -e ' 83 | using Pkg 84 | Pkg.develop(PackageSpec(path=pwd())) 85 | Pkg.instantiate() 86 | include("docs/make.jl")' 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExprTools 2 | 3 | [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaTesting.github.io/ExprTools.jl/stable) 4 | [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaTesting.github.io/ExprTools.jl/dev) 5 | [![CI](https://github.com/JuliaTesting/ExprTools.jl/workflows/CI/badge.svg)](https://github.com/JuliaTesting/ExprTools.jl/actions?query=workflow%3ACI) 6 | [![Coverage](https://codecov.io/gh/JuliaTesting/ExprTools.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaTesting/ExprTools.jl) 7 | 8 | ExprTools provides tooling for working with Julia expressions during [metaprogramming](https://docs.julialang.org/en/v1/manual/metaprogramming/). 9 | This package aims to provide light-weight performant tooling without requiring additional package dependencies. 10 | 11 | Currently, this package provides the `splitdef`, `signature` and `combinedef` functions which are useful for inspecting and manipulating function definition expressions. 12 | - `splitdef` works on a function definition expression and returns a `Dict` of its parts. 13 | - `combinedef` takes a `Dict` from `splitdef` and builds it into an expression. 14 | - `signature` works on a `Method`, or the type-tuple `sig` field of a method, returning a similar `Dict` that holds the parts of the expressions that would form its signature. 15 | 16 | As well as several helpers that are useful in combination with them. 17 | - `args_tuple_expr` applies to a `Dict` from `splitdef` or `signature` to generate an expression for a tuple of its arguments. 18 | - `parameters` which return the type-parameters of a type, and so is useful for working with the type-tuple that comes out of the `sig` field of a `Method` 19 | 20 | e.g. 21 | ```julia 22 | julia> using ExprTools 23 | 24 | julia> ex = :( 25 | function Base.f(x::T, y::T) where T 26 | x + y 27 | end 28 | ) 29 | :(function Base.f(x::T, y::T) where T 30 | #= none:3 =# 31 | x + y 32 | end) 33 | 34 | julia> def = splitdef(ex) 35 | Dict{Symbol,Any} with 5 entries: 36 | :args => Any[:(x::T), :(y::T)] 37 | :body => quote… 38 | :name => :(Base.f) 39 | :head => :function 40 | :whereparams => Any[:T] 41 | 42 | 43 | julia> def[:name] = :g; 44 | 45 | julia> def[:head] = :(=); 46 | 47 | julia> args_tuple_expr(def) 48 | :((x, y)) 49 | 50 | julia> def[:body] = :(*($(args_tuple_expr(def))...)); 51 | 52 | julia> g_expr = combinedef(def) 53 | :((g(x::T, y::T) where T) = (*)((x, y)...)) 54 | 55 | julia> eval(g_expr) 56 | g (generic function with 1 method) 57 | 58 | julia> g_method = first(methods(g)) 59 | g(x::T, y::T) where T in Main 60 | 61 | julia> parameters(g_method.sig) 62 | svec(typeof(g), T, T) 63 | 64 | julia> signature(g_method) 65 | Dict{Symbol, Any} with 3 entries: 66 | :name => :g 67 | :args => Expr[:(x::T), :(y::T)] 68 | :whereparams => Any[:T] 69 | ``` 70 | 71 | ### JuliaCon 2021 Video 72 | "ExprTools: Metaprogramming from reflection" by Frames White 73 | 74 | [![YouTube Video](https://img.youtube.com/vi/CREWoLxpDMo/0.jpg)](https://www.youtube.com/watch?v=CREWoLxpDMo) 75 | -------------------------------------------------------------------------------- /src/function.jl: -------------------------------------------------------------------------------- 1 | """ 2 | splitdef(ex::Expr; throw::Bool=true) -> Union{Dict{Symbol,Any}, Nothing} 3 | 4 | Split a function definition expression into its various components including: 5 | 6 | - `:head`: Expression head of the function definition (`:function`, `:(=)`, `:(->)`) 7 | - `:name`: Name of the function (not present for anonymous functions) 8 | - `:params`: Parametric types defined on constructors 9 | - `:args`: Positional arguments of the function 10 | - `:kwargs`: Keyword arguments of the function 11 | - `:rtype`: Return type of the function 12 | - `:whereparams`: Where parameters 13 | - `:body`: Function body (not present for empty functions) 14 | 15 | All components listed may not be present in the returned dictionary with the exception of 16 | `:head` which will always be present. 17 | 18 | If the provided expression is not a function then an exception will be raised when 19 | `throw=true`. Use `throw=false` avoid raising an exception and return `nothing` instead. 20 | 21 | See also: [`combinedef`](@ref) 22 | """ 23 | function splitdef(ex::Expr; throw::Bool=true) 24 | def = Dict{Symbol,Any}() 25 | full_ex = ex # Keep a reference to the full expression 26 | 27 | function invalid_def(section) 28 | if throw 29 | msg = "Function definition contains $section\n$(sprint(Meta.dump, full_ex))" 30 | Base.throw(ArgumentError(msg)) 31 | else 32 | nothing 33 | end 34 | end 35 | 36 | if !(ex.head === :function || ex.head === :(=) || ex.head === :(->)) 37 | return invalid_def("invalid function head `$(repr(ex.head))`") 38 | end 39 | 40 | def[:head] = ex.head 41 | 42 | if ex.head === :function && length(ex.args) == 1 && ex.args[1] isa Symbol 43 | # empty function definition 44 | def[:name] = ex.args[1] 45 | return def 46 | elseif length(ex.args) == 2 # Expect signature and body 47 | def[:body] = ex.args[2] 48 | ex = ex.args[1] # Focus on the function signature 49 | else 50 | quan = length(ex.args) > 2 ? "too many" : "too few" 51 | return invalid_def("$quan of expression arguments for `$(repr(def[:head]))`") 52 | end 53 | 54 | # Where parameters 55 | if ex isa Expr && ex.head === :where 56 | def[:whereparams] = Any[] 57 | 58 | while ex isa Expr && ex.head === :where 59 | append!(def[:whereparams], ex.args[2:end]) 60 | ex = ex.args[1] 61 | end 62 | end 63 | 64 | # Return type 65 | if def[:head] !== :(->) && ex isa Expr && ex.head === :(::) 66 | def[:rtype] = ex.args[2] 67 | ex = ex.args[1] 68 | end 69 | 70 | # Determine if the function is anonymous 71 | anon = ( 72 | def[:head] === :(->) || 73 | def[:head] === :function && !(ex isa Expr && ex.head === :call) 74 | ) 75 | 76 | # Arguments and keywords 77 | if ex isa Expr && (anon && ex.head === :tuple || !anon && ex.head === :call) 78 | i = anon ? 1 : 2 79 | 80 | if length(ex.args) >= i 81 | if ex.args[i] isa Expr && ex.args[i].head === :parameters 82 | def[:kwargs] = ex.args[i].args 83 | 84 | if length(ex.args) > i 85 | def[:args] = ex.args[(i + 1):end] 86 | end 87 | else 88 | def[:args] = ex.args[i:end] 89 | end 90 | end 91 | elseif ex isa Expr && anon && ex.head === :block 92 | # Note: Short-form anonymous functions (:->) will use a block expression when the 93 | # arguments are divided by semi-colons but do not use commas: 94 | # 95 | # (;) -> ... 96 | # (x;) -> ... 97 | # (x;y) -> ... 98 | # (;x) -> ... # Note: this is an exception to this rule 99 | 100 | for arg in ex.args 101 | arg isa LineNumberNode && continue 102 | 103 | if !haskey(def, :args) 104 | def[:args] = [arg] 105 | elseif !haskey(def, :kwargs) 106 | if arg isa Expr && arg.head == :(=) 107 | def[:kwargs] = [:($(Expr(:kw, arg.args...)))] 108 | else 109 | def[:kwargs] = [arg] 110 | end 111 | else 112 | return invalid_def("an invalid block expression as arguments") 113 | end 114 | end 115 | 116 | !haskey(def, :kwargs) && (def[:kwargs] = []) 117 | 118 | elseif def[:head] === :(->) 119 | def[:args] = [ex] 120 | else 121 | return invalid_def("invalid or missing arguments") 122 | end 123 | 124 | # Function name and type parameters 125 | if !anon 126 | ex = ex.args[1] 127 | 128 | if ex isa Expr && ex.head === :curly 129 | def[:params] = ex.args[2:end] 130 | ex = ex.args[1] 131 | end 132 | 133 | def[:name] = ex 134 | end 135 | 136 | return def 137 | end 138 | 139 | """ 140 | combinedef(def::Dict{Symbol,Any}) -> Expr 141 | 142 | Create a function definition expression from various components. Typically used to construct 143 | a function using the result of [`splitdef`](@ref). 144 | 145 | If `def[:head]` is not provided it will default to `:function`. 146 | 147 | For more details see the documentation on [`splitdef`](@ref). 148 | """ 149 | function combinedef(def::Dict{Symbol,Any}) 150 | head = get(def, :head, :function) 151 | 152 | # Determine the name of the function including parameterization 153 | name = if haskey(def, :params) 154 | Expr(:curly, def[:name], def[:params]...) 155 | elseif haskey(def, :name) 156 | def[:name] 157 | else 158 | nothing 159 | end 160 | 161 | # Empty generic function definitions must not contain additional keys 162 | empty_extras = (:args, :kwargs, :rtype, :whereparams) 163 | if !haskey(def, :body) && any(k -> haskey(def, k), empty_extras) 164 | throw(ArgumentError(string( 165 | "Function definitions without a body must not contain keys: ", 166 | join(string.('`', repr.(setdiff(empty_extras, keys(def))), '`'), ", ", ", or "), 167 | ))) 168 | end 169 | 170 | # Combine args and kwargs 171 | args = Any[] 172 | haskey(def, :kwargs) && push!(args, Expr(:parameters, def[:kwargs]...)) 173 | haskey(def, :args) && append!(args, def[:args]) 174 | 175 | # Create a partial function signature including the name and arguments 176 | sig = if name !== nothing 177 | :($name($(args...))) # Equivalent to `Expr(:call, name, args...)` but faster 178 | elseif head === :(->) && length(args) == 1 && !haskey(def, :kwargs) 179 | args[1] 180 | else 181 | :(($(args...),)) # Equivalent to `Expr(:tuple, args...)` but faster 182 | end 183 | 184 | # Add the return type 185 | if haskey(def, :rtype) 186 | sig = Expr(:(::), sig, def[:rtype]) 187 | end 188 | 189 | # Add any where parameters. Note: Always uses the curly where syntax 190 | if haskey(def, :whereparams) 191 | sig = Expr(:where, sig, def[:whereparams]...) 192 | end 193 | 194 | func = if haskey(def, :body) 195 | Expr(head, sig, def[:body]) 196 | else 197 | Expr(head, name) 198 | end 199 | 200 | return func 201 | end 202 | -------------------------------------------------------------------------------- /src/method.jl: -------------------------------------------------------------------------------- 1 | """ 2 | signature(m::Method) -> Dict{Symbol,Any} 3 | 4 | Finds the expression for a method's signature as broken up into its various components 5 | including: 6 | 7 | - `:name`: Name of the function 8 | - `:params`: Parametric types defined on constructors 9 | - `:args`: Positional arguments of the function 10 | - `:whereparams`: Where parameters 11 | 12 | All components listed above may not be present in the returned dictionary if they are 13 | not in the function definition. 14 | 15 | Limited support for: 16 | - `:kwargs`: Keyword arguments of the function. 17 | Only the names will be included, not the default values or type constraints. 18 | 19 | Unsupported: 20 | - `:rtype`: Return type of the function 21 | - `:body`: Function body0 22 | - `:head`: Expression head of the function definition (`:function`, `:(=)`, `:(->)`) 23 | 24 | For more complete coverage, consider using [`splitdef`](@ref) 25 | with [`CodeTracking.definition`](https://github.com/timholy/CodeTracking.jl). 26 | 27 | The dictionary of components returned by `signature` match those returned by 28 | [`splitdef`](@ref) and include all that are required by [`combinedef`](@ref), except for 29 | the `:body` component. 30 | 31 | # keywords 32 | 33 | - `extra_hygiene=false`: if set to `true` this forces name-hygiene on the `TypeVar`s in 34 | `UnionAll`s, regenerating each with a unique name via `gensym`. This shouldn't actually 35 | be required as they are scoped such that they are not supposed to leak. However, there is 36 | a long-standing [julia bug](https://github.com/JuliaLang/julia/issues/39876) that means 37 | they do leak if they clash with function type-vars. 38 | """ 39 | function signature(m::Method; extra_hygiene=false) 40 | sig = extra_hygiene ? _truly_rename_unionall(m.sig) : m.sig 41 | 42 | def = Dict{Symbol, Any}() 43 | def[:name] = m.name 44 | 45 | def[:args] = arguments(m, sig) 46 | def[:whereparams] = where_parameters(sig) 47 | def[:params] = type_parameters(m) 48 | def[:kwargs] = kwargs(m) 49 | 50 | return filter!(kv->last(kv)!==nothing, def) # filter out nonfields. 51 | end 52 | 53 | 54 | """ 55 | signature(sig::Type{<:Tuple}) 56 | 57 | Like `ExprTools.signature(::Method)` but on the underlying signature type-tuple, rather than 58 | the Method`. 59 | For `sig` being a tuple-type representing a methods type signature, this generates a 60 | dictionary that can be passes to `ExprTools.combinedef` to define that function, 61 | Provided that you assign the `:body` key on the dictionary first. 62 | 63 | The quality of the output, in terms of matching names etc is not as high as for the 64 | `signature(::Method)`, but all the key information is present; and the type-tuple is for 65 | other purposes generally easier to manipulate. 66 | 67 | Examples 68 | ```julia 69 | julia> signature(Tuple{typeof(identity), Any}) 70 | Dict{Symbol, Any} with 2 entries: 71 | :name => :(op::typeof(identity)) 72 | :args => Expr[:(x1::Any)] 73 | 74 | julia> signature(Tuple{typeof(+), Vector{T}, Vector{T}} where T<:Number) 75 | Dict{Symbol, Any} with 3 entries: 76 | :name => :(op::typeof(+)) 77 | :args => Expr[:(x1::Array{var"##T#5492", 1}), :(x2::Array{var"##T#5492", 1})] 78 | :whereparams => Any[:(var"##T#5492" <: Number)] 79 | ``` 80 | 81 | # keywords 82 | 83 | - `extra_hygiene=false`: if set to `true` this forces name-hygine on the `TypeVar`s in 84 | `UnionAll`s, regenerating each with a unique name via `gensym`. This shouldn't actually 85 | be required as they are scoped such that they are not supposed to leak. However, there is 86 | a long-standing [julia bug](https://github.com/JuliaLang/julia/issues/39876) that means 87 | they do leak if they clash with function type-vars. 88 | """ 89 | function signature(orig_sig::Type{<:Tuple}; extra_hygiene=false) 90 | sig = extra_hygiene ? _truly_rename_unionall(orig_sig) : orig_sig 91 | def = Dict{Symbol, Any}() 92 | 93 | opT = parameters(sig)[1] 94 | def[:name] = :(op::$opT) 95 | 96 | arg_types = name_of_type.(argument_types(sig)) 97 | arg_names = [Symbol(:x, ii) for ii in eachindex(arg_types)] 98 | def[:args] = Expr.(:(::), arg_names, arg_types) 99 | def[:whereparams] = where_parameters(sig) 100 | 101 | filter!(kv->last(kv)!==nothing, def) # filter out nonfields. 102 | return def 103 | end 104 | 105 | function slot_names(m::Method) 106 | ci = Base.uncompressed_ast(m) 107 | return ci.slotnames 108 | end 109 | 110 | function argument_names(m::Method) 111 | slot_syms = slot_names(m) 112 | # nargs includes 1 for `#self#` or the function object name; 113 | arg_names = slot_syms[2:m.nargs] 114 | return arg_names 115 | end 116 | 117 | argument_types(m::Method) = argument_types(m.sig) 118 | function argument_types(sig) 119 | # First parameter of `sig` is the type of the function itself 120 | return parameters(sig)[2:end] 121 | end 122 | 123 | module DummyThatHasOnlyDefaultImports end # for working out visibility 124 | 125 | function name_of_module(m::Module) 126 | if Base.is_root_module(m) 127 | return nameof(m) 128 | else 129 | return :($(name_of_module(parentmodule(m))).$(nameof(m))) 130 | end 131 | end 132 | function name_of_type(x::Core.TypeName) 133 | # TODO: could let user pass this in, then we could be using what is inscope for them 134 | # but this is not important as we will give a correct (if overly verbose) output as is. 135 | from = DummyThatHasOnlyDefaultImports 136 | if Base.isvisible(x.name, x.module, from) # avoid qualifying things that are in scope 137 | return x.name 138 | else 139 | return :($(name_of_module(x.module)).$(x.name)) 140 | end 141 | end 142 | 143 | name_of_type(x::Symbol) = QuoteNode(x) # Literal type-param e.g. `Val{:foo}` 144 | function name_of_type(x::T) where {T} # Literal type-param e.g. `Val{1}` 145 | # If this error is thrown, there is an issue with our implementation 146 | isbits(x) || throw(DomainError((x, T), "not a valid type-param")) 147 | return x 148 | end 149 | name_of_type(tv::TypeVar) = tv.name 150 | function name_of_type(x::DataType) 151 | name = name_of_type(x.name) 152 | # because tuples are varadic in number of type parameters having no parameters does not 153 | # mean you should not write the `{}`, so we special case them here. 154 | if isempty(x.parameters) && x != Tuple{} 155 | return name 156 | else 157 | parameter_names = map(name_of_type, x.parameters) 158 | return :($(name){$(parameter_names...)}) 159 | end 160 | end 161 | 162 | 163 | function name_of_type(x::UnionAll) 164 | # we do nested union all unwrapping so we can make the more compact: 165 | # `foo{T,A} where {T, A}`` rather than the longer: `(foo{T,A} where T) where A` 166 | where_params = [] 167 | while x isa UnionAll 168 | push!(where_params, where_constraint(x.var)) 169 | x = x.body 170 | end 171 | 172 | name = name_of_type(x) 173 | return :($name where {$(where_params...)}) 174 | end 175 | 176 | function name_of_type(x::Union) 177 | parameter_names = map(name_of_type, Base.uniontypes(x)) 178 | return :(Union{$(parameter_names...)}) 179 | end 180 | 181 | # Vararg was changed not to be a type anymore in Julia 1.7 182 | if isdefined(Core, :TypeofVararg) 183 | function name_of_type(x::Core.TypeofVararg) 184 | of_T = isdefined(x, :T) ? name_of_type(x.T) : :Any 185 | if isdefined(x, :N) 186 | :(Vararg{$of_T,$(name_of_type(x.N))}) 187 | else 188 | :(Vararg{$of_T}) 189 | end 190 | end 191 | end 192 | 193 | function arguments(m::Method, sig=m.sig) 194 | arg_names = argument_names(m) 195 | arg_types = argument_types(sig) 196 | map(arg_names, arg_types) do name, type 197 | has_name = name !== Symbol("#unused#") 198 | type_name = name_of_type(type) 199 | if type === Any && has_name 200 | name 201 | elseif has_name 202 | :($name::$type_name) 203 | else 204 | :(::$type_name) 205 | end 206 | end 207 | end 208 | 209 | function where_constraint(x::TypeVar) 210 | if x.lb === Union{} && x.ub === Any 211 | return x.name 212 | elseif x.lb === Union{} 213 | return :($(x.name) <: $(name_of_type(x.ub))) 214 | elseif x.ub === Any 215 | return :($(x.name) >: $(name_of_type(x.lb))) 216 | else 217 | return :($(name_of_type(x.lb)) <: $(x.name) <: $(name_of_type(x.ub))) 218 | end 219 | end 220 | 221 | where_parameters(m::Method) = where_parameters(m.sig) 222 | where_parameters(sig) = nothing 223 | function where_parameters(sig::UnionAll) 224 | whereparams = [] 225 | while sig isa UnionAll 226 | push!(whereparams, where_constraint(sig.var)) 227 | sig = sig.body 228 | end 229 | return whereparams 230 | end 231 | 232 | type_parameters(m::Method) = type_parameters(m.sig) 233 | function type_parameters(sig) 234 | typeof_type = first(parameters(sig)) # will be e.g Type{Foo{P}} if it has any parameters 235 | typeof_type <: Type{<:Any} || return nothing 236 | 237 | function_type = first(parameters(typeof_type)) # will be e.g. Foo{P} 238 | parameter_types = parameters(function_type) 239 | return map(name_of_type, parameter_types) 240 | end 241 | 242 | function kwargs(m::Method) 243 | names = kwarg_names(m) 244 | isempty(names) && return nothing # we know it has no keywords. 245 | # TODO: Enhance this to support more than just their names 246 | # see https://github.com/JuliaTesting/ExprTools.jl/issues/6 247 | return names 248 | end 249 | 250 | if VERSION >= v"1.4-" 251 | kwarg_names(m::Method) = Base.kwarg_decl(m) 252 | else 253 | function kwarg_names(m::Method) 254 | mt = Base.get_methodtable(m) 255 | !isdefined(mt, :kwsorter) && return [] # no kwsorter means no keywords for sure. 256 | return Base.kwarg_decl(m, typeof(mt.kwsorter)) 257 | end 258 | end 259 | 260 | 261 | """ 262 | _truly_rename_unionall(@nospecialize(u)) 263 | 264 | For `u` being a `UnionAll` this replaces every `TypeVar` with a new one with a `gensym`ed 265 | names. 266 | This shouldn't actually be required as they are scoped such that they are not supposed to leak. However, there is 267 | a long standing [julia bug](https://github.com/JuliaLang/julia/issues/39876) that means 268 | they do leak if they clash with function type-vars. 269 | 270 | Example: 271 | ```julia 272 | julia> _truly_rename_unionall(Array{T, N} where {T<:Number, N}) 273 | Array{var"##T#2881", var"##N#2880"} where var"##N#2880" where var"##T#2881"<:Number 274 | ``` 275 | 276 | Note that the similar `Base.rename_unionall`, though `Base.rename_unionall` does not 277 | `gensym` the names just replaces the instances with new instances with identical names. 278 | """ 279 | function _truly_rename_unionall(@nospecialize(u)) 280 | # This works by recursively unwrapping UnionAlls to seperate the TypeVars from body 281 | # changing the name in the TypeVar, and then rewrapping it back up. 282 | # The code is basically the same as `Base.rename_unionall`, but with gensym added 283 | isa(u, UnionAll) || return u 284 | body = _truly_rename_unionall(u.body) 285 | if body === u.body 286 | body = u 287 | else 288 | body = UnionAll(u.var, body) 289 | end 290 | var = u.var::TypeVar 291 | nv = TypeVar(gensym(var.name), var.lb, var.ub) 292 | return UnionAll(nv, body{nv}) 293 | end 294 | -------------------------------------------------------------------------------- /test/method.jl: -------------------------------------------------------------------------------- 1 | macro test_signature(function_def_expr, method=nothing) 2 | _target = splitdef(function_def_expr) 3 | return quote 4 | fun = $(esc(function_def_expr)) 5 | m = if ($method === nothing) 6 | only_method(fun) 7 | else 8 | $(esc(method)) 9 | end 10 | sig = signature(m) 11 | test_matches(sig, $(_target)) 12 | end 13 | end 14 | 15 | function test_matches(candidate::AbstractDict, target::Dict) 16 | # we want to use literals in the tests so that @test gives useful output on failure 17 | haskey(target, :name) && @test target[:name] == get(candidate, :name, nothing) 18 | haskey(target, :params) && @test target[:params] == get(candidate, :params, nothing) 19 | haskey(target, :args) && @test target[:args] == get(candidate, :args, nothing) 20 | if haskey(target, :whereparams) 21 | @test target[:whereparams] == get(candidate, :whereparams, nothing) 22 | end 23 | haskey(target, :kwargs) && @test target[:kwargs] == get(candidate, :kwargs, nothing) 24 | 25 | # TODO: support return value declaration in signature 26 | # See https://github.com/JuliaTesting/ExprTools.jl/issues/5 27 | haskey(target, :rtype) && @test_broken target[:rtype] == get(candidate, :rtype, nothing) 28 | return nothing 29 | end 30 | 31 | """ 32 | only_method(f, [typ]) 33 | 34 | Return the only method of `f`, 35 | Similar to `only(methods(f, typ))` in Julia 1.4. 36 | """ 37 | function only_method(f, typ=Tuple{Vararg{Any}}) 38 | ms = methods(f, typ) 39 | if length(ms) !== 1 40 | error("not just one method matches the given types. Found $(length(ms))") 41 | end 42 | return first(ms) 43 | end 44 | 45 | struct TestCallableStruct end 46 | (self::TestCallableStruct)(x) = 2x 47 | (self::TestCallableStruct)(x::T,y::R) where {T,R} = 2x + y 48 | 49 | @testset "method.jl: signature" begin 50 | @testset "Basics" begin 51 | @test_signature basic1(x) = 2x 52 | @test_signature basic2(x::Int64) = 2x 53 | @test_signature basic3(x::Int64, y) = 2x 54 | @test_signature basic4() = 2 55 | end 56 | 57 | @testset "Tuple{}" begin 58 | @test_signature empty_tuple_constraint(x::Tuple{}) = 2 59 | end 60 | 61 | @testset "varadic Tuple" begin 62 | @test_signature vt1(::Tuple{Vararg{Int64,N}}) where {N} = 2 63 | VERSION >= v"1.7" && @test_signature vt2(::Tuple{Vararg{Int64}}) = 2 64 | end 65 | 66 | @testset "Scope Qualification" begin 67 | @test_signature qualified_constraint(x::Base.CoreLogging.LogLevel) = 2 68 | end 69 | 70 | @testset "missing argnames" begin 71 | @test_signature ma1(::Int32) = 2x 72 | @test_signature ma2(::Int32, ::Bool) = 2x 73 | @test_signature ma3(x, ::Int32) = 2x 74 | end 75 | 76 | @testset "Whereparams" begin 77 | @test_signature f4(x::T, y::T) where T = 2x 78 | 79 | @test_signature f5(x::S, y::T) where {S,T} = 2x 80 | @test_signature f6(x::S, y::T) where {T,S} = 2x 81 | @test_signature f7(x::S, y::T) where T where S = 2x 82 | end 83 | 84 | @testset "Whereparams with constraints" begin 85 | @test_signature f8(x::S) where S<:Integer = 2x 86 | @test_signature f9(x::S, y::S) where S<:Integer = 2x 87 | @test_signature f10(x::S, y::T) where {S<:Integer, T<:Real} = 2x 88 | @test_signature f11(x::S, y::Int64) where S<:Integer = 2x 89 | 90 | @test_signature f12(x::S) where {S>:Integer} = 2x 91 | @test_signature f13(x::S) where Integer<:S<:Number = 2x 92 | 93 | @test_signature f_where_union(x::T) where T<:Union{Bool, Int32} = 2x 94 | end 95 | 96 | @testset "Arg types with type-parameters" begin 97 | @test_signature f14(x::Array{Int64, 1}) = 2x 98 | @test_signature f15(x::Array{T, 1}) where T = 2x 99 | 100 | @test_signature f16(x::Array{T, 1} where T<:Real) = 2x 101 | 102 | # This is the same method as f16 (other than name), and one displaces the other 103 | # but they have different method objects. And different (but equivelent) ASTd 104 | # this generates something that should be the same as what `signature(f16)` does 105 | # but with a gensym'd variable name 106 | f16_alt(x::Array{<:Real, 1}) = 2x 107 | f16_alt_sig = signature(only_method(f16_alt)) 108 | @test f16_alt_sig[:name] == :f16_alt 109 | @test occursin( # Hack to deal with gensymed name. Make it a string and use regex 110 | r"^\QExpr[:(x::(Array{\E(.*?)\Q, 1} where \E\1\Q <: Real))]\E$", 111 | string(f16_alt_sig[:args]) 112 | ) 113 | 114 | @test_signature f_symbol_param(x::Val{:foobar}) where T = 2x 115 | end 116 | 117 | @testset "anon functions" begin 118 | @test_signature (x) -> x # no args 119 | @test_signature (x) -> 2x 120 | 121 | @test_signature ((::T) where T) -> 0 # Anonymous parameter 122 | end 123 | 124 | @testset "callable structs" begin 125 | ms = collect(methods(TestCallableStruct())) 126 | sig1 = signature(first(filter(m -> m.nargs == 2, ms))) 127 | @test sig1[:name] == :TestCallableStruct 128 | @test sig1[:args] == [:x] 129 | sig2 = signature(first(filter(m -> m.nargs == 3, ms))) 130 | @test sig2[:name] == :TestCallableStruct 131 | @test sig2[:args] == Expr[:(x::T),:(y::R)] 132 | end 133 | 134 | @testset "vararg" begin 135 | if VERSION >= v"1.7" 136 | @test_signature f17(xs::Vararg{Any}) = 2 137 | # `f17_alt(xs...) = 2` lowers to the same method as `f17` 138 | # but has a different AST according to `splitdef` so we can't us @test_signature 139 | f17_alt(xs...) = 2 140 | test_matches( 141 | signature(only_method(f17_alt)), 142 | Dict(:name => :f17_alt, :args => [:(xs::Vararg{Any})]), 143 | ) 144 | 145 | @test_signature f18(xs::Vararg{Int64}) = 2 146 | @test_signature f19(x, xs::Vararg{Any}) = 2x 147 | else 148 | @test_signature f17b(xs::Vararg{Any,N} where {N}) = 2 149 | # `f17b_alt(xs...) = 2` lowers to the same method as `f17b` 150 | # but has a different AST according to `splitdef` so we can't us @test_signature 151 | f17b_alt(xs...) = 2 152 | test_matches( 153 | signature(only_method(f17b_alt)), 154 | Dict(:name => :f17b_alt, :args => [:(xs::(Vararg{Any,N} where {N}))]), 155 | ) 156 | 157 | @test_signature f18b(xs::Vararg{Int64,N} where {N}) = 2 158 | @test_signature f19b(x, xs::Vararg{Any,N} where {N}) = 2x 159 | end 160 | end 161 | 162 | @testset "kwargs" begin 163 | @test_signature kwargs1(x; a, b, c) = 2 164 | 165 | # The following is broken as it doesn't get the kwarg default value 166 | #@test_signature kwargs2(x; y=3) = 2x 167 | # at least be sure we get the rest right: 168 | kwargs2(x; y=3) = 2x 169 | test_matches( 170 | signature(only_method(kwargs2)), 171 | Dict( 172 | :name => :kwargs2, 173 | :args => [:x], 174 | :kwargs => [:y], # this should have been `[Expr(:kw, :y, 3))]` 175 | ) 176 | ) 177 | 178 | # The following is broken as it doesn't get the kwarg type-constraint 179 | #@test_signature kwargs3(x; y::Int32) = 2x 180 | # at least be sure we get the rest right: 181 | kwargs3(x; y::Int32) = 2x 182 | test_matches( 183 | signature(only_method(kwargs3)), 184 | Dict( 185 | :name => :kwargs3, 186 | :args => [:x], 187 | :kwargs => [:y], # should be `[:(y::Int32)]` 188 | ) 189 | ) 190 | 191 | # The following is broken as it doesn't get the kwarg type-constraint nor default 192 | #@test_signature kwargs3(x; y::Int32=4) = 2x 193 | # at least be sure we get the rest right: 194 | kwargs4(x; y::Int32=4) = 2x 195 | test_matches( 196 | signature(only_method(kwargs4)), 197 | Dict( 198 | :name => :kwargs4, 199 | :args => [:x], 200 | :kwargs => [:y], # should be `[Expr(:kw, :(y::Int32), 4)] 201 | ) 202 | ) 203 | end 204 | 205 | @testset "Return Type" begin 206 | # These all hit the `@test_broken` 207 | @test_signature rt1(x)::Int32 = 2x 208 | 209 | # need to use long-form becase https://github.com/JuliaLang/julia/issues/35471 210 | @test_signature function rt2(x::T)::T where T 211 | return 2x 212 | end 213 | end 214 | 215 | # Only test on 1.3 because of issues with declaring structs in 1.0-1.2 216 | # TODO: https://github.com/JuliaTesting/ExprTools.jl/issues/7 217 | VERSION >= v"1.3" && @eval @testset "Constructors (basic)" begin 218 | # demo type for testing on 219 | struct NoParamStruct 220 | x 221 | end 222 | 223 | test_matches( # default constructor 224 | signature(only_method(NoParamStruct, Tuple{Any})), 225 | Dict(:name => :NoParamStruct, :args => [:x]), 226 | ) 227 | 228 | 229 | @test_signature( 230 | NoParamStruct(x::Bool, y::Int32) = NoParamStruct(x || y > 2), 231 | only_method(NoParamStruct, Tuple{Bool, Int32}) 232 | ) 233 | 234 | struct OneParamStructBasic{T} 235 | x::T 236 | end 237 | 238 | test_matches( # default constructor 239 | signature(only_method(OneParamStructBasic, Tuple{Any})), 240 | Dict(:name => :OneParamStructBasic, :args => [:(x::T)]), 241 | ) 242 | 243 | @test_signature( 244 | OneParamStructBasic(x::Bool, y::Int32) = OneParamStruct(x || y > 2), 245 | only_method(OneParamStructBasic, Tuple{Bool, Int32}) 246 | ) 247 | end 248 | 249 | # Only test on 1.3 because of issues with declaring structs in 1.0-1.2 250 | # TODO: https://github.com/JuliaTesting/ExprTools.jl/issues/7 251 | VERSION >= v"1.3" && @eval @testset "params (via Constructors with type params)" begin 252 | struct OneParamStruct{T} 253 | x::T 254 | end 255 | 256 | @test_signature( 257 | OneParamStruct{String}(x::Int32, y::Bool) = OneParamStruct(string(x^y)), 258 | only_method(OneParamStruct{String}, Tuple{Int32, Bool}) 259 | ) 260 | 261 | @test_signature( # whereparams on params 262 | OneParamStruct{T}(x::Float32, y::Bool) where T<:AbstractFloat = OneParamStruct(x^y), 263 | only_method(OneParamStruct{Float32}, Tuple{Float32, Bool}) 264 | ) 265 | end 266 | 267 | @testset "extra_hygiene" begin 268 | hy1(::T,::Array) where T = 2 269 | no_hygiene = signature(only_method(hy1)) 270 | @test no_hygiene == Dict( 271 | :name => :hy1, 272 | :args => Expr[:(::T), :(::(Array{T, N} where {T, N}))], 273 | :whereparams => Any[:T], 274 | ) 275 | hygiene = signature(only_method(hy1); extra_hygiene=true) 276 | @test no_hygiene[:name] == hygiene[:name] 277 | @test length(no_hygiene[:args]) == 2 278 | @test no_hygiene[:args][1] != hygiene[:args][1] # different Symbols 279 | @test no_hygiene[:args][2] == hygiene[:args][2] 280 | 281 | @test length(no_hygiene[:whereparams]) == 1 282 | @test no_hygiene[:whereparams] != hygiene[:whereparams] # different Symbols 283 | # very coarse test to make sure the renamed arg is in the expression it should be 284 | @test occursin(string(no_hygiene[:whereparams][1]), string(no_hygiene[:args][1])) 285 | end 286 | 287 | @testset "signature(type_tuple)" begin 288 | # our tests here are much less comprehensive than for `signature(::Method)` 289 | # but that is OK, as most of the code is shared between the two 290 | 291 | @test signature(Tuple{typeof(+), Float32, Float32}) == Dict( 292 | # Notice the type of the function object is actually interpolated in to the Expr 293 | # This is useful because it bypasses julia's pretection for overloading things 294 | # which Nabla (and probably others generating overloads) depends upon 295 | :name => :(op::$(typeof(+))), 296 | :args => Expr[:(x1::Float32), :(x2::Float32)], 297 | ) 298 | 299 | @test signature(Tuple{typeof(+), Array}) == Dict( 300 | :name => :(op::$(typeof(+))), 301 | :args => Expr[:(x1::(Array{T, N} where {T, N}))], 302 | ) 303 | 304 | @test signature(Tuple{typeof(+), Vector{T}, Matrix{T}} where T<:Real) == Dict( 305 | :name => :(op::$(typeof(+))), 306 | :args => Expr[:(x1::Array{T, 1}), :(x2::Array{T, 2})], 307 | :whereparams => Any[:(T <: Real)], 308 | ) 309 | 310 | @testset "extra_hygiene" begin 311 | no_hygiene = signature(Tuple{typeof(+),T,Array} where T) 312 | @test no_hygiene == Dict( 313 | :name => :(op::$(typeof(+))), 314 | :args => Expr[:(x1::T), :(x2::(Array{T, N} where {T, N}))], 315 | :whereparams => Any[:T], 316 | ) 317 | hygiene = signature(Tuple{typeof(+),T,Array} where T; extra_hygiene=true) 318 | @test no_hygiene[:name] == hygiene[:name] 319 | @test length(no_hygiene[:args]) == 2 320 | @test no_hygiene[:args][1] != hygiene[:args][1] # different Symbols 321 | @test no_hygiene[:args][2] == hygiene[:args][2] 322 | 323 | @test length(no_hygiene[:whereparams]) == 1 324 | @test no_hygiene[:whereparams] != hygiene[:whereparams] # different Symbols 325 | # very coarse test to make sure the renamed arg is in the expression it should be 326 | @test occursin( 327 | string(no_hygiene[:whereparams][1]), string(no_hygiene[:args][1]) 328 | ) 329 | end 330 | end 331 | 332 | @testset "internals" begin 333 | @testset "name_of_type" begin 334 | # This isn't part of the public API, and isn't currently hit by anything that is 335 | # but it really seems like it should work. 336 | VERSION >= v"1.7" && @test ExprTools.name_of_type(Vararg) == :(Vararg{Any}) 337 | end 338 | end 339 | end 340 | -------------------------------------------------------------------------------- /test/function.jl: -------------------------------------------------------------------------------- 1 | """ 2 | @audit expr -> Tuple{Any,Expr} 3 | 4 | Evaluate the expression and return both the result and the original expression. Useful for 5 | ensuring that the provided expression is syntactically valid. If provided expression cannot 6 | be evaluated the exception will be returned instead of the result. 7 | """ 8 | macro audit(expr::Expr) 9 | result = quote 10 | tuple( 11 | try 12 | @eval let 13 | $expr 14 | end 15 | catch e 16 | e 17 | end, 18 | $(QuoteNode(expr)), 19 | ) 20 | end 21 | return esc(result) 22 | end 23 | 24 | macro expr(expr::Expr) 25 | esc(QuoteNode(expr)) 26 | end 27 | 28 | function strip_lineno!(expr::Expr) 29 | filter!(expr.args) do ex 30 | isa(ex, LineNumberNode) && return false 31 | if isa(ex, Expr) 32 | ex.head === :line && return false 33 | strip_lineno!(ex::Expr) 34 | end 35 | return true 36 | end 37 | return expr 38 | end 39 | 40 | macro test_splitdef_invalid(expr) 41 | result = quote 42 | @test_throws ArgumentError splitdef($expr) 43 | @test splitdef($expr, throw=false) === nothing 44 | end 45 | return esc(result) 46 | end 47 | 48 | function_form(short::Bool) = string(short ? "short" : "long", "-form") 49 | 50 | 51 | @testset "splitdef / combinedef" begin 52 | @testset "empty function" begin 53 | f, expr = @audit function f end 54 | @test length(methods(f)) == 0 55 | 56 | d = splitdef(expr) 57 | @test keys(d) == Set([:head, :name]) 58 | @test d[:head] == :function 59 | @test d[:name] == :f 60 | 61 | c_expr = combinedef(d) 62 | @test c_expr == expr 63 | end 64 | 65 | @testset "long-form function" begin 66 | f, expr = @audit function f() end 67 | @test length(methods(f)) == 1 68 | @test f() === nothing 69 | 70 | d = splitdef(expr) 71 | @test keys(d) == Set([:head, :name, :body]) 72 | @test d[:head] == :function 73 | @test d[:name] == :f 74 | @test strip_lineno!(d[:body]) == Expr(:block) 75 | 76 | c_expr = combinedef(d) 77 | @test c_expr == expr 78 | end 79 | 80 | @testset "short-form function" begin 81 | f, expr = @audit f() = nothing 82 | @test length(methods(f)) == 1 83 | @test f() === nothing 84 | 85 | d = splitdef(expr) 86 | @test keys(d) == Set([:head, :name, :body]) 87 | @test d[:head] == :(=) 88 | @test d[:name] == :f 89 | @test strip_lineno!(d[:body]) == Expr(:block, :nothing) 90 | 91 | c_expr = combinedef(d) 92 | @test c_expr == expr 93 | end 94 | 95 | @testset "long-form anonymous function" begin 96 | f, expr = @audit function () end 97 | @test length(methods(f)) == 1 98 | @test f() === nothing 99 | 100 | d = splitdef(expr) 101 | @test keys(d) == Set([:head, :body]) 102 | @test d[:head] == :function 103 | @test strip_lineno!(d[:body]) == Expr(:block) 104 | 105 | c_expr = combinedef(d) 106 | @test c_expr == expr 107 | end 108 | 109 | @testset "short-form anonymous function" begin 110 | f, expr = @audit () -> nothing 111 | @test length(methods(f)) == 1 112 | @test f() === nothing 113 | 114 | d = splitdef(expr) 115 | @test keys(d) == Set([:head, :body]) 116 | @test d[:head] == :(->) 117 | @test strip_lineno!(d[:body]) == Expr(:block, :nothing) 118 | 119 | c_expr = combinedef(d) 120 | @test c_expr == expr 121 | end 122 | 123 | @testset "args ($(function_form(short)) function)" for short in (true, false) 124 | @testset "f(x)" begin 125 | f, expr = if short 126 | @audit f(x) = x 127 | else 128 | @audit function f(x) x end 129 | end 130 | @test length(methods(f)) == 1 131 | @test f(0) == 0 132 | 133 | d = splitdef(expr) 134 | @test keys(d) == Set([:head, :name, :args, :body]) 135 | @test d[:args] == [:x] 136 | 137 | c_expr = combinedef(d) 138 | @test c_expr == expr 139 | end 140 | 141 | @testset "f(x::Integer)" begin 142 | f, expr = if short 143 | @audit f(x::Integer) = x 144 | else 145 | @audit function f(x::Integer) x end 146 | end 147 | @test length(methods(f)) == 1 148 | @test f(0) == 0 149 | 150 | d = splitdef(expr) 151 | @test keys(d) == Set([:head, :name, :args, :body]) 152 | @test d[:args] == [:(x::Integer)] 153 | 154 | c_expr = combinedef(d) 155 | @test c_expr == expr 156 | end 157 | 158 | @testset "f(x=1)" begin 159 | f, expr = if short 160 | @audit f(x=1) = x 161 | else 162 | @audit function f(x=1) x end 163 | end 164 | @test length(methods(f)) == 2 165 | @test f(0) == 0 166 | @test f() == 1 167 | 168 | d = splitdef(expr) 169 | @test keys(d) == Set([:head, :name, :args, :body]) 170 | @test d[:args] == [Expr(:kw, :x, 1)] 171 | 172 | c_expr = combinedef(d) 173 | @test c_expr == expr 174 | end 175 | 176 | @testset "f(x::Integer=1)" begin 177 | f, expr = if short 178 | @audit f(x::Integer=1) = x 179 | else 180 | @audit function f(x::Integer=1) x end 181 | end 182 | @test length(methods(f)) == 2 183 | @test f(0) == 0 184 | @test f() == 1 185 | 186 | d = splitdef(expr) 187 | @test keys(d) == Set([:head, :name, :args, :body]) 188 | @test d[:args] == [Expr(:kw, :(x::Integer), 1)] 189 | 190 | c_expr = combinedef(d) 191 | @test c_expr == expr 192 | end 193 | end 194 | 195 | @testset "args ($(function_form(short)) anonymous function)" for short in (true, false) 196 | @testset "x" begin 197 | f, expr = if short 198 | @audit x -> x 199 | else 200 | @audit function (x) x end 201 | end 202 | @test length(methods(f)) == 1 203 | @test f(0) == 0 204 | 205 | d = splitdef(expr) 206 | @test keys(d) == Set([:head, :args, :body]) 207 | @test d[:args] == [:x] 208 | 209 | c_expr = combinedef(d) 210 | @test c_expr == expr 211 | end 212 | 213 | @testset "x::Integer" begin 214 | f, expr = if short 215 | @audit x::Integer -> x 216 | else 217 | @audit function (x::Integer) x end 218 | end 219 | @test length(methods(f)) == 1 220 | @test f(0) == 0 221 | 222 | d = splitdef(expr) 223 | @test keys(d) == Set([:head, :args, :body]) 224 | @test d[:args] == [:(x::Integer)] 225 | 226 | c_expr = combinedef(d) 227 | @test c_expr == expr 228 | end 229 | 230 | @testset "(x=1)" begin 231 | f, expr = if short 232 | @audit (x=1) -> x 233 | else 234 | @audit function (x=1) x end 235 | end 236 | @test length(methods(f)) == 2 237 | @test f(0) == 0 238 | @test f() == 1 239 | 240 | d = splitdef(expr) 241 | @test keys(d) == Set([:head, :args, :body]) 242 | @test d[:args] == [:(x=1)] 243 | 244 | c_expr = combinedef(d) 245 | @test c_expr == expr 246 | end 247 | 248 | @testset "(x::Integer=1)" begin 249 | f, expr = if short 250 | @audit (x::Integer=1) -> x 251 | else 252 | @audit function (x::Integer=1) x end 253 | end 254 | @test length(methods(f)) == 2 255 | @test f(0) == 0 256 | @test f() == 1 257 | 258 | d = splitdef(expr) 259 | @test keys(d) == Set([:head, :args, :body]) 260 | @test d[:args] == [:(x::Integer=1)] 261 | 262 | c_expr = combinedef(d) 263 | @test c_expr == expr 264 | end 265 | 266 | @testset "(x,)" begin 267 | f, expr = if short 268 | @audit (x,) -> x 269 | else 270 | @audit function (x,) x end 271 | end 272 | @test length(methods(f)) == 1 273 | @test f(0) == 0 274 | 275 | d = splitdef(expr) 276 | @test keys(d) == Set([:head, :args, :body]) 277 | @test d[:args] == [:x] 278 | 279 | c_expr = combinedef(d) 280 | expr = short ? (@expr x -> x) : (@expr function (x) x end) 281 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 282 | end 283 | 284 | @testset "(x::Integer,)" begin 285 | f, expr = if short 286 | @audit (x::Integer,) -> x 287 | else 288 | @audit function (x::Integer,) x end 289 | end 290 | @test length(methods(f)) == 1 291 | @test f(0) == 0 292 | 293 | d = splitdef(expr) 294 | @test keys(d) == Set([:head, :args, :body]) 295 | @test d[:args] == [:(x::Integer)] 296 | 297 | c_expr = combinedef(d) 298 | expr = short ? (@expr x::Integer -> x) : (@expr function (x::Integer) x end) 299 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 300 | end 301 | 302 | @testset "(x=1,)" begin 303 | f, expr = if short 304 | @audit (x=1,) -> x 305 | else 306 | @audit function (x=1,) x end 307 | end 308 | @test length(methods(f)) == 2 309 | @test f(0) === 0 310 | @test f() === 1 311 | 312 | d = splitdef(expr) 313 | @test keys(d) == Set([:head, :args, :body]) 314 | @test d[:args] == [:(x=1)] 315 | 316 | c_expr = combinedef(d) 317 | expr = short ? (@expr (x=1) -> x) : (@expr function (x=1) x end) 318 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 319 | end 320 | 321 | @testset "(x::Integer=1,)" begin 322 | f, expr = if short 323 | @audit (x::Integer=1,) -> x 324 | else 325 | @audit function (x::Integer=1,) x end 326 | end 327 | @test length(methods(f)) == 2 328 | @test f(0) == 0 329 | @test f() == 1 330 | 331 | d = splitdef(expr) 332 | @test keys(d) == Set([:head, :args, :body]) 333 | @test d[:args] == [:(x::Integer=1)] 334 | 335 | c_expr = combinedef(d) 336 | expr = short ? (@expr (x::Integer=1) -> x) : (@expr function (x::Integer=1) x end) 337 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 338 | end 339 | end 340 | 341 | @testset "kwargs ($(function_form(short)) function)" for short in (true, false) 342 | @testset "f(; x)" begin 343 | f, expr = if short 344 | @audit f(; x) = x 345 | else 346 | @audit function f(; x) x end 347 | end 348 | @test length(methods(f)) == 1 349 | @test f(x=0) == 0 350 | 351 | d = splitdef(expr) 352 | @test keys(d) == Set([:head, :name, :kwargs, :body]) 353 | @test d[:kwargs] == [:x] 354 | 355 | c_expr = combinedef(d) 356 | @test c_expr == expr 357 | end 358 | 359 | @testset "f(; x::Integer)" begin 360 | f, expr = if short 361 | @audit f(; x::Integer) = x 362 | else 363 | @audit function f(; x::Integer) x end 364 | end 365 | @test length(methods(f)) == 1 366 | @test f(x=0) == 0 367 | 368 | d = splitdef(expr) 369 | @test keys(d) == Set([:head, :name, :kwargs, :body]) 370 | @test d[:kwargs] == [:(x::Integer)] 371 | 372 | c_expr = combinedef(d) 373 | @test c_expr == expr 374 | end 375 | 376 | @testset "f(; x=1)" begin 377 | f, expr = if short 378 | @audit f(; x=1) = x 379 | else 380 | @audit function f(; x=1) x end 381 | end 382 | @test length(methods(f)) == 1 383 | @test f(x=0) == 0 384 | 385 | d = splitdef(expr) 386 | @test keys(d) == Set([:head, :name, :kwargs, :body]) 387 | @test d[:kwargs] == [Expr(:kw, :x, 1)] 388 | 389 | c_expr = combinedef(d) 390 | @test c_expr == expr 391 | end 392 | 393 | @testset "f(; x::Integer=1)" begin 394 | f, expr = if short 395 | @audit f(; x::Integer=1) = x 396 | else 397 | @audit function f(; x::Integer=1) x end 398 | end 399 | @test length(methods(f)) == 1 400 | @test f(x=0) == 0 401 | 402 | d = splitdef(expr) 403 | @test keys(d) == Set([:head, :name, :kwargs, :body]) 404 | @test d[:kwargs] == [Expr(:kw, :(x::Integer), 1)] 405 | 406 | c_expr = combinedef(d) 407 | @test c_expr == expr 408 | end 409 | end 410 | 411 | @testset "kwargs ($(function_form(short)) function)" for short in (true, false) 412 | @testset "(; x)" begin 413 | f, expr = if short 414 | @audit (; x) -> x 415 | else 416 | @audit function (; x) x end 417 | end 418 | @test length(methods(f)) == 1 419 | @test f(x=0) == 0 420 | 421 | d = splitdef(expr) 422 | @test keys(d) == Set([:head, :kwargs, :body]) 423 | @test d[:kwargs] == [:x] 424 | 425 | c_expr = combinedef(d) 426 | @test c_expr == expr 427 | end 428 | 429 | @testset "(; x::Integer)" begin 430 | f, expr = if short 431 | @audit (; x::Integer) -> x 432 | else 433 | @audit function (; x::Integer) x end 434 | end 435 | @test length(methods(f)) == 1 436 | @test f(x=0) == 0 437 | 438 | d = splitdef(expr) 439 | @test keys(d) == Set([:head, :kwargs, :body]) 440 | @test d[:kwargs] == [:(x::Integer)] 441 | 442 | c_expr = combinedef(d) 443 | @test c_expr == expr 444 | end 445 | 446 | @testset "(; x=1)" begin 447 | f, expr = if short 448 | @audit (; x=1) -> x 449 | else 450 | @audit function (; x=1) x end 451 | end 452 | @test length(methods(f)) == 1 453 | @test f(x=0) == 0 454 | 455 | d = splitdef(expr) 456 | @test keys(d) == Set([:head, :kwargs, :body]) 457 | @test d[:kwargs] == [Expr(:kw, :x, 1)] 458 | 459 | c_expr = combinedef(d) 460 | @test c_expr == expr 461 | end 462 | 463 | @testset "(; x::Integer=1)" begin 464 | f, expr = if short 465 | @audit (; x::Integer=1) -> x 466 | else 467 | @audit function (; x::Integer=1) x end 468 | end 469 | @test length(methods(f)) == 1 470 | @test f(x=0) == 0 471 | 472 | d = splitdef(expr) 473 | @test keys(d) == Set([:head, :kwargs, :body]) 474 | @test d[:kwargs] == [Expr(:kw, :(x::Integer), 1)] 475 | 476 | c_expr = combinedef(d) 477 | @test c_expr == expr 478 | end 479 | end 480 | 481 | # When using :-> there are a few definitions that use a block expression instead of the 482 | # typical tuple. 483 | @testset "block expression ($(function_form(short)) anonymous function)" for short in (true, false) 484 | @testset "(;)" begin 485 | # The `(;)` syntax was deprecated in 1.4.0-DEV.585 (ce29ec547e) but we can still 486 | # test the behavior with an explicit Expr 487 | expr = if short 488 | # `(;) -> nothing` 489 | Expr( 490 | :->, 491 | Expr(:block), 492 | Expr(:block, LineNumberNode(@__LINE__, @__FILE__), :nothing), 493 | ) 494 | else 495 | # `function (;) nothing end` 496 | Expr( 497 | :function, 498 | Expr(:block), 499 | Expr(:block, LineNumberNode(@__LINE__, @__FILE__), :nothing), 500 | ) 501 | end 502 | f = eval(expr) 503 | 504 | @test length(methods(f)) == 1 505 | @test f() === nothing 506 | 507 | # Note: the semi-colon is missing from the expression 508 | d = splitdef(expr) 509 | @test keys(d) == Set([:head, :kwargs, :body]) 510 | @test d[:kwargs] == [] 511 | 512 | c_expr = combinedef(d) 513 | expr = Expr(:->, Expr(:tuple, Expr(:parameters)), Expr(:block, :nothing)) 514 | expr.head = short ? :-> : :function 515 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 516 | end 517 | 518 | @testset "(x;)" begin 519 | f, expr = if short 520 | @audit (x;) -> x 521 | else 522 | @audit function (x;) x end 523 | end 524 | @test length(methods(f)) == 1 525 | @test f(0) == 0 526 | 527 | # Note: the semi-colon is missing from the expression 528 | d = splitdef(expr) 529 | @test keys(d) == Set([:head, :args, :kwargs, :body]) 530 | @test d[:args] == [:x] 531 | @test d[:kwargs] == [] 532 | 533 | c_expr = combinedef(d) 534 | expr = Expr(:->, Expr(:tuple, Expr(:parameters), :x), Expr(:block, :x)) 535 | expr.head = short ? :-> : :function 536 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 537 | end 538 | 539 | @testset "(x; y)" begin 540 | f, expr = if short 541 | @audit (x; y) -> (x, y) 542 | else 543 | @audit function (x; y); (x, y) end 544 | end 545 | @test length(methods(f)) == 1 546 | @test f(0, y=1) == (0, 1) 547 | 548 | # Note: the semi-colon is missing from the expression 549 | d = splitdef(expr) 550 | @test keys(d) == Set([:head, :args, :kwargs, :body]) 551 | @test d[:args] == [:x] 552 | @test d[:kwargs] == [:y] 553 | 554 | c_expr = combinedef(d) 555 | expr = Expr(:->, Expr(:tuple, Expr(:parameters, :y), :x), Expr(:block, :((x, y)))) 556 | expr.head = short ? :-> : :function 557 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 558 | end 559 | 560 | @testset "(x; y = 0)" begin 561 | f, expr = if short 562 | @audit (x; y = 0) -> (x, y) 563 | else 564 | @audit function (x; y = 0); (x, y) end 565 | end 566 | @test length(methods(f)) == 1 567 | @test f(0) == (0, 0) 568 | @test f(0, y=1) == (0, 1) 569 | 570 | # Note: the semi-colon is missing from the expression 571 | d = splitdef(expr) 572 | @test keys(d) == Set([:head, :args, :kwargs, :body]) 573 | @test d[:args] == [:x] 574 | @test d[:kwargs] == [Expr(:kw, :y, 0)] 575 | 576 | c_expr = combinedef(d) 577 | expr = Expr(:->, Expr(:tuple, Expr(:parameters, Expr(:kw, :y, 0)), :x), Expr(:block, :((x, y)))) 578 | expr.head = short ? :-> : :function 579 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 580 | end 581 | 582 | @testset "(x; y = 0, _...)" begin 583 | f, expr = if short 584 | @audit (x; y = 0, _...) -> (x, y) 585 | else 586 | @audit function (x; y = 0, _...); (x, y) end 587 | end 588 | @test length(methods(f)) == 1 589 | @test f(0) == (0, 0) 590 | @test f(0, y=1) == (0, 1) 591 | @test f(0, y=1, z=2) == (0, 1) 592 | 593 | # Note: the semi-colon is missing from the expression 594 | d = splitdef(expr) 595 | @test keys(d) == Set([:head, :args, :kwargs, :body]) 596 | @test d[:args] == [:x] 597 | @test d[:kwargs] == [Expr(:kw, :y, 0), :(_...)] 598 | 599 | c_expr = combinedef(d) 600 | expr = Expr(:->, Expr(:tuple, Expr(:parameters, Expr(:kw, :y, 0), :(_...)), :x), Expr(:block, :((x, y)))) 601 | expr.head = short ? :-> : :function 602 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 603 | end 604 | 605 | @testset "Expr(:block, :x, :y)" begin 606 | expr = Expr(:->, Expr(:block, :x, :y), Expr(:block, :((x, y)))) 607 | expr.head = short ? :-> : :function 608 | f = @eval $expr 609 | @test length(methods(f)) == 1 610 | @test f(0, y=1) == (0, 1) 611 | 612 | # Note: the semi-colon is missing from the expression 613 | d = splitdef(expr) 614 | @test keys(d) == Set([:head, :args, :kwargs, :body]) 615 | @test d[:args] == [:x] 616 | @test d[:kwargs] == [:y] 617 | 618 | c_expr = combinedef(d) 619 | expr = Expr(:->, Expr(:tuple, Expr(:parameters, :y), :x), Expr(:block, :((x, y)))) 620 | expr.head = short ? :-> : :function 621 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 622 | end 623 | end 624 | 625 | @testset "where ($(function_form(short)) function)" for short in (true, false) 626 | @testset "single where" begin 627 | f, expr = if short 628 | @audit f(::A) where A = nothing 629 | else 630 | @audit function f(::A) where A; nothing end 631 | end 632 | @test length(methods(f)) == 1 633 | 634 | d = splitdef(expr) 635 | @test keys(d) == Set([:head, :name, :args, :whereparams, :body]) 636 | @test d[:whereparams] == [:A] 637 | 638 | c_expr = combinedef(d) 639 | @test c_expr == expr 640 | end 641 | 642 | @testset "curly where" begin 643 | f, expr = if short 644 | @audit f(::A, ::B) where {A, B <: A} = nothing 645 | else 646 | @audit function f(::A, ::B) where {A, B <: A}; nothing end 647 | end 648 | @test length(methods(f)) == 1 649 | 650 | d = splitdef(expr) 651 | @test keys(d) == Set([:head, :name, :args, :whereparams, :body]) 652 | @test d[:whereparams] == [:A, :(B <: A)] 653 | 654 | c_expr = combinedef(d) 655 | @test c_expr == expr 656 | end 657 | 658 | @testset "multiple where" begin 659 | f, expr = if short 660 | @audit f(::A, ::B) where B <: A where A = nothing 661 | else 662 | @audit function f(::A, ::B) where B <: A where A; nothing end 663 | end 664 | @test length(methods(f)) == 1 665 | 666 | d = splitdef(expr) 667 | @test keys(d) == Set([:head, :name, :args, :whereparams, :body]) 668 | @test d[:whereparams] == [:A, :(B <: A)] 669 | 670 | c_expr = combinedef(d) 671 | expr = @expr f(::A, ::B) where {A, B <: A} = nothing 672 | expr.head = short ? :(=) : :function 673 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 674 | end 675 | end 676 | 677 | @testset "where ($(function_form(short)) anonymous function)" for short in (true, false) 678 | @testset "where" begin 679 | f, expr = if short 680 | @audit ((::A) where A) -> nothing 681 | else 682 | @audit function (::A) where A; nothing end 683 | end 684 | @test length(methods(f)) == 1 685 | 686 | d = splitdef(expr) 687 | @test keys(d) == Set([:head, :args, :whereparams, :body]) 688 | @test d[:whereparams] == [:A] 689 | 690 | c_expr = combinedef(d) 691 | @test c_expr == expr 692 | end 693 | 694 | @testset "curly where" begin 695 | f, expr = if short 696 | @audit ((::A, ::B) where {A, B <: A}) -> nothing 697 | else 698 | @audit function (::A, ::B) where {A, B <: A}; nothing end 699 | end 700 | @test length(methods(f)) == 1 701 | 702 | d = splitdef(expr) 703 | @test keys(d) == Set([:head, :args, :whereparams, :body]) 704 | @test d[:whereparams] == [:A, :(B <: A)] 705 | 706 | c_expr = combinedef(d) 707 | @test c_expr == expr 708 | end 709 | 710 | @testset "multiple where" begin 711 | f, expr = if short 712 | @audit ((::A, ::B) where B <: A where A) -> nothing 713 | else 714 | @audit function (::A, ::B) where B <: A where A; nothing end 715 | end 716 | @test length(methods(f)) == 1 717 | 718 | d = splitdef(expr) 719 | @test keys(d) == Set([:head, :args, :whereparams, :body]) 720 | @test d[:whereparams] == [:A, :(B <: A)] 721 | 722 | c_expr = combinedef(d) 723 | expr = @expr ((::A, ::B) where {A, B <: A}) -> nothing 724 | expr.head = short ? :-> : :function 725 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 726 | end 727 | end 728 | 729 | @testset "return-type ($(function_form(short)) function)" for short in (true, false) 730 | @testset "f(x)::Integer" begin 731 | f, expr = if short 732 | @audit f(x)::Integer = x 733 | else 734 | @audit function f(x)::Integer; x end 735 | end 736 | @test length(methods(f)) == 1 737 | @test f(0.0) isa Integer 738 | 739 | d = splitdef(expr) 740 | @test keys(d) == Set([:head, :name, :args, :rtype, :body]) 741 | @test d[:rtype] == :Integer 742 | 743 | c_expr = combinedef(d) 744 | @test c_expr == expr 745 | end 746 | 747 | @testset "(f(x::T)::Integer) where T" begin 748 | f, expr = if short 749 | @audit (f(x::T)::Integer) where T = x 750 | else 751 | @audit function (f(x::T)::Integer) where T; x end 752 | end 753 | @test length(methods(f)) == 1 754 | @test f(0.0) isa Integer 755 | 756 | d = splitdef(expr) 757 | @test keys(d) == Set([:head, :name, :args, :rtype, :whereparams, :body]) 758 | @test d[:rtype] == :Integer 759 | 760 | c_expr = combinedef(d) 761 | @test c_expr == expr 762 | end 763 | end 764 | 765 | @testset "return-type (short-form anonymous function)" begin 766 | @testset "(x,)::Integer" begin 767 | f, expr = @audit (x,)::Integer -> x # Interpreted as `(x::Integer,) -> x` 768 | @test length(methods(f)) == 1 769 | @test f(0) == 0 770 | @test_throws MethodError f(0.0) 771 | 772 | d = splitdef(expr) 773 | @test keys(d) == Set([:head, :args, :body]) 774 | @test d[:args] == [:((x,)::Integer)] 775 | 776 | c_expr = combinedef(d) 777 | @test c_expr == expr 778 | end 779 | 780 | @testset "(((x::T,)::Integer) where T)" begin 781 | f, expr = @audit (((x::T,)::Integer) where T) -> x 782 | @test f isa ErrorException 783 | 784 | @test_broken splitdef(expr, throw=false) === nothing 785 | 786 | d = Dict( 787 | :head => :(->), 788 | :args => [:(x::T)], 789 | :rtype => :Integer, 790 | :whereparams => [:T], 791 | :body => quote 792 | x 793 | end 794 | ) 795 | c_expr = combinedef(d) 796 | expr = @expr (((x::T)::Integer) where T) -> x 797 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 798 | end 799 | end 800 | 801 | @testset "return-type (long-form anonymous function)" begin 802 | @testset "(x)::Integer" begin 803 | # Older parsers interpret 804 | # `function (x)::Integer; x end` 805 | # as 806 | # `function (x::Integer); x end` 807 | expr = Expr( 808 | :function, 809 | Expr(:tuple, Expr(:(::), :x, :Integer)), 810 | Expr(:block, LineNumberNode(@__LINE__, @__FILE__), :x), 811 | ) 812 | f = eval(expr) 813 | @test length(methods(f)) == 1 814 | @test f(0) == 0 815 | @test_throws MethodError f(0.0) 816 | 817 | d = splitdef(expr) 818 | @test keys(d) == Set([:head, :args, :body]) 819 | @test d[:args] == [:(x::Integer)] 820 | 821 | c_expr = combinedef(d) 822 | @test c_expr == expr 823 | end 824 | 825 | @testset "(((x::T)::Integer) where T)" begin 826 | expr = Expr(:function, 827 | Expr(:where, Expr(:(::), Expr(:tuple, :(x::T)), :Integer), :T), 828 | Expr(:block, :x), 829 | ) 830 | @test_throws ErrorException eval(expr) 831 | 832 | @test_broken splitdef(expr, throw=false) === nothing 833 | 834 | d = Dict( 835 | :head => :function, 836 | :args => [:(x::T)], 837 | :rtype => :Integer, 838 | :whereparams => [:T], 839 | :body => quote 840 | x 841 | end 842 | ) 843 | c_expr = combinedef(d) 844 | @test strip_lineno!(c_expr) == strip_lineno!(expr) 845 | end 846 | end 847 | 848 | @testset "combinedef with no `:head`" begin 849 | # should default to `:function` 850 | f, expr = @audit function f() end 851 | 852 | d = splitdef(expr) 853 | delete!(d, :head) 854 | @assert !haskey(d, :head) 855 | 856 | c_expr = combinedef(d) 857 | @test c_expr == expr 858 | end 859 | 860 | @testset "invalid definitions" begin 861 | # Invalid function type 862 | @test_splitdef_invalid Expr(:block) 863 | 864 | # Too few expression arguments 865 | @test_splitdef_invalid Expr(:function) 866 | @test_splitdef_invalid Expr(:(=), :f) 867 | @test_splitdef_invalid Expr(:function, :(f(x))) 868 | 869 | # Too many expression arguments 870 | @test_splitdef_invalid Expr(:function, :f, :x, :y) 871 | @test_splitdef_invalid Expr(:(=), :f, :x, :y) 872 | 873 | # Invalid or missing arguments 874 | @test_splitdef_invalid :(f{S} = 0) 875 | @test_broken splitdef(:(a::Number::Int -> a); throws=false) === nothing 876 | 877 | # Invalid argument block expression 878 | ex = :((x; y; z) -> 0) # Note: inlining this strips LineNumberNodes from the block 879 | @test any(arg -> arg isa LineNumberNode, ex.args[1].args) 880 | @test_splitdef_invalid ex 881 | @test_splitdef_invalid Expr(:->, Expr(:block, :x, :y, :z), Expr(:block, 0)) 882 | 883 | # Empty function contains extras 884 | @test_throws ArgumentError combinedef(Dict(:head => :function, :name => :f, :args => [])) 885 | end 886 | end 887 | --------------------------------------------------------------------------------