Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Compat"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "4.17.0"
version = "4.18.0"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ changes in `julia`.

## Supported features

* `@__FUNCTION__` is a macro that returns the innermost enclosing function ([#58940]) (since Compat 4.18.0)

* `filter` can now act on a `NamedTuple` [#50795]. (since Compat 4.17.0)

* `insertdims(D; dims)` is the opposite of `dropdims` ([#45793]) (since Compat 4.16.0)
Expand Down Expand Up @@ -201,3 +203,4 @@ Note that you should specify the correct minimum version for `Compat` in the
[#50105]: https://github.com/JuliaLang/julia/issues/50105
[#50795]: https://github.com/JuliaLang/julia/issues/50795
[#54653]: https://github.com/JuliaLang/julia/issues/54653
[#58940]: https://github.com/JuliaLang/julia/issues/58940
54 changes: 54 additions & 0 deletions src/Compat.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,60 @@ else
using Base: Fix, Fix1, Fix2
end

# https://github.com/JuliaLang/julia/pull/58940
@static if !isdefined(Base, Symbol("@__FUNCTION__"))
_function_macro_error() = throw(ArgumentError("`Compat.@__FUNCTION__` is not available in this context"))

macro __FUNCTION__()
esc(quote
$(Expr(:isdefined, :var"#self#")) || $(_function_macro_error)()
var"#self#"
end)
end

@doc """
@__FUNCTION__

Get the innermost enclosing function object.

!!! warning
The Compat.jl version of `@__FUNCTION__` differs from the Base version in
VERSION < v"1.13.0-DEV.880" in two contexts:
(1) it _will_ throw an error when used in certain contexts (such as callable structs), and
(2) it will _not_ throw an error when used inside a comprehension or generator.

!!! note
`@__FUNCTION__` has the same scoping behavior as `return`: when used
inside a closure, it refers to the closure and not the outer function.
Some macros, including [`@spawn`](@ref Threads.@spawn), [`@async`](@ref), etc.,
wrap their input in closures. When `@__FUNCTION__` is used within such code,
it will refer to the closure created by the macro rather than the enclosing function.

# Examples

`@__FUNCTION__` enables recursive anonymous functions:

```jldoctest
julia> factorial = (n -> n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1));

julia> factorial(5)
120
```

`@__FUNCTION__` can be combined with `nameof` to identify a function's
name from within its body:

```jldoctest
julia> bar() = nameof(@__FUNCTION__);

julia> bar()
:bar
```
""" :(@__FUNCTION__)

export @__FUNCTION__
end

include("deprecated.jl")

end # module Compat
191 changes: 190 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1079,4 +1079,193 @@ end
longnt = NamedTuple{ntuple(i -> Symbol(:a, i), 20)}(ntuple(identity, 20))
@test filter(iseven, longnt) === NamedTuple{ntuple(i -> Symbol(:a, 2i), 10)}(ntuple(i -> 2i, 10))
@test filter(x -> x<2, (longnt..., z=1.5)) === (a1=1, z=1.5)
end
end

# https://github.com/JuliaLang/julia/pull/58940
@testset "@__FUNCTION__" begin
@testset "Basic usage" begin
# @__FUNCTION__ in regular functions
test_function_basic() = @__FUNCTION__
@test test_function_basic() === test_function_basic
end

@testset "Recursion" begin
# Factorial with @__FUNCTION__
factorial_function(n) = n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1)
@test factorial_function(5) == 120

# Fibonacci with @__FUNCTION__
struct RecursiveCallableStruct; end
@eval (::RecursiveCallableStruct)(n) = n <= 1 ? n : (@__FUNCTION__)(n-1) + (@__FUNCTION__)(n-2)
@test RecursiveCallableStruct()(10) === 55

# Anonymous function recursion
@test (n -> n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1))(5) == 120
end

@testset "Closures and nested functions" begin
# Prevents boxed closures
function make_closure()
fib(n) = n <= 1 ? 1 : (@__FUNCTION__)(n - 1) + (@__FUNCTION__)(n - 2)
return fib
end
Test.@inferred make_closure()
closure = make_closure()
@test closure(5) == 8
Test.@inferred closure(5)

# Complex closure of closures
function f1()
function f2()
function f3()
return @__FUNCTION__
end
return (@__FUNCTION__), f3()
end
return (@__FUNCTION__), f2()...
end
Test.@inferred f1()
@test f1()[1] === f1
@test f1()[2] !== f1
@test f1()[3] !== f1
@test f1()[3]() === f1()[3]
@test f1()[2]()[2]() === f1()[3]
end

@testset "Do blocks" begin
function test_do_block()
result = map([1, 2, 3]) do x
return (@__FUNCTION__, x)
end
# All should refer to the same do-block function
@test all(r -> r[1] === result[1][1], result)
# Values should be different
@test [r[2] for r in result] == [1, 2, 3]
# It should be different than `test_do_block`
@test result[1][1] !== test_do_block
end
test_do_block()
end

@testset "Keyword arguments" begin
# @__FUNCTION__ with kwargs
foo(; n) = n <= 1 ? 1 : n * (@__FUNCTION__)(; n = n - 1)
@test foo(n = 5) == 120
end

@testset "Callable structs" begin
# @__FUNCTION__ in callable structs
@gensym A
@eval module $A
import ..@__FUNCTION__

struct CallableStruct{T}; val::T; end
(c::CallableStruct)() = @__FUNCTION__
end
@eval using .$A: CallableStruct
c = CallableStruct(5)
if isdefined(Base, Symbol("@__FUNCTION__"))
@test c() === c
else
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") c()
end

# In closures, var"#self#" should refer to the enclosing function,
# NOT the enclosing struct instance
struct CallableStruct2; end
@eval function (obj::CallableStruct2)()
function inner_func()
@__FUNCTION__
end
inner_func
end

let cs = CallableStruct2()
@test cs()() === cs()
@test cs()() !== cs
end

# Accessing values via self-reference
struct CallableStruct3
value::Int
end
@eval (obj::CallableStruct3)() = @__FUNCTION__
@eval (obj::CallableStruct3)(x) = (@__FUNCTION__).value + x

let cs = CallableStruct3(42)
if isdefined(Base, Symbol("@__FUNCTION__"))
@test cs() === cs
@test cs(10) === 52
else
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") cs()
end
end

# Callable struct with args and kwargs
struct CallableStruct4
end
@eval function (obj::CallableStruct4)(x, args...; y=2, kws...)
return (; func=(@__FUNCTION__), x, args, y, kws)
end
c = CallableStruct4()
if isdefined(Base, Symbol("@__FUNCTION__"))
@test c(1).func === c
@test c(2, 3).args == (3,)
@test c(2; y=4).y == 4
@test c(2; y=4, a=5, b=6, c=7).kws[:c] == 7
else
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") c(1)
end
end

@testset "Special cases" begin
# Generated functions
let @generated foo2() = :(@__FUNCTION__)
@test foo2() === foo2
end

# Struct constructors
let
@eval struct Cols{T<:Tuple}
cols::T
operator
Cols(args...; operator=union) = (new{typeof(args)}(args, operator); string(@__FUNCTION__))
end
if isdefined(Base, Symbol("@__FUNCTION__"))
@test occursin("Cols", Cols(1, 2, 3))
elseif VERSION > v"1.9.0-"
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") Cols(1, 2, 3)
end
end

# Should not access arg-map for local variables
@gensym f
@eval begin
function $f end
function ($f::typeof($f))()
$f = 1
@__FUNCTION__
end
end
if isdefined(Base, Symbol("@__FUNCTION__"))
@test @eval($f() === $f)
else
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") @eval($f())
end
end

if isdefined(Base, Symbol("@__FUNCTION__"))
@testset "Error upon misuse" begin
@gensym B
@test_throws(
"\"@__FUNCTION__\" can only be used inside a function",
@eval(module $B; @__FUNCTION__; end)
)

@test_throws(
"\"@__FUNCTION__\" not allowed inside comprehension or generator",
@eval([(@__FUNCTION__) for _ in 1:10])
)
end
end
end
Loading