Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Insert explicit return expressions in functions/macros/do-blocks #48

Merged
merged 1 commit into from
Oct 18, 2024

Conversation

fredrikekre
Copy link
Owner

This patch make sure that function definitions end with an explicit return statement before the last expression in the body.

If for or while is the last expression a return nothing node is inserted after the loop instead of return for ....

if, try, let, and begin are currently are left as they are. Perhaps in the future Runic will recurse down into branches, or insert return if but for now this is left alone.

Another exception is also made whenever the last expression is a call to throw or error because it is pretty clear that the function will not return in this case (return throw(...) look silly).

Closes #43.

@vchuravy
Copy link

If for or while is the last expression a return nothing node is inserted after the loop instead of return for ....

Currently broken for @inbounds for ... or @testset "" for

For inserting into macro's like @kernel from KernelAbstractions. Those functions are semantically not allowed to have return statements (yes, I know weird) but they are SPMD programs and one of the assumptions is that instances don't early return.

@fredrikekre
Copy link
Owner Author

Currently broken for @inbounds for ... or @testset "" for

Not broken but simply that the last expression is the macrocall here. It does't seem valid to peek inside the macro, but perhaps it can be done for macros that have one argument and the argument is a code block or something? Alternatively, if the last expression is a macrocall it could just be left as is...

For inserting into macro's like @kernel from KernelAbstractions. Those functions are semantically not allowed to have return statements (yes, I know weird) but they are SPMD programs and one of the assumptions is that instances don't early return.

Yea, maybe this (and other existing transformations?) need to be disabled inside macros.

@fredrikekre
Copy link
Owner Author

Perhaps it can be done for macros that have one argument and the argument is a code block or something

This would rule out @testset "" for though. It simply isn't possible for Runic to know what comes out of a macro :/

@fredrikekre fredrikekre force-pushed the fe/return branch 2 times, most recently from f859303 to ff383c0 Compare August 28, 2024 09:08
README.md Outdated Show resolved Hide resolved
@vchuravy
Copy link

I think I like normalizing to return instead of return nothing.

@fredrikekre
Copy link
Owner Author

fredrikekre commented Aug 28, 2024

I think I like normalizing to return instead of return nothing.

Right now it will leave return nothing as is but insert return nothing. @KristofferC made a comment that there are two somewhat distinct cases:

  1. You want to explicitly indicate you are returning nothing (e.g. findfirst-like functions) and the caller is expected to check === nothing
  2. You just want the function to terminate

I think it would be slightly annoying if Runic removed the nothing for 1, and likewise that it added nothing for case 2. I think leaving whats in the source is perhaps the right choice.

Is this meant to be return? Instead of return nothing?

Sure, maybe that is better.

@vchuravy
Copy link

Yeah, I just noticed that Runic wasn't consistent in what it inserted. I do agree with not removing explicit "return nothing".

@fredrikekre
Copy link
Owner Author

Yeah, I just noticed that Runic wasn't consistent in what it inserted.

Hmm, it should be? If you are thinking about the diff in JuliaGPU/KernelAbstractions.jl#516 the cases where there is just a return added where manual insertions (to avoid e.g. return @testset).

@fredrikekre
Copy link
Owner Author

I also noticed in JuliaGPU/KernelAbstractions.jl#516 and JuliaDocs/Documenter.jl#2564 that whenever the function ends with and if it almost always make more sense to add return inside the branches. Thoughts? The question is then if you should also add a else return nothing branch in the cases where no else branch exist? If you reallly want the return if then Runic will leave it as such of course.

@fredrikekre fredrikekre force-pushed the fe/return branch 2 times, most recently from 222fc20 to 5d9634e Compare August 28, 2024 16:26
@jariji
Copy link

jariji commented Aug 28, 2024

Fwiw my preference would be

  • A function with only one exit point doesn't need return.
  • If one branch returns a value, all branches should return a value.
  • A function with multiple exit points in which no branch returns a value just uses return, not return nothing.

@fredrikekre
Copy link
Owner Author

A function in which at least one branch returns a value, all branches should return a value explicitly, including return nothing.

Would you then also insert else branches if there is none? E.g.

if cond
    x
end

should become

if cond
    return x
else
    return
end

?

A function in which no branch returns a value just uses return, not return nothing.

If I understand you correctly then this isn't a valid transformation because the value of the complete if block isn't nothing just because there are no return inside.

