Skip to content

Commit 15928df

Browse files
committed
Implement LoopStateMachine
Utilities for returning state from functions that run inside a loop. This is used in e.g clipping, where we may need to break or transition states. The main entry point is to return an [`Action`](@ref) from a function that is wrapped in a `@controlflow f(...)` macro in a loop. When a known `Action` (currently, `Continue`, `Break`, `Return`, or `FullReturn`) is returned, it is processed by the `@controlflow` macro, which allows the function to break out of the loop early, continue to the next iteration, or return a value, without being syntactically inside the loop.
1 parent c42fcad commit 15928df

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
LoopStateMachine
3+
4+
Utilities for returning state from functions that run inside a loop.
5+
6+
This is used in e.g clipping, where we may need to break or transition states.
7+
8+
The main entry point is to return an [`Action`](@ref) from a function that
9+
is wrapped in a `@controlflow f(...)` macro in a loop. When a known `Action`
10+
(currently, `Continue`, `Break`, `Return`, or `FullReturn`) is returned, it is processed
11+
by the `@controlflow` macro, which allows the function to break out of the loop
12+
early, continue to the next iteration, or return a value, without being
13+
syntactically inside the loop.
14+
"""
15+
module LoopStateMachine
16+
17+
struct Action{name, T}
18+
x::T
19+
end
20+
21+
Action{name}() where name = Action{name, Nothing}(nothing)
22+
Action{name}(x::T) where name where T = new{name, T}(x)
23+
Action(x::T) where T = Action{:unnamed, T}(x)
24+
25+
Action{name, Nothing}() where name = Action{name, Nothing}(nothing)
26+
27+
function Base.show(io::IO, action::Action{name, T}) where {name, T}
28+
print(io, "Action ", name)
29+
if isnothing(action.x)
30+
print(io, "()")
31+
else
32+
print(io, "(", action.x, ")")
33+
end
34+
end
35+
36+
37+
# Some common actions
38+
"""
39+
Break()
40+
41+
Break out of the loop.
42+
"""
43+
const Break = Action{:Break, Nothing}
44+
45+
"""
46+
Continue()
47+
48+
Continue to the next iteration of the loop.
49+
"""
50+
const Continue = Action{:Continue, Nothing}
51+
52+
"""
53+
Return(x)
54+
55+
Cause the function executing the loop to return. Use with great caution!
56+
"""
57+
const Return = Action{:Return}
58+
59+
"""
60+
FullReturn(x)
61+
62+
Cause the function executing the loop to return `FullReturn(x)`.
63+
64+
This allows you to break completely out of a recursive function.
65+
"""
66+
const FullReturn = Action{:FullReturn}
67+
68+
"""
69+
@controlflow f(...)
70+
71+
Process the result of `f(...)` and return the result if it's not a [`Continue`](@ref), [`Break`](@ref), or [`Return`](@ref) [`Action`](@ref).
72+
73+
- `Continue`: continue to the next iteration of the loop.
74+
- `Break`: break out of the loop.
75+
- `Return`: cause the function executing the loop to return with the wrapped value.
76+
- `FullReturn`: cause the function executing the loop to return `FullReturn(x)`.
77+
78+
!!! warning
79+
Only use this inside a loop, otherwise you'll get a syntax error!
80+
"""
81+
macro controlflow(expr)
82+
varname = gensym("lsm-f-ret")
83+
return quote
84+
$varname = $(esc(expr))
85+
if $varname isa Continue
86+
continue
87+
elseif $varname isa Break
88+
break
89+
elseif $varname isa Return
90+
return $varname.x
91+
elseif $varname isa FullReturn
92+
return $varname
93+
else
94+
$varname
95+
end
96+
end
97+
end
98+
99+
# You can define more actions as you desire.
100+
101+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ using SafeTestsets
55
include("helpers.jl")
66

77
@safetestset "Primitives" begin include("primitives.jl") end
8+
@safetestset "LoopStateMachine" begin include("utils/LoopStateMachine.jl") end
89
# Methods
910
@safetestset "Angles" begin include("methods/angles.jl") end
1011
@safetestset "Area" begin include("methods/area.jl") end

test/utils/LoopStateMachine.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using Test
2+
using GeometryOps.LoopStateMachine: @controlflow, Continue, Break
3+
4+
@testset "Continue action" begin
5+
count = 0
6+
f(i) = begin
7+
count += 1
8+
if i == 3
9+
return Continue()
10+
end
11+
count += 1
12+
end
13+
for i in 1:5
14+
@controlflow f(i)
15+
end
16+
@test count == 9 # Adds 1 for each iteration, but skips second +1 on i=3
17+
end
18+
19+
@testset "Break action" begin
20+
count = 0
21+
function f(i)
22+
count += 1
23+
if i == 3
24+
return Break()
25+
end
26+
count += 1
27+
end
28+
for i in 1:5
29+
@controlflow f(i)
30+
end
31+
@test count == 5 # Counts up to i=3, adding 2 for i=1,2 and 1 for i=3
32+
end
33+
34+
@testset "Return value" begin
35+
results = Int[]
36+
for i in 1:3
37+
val = @controlflow begin
38+
i * 2
39+
end
40+
push!(results, val)
41+
end
42+
@test results == [2, 4, 6]
43+
end
44+

0 commit comments

Comments
 (0)