Skip to content

Commit

Permalink
Insert explicit return expressions in functions/macros/do-blocks
Browse files Browse the repository at this point in the history
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 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.
  • Loading branch information
fredrikekre committed Aug 28, 2024
1 parent 21808b0 commit 5d9634e
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 23 deletions.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ This is a list of things that Runic currently is doing:
- [Line width limit](#line-width-limit)
- [Newlines in blocks](#newlines-in-blocks)
- [Indentation](#indentation)
- [Explicit `return`](#explicit-return)
- [Spaces around operators, assignment, etc](#spaces-around-operators-assignment-etc)
- [Spaces around keywords](#spaces-around-keywords)
- [Multiline listlike expressions](#multiline-listlike-expressions)
Expand Down Expand Up @@ -399,6 +400,62 @@ x = a + b *
d
```
### Explicit `return`
Explicit `return` statements are ensured in function/macro definitions as well as in
`do`-blocks by adding `return` in front of the last expression, with some exceptions listed
below.
- If the last expression is a `for` or `while` loop (which both always evaluate to
`nothing`) `return nothing` 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 function call, and the function name is `throw`, `rethrow`,
or `error`, no `return` is added. This is because it is already obvious that these calls
terminate the function and don't return any value.
Note that adding `return` changes the expression in a way that is visible to macros.
Therefore it is, in general, not valid to add `return` to a function defined inside a macro
since it isn't possible to know what the macro will expand to. For this reason this
formatting rule is disabled for functions defined inside macros with the exception of some
known and safe ones from Base (e.g. `@inline`, `@generated`, ...).
For the same reason mentioned above, if the last expression in a function is a macro call it
isn't valid to step in and add `return` inside. Instead the `return` will be added in front
of the macro call like any other expression.
Examples:
```diff
function f(n)
- sum(rand(n))
+ return sum(rand(n))
end
macro m(args...)
- :(generate_expr(args...))
+ return :(generate_expr(args...))
end
function g()
- open("/dev/random", "r") do f
- read(f, 8)
+ return open("/dev/random", "r") do f
+ return read(f, 8)
end
end
```
#### Potential changes
- If the last expression is a `if` or `try` block it might be better to
recurse into the branches and add `return` there. Looking at real code, if a
function ends with an `if` block, it seems about 50/50 whether adding return
*after* the block or adding return inside the branches is the best choice.
Quite often `return if` is not the best but at least Runic's current
formatting will force to think about the return value.
See issue [#52](https://github.com/fredrikekre/Runic.jl/issues/52).
### Spaces around operators, assignment, etc
Runic formats spaces around infix operators, assignments, comparison chains, and type
Expand Down
1 change: 1 addition & 0 deletions src/JuliaSyntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ function _show_green_node(io, node, indent, pos, str, show_trivia)
p += x.span
end
end
return
end
12 changes: 11 additions & 1 deletion src/Runic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function Base.show(io::IO, ::MIME"text/plain", node::Node)
show(io, node)
println(io)
_show_green_node(io, node, "", 1, nothing, true)
return nothing
end

function Base.show(io::IO, node::Node)
Expand Down Expand Up @@ -137,6 +138,7 @@ mutable struct Context
next_sibling::Union{Node, Nothing}
# parent::Union{Node, Nothing}
lineage_kinds::Vector{JuliaSyntax.Kind}
lineage_macros::Vector{String}
end

function Context(
Expand Down Expand Up @@ -167,11 +169,12 @@ function Context(
call_depth = 0
prev_sibling = next_sibling = nothing
lineage_kinds = JuliaSyntax.Kind[]
lineage_macros = String[]
format_on = true
return Context(
src_str, src_tree, src_io, fmt_io, fmt_tree, quiet, verbose, assert, debug, check,
diff, filemode, indent_level, call_depth, format_on, prev_sibling, next_sibling,
lineage_kinds
lineage_kinds, lineage_macros
)
end

Expand Down Expand Up @@ -306,6 +309,9 @@ function format_node_with_kids!(ctx::Context, node::Node)
ctx.prev_sibling = nothing
ctx.next_sibling = nothing
push!(ctx.lineage_kinds, kind(node))
if kind(node) === K"macrocall"
push!(ctx.lineage_macros, macrocall_name(ctx, node))
end

# The new node parts. `kids′` aliases `kids` and only copied below if any of the
# nodes change ("copy-on-write").
Expand Down Expand Up @@ -381,6 +387,9 @@ function format_node_with_kids!(ctx::Context, node::Node)
ctx.prev_sibling = prev_sibling
ctx.next_sibling = next_sibling
pop!(ctx.lineage_kinds)
if kind(node) === K"macrocall"
pop!(ctx.lineage_macros)
end
ctx.call_depth -= 1
# Return a new node if any of the kids changed
if any_kid_changed
Expand Down Expand Up @@ -435,6 +444,7 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
@return_something no_spaces_around_colon_etc(ctx, node)
@return_something parens_around_op_calls_in_colon(ctx, node)
@return_something for_loop_use_in(ctx, node)
@return_something explicit_return(ctx, node)
@return_something braces_around_where_rhs(ctx, node)
@return_something indent_multiline_strings(ctx, node)
@return_something four_space_indent(ctx, node)
Expand Down
4 changes: 2 additions & 2 deletions src/ToggleableAsserts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ assert_enabled() = true

macro assert(expr)
code = macroexpand_assert(expr)
:(assert_enabled() ? $(code) : nothing)
return :(assert_enabled() ? $(code) : nothing)
end

const toggle_lock = ReentrantLock()

function enable_assert(enable::Bool)
@lock toggle_lock begin
return @lock toggle_lock begin
if assert_enabled() != enable
@eval Runic assert_enabled() = $enable
end
Expand Down
51 changes: 51 additions & 0 deletions src/chisels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,57 @@ function kmatch(kids, kinds, i = firstindex(kids))
return true
end

# Extract the macro name as written in the source.
function macrocall_name(ctx, node)
@assert kind(node) === K"macrocall"
kids = verified_kids(node)
pred = x -> kind(x) in KSet"MacroName StringMacroName CmdMacroName core_@cmd"
mkind = kind(first_leaf_predicate(node, pred)::Node)
if kmatch(kids, KSet"@ MacroName")
p = position(ctx.fmt_io)
bytes = read(ctx.fmt_io, span(kids[1]) + span(kids[2]))
seek(ctx.fmt_io, p)
return String(bytes)
elseif kmatch(kids, KSet".") || kmatch(kids, KSet"CmdMacroName") ||
kmatch(kids, KSet"StringMacroName")
bytes = read_bytes(ctx, kids[1])
if mkind === K"CmdMacroName"
append!(bytes, "_cmd")
elseif mkind === K"StringMacroName"
append!(bytes, "_str")
end
return String(bytes)
elseif kmatch(kids, KSet"core_@cmd")
bytes = read_bytes(ctx, kids[1])
@assert length(bytes) == 0
return "core_@cmd"
else
# Don't bother looking in more complex expressions, just return unknown
return "<unknown macro>"
end
end

# Inserting `return` modifies the AST in a way that is visible to macros.. In general it is
# never safe to change the AST inside a macro, but we make an exception for some common
# "known" macros in order to be able to format functions that e.g. have an `@inline`
# annotation in front.
const MACROS_SAFE_TO_INSERT_RETURN = let set = Set{String}()
for m in (
"inline", "noinline", "propagate_inbounds", "generated", "eval",
"assume_effects", "doc",
)
push!(set, "@$m", "Base.@$m")
end
push!(set, "Core.@doc")
set
end
function safe_to_insert_return(ctx, node)
for m in ctx.lineage_macros
m in MACROS_SAFE_TO_INSERT_RETURN || return false
end
return true
end

##########################
# Utilities for IOBuffer #
##########################
Expand Down
1 change: 1 addition & 0 deletions src/debug.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function Base.showerror(io::IO, err::AssertionError)
"please file an issue with a reproducible example at " *
"https://github.com/fredrikekre/Runic.jl/issues/new."
)
return nothing
end

function macroexpand_assert(expr)
Expand Down
2 changes: 2 additions & 0 deletions src/main.jl
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function maybe_expand_directory!(outfiles, dir)
end
end
end
return nothing
end

function main(argv)
Expand Down Expand Up @@ -305,6 +306,7 @@ function main(argv)
cmd = setenv(ignorestatus(cmd); dir = dir)
cmd = pipeline(cmd, stdout = stderr, stderr = stderr)
run(cmd)
return nothing
end
end

Expand Down
Loading

0 comments on commit 5d9634e

Please sign in to comment.