@jariji
Copy link

jariji commented Aug 28, 2024

Are you asking about this function?

function foo1(cond)
    if cond
        1
    end
end

This code has a hidden nothing branch and I don't like that, so I would write it as

function foo1(cond)
    if cond
        1
    else
        nothing
    end
end

@fredrikekre
Copy link
Owner Author

Yes that's what I meant, and I agree that adding the else branch makes the nothing return value more obvious.

@jariji
Copy link

jariji commented Aug 29, 2024

Just to show what I mean, here's some more code.

A function with only one (1) exit point doesn't need return.

function f(x)
  y = x + 1
  x + y
end

A function in which at least one (>=1) exit point returns a value, all exit points should return a value explicitly, including return nothing.

function f(xs)
  if isempty(xs)
    # Explicitly return `nothing` here.
    return nothing
  end
  println("hello") # side effect in branch
  return Some(first(xs))
end

A function in which no branch (0) returns a value just uses return, not return nothing.

function f(xs)
    while true
        if isempty(xs)
            return
        end
        pop!(xs)        
    end    
end

@KristofferC
Copy link
Contributor

KristofferC commented Aug 29, 2024

A function with only one (1) exit point doesn't need return.

I disagree with this. If for example the last statement is a function call it is unclear if it's value is indented to be returned or if it just happens to be the last statement.

@jariji
Copy link

jariji commented Aug 29, 2024

A function's return value is an important part of its contract, so I don't think it should be left unclear. However, leaving the bare expression at the end doesn't prevent accidentally returning values, so adding return wouldn't help me - it's just adding verbosity.

When I don't want to return the last value, I will add nothing at the end to ensure no (possibly aliased) values are accidentally returned.

When I do want to return a value at the end, I just leave it. It's concise and effective.

@tecosaur
Copy link

tecosaur commented Sep 4, 2024

Just to add another perspective into the mix, I'm rather attached to expression-oriented over statement-oriented programming.

In my codebases, in line with this, I only use return for early returns. In the rare case that I don't want to return anything (usually I like my functions to do work, and give the result), I explicitly leave a nothing at the end of the function expression.

Thoughts on the `if ... end` example

I much prefer having

function preferred(args...)
    # some work...
    if cond value end # I know people have thoughts on inline if statements too, but I like them when they're short
end

over

function disliked(args...)
    # some work...
    if cond
        return value
    else
        return nothing
    end
end

I find arguments[1] around accidental[2] return values somewhat convincing, but would find it a pity if this resulted in autoformatting[3] that structured Julia code more like a statement-oriented language despite Julia being largely expression-oriented by design.

[1]: Some earlier discussion on the topic, https://groups.google.com/g/julia-users/c/4RVR8qQDrUg
[2]: I've seen this described as an "implicit return", but in a (mostly) expression-based language this is the wrong mental model I'd argue.
[3]: Personally, I'd actually like it if there was a rule that removed redundant final returns.

This patch make sure that function and macro definitions, as well as
do-blocks, end with an explicit `return` statement before the last
expression in the body. The following exceptions are made:

 - If the last expression is a `for` or `while` loop (which both always
   evaluate to `nothing`) `return` is added *after* the loop.
 - If the last expression is a `if` or `try` block the `return` is only
   added in case there is no `return` inside any of the branches.
 - If the last expression is a `let` or `begin` block the `return` is
   only added in case there is no `return` inside the block.
 - If the last expression is a macro call the `return` is only added in
   case there is no `return` inside the macro.
 - If the last expression is a function call, and the function name is
   `throw`, `rethrow`, or `error`, no `return` is added. This is because
   it is pretty obvious that these calls terminate the function without
   the explicit `return`.

Since adding `return` changes the expression that a macro will see, this
rule is disabled for function definitions inside of macros with the
exception of some known ones from Base (e.g. `@inline`, `@generated`,
...).

Closes #43.
@fredrikekre fredrikekre merged commit cca294f into master Oct 18, 2024
8 of 10 checks passed
@fredrikekre fredrikekre deleted the fe/return branch October 18, 2024 07:15
tecosaur added a commit to tecosaur/Runic.jl that referenced this pull request Dec 18, 2024
@tecosaur
Copy link

Just FYI, for my own usage I'll be using a fork of Runic with this rule disabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Explicit return in function definitions
5 participants