diff --git a/NEWS.md b/NEWS.md index 5d9bf83467b77..39d798c0dac5d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ New language features - New `Base.@acquire` macro for a non-closure version of `Base.acquire(f, s::Base.Semaphore)`, like `@lock`. ([#56845]) - New `nth` function to access the `n`-th element of a generic iterable. ([#56580]) + - New `@__FUNCTION__` macro to refer to the innermost enclosing function. ([#58909]) - The character U+1F8B2 🢲 (RIGHTWARDS ARROW WITH LOWER HOOK), newly added by Unicode 16, is now a valid operator with arrow precedence, accessible as `\hookunderrightarrow` at the REPL. ([JuliaLang/JuliaSyntax.jl#525], [#57143]) diff --git a/base/exports.jl b/base/exports.jl index 53f6152ea55f2..2c30f095a3998 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -1059,6 +1059,7 @@ export @__DIR__, @__LINE__, @__MODULE__, + @__FUNCTION__, @int128_str, @uint128_str, @big_str, diff --git a/base/runtime_internals.jl b/base/runtime_internals.jl index 98dd111ccbf68..bb5c09d80db43 100644 --- a/base/runtime_internals.jl +++ b/base/runtime_internals.jl @@ -173,6 +173,43 @@ false """ ispublic(m::Module, s::Symbol) = ccall(:jl_module_public_p, Cint, (Any, Any), m, s) != 0 +""" + @__FUNCTION__ + +Get the innermost enclosing function object. + +!!! 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 +``` +""" +macro __FUNCTION__() + Expr(:thisfunction) +end + # TODO: this is vaguely broken because it only works for explicit calls to # `Base.deprecate`, not the @deprecated macro: isdeprecated(m::Module, s::Symbol) = ccall(:jl_is_binding_deprecated, Cint, (Any, Any), m, s) != 0 diff --git a/doc/src/base/base.md b/doc/src/base/base.md index fafe08a6e5125..6e795668dbef2 100644 --- a/doc/src/base/base.md +++ b/doc/src/base/base.md @@ -480,6 +480,7 @@ Base.moduleroot __module__ __source__ Base.@__MODULE__ +Base.@__FUNCTION__ Base.@__FILE__ Base.@__DIR__ Base.@__LINE__ diff --git a/doc/src/manual/performance-tips.md b/doc/src/manual/performance-tips.md index fa197196dad4f..90a40337e081a 100644 --- a/doc/src/manual/performance-tips.md +++ b/doc/src/manual/performance-tips.md @@ -919,6 +919,40 @@ In the mean time, some user-contributed packages like [FastClosures](https://github.com/c42f/FastClosures.jl) automate the insertion of `let` statements as in `abmult3`. +#### Use `@__FUNCTION__` for recursive closures + +For recursive closures specifically, the [`@__FUNCTION__`](@ref) macro can avoid both type instability and boxing. + +First, let's see the unoptimized version: + +```julia +function make_fib_unoptimized() + fib(n) = n <= 1 ? 1 : fib(n - 1) + fib(n - 2) # fib is boxed + return fib +end +``` + +The `fib` function is boxed, meaning the return type is inferred as `Any`: + +```julia +@code_warntype make_fib_unoptimized() +``` + +Now, to eliminate this type instability, we can instead use `@__FUNCTION__` to refer to the concrete function object: + +```julia +function make_fib_optimized() + fib(n) = n <= 1 ? 1 : (@__FUNCTION__)(n - 1) + (@__FUNCTION__)(n - 2) + return fib +end +``` + +This gives us a concrete return type: + +```julia +@code_warntype make_fib_optimized() +``` + ### [Types with values-as-parameters](@id man-performance-value-type) diff --git a/src/ast.scm b/src/ast.scm index 15e55fc616041..ea538e0aede4e 100644 --- a/src/ast.scm +++ b/src/ast.scm @@ -466,6 +466,7 @@ (define (make-assignment l r) `(= ,l ,r)) (define (assignment? e) (and (pair? e) (eq? (car e) '=))) (define (return? e) (and (pair? e) (eq? (car e) 'return))) +(define (thisfunction? e) (and (pair? e) (eq? (car e) 'thisfunction))) (define (tuple-call? e) (and (length> e 1) diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index a73044a228b5a..052e0000ebe87 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -549,7 +549,9 @@ (insert-after-meta `(block ,@stmts) (cons `(meta nkw ,(+ (length vars) (length restkw))) - annotations)) + (if (has-thisfunction? `(block ,@stmts)) + (cons `(meta thisfunction-original ,(arg-name (car not-optional))) annotations) + annotations))) rett) ;; call with no keyword args @@ -2911,6 +2913,7 @@ 'generator (lambda (e) (check-no-return e) + (check-no-thisfunction e) (expand-generator e #f '())) 'flatten @@ -2995,6 +2998,13 @@ (if (has-return? e) (error "\"return\" not allowed inside comprehension or generator"))) +(define (has-thisfunction? e) + (expr-contains-p thisfunction? e (lambda (x) (not (function-def? x))))) + +(define (check-no-thisfunction e) + (if (has-thisfunction? e) + (error "\"@__FUNCTION__\" not allowed inside comprehension or generator"))) + (define (has-break-or-continue? e) (expr-contains-p (lambda (x) (and (pair? x) (memq (car x) '(break continue)))) e @@ -3003,6 +3013,7 @@ (define (lower-comprehension ty expr itrs) (check-no-return expr) + (check-no-thisfunction expr) (if (has-break-or-continue? expr) (error "break or continue outside loop")) (let ((result (make-ssavalue)) @@ -3434,7 +3445,7 @@ vi) tab)) -;; env: list of vinfo (includes any closure #self#; should not include globals) +;; env: list of vinfo (should not include globals) ;; captvars: list of vinfo ;; sp: list of symbol ;; new-sp: list of symbol (static params declared here) @@ -3855,7 +3866,7 @@ f(x) = yt(x) (Set '(quote top core lineinfo line inert local-def unnecessary copyast meta inbounds boundscheck loopinfo decl aliasscope popaliasscope thunk with-static-parameters toplevel-only - global globalref global-if-global assign-const-if-global isglobal thismodule + global globalref global-if-global assign-const-if-global isglobal thismodule thisfunction const atomic null true false ssavalue isdefined toplevel module lambda error gc_preserve_begin gc_preserve_end export public inline noinline purity))) @@ -4093,7 +4104,7 @@ f(x) = yt(x) ((atom? e) e) (else (case (car e) - ((quote top core global globalref thismodule lineinfo line break inert module toplevel null true false meta) e) + ((quote top core global globalref thismodule thisfunction lineinfo line break inert module toplevel null true false meta) e) ((toplevel-only) ;; hack to avoid generating a (method x) expr for struct types (if (eq? (cadr e) 'struct) @@ -5133,6 +5144,30 @@ f(x) = yt(x) ((error) (error (cadr e))) + + ;; thisfunction replaced with first argument name + ((thisfunction) + (let ((first-arg (and (pair? (lam:args lam)) (car (lam:args lam))))) + (if first-arg + (let* ((arg-name (arg-name first-arg)) + ;; Check for thisfunction-original metadata in keyword wrapper functions + (original-name (let ((body (lam:body lam))) + (and (pair? body) (pair? (cdr body)) + (let loop ((stmts (cdr body))) + (if (pair? stmts) + (let ((stmt (car stmts))) + (if (and (pair? stmt) (eq? (car stmt) 'meta) + (pair? (cdr stmt)) (eq? (cadr stmt) 'thisfunction-original) + (pair? (cddr stmt))) + (caddr stmt) + (loop (cdr stmts)))) + #f))))) + (final-name (or original-name arg-name))) + (cond (tail (emit-return tail final-name)) + (value final-name) + (else (emit final-name) #f))) + (error "\"@__FUNCTION__\" can only be used inside a function")))) + (else (error (string "invalid syntax " (deparse e))))))) ;; introduce new slots for assigned arguments diff --git a/test/syntax.jl b/test/syntax.jl index dcd921823d273..25a683a3b9b31 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -1526,8 +1526,11 @@ end @test Meta.lower(@__MODULE__, :(return 0 for i=1:2)) == Expr(:error, "\"return\" not allowed inside comprehension or generator") @test Meta.lower(@__MODULE__, :([ return 0 for i=1:2 ])) == Expr(:error, "\"return\" not allowed inside comprehension or generator") @test Meta.lower(@__MODULE__, :(Int[ return 0 for i=1:2 ])) == Expr(:error, "\"return\" not allowed inside comprehension or generator") +@test Meta.lower(@__MODULE__, :([ $(Expr(:thisfunction)) for i=1:2 ])) == Expr(:error, "\"@__FUNCTION__\" not allowed inside comprehension or generator") +@test Meta.lower(@__MODULE__, :($(Expr(:thisfunction)) for i=1:2)) == Expr(:error, "\"@__FUNCTION__\" not allowed inside comprehension or generator") @test [ ()->return 42 for i = 1:1 ][1]() == 42 @test Function[ identity() do x; return 2x; end for i = 1:1 ][1](21) == 42 +@test @eval let f=[ ()->$(Expr(:thisfunction)) for i = 1:1 ][1]; f() === f; end # issue #27155 macro test27155() @@ -4351,3 +4354,179 @@ let f = NoSpecClosure.K(1) @test f(2) == 1 @test typeof(f).parameters == Core.svec() end + +@testset "@__FUNCTION__ and Expr(:thisfunction)" begin + @testset "Basic usage" begin + # @__FUNCTION__ in regular functions + test_function_basic() = @__FUNCTION__ + @test test_function_basic() === test_function_basic + + # Expr(:thisfunction) in regular functions + @eval regular_func() = $(Expr(:thisfunction)) + @test regular_func() === regular_func + end + + @testset "Recursion" begin + # Factorial with @__FUNCTION__ + factorial_function(n) = n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1) + @test factorial_function(5) == 120 + + # Fibonacci with Expr(:thisfunction) + struct RecursiveCallableStruct; end + @eval (::RecursiveCallableStruct)(n) = n <= 1 ? n : $(Expr(:thisfunction))(n-1) + $(Expr(:thisfunction))(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 + + # Expr(:thisfunction) with kwargs + let + @eval f2(; n=1) = n <= 1 ? n : n * $(Expr(:thisfunction))(; n=n-1) + result = f2(n=5) + @test result == 120 + end + end + + @testset "Callable structs" begin + # @__FUNCTION__ in callable structs + @gensym A + @eval module $A + struct CallableStruct{T}; val::T; end + (c::CallableStruct)() = @__FUNCTION__ + end + @eval using .$A: CallableStruct + c = CallableStruct(5) + @test c() === c + + # 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() + $(Expr(:thisfunction)) + 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)() = $(Expr(:thisfunction)) + @eval (obj::CallableStruct3)(x) = $(Expr(:thisfunction)).value + x + + let cs = CallableStruct3(42) + @test cs() === cs + @test cs(10) === 52 + 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() + @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 + end + + @testset "Special cases" begin + # Generated functions + let @generated foo2() = Expr(:thisfunction) + @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($(Expr(:thisfunction)))) + end + result = Cols(1, 2, 3) + @test occursin("Cols", result) + end + + # Should not access arg-map for local variables + @gensym f + @eval begin + function $f end + function ($f::typeof($f))() + $f = 1 + $(Expr(:thisfunction)) + end + end + @test @eval($f() === $f) + end + + @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