Skip to content

Commit 0b7f721

Browse files
committed
Add LoopStateMachine and tests
1 parent d8b6a4b commit 0b7f721

File tree

3 files changed

+158
-1
lines changed

3 files changed

+158
-1
lines changed

src/GeometryOps.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,17 @@ const Edge{T} = Tuple{TuplePoint{T},TuplePoint{T}} where T
3636

3737
include("types.jl")
3838
include("primitives.jl")
39-
include("utils.jl")
4039
include("not_implemented_yet.jl")
4140

41+
include("utils/utils.jl")
42+
43+
include("utils/LoopStateMachine/LoopStateMachine.jl")
44+
using .LoopStateMachine
45+
46+
include("utils/SpatialTreeInterface/SpatialTreeInterface.jl")
47+
using .SpatialTreeInterface
48+
49+
4250
include("methods/angles.jl")
4351
include("methods/area.jl")
4452
include("methods/barycentric.jl")
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 `:full_return` actions) is returned,
11+
it is processed 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, basically a way to provoke syntactic
13+
behaviour from a function called from a inside a loop, where you do not have access to that loop.
14+
15+
## Example
16+
17+
```julia
18+
```
19+
"""
20+
module LoopStateMachine
21+
22+
export Action, @controlflow
23+
24+
import ..GeometryOps as GO
25+
26+
const ALL_ACTION_DESCRIPTIONS = """
27+
- `:continue`: continue to the next iteration of the loop.
28+
This is the `continue` keyword in Julia. The contents of the action are not used.
29+
- `:break`: break out of the loop.
30+
This is the `break` keyword in Julia. The contents of the action are not used.
31+
- `:return`: cause the function executing the loop to return with the wrapped value.
32+
- `:full_return`: cause the function executing the loop to return `Action(:full_return, x)`.
33+
This is very useful to terminate recursive funtions, like tree queries terminating after you
34+
have found a single intersecting segment.
35+
"""
36+
37+
"""
38+
Action(name::Symbol, [x])
39+
40+
Create an `Action` with the name `name` and optional contents `x`.
41+
42+
`Action`s are returned from functions wrapped in a `@controlflow` macro, which
43+
does something based on the return value of that function if it is an `Action`.
44+
45+
## Available actions
46+
47+
$ALL_ACTION_DESCRIPTIONS
48+
"""
49+
struct Action{T}
50+
name::Symbol
51+
x::T
52+
end
53+
54+
Action() = Action{Nothing}(:unnamed, nothing)
55+
Action(x::T) where T = Action{:unnamed, T}(:unnamed, x)
56+
Action(x::Symbol) = Action(x, nothing)
57+
58+
function Base.show(io::IO, action::Action{T}) where T
59+
print(io, "Action ", action.name)
60+
if isnothing(action.x)
61+
print(io, "()")
62+
else
63+
print(io, "(", action.x, ")")
64+
end
65+
end
66+
67+
"""
68+
@controlflow f(...)
69+
70+
Process the result of `f(...)` and return the result if it's not an `Action`(@ref LoopStateMachine.Action).
71+
72+
If it is an `Action`, then process it according to the following rules, and throw an error if it's not recognized.
73+
`:continue`, `:break`, `:return`, or `:full_return` are valid actions.
74+
75+
$ALL_ACTION_DESCRIPTIONS
76+
77+
!!! warning
78+
Only use this inside a loop, otherwise you'll get a syntax error, especially if you use `:continue` or `:break`.
79+
80+
## Examples
81+
"""
82+
macro controlflow(expr)
83+
varname = gensym("loop-state-machine-returned-value")
84+
return quote
85+
$varname = $(esc(expr))
86+
if $varname isa Action
87+
if $varname.name == :continue
88+
continue
89+
elseif $varname.name == :break
90+
break
91+
elseif $varname.name == :return
92+
return $varname.x
93+
elseif $varname.name == :full_return
94+
return $varname
95+
else
96+
throw(ArgumentError("Unknown action: $($varname.name)"))
97+
end
98+
else
99+
$varname
100+
end
101+
end
102+
end
103+
104+
# You can define more actions as you desire.
105+
106+
end

test/utils/LoopStateMachine.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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

0 commit comments

Comments
 (0)