Skip to content

Commit a7eb0dc

Browse files
committed
Insert explicit return expressions in functions/macros/do-blocks
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.
1 parent 500ba09 commit a7eb0dc

File tree

9 files changed

+435
-22
lines changed

9 files changed

+435
-22
lines changed

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ This is a list of things that Runic currently is doing:
244244
- [Line width limit](#line-width-limit)
245245
- [Newlines in blocks](#newlines-in-blocks)
246246
- [Indentation](#indentation)
247+
- [Explicit `return`](#explicit-return)
247248
- [Spaces around operators, assignment, etc](#spaces-around-operators-assignment-etc)
248249
- [Spaces around keywords](#spaces-around-keywords)
249250
- [Multiline listlike expressions](#multiline-listlike-expressions)
@@ -399,6 +400,65 @@ x = a + b *
399400
d
400401
```
401402
403+
### Explicit `return`
404+
405+
Explicit `return` statements are ensured in function/macro definitions as well as in
406+
`do`-blocks by adding `return` in front of the last expression, with some exceptions listed
407+
below.
408+
409+
- If the last expression is a `for` or `while` loop (which both always evaluate to
410+
`nothing`) `return` is added *after* the loop.
411+
- If the last expression is a `if` or `try` block the `return` is only added in case
412+
there is no `return` inside any of the branches.
413+
- If the last expression is a `let` or `begin` block the `return` is only added in case
414+
there is no `return` inside the block.
415+
- If the last expression is a macro call, the `return` is only added in case there is no
416+
`return` inside the macro.
417+
- If the last expression is a function call, and the function name is (or contains) `throw`
418+
or `error`, no `return` is added. This is because it is already obvious that these calls
419+
terminate the function and don't return any value.
420+
421+
Note that adding `return` changes the expression in a way that is visible to macros.
422+
Therefore it is, in general, not valid to add `return` to a function defined inside a macro
423+
since it isn't possible to know what the macro will expand to. For this reason this
424+
formatting rule is disabled for functions defined inside macros with the exception of some
425+
known and safe ones from Base (e.g. `@inline`, `@generated`, ...).
426+
427+
For the same reason mentioned above, if the last expression in a function is a macro call it
428+
isn't valid to step in and add `return` inside. Instead the `return` will be added in front
429+
of the macro call like any other expression (unless there is already a `return` inside of
430+
the macro as described above).
431+
432+
Examples:
433+
```diff
434+
function f(n)
435+
- sum(rand(n))
436+
+ return sum(rand(n))
437+
end
438+
439+
macro m(args...)
440+
- :(generate_expr(args...))
441+
+ return :(generate_expr(args...))
442+
end
443+
444+
function g()
445+
- open("/dev/random", "r") do f
446+
- read(f, 8)
447+
+ return open("/dev/random", "r") do f
448+
+ return read(f, 8)
449+
end
450+
end
451+
```
452+
453+
#### Potential changes
454+
- If the last expression is a `if` or `try` block it might be better to
455+
recurse into the branches and add `return` there. Looking at real code, if a
456+
function ends with an `if` block, it seems about 50/50 whether adding return
457+
*after* the block or adding return inside the branches is the best choice.
458+
Quite often `return if` is not the best but at least Runic's current
459+
formatting will force to think about the return value.
460+
See issue [#52](https://github.com/fredrikekre/Runic.jl/issues/52).
461+
402462
### Spaces around operators, assignment, etc
403463
404464
Runic formats spaces around infix operators, assignments, comparison chains, and type

src/JuliaSyntax.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ function _show_green_node(io, node, indent, pos, str, show_trivia)
4848
p += x.span
4949
end
5050
end
51+
return
5152
end

src/Runic.jl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function Base.show(io::IO, ::MIME"text/plain", node::Node)
6060
show(io, node)
6161
println(io)
6262
_show_green_node(io, node, "", 1, nothing, true)
63+
return
6364
end
6465

6566
function Base.show(io::IO, node::Node)
@@ -136,6 +137,7 @@ mutable struct Context
136137
next_sibling::Union{Node, Nothing}
137138
# parent::Union{Node, Nothing}
138139
lineage_kinds::Vector{JuliaSyntax.Kind}
140+
lineage_macros::Vector{String}
139141
end
140142

141143
function Context(
@@ -166,11 +168,12 @@ function Context(
166168
call_depth = 0
167169
prev_sibling = next_sibling = nothing
168170
lineage_kinds = JuliaSyntax.Kind[]
171+
lineage_macros = String[]
169172
format_on = true
170173
return Context(
171174
src_str, src_tree, src_io, fmt_io, fmt_tree, quiet, verbose, assert, debug, check,
172175
diff, filemode, indent_level, call_depth, format_on, prev_sibling, next_sibling,
173-
lineage_kinds
176+
lineage_kinds, lineage_macros
174177
)
175178
end
176179

@@ -305,6 +308,9 @@ function format_node_with_kids!(ctx::Context, node::Node)
305308
ctx.prev_sibling = nothing
306309
ctx.next_sibling = nothing
307310
push!(ctx.lineage_kinds, kind(node))
311+
if kind(node) === K"macrocall"
312+
push!(ctx.lineage_macros, macrocall_name(ctx, node))
313+
end
308314

309315
# The new node parts. `kids′` aliases `kids` and only copied below if any of the
310316
# nodes change ("copy-on-write").
@@ -380,6 +386,9 @@ function format_node_with_kids!(ctx::Context, node::Node)
380386
ctx.prev_sibling = prev_sibling
381387
ctx.next_sibling = next_sibling
382388
pop!(ctx.lineage_kinds)
389+
if kind(node) === K"macrocall"
390+
pop!(ctx.lineage_macros)
391+
end
383392
ctx.call_depth -= 1
384393
# Return a new node if any of the kids changed
385394
if any_kid_changed
@@ -435,6 +444,7 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
435444
@return_something no_spaces_around_colon_etc(ctx, node)
436445
@return_something parens_around_op_calls_in_colon(ctx, node)
437446
@return_something for_loop_use_in(ctx, node)
447+
@return_something explicit_return(ctx, node)
438448
@return_something braces_around_where_rhs(ctx, node)
439449
@return_something indent_multiline_strings(ctx, node)
440450
@return_something four_space_indent(ctx, node)

src/ToggleableAsserts.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ assert_enabled() = true
3030

3131
macro assert(expr)
3232
code = macroexpand_assert(expr)
33-
:(assert_enabled() ? $(code) : nothing)
33+
return :(assert_enabled() ? $(code) : nothing)
3434
end
3535

3636
const toggle_lock = ReentrantLock()
@@ -41,4 +41,5 @@ function enable_assert(enable::Bool)
4141
@eval Runic assert_enabled() = $enable
4242
end
4343
end
44+
return
4445
end

src/chisels.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,57 @@ function kmatch(kids, kinds, i = firstindex(kids))
779779
return true
780780
end
781781

782+
# Extract the macro name as written in the source.
783+
function macrocall_name(ctx, node)
784+
@assert kind(node) === K"macrocall"
785+
kids = verified_kids(node)
786+
pred = x -> kind(x) in KSet"MacroName StringMacroName CmdMacroName core_@cmd"
787+
mkind = kind(first_leaf_predicate(node, pred)::Node)
788+
if kmatch(kids, KSet"@ MacroName")
789+
p = position(ctx.fmt_io)
790+
bytes = read(ctx.fmt_io, span(kids[1]) + span(kids[2]))
791+
seek(ctx.fmt_io, p)
792+
return String(bytes)
793+
elseif kmatch(kids, KSet".") || kmatch(kids, KSet"CmdMacroName") ||
794+
kmatch(kids, KSet"StringMacroName")
795+
bytes = read_bytes(ctx, kids[1])
796+
if mkind === K"CmdMacroName"
797+
append!(bytes, "_cmd")
798+
elseif mkind === K"StringMacroName"
799+
append!(bytes, "_str")
800+
end
801+
return String(bytes)
802+
elseif kmatch(kids, KSet"core_@cmd")
803+
bytes = read_bytes(ctx, kids[1])
804+
@assert length(bytes) == 0
805+
return "core_@cmd"
806+
else
807+
# Don't bother looking in more complex expressions, just return unknown
808+
return "<unknown macro>"
809+
end
810+
end
811+
812+
# Inserting `return` modifies the AST in a way that is visible to macros.. In general it is
813+
# never safe to change the AST inside a macro, but we make an exception for some common
814+
# "known" macros in order to be able to format functions that e.g. have an `@inline`
815+
# annotation in front.
816+
const MACROS_SAFE_TO_INSERT_RETURN = let set = Set{String}()
817+
for m in (
818+
"inline", "noinline", "propagate_inbounds", "generated", "eval",
819+
"assume_effects", "doc",
820+
)
821+
push!(set, "@$m", "Base.@$m")
822+
end
823+
push!(set, "Core.@doc")
824+
set
825+
end
826+
function safe_to_insert_return(ctx, node)
827+
for m in ctx.lineage_macros
828+
m in MACROS_SAFE_TO_INSERT_RETURN || return false
829+
end
830+
return true
831+
end
832+
782833
##########################
783834
# Utilities for IOBuffer #
784835
##########################

src/debug.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function Base.showerror(io::IO, err::AssertionError)
2020
"please file an issue with a reproducible example at " *
2121
"https://github.com/fredrikekre/Runic.jl/issues/new."
2222
)
23+
return
2324
end
2425

2526
function macroexpand_assert(expr)

src/main.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ function main(argv)
424424
cmd = setenv(ignorestatus(cmd); dir = dir)
425425
cmd = pipeline(cmd, stdout = stderr, stderr = stderr)
426426
run_cmd(cmd)
427+
return
427428
end
428429
end
429430

0 commit comments

Comments
 (0)