Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI
on:
push:
branches: [master]
tags: ["*"]
pull_request:
jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
version:
- '1.0' # Lowest supported.
- '1.6' # Old LTS version.
- '1.10' # LTS version.
- '1' # Expands to latest 1.x.
- 'nightly'
os:
- ubuntu-latest
- macos-latest
- windows-latest
arch:
- x64
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v2
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v5
with:
file: lcov.info
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*.jl.mem
*~
*.jld
Manifest.toml
18 changes: 0 additions & 18 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Multibreak"
uuid = "2db7fbaf-c681-4fb2-9fc5-9b884bdd742d"
authors = ["Gunnar Farnebäck <gunnar@lysator.liu.se>"]
version = "0.2.0"
version = "1.0.0"

[compat]
julia = "1"
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ enclosing loop.
The tests are the documentation. The [tutorial](test/tutorial.jl) explores the
functionality provided by the `@multibreak` macro.

## Background
## History

The `@multibreak` macro was first implemented as a
[gist](https://gist.github.com/GunnarFarneback/c970c9e63a33720bb71d0023f2c8a10f),
Expand All @@ -43,3 +43,31 @@ issue. The proposed syntax differs by using comma instead of semicolon
between `break`/`continue`. The former is a syntax error in Julia 1.x,
whereas the latter is syntactically valid but semantically useless,
making it ideal for a macro implementation.

After the addition of [labeled
break](https://github.com/JuliaLang/julia/pull/60481), the
`@multibreak` macro was rewritten to use such expressions when available.

## Labeled break

From Julia 1.14 it will be possible to break out of multiple loops
without using a package, through labeled break/continue, using this
syntax:

```
@label outer for i = 1:5
if i % 3 > 0
for j = 1:5
@show i, j
if (i + j^2) % 7 == 0
break outer
end
end
end
end
```

Whether to use this package or the labeled break syntax is entirely a
question of taste. After macro expansions the code will be the same.
However, labeled break cannot be used if you intend to support any
Julia version prior to 1.14.
128 changes: 97 additions & 31 deletions src/Multibreak.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ for the full documentation.
module Multibreak
export @multibreak

function multibreak_transform_break_and_continue(args, break_labels,
continue_labels)
# This may be changed to just v"1.14" after it has been released.
const use_labeled_break = VERSION >= v"1.14.0-DEV.1613"

function multibreak_transform_break_and_continue(args, break_labels)
out = Any[]
nbreak = 0
ncontinue = 0
Expand All @@ -30,60 +32,117 @@ function multibreak_transform_break_and_continue(args, break_labels,
if ncontinue > 0
push!(out, :(error("multibreak: continue cannot precede a break")))
end
nbreak += 1
if isempty(arg.args)
nbreak += 1
elseif nbreak > 0
push!(out, :(error("multibreak: labeled break cannot be used in a multibreak combination")))
else
push!(out, arg)
end
elseif Meta.isexpr(arg, :continue)
if ncontinue > 0
push!(out, :(error("multibreak: multiple continue not allowed")))
end
ncontinue += 1
if isempty(arg.args)
ncontinue += 1
elseif nbreak > 0
push!(out, :(error("multibreak: labeled continue cannot be used in a multibreak combination")))
else
push!(out, arg)
end
elseif typeof(arg) == LineNumberNode
push!(out, arg)
else
if nbreak + ncontinue > 0
emit_labeled_break!(out, break_labels, nbreak, ncontinue)
nbreak = 0
ncontinue = 0
end
push!(out, arg)
end
end
if nbreak + ncontinue > 0
emit_labeled_break!(out, break_labels, nbreak, ncontinue)
end
return out
end

function emit_labeled_break!(out, break_labels, nbreak, ncontinue)
n = nbreak + ncontinue
if n > length(break_labels)
break_or_continue = (ncontinue == 0) ? (:break) : (:continue)
if n > length(break_labels)
push!(out, :(error("multibreak: not enough nested loops for requested multiple break/continue")))
elseif n > 0
push!(out, Expr(:symbolicgoto,
ifelse(ncontinue == 0,
break_labels[end - n + 1],
continue_labels[end - n + 1])))
return
elseif n == 1
push!(out, Expr(break_or_continue))
return
elseif n == 0
return
end
return out

if use_labeled_break
push!(out, Expr(break_or_continue, break_labels[end - n + 1]))
else
label = break_labels[end - n + 1]
if ncontinue > 0
label = Symbol(label, "#cont")
end
push!(out, Expr(:symbolicgoto, label))
end
return
end

function multibreak_transform_ast(ast, loop_counter = [1],
break_labels = Symbol[],
continue_labels = Symbol[])
function multibreak_transform_ast(ast, break_labels = Symbol[],
label = nothing)
if typeof(ast) != Expr
return ast
end

if ast.head == :for || ast.head == :while
n = loop_counter[1]
break_labels = push!(copy(break_labels),
gensym(Symbol("loop", n, "break")))
continue_labels = push!(copy(continue_labels),
gensym(Symbol("loop", n, "continue")))
loop_counter[1] += 1
if label === nothing
break_label = gensym(Symbol("break"))
else
break_label = label
end
break_labels = vcat(break_labels, break_label)
end

args = [multibreak_transform_ast(arg, loop_counter, break_labels,
continue_labels) for arg in ast.args]
# If we find a labeled loop, reuse that label instead of adding
# our own label.
recursed_label = nothing
if (ast.head == :macrocall
&& length(ast.args) >= 3
&& ast.args[1] == Symbol("@label")
&& any(Meta.isexpr(arg, (:for, :while)) for arg in ast.args[2:end]))

if ast.head == :for || ast.head == :while
arg2 = Expr(:block,
args[2],
Expr(:symboliclabel, last(continue_labels)))
return Expr(:block,
Expr(ast.head, args[1], arg2),
Expr(:symboliclabel, last(break_labels)))
symbols = filter(x -> x isa Symbol, ast.args[2:end])
if !isempty(symbols)
recursed_label = first(symbols)
end
end

args = [multibreak_transform_ast(arg, break_labels, recursed_label)
for arg in ast.args]

if (ast.head == :for || ast.head == :while) && label === nothing
if use_labeled_break
return Expr(:symbolicblock,
last(break_labels),
Expr(ast.head, args[1],
Expr(:symbolicblock,
Symbol(last(break_labels), "#cont"),
args[2])))
else
arg2 = Expr(:block,
args[2],
Expr(:symboliclabel, Symbol(last(break_labels), "#cont")))
return Expr(:block,
Expr(ast.head, args[1], arg2),
Expr(:symboliclabel, last(break_labels)))
end
elseif ast.head == :block
return Expr(:block,
multibreak_transform_break_and_continue(args, break_labels,
continue_labels)...)
multibreak_transform_break_and_continue(args, break_labels)...)
end

return Expr(ast.head, args...)
Expand All @@ -92,6 +151,13 @@ end
# TODO: This string repeats parts of the module documentation
# string. The common parts should be reused but first find out how to
# do that without running into scoping issues.
#
# The scoping issue is that the module docstring needs to be defined
# outside of the module, which is not allowed. It cannot be defined
# inside the module, since the module has not been evaluated at the
# time the docstring is attached. It's also not possible to use `@doc`
# to attach the module docstring afterwards.
const macro_docstring =
"""
`@multibreak` allows breaking out of, and optionally continuing,
several nested loops at once.
Expand Down
51 changes: 34 additions & 17 deletions test/tutorial.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ end

# This shows how the previous example can be coded with the `goto` and
# `label` macros instead of the `@multibreak` macro. This is in fact
# how the `@multibreak` macro works internally and should make the
# semantics clear.
# how the `@multibreak` macro works internally in Julia 1.0 - 1.13 and
# should make the semantics clear. In Julia 1.14 and later a different
# mechanism is used, which is discussed in tutorial_part2.
#
# To be more precise this also changed with Multibreak 1.0. Single
# break and continue are left as they are instead of being transformed
# into goto, but the effect is the same.
@testset MBTestSet "goto and label" begin
out = []
for i = 1:5
Expand Down Expand Up @@ -256,11 +261,10 @@ end
end
end

# Note, in sufficiently simple cases, nested loops can be written as a
# single for statement with multiple variables. In that case a single
# `break` breaks out of all the loops whereas a single `continue`
# continues the inner loop. If that is enough, there is no need to
# involve the `@multibreak` macro.
# Nested loops can also be written as a single for statement with
# multiple variables. In that case a single `break` breaks out of all
# the loops whereas a single `continue` continues the inner loop. If
# that is enough, there is no need to involve the `@multibreak` macro.
@testset MBTestSet "single loop with multiple variables" begin
n = 0
for i = 1:5, j = 1:3
Expand All @@ -282,7 +286,8 @@ end
end

# Naturally for loops with multiple variables can in turn be nested,
# in which case the `@multibreak` macro can be utilized.
# in which case the `@multibreak` macro can be utilized. Each `for`
# statement is considered as a unit for multibreaking.
@testset MBTestSet "nested loops with multiple variables" begin
n = 0
@multibreak begin
Expand Down Expand Up @@ -356,21 +361,33 @@ end
@test n == 5
end

# A word of warning. The `@multibreak` macro also transforms single
# `break` and `continue` to `@goto` and `@label`. A side effect of the
# implementation is that dead code can come alive. The macro could be
# refined to place the `@goto` at the position of the first
# `break`/`continue` rather than at the end of the block but it's not
# really worth the added complexity. Just don't do this in code where
# you need the `@multibreak`.
# Earlier versions of Multibreak (before 1.0) used to place the jumps
# for both single and multiple breaks at the end of the block they
# were defined in, with the side effect that dead code could come
# alive. This is no longer the case but we keep this test to make sure
# that it does not regress.
#
# (Technically this was a breaking change in Multibreak 1.0, although
# nobody should write code like this.)
@testset MBTestSet "zombie code" begin
I_am_a_zombie = false

@multibreak begin
while true
break
I_am_a_zombie = true
end
end
@test I_am_a_zombie
@test !I_am_a_zombie
end

# Julia 1.14 introduced labeled break/continue, which can also be used
# to break out of multiple loops. We want to test that those interact
# nicely with the multibreak macro. However, that syntax cannot be
# parsed in earlier Julia versions, so we need to split those
# tests/examples off to a new file.
#
# Please continue the tutorial in test/tutorial_part2.jl.
@static if VERSION >= v"1.14.0-DEV.1651"
Base.Experimental.@set_syntax_version v"1.14"
include("tutorial_part2.jl")
end
Loading
Loading