state_machine_checker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +102 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/state_machine_checker/adapters/state_machines.rb +40 -0
- data/lib/state_machine_checker/check_result.rb +40 -0
- data/lib/state_machine_checker/ctl/a_f.rb +20 -0
- data/lib/state_machine_checker/ctl/a_g.rb +20 -0
- data/lib/state_machine_checker/ctl/a_u.rb +23 -0
- data/lib/state_machine_checker/ctl/a_x.rb +20 -0
- data/lib/state_machine_checker/ctl/and.rb +42 -0
- data/lib/state_machine_checker/ctl/api.rb +61 -0
- data/lib/state_machine_checker/ctl/atom.rb +62 -0
- data/lib/state_machine_checker/ctl/binary_formula.rb +23 -0
- data/lib/state_machine_checker/ctl/e_f.rb +34 -0
- data/lib/state_machine_checker/ctl/e_g.rb +139 -0
- data/lib/state_machine_checker/ctl/e_u.rb +42 -0
- data/lib/state_machine_checker/ctl/e_x.rb +40 -0
- data/lib/state_machine_checker/ctl/formula.rb +42 -0
- data/lib/state_machine_checker/ctl/implication.rb +19 -0
- data/lib/state_machine_checker/ctl/not.rb +36 -0
- data/lib/state_machine_checker/ctl/or.rb +40 -0
- data/lib/state_machine_checker/ctl/unary_operator.rb +22 -0
- data/lib/state_machine_checker/finite_state_machine.rb +120 -0
- data/lib/state_machine_checker/labeled_machine.rb +51 -0
- data/lib/state_machine_checker/labeling.rb +48 -0
- data/lib/state_machine_checker/rspec_matchers.rb +60 -0
- data/lib/state_machine_checker/state_result.rb +57 -0
- data/lib/state_machine_checker/transition.rb +49 -0
- data/lib/state_machine_checker/version.rb +3 -0
- data/lib/state_machine_checker.rb +43 -0
- data/state_machine_checker.gemspec +47 -0
- metadata +186 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require_relative "formula"
|
|
2
|
+
|
|
3
|
+
module StateMachineChecker
|
|
4
|
+
module CTL
|
|
5
|
+
class BinaryFormula < Formula
|
|
6
|
+
def initialize(subformula1, subformula2)
|
|
7
|
+
@subformula1 = subformula1
|
|
8
|
+
@subformula2 = subformula2
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Return an enumerator over the atoms of the sub-formulae.
|
|
12
|
+
#
|
|
13
|
+
# @return [Enumerator<Atom>]
|
|
14
|
+
def atoms
|
|
15
|
+
subformula1.atoms + subformula2.atoms
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :subformula1, :subformula2
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative "unary_operator"
|
|
2
|
+
|
|
3
|
+
module StateMachineChecker
|
|
4
|
+
module CTL
|
|
5
|
+
# The existential eventually operator.
|
|
6
|
+
class EF < UnaryOperator
|
|
7
|
+
# Check which states of the model have as a successor a state
|
|
8
|
+
# satisfying the subformula.
|
|
9
|
+
#
|
|
10
|
+
# @param [LabeledMachine] model
|
|
11
|
+
# @return [CheckResult]
|
|
12
|
+
def check(model)
|
|
13
|
+
subresult = subformula.check(model)
|
|
14
|
+
result = subresult.to_h
|
|
15
|
+
model.states.each do |state|
|
|
16
|
+
sub_state_result = subresult.for_state(state)
|
|
17
|
+
|
|
18
|
+
if sub_state_result.satisfied? # Mark predecessors as satisfied.
|
|
19
|
+
model.traverse(state, reverse: true) do |s, transitions|
|
|
20
|
+
witness = transitions + sub_state_result.witness
|
|
21
|
+
result[s] = StateResult.new(true, witness)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
CheckResult.new(result)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
"EF(#{subformula})"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module StateMachineChecker
|
|
2
|
+
module CTL
|
|
3
|
+
# The existential universal operator.
|
|
4
|
+
class EG < UnaryOperator
|
|
5
|
+
# Check which states of the model have a path for which the subformula is
|
|
6
|
+
# always satisfied
|
|
7
|
+
#
|
|
8
|
+
# @param [LabeledMachine] model
|
|
9
|
+
# @return [CheckResult]
|
|
10
|
+
def check(model)
|
|
11
|
+
subresult = subformula.check(model)
|
|
12
|
+
projection = subformula_projection(model, subresult)
|
|
13
|
+
scc = strongly_connected_components(projection)
|
|
14
|
+
|
|
15
|
+
# Components must have more than one element, a self loop, or no
|
|
16
|
+
# transitions in the original fsm.
|
|
17
|
+
# TODO: this being necessary probably means we're doing something wrong.
|
|
18
|
+
scc.select! do |states|
|
|
19
|
+
states.length > 1 ||
|
|
20
|
+
begin
|
|
21
|
+
c = states.first
|
|
22
|
+
transitions = model.transitions_from(c)
|
|
23
|
+
|
|
24
|
+
transitions.empty? ||
|
|
25
|
+
transitions.any? { |t| t.to == c }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
build_check_result(scc, model, projection)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
"EG(#{subformula})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# A graph containing only states for which the subformula is true.
|
|
39
|
+
def subformula_projection(model, subresult)
|
|
40
|
+
transitions = model.transitions.select { |t|
|
|
41
|
+
subresult.for_state(t.from).satisfied? &&
|
|
42
|
+
subresult.for_state(t.to).satisfied?
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
FiniteStateMachine.new(model.initial_state, transitions)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Implements Kosaraju's algorithm.
|
|
49
|
+
def strongly_connected_components(projection)
|
|
50
|
+
visited = Set.new
|
|
51
|
+
l = []
|
|
52
|
+
|
|
53
|
+
projection.states.each do |s|
|
|
54
|
+
visit(s, visited, l, projection)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
assigned = Set.new
|
|
58
|
+
assignments = {} # root -> set of states in component
|
|
59
|
+
l.reverse_each do |s|
|
|
60
|
+
assign(s, s, assigned, assignments, projection)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
assignments.values
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_check_result(scc, model, projection)
|
|
67
|
+
# Initialize hash with every state unsatisfied.
|
|
68
|
+
result = model.states.each_with_object({}) { |s, h|
|
|
69
|
+
h[s] = StateResult.new(false, [])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
scc.each do |component_states|
|
|
73
|
+
# For each state of the component search backwards.
|
|
74
|
+
component_states.each do |state|
|
|
75
|
+
loop_witness = scc_loop(component_states, state, projection)
|
|
76
|
+
|
|
77
|
+
projection.traverse(state, reverse: true) do |s, transitions|
|
|
78
|
+
# Ignore other states in the component.
|
|
79
|
+
if s == state || !component_states.include?(s)
|
|
80
|
+
result[s] = StateResult.new(true, transitions + loop_witness)
|
|
81
|
+
else
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
CheckResult.new(result)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Find a series of transitions within the component_states which start and
|
|
92
|
+
# end with the given state.
|
|
93
|
+
def scc_loop(component_states, start, model)
|
|
94
|
+
model.traverse(start) do |state, path|
|
|
95
|
+
# Only search within the component states.
|
|
96
|
+
if component_states.include?(state)
|
|
97
|
+
transitions = model.transitions_from(state)
|
|
98
|
+
to_start = transitions.find { |t| t.to == start }
|
|
99
|
+
|
|
100
|
+
if to_start
|
|
101
|
+
return path.push(to_start.name)
|
|
102
|
+
else
|
|
103
|
+
true # continue
|
|
104
|
+
end
|
|
105
|
+
else
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
[]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def visit(s, visited, l, projection)
|
|
114
|
+
unless visited.include?(s)
|
|
115
|
+
visited << s
|
|
116
|
+
projection.transitions_from(s).each do |transition|
|
|
117
|
+
visit(transition.to, visited, l, projection)
|
|
118
|
+
end
|
|
119
|
+
l << s
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def assign(s, root, assigned, assignments, projection)
|
|
124
|
+
unless assigned.include?(s)
|
|
125
|
+
assigned << s
|
|
126
|
+
|
|
127
|
+
if assignments[root].nil?
|
|
128
|
+
assignments[root] = Set.new
|
|
129
|
+
end
|
|
130
|
+
assignments[root] << s
|
|
131
|
+
|
|
132
|
+
projection.transitions_to(s).each do |transition|
|
|
133
|
+
assign(transition.from, root, assigned, assignments, projection)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require_relative "binary_formula"
|
|
2
|
+
|
|
3
|
+
module StateMachineChecker
|
|
4
|
+
module CTL
|
|
5
|
+
# The existential until operator. This is the "strong" until, it is only
|
|
6
|
+
# satisfied if the second sub-formula is eventually satisifed.
|
|
7
|
+
class EU < BinaryFormula
|
|
8
|
+
# Check which states of the model have a path for which the first
|
|
9
|
+
# subformula is satisifed until the second subformula is.
|
|
10
|
+
#
|
|
11
|
+
# @param [LabeledMachine] model
|
|
12
|
+
# @return [CheckResult]
|
|
13
|
+
def check(model)
|
|
14
|
+
subresult1 = subformula1.check(model)
|
|
15
|
+
subresult2 = subformula2.check(model)
|
|
16
|
+
|
|
17
|
+
result = subresult2.to_h # States satisfying sub-formula2 satisfy this.
|
|
18
|
+
|
|
19
|
+
model.states.lazy.select { |s| subresult2.for_state(s).satisfied? }.each do |end_state|
|
|
20
|
+
model.traverse(end_state, reverse: true) do |state, path|
|
|
21
|
+
if state == end_state || subresult1.for_state(state).satisfied?
|
|
22
|
+
# Don't update states that are already satisfied, to keep the
|
|
23
|
+
# simpler witness.
|
|
24
|
+
unless result[state].satisfied?
|
|
25
|
+
result[state] = StateResult.new(true, path)
|
|
26
|
+
end
|
|
27
|
+
true
|
|
28
|
+
else
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
CheckResult.new(result)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
"(#{subformula1}) EU (#{subformula2})"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative "unary_operator"
|
|
2
|
+
require "state_machine_checker/check_result"
|
|
3
|
+
require "state_machine_checker/state_result"
|
|
4
|
+
|
|
5
|
+
module StateMachineChecker
|
|
6
|
+
module CTL
|
|
7
|
+
# The existential next operator.
|
|
8
|
+
class EX < UnaryOperator
|
|
9
|
+
# Check which states of the model have as a direct successor a state
|
|
10
|
+
# satisfying the subformula.
|
|
11
|
+
#
|
|
12
|
+
# @param [LabeledMachine] model
|
|
13
|
+
# @return [CheckResult]
|
|
14
|
+
def check(model)
|
|
15
|
+
# Initialize hash with every state unsatisfied.
|
|
16
|
+
result = model.states.each_with_object({}) { |s, h|
|
|
17
|
+
h[s] = StateResult.new(false, [])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
subresult = subformula.check(model)
|
|
21
|
+
model.states.each do |state|
|
|
22
|
+
sub_state_result = subresult.for_state(state)
|
|
23
|
+
|
|
24
|
+
if sub_state_result.satisfied? # Mark direct predecessors as satisfied.
|
|
25
|
+
model.transitions_to(state).each do |transition|
|
|
26
|
+
witness = [transition.name] + sub_state_result.witness
|
|
27
|
+
result[transition.from] = StateResult.new(true, witness)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
CheckResult.new(result)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
"EX(#{subformula})"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module StateMachineChecker
|
|
2
|
+
module CTL
|
|
3
|
+
# Abstract base class for CTL formulae.
|
|
4
|
+
class Formula
|
|
5
|
+
# The logical conjuction of this formula with others.
|
|
6
|
+
def and(*other_subformulae)
|
|
7
|
+
other_subformulae.map! { |f| atom_or_formula(f) }
|
|
8
|
+
And.new(other_subformulae << self)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# The logical disjunction of this formula with others.
|
|
12
|
+
def or(*other_subformulae)
|
|
13
|
+
other_subformulae.map! { |f| atom_or_formula(f) }
|
|
14
|
+
Or.new(other_subformulae << self)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Logical implication
|
|
18
|
+
def implies(other_subformula)
|
|
19
|
+
Implication.new(self, atom_or_formula(other_subformula))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The existential until operator.
|
|
23
|
+
def EU(end_formula) # rubocop:disable Naming/MethodName
|
|
24
|
+
EU.new(self, atom_or_formula(end_formula))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def AU(end_formula) # rubocop:disable Naming/MethodName
|
|
28
|
+
AU.new(self, atom_or_formula(end_formula))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def atom_or_formula(subformula)
|
|
34
|
+
if subformula.is_a? Formula
|
|
35
|
+
subformula
|
|
36
|
+
else
|
|
37
|
+
Atom.new(subformula)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative "binary_formula"
|
|
2
|
+
require_relative "not"
|
|
3
|
+
|
|
4
|
+
module StateMachineChecker
|
|
5
|
+
module CTL
|
|
6
|
+
# Logical implication.
|
|
7
|
+
class Implication < BinaryFormula
|
|
8
|
+
# @param [LabeledMachine] model
|
|
9
|
+
# @return [CheckResult]
|
|
10
|
+
def check(model)
|
|
11
|
+
subformula1.and(subformula2).or(Not.new(subformula1)).check(model)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"(#{subformula1}) ⇒ (#{subformula2})"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative "unary_operator"
|
|
2
|
+
require "state_machine_checker/check_result"
|
|
3
|
+
require "state_machine_checker/state_result"
|
|
4
|
+
|
|
5
|
+
module StateMachineChecker
|
|
6
|
+
module CTL
|
|
7
|
+
# The logical negation of a formula.
|
|
8
|
+
class Not < UnaryOperator
|
|
9
|
+
# Check whether each state is not satisfied by the subformula.
|
|
10
|
+
#
|
|
11
|
+
# @param [LabeledMachine] model
|
|
12
|
+
# @return [CheckResult]
|
|
13
|
+
def check(model)
|
|
14
|
+
subresult = subformula.check(model)
|
|
15
|
+
|
|
16
|
+
result = model.states.each_with_object({}) { |state, h|
|
|
17
|
+
state_result = subresult.for_state(state)
|
|
18
|
+
|
|
19
|
+
# Negate whether it was satisfied, keep the same path.
|
|
20
|
+
path = if state_result.satisfied?
|
|
21
|
+
state_result.witness
|
|
22
|
+
else
|
|
23
|
+
state_result.counterexample
|
|
24
|
+
end
|
|
25
|
+
h[state] = StateResult.new(!state_result.satisfied?, path)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
CheckResult.new(result)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_s
|
|
32
|
+
"¬(#{subformula})"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative "formula"
|
|
2
|
+
|
|
3
|
+
module StateMachineChecker
|
|
4
|
+
module CTL
|
|
5
|
+
# The logical disjunction of two or more sub-formulae.
|
|
6
|
+
class Or < Formula
|
|
7
|
+
# Disjoin several formulae.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# Or.new([Atom.new(:even?), Atom.new(:positive?)])
|
|
11
|
+
def initialize(subformulae)
|
|
12
|
+
@subformulae = subformulae
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Return an enumerator over the atoms of all sub-formulae.
|
|
16
|
+
#
|
|
17
|
+
# @return [Enumerator]
|
|
18
|
+
def atoms
|
|
19
|
+
subformulae.lazy.flat_map(&:atoms)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check which states of the model are satisfied by at least one subformulae.
|
|
23
|
+
#
|
|
24
|
+
# @param [LabeledMachine] model
|
|
25
|
+
# @return [CheckResult]
|
|
26
|
+
def check(model)
|
|
27
|
+
sub_results = subformulae.lazy.map { |f| f.check(model) }
|
|
28
|
+
sub_results.reduce(&:union)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_s
|
|
32
|
+
subformulae.map(&:to_s).map { |s| "(#{s})" }.join(" ∨ ")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :subformulae
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module StateMachineChecker
|
|
2
|
+
module CTL
|
|
3
|
+
# Abstract base class for operators with a single sub-formula.
|
|
4
|
+
class UnaryOperator < Formula
|
|
5
|
+
# @param [Formula] subformula
|
|
6
|
+
def initialize(subformula)
|
|
7
|
+
@subformula = subformula
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Return an enumerator over the atoms of the sub-formula
|
|
11
|
+
#
|
|
12
|
+
# @return [Enumerator<Atom>]
|
|
13
|
+
def atoms
|
|
14
|
+
subformula.atoms
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :subformula
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module StateMachineChecker
|
|
2
|
+
# Represents a finite state machine: a set of states, an initial state, and
|
|
3
|
+
# transitions among them.
|
|
4
|
+
#
|
|
5
|
+
# This class is used to limit the dependency on any particular state machine
|
|
6
|
+
# library.
|
|
7
|
+
class FiniteStateMachine
|
|
8
|
+
attr_reader :initial_state, :transitions
|
|
9
|
+
|
|
10
|
+
# @param [Symbol] initial_state the name of the initial state.
|
|
11
|
+
# @param [Array<Transition>] transitions the transitions of the FSM.
|
|
12
|
+
def initialize(initial_state, transitions)
|
|
13
|
+
@initial_state = initial_state
|
|
14
|
+
@transitions = transitions
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Enumerate the states and for each provide a path to it.
|
|
18
|
+
#
|
|
19
|
+
# @return [Enumerator<Array(Symbol, Array<Transition>)>] an enumerator where
|
|
20
|
+
# each element is a pair of a state and an array of transitions to reach the
|
|
21
|
+
# state.
|
|
22
|
+
def state_paths
|
|
23
|
+
Enumerator.new do |yielder|
|
|
24
|
+
depth_first_search(Set.new, initial_state, [], yielder)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Enumerate the states.
|
|
29
|
+
#
|
|
30
|
+
# @return [Enumerator<Symbol>] an enumerator over the names of the states.
|
|
31
|
+
def states
|
|
32
|
+
seen = Set.new
|
|
33
|
+
|
|
34
|
+
Enumerator.new do |yielder|
|
|
35
|
+
seen << initial_state
|
|
36
|
+
yielder << initial_state
|
|
37
|
+
|
|
38
|
+
transitions.each do |transition|
|
|
39
|
+
unless seen.include?(transition.from)
|
|
40
|
+
seen << transition.from
|
|
41
|
+
yielder << transition.from
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless seen.include?(transition.to)
|
|
45
|
+
seen << transition.to
|
|
46
|
+
yielder << transition.to
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Traverse the graph from the given state. Yield each state and the
|
|
53
|
+
# transitions from it to the from_state. If the result of the block is falsey
|
|
54
|
+
# for any state then the search will not continue to the children of that
|
|
55
|
+
# state.
|
|
56
|
+
#
|
|
57
|
+
# @param [Symbol] from_state
|
|
58
|
+
# @param [true, false] reverse traverse in reverse?
|
|
59
|
+
# @yield [Symbol, Array<Symbol>]
|
|
60
|
+
def traverse(from_state, reverse: false, &block)
|
|
61
|
+
rec_traverse(from_state, [], Set[from_state], reverse, &block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def transitions_from(state)
|
|
65
|
+
transitions.select { |t| t.from == state }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def transitions_to(state)
|
|
69
|
+
transitions.select { |t| t.to == state }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Traverse the graph, maintaining a stack of transitions.
|
|
75
|
+
def rec_traverse(state, stack, seen, reverse, &block)
|
|
76
|
+
# If we are reverse searching than the stack will be in reverse order.
|
|
77
|
+
path = reverse ? stack.reverse : stack.clone
|
|
78
|
+
continue = block.yield(state, path.map(&:name)) != false
|
|
79
|
+
|
|
80
|
+
if continue
|
|
81
|
+
trans = if reverse
|
|
82
|
+
transitions_to(state)
|
|
83
|
+
else
|
|
84
|
+
transitions_from(state)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
trans.each do |transition|
|
|
88
|
+
next_state = if reverse
|
|
89
|
+
transition.from
|
|
90
|
+
else
|
|
91
|
+
transition.to
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
unless seen.include?(next_state)
|
|
95
|
+
seen.add(next_state)
|
|
96
|
+
stack.push(transition)
|
|
97
|
+
|
|
98
|
+
rec_traverse(next_state, stack, seen, reverse, &block)
|
|
99
|
+
|
|
100
|
+
stack.pop
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def depth_first_search(visited, state, transitions, yielder)
|
|
107
|
+
yielder << [state, transitions]
|
|
108
|
+
|
|
109
|
+
visited.add(state)
|
|
110
|
+
|
|
111
|
+
transitions_from(state).each do |transition|
|
|
112
|
+
unless visited.include?(transition.to)
|
|
113
|
+
new_transitions = transitions.clone
|
|
114
|
+
new_transitions << transition
|
|
115
|
+
depth_first_search(visited, transition.to, new_transitions, yielder)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module StateMachineChecker
|
|
2
|
+
# A finite state machine where every node is mapped to a set of labels. AKA a
|
|
3
|
+
# Kripke structure.
|
|
4
|
+
class LabeledMachine
|
|
5
|
+
# @param [FiniteStateMachine] fsm
|
|
6
|
+
# @param [Labeling] labeling
|
|
7
|
+
def initialize(fsm, labeling)
|
|
8
|
+
@fsm = fsm
|
|
9
|
+
@labeling = labeling
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# (see StateMachineChecker::FiniteStateMachine#initial_state)
|
|
13
|
+
def initial_state
|
|
14
|
+
fsm.initial_state
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# (see StateMachineChecker::FiniteStateMachine#transitions)
|
|
18
|
+
def transitions
|
|
19
|
+
fsm.transitions
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# (see StateMachineChecker::FiniteStateMachine#states)
|
|
23
|
+
def states
|
|
24
|
+
fsm.states
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# (see StateMachineChecker::FiniteStateMachine#traverse)
|
|
28
|
+
def traverse(from_state, reverse: false, &block)
|
|
29
|
+
fsm.traverse(from_state, reverse: reverse, &block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# (see StateMachineChecker::FiniteStateMachine#transitions_to)
|
|
33
|
+
def transitions_to(state)
|
|
34
|
+
fsm.transitions_to(state)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# (see StateMachineChecker::FiniteStateMachine#transitions_from)
|
|
38
|
+
def transitions_from(state)
|
|
39
|
+
fsm.transitions_from(state)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# (see StateMachineChecker::Labeling#for_state)
|
|
43
|
+
def labels_for_state(state)
|
|
44
|
+
labeling.for_state(state)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :fsm, :labeling
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module StateMachineChecker
|
|
2
|
+
# A mapping from states to the values of each atom.
|
|
3
|
+
class Labeling
|
|
4
|
+
# @param [Enumerator<CTL::Atom>] atoms the atoms which will be the labels.
|
|
5
|
+
# @param [FiniteStateMachine] machine the machine to generate labels for.
|
|
6
|
+
# @param [Proc] instance_generator a nullary function which returns an
|
|
7
|
+
# instance of an object in the initial state.
|
|
8
|
+
def initialize(atoms, machine, instance_generator)
|
|
9
|
+
@labels_by_state = build_map(atoms, machine, instance_generator)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get the labels for the given state.
|
|
13
|
+
#
|
|
14
|
+
# @param [Symbol] state
|
|
15
|
+
# @return [Set<CTL::Atom>] the atoms which are true in the state
|
|
16
|
+
def for_state(state)
|
|
17
|
+
labels_by_state[state]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :labels_by_state
|
|
23
|
+
|
|
24
|
+
# Generate a hash of state -> atoms
|
|
25
|
+
def build_map(atoms, machine, instance_generator)
|
|
26
|
+
machine.state_paths.each_with_object({}) { |(state, transitions), states_map|
|
|
27
|
+
instance = instance_from_transitions(transitions, instance_generator)
|
|
28
|
+
|
|
29
|
+
states_map[state] = atoms.each_with_object(Set.new) { |atom, labels|
|
|
30
|
+
if atom.apply(instance)
|
|
31
|
+
labels << atom
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Walk an instance through the given transitions.
|
|
38
|
+
def instance_from_transitions(transitions, instance_generator)
|
|
39
|
+
instance = instance_generator.call
|
|
40
|
+
|
|
41
|
+
transitions.each do |transition|
|
|
42
|
+
transition.execute(instance)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
instance
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|