pg-verify 0.1.1 → 0.1.2

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.
@@ -0,0 +1,188 @@
1
+ model :VendingMachine do
2
+
3
+ products = {
4
+ cola: 1.2,
5
+ chips: 2.5
6
+ }
7
+ products = products.map { |c, v| [c, (v * 10).to_i] }.to_h
8
+
9
+ coins = {
10
+ ten_cents: 10,
11
+ twenty_cents: 20,
12
+ fifty_cents: 50,
13
+ one_euro: 100,
14
+ two_euro: 200
15
+ }
16
+ coins = coins.map { |c, v| [c, (v / 10).to_i] }.to_h
17
+
18
+
19
+ max_money = 100
20
+ start_money = 20
21
+
22
+ transient error :CoinReadFails
23
+
24
+ graph :User do
25
+ press_states = products.keys.map { |product| :"press_#{product}" }
26
+ grab_states = products.keys.map { |product| :"grab_#{product}" }
27
+ insert_states = coins.keys.map { |coin| :"insert_#{coin}" }
28
+
29
+ var pocket_money: (0..max_money), init: start_money
30
+ var value_in_products: (0..max_money), init: 0
31
+
32
+ states :inserting, *insert_states, \
33
+ :pressing, *press_states, \
34
+ :waiting, :grab_product, \
35
+ :grab_change, \
36
+ :done, \
37
+ init: :inserting
38
+
39
+ # The user can insert coins until the money runs out
40
+ coins.each { |coin, value|
41
+ transition :inserting => :"insert_#{coin}" do
42
+ guard "pocket_money - #{value} >= 0"
43
+ action "pocket_money := pocket_money - #{value}"
44
+ end
45
+ transition :"insert_#{coin}" => :inserting
46
+ }
47
+
48
+ # The user can decide at any point to start pressing buttons
49
+ transition :inserting => :pressing
50
+
51
+ # Press one button and wait for the result
52
+ products.each { |product, value|
53
+ transition :pressing => :"press_#{product}"
54
+ transition :"press_#{product}" => :waiting
55
+ }
56
+
57
+ # Grab a product and take note of the value
58
+ products.each { |product, value|
59
+ transition :waiting => :grab_product do
60
+ precon "value_in_products + #{value} < #{max_money}"
61
+ guard "LED == green && Dispenser == dispensed_#{product}"
62
+ action "value_in_products := value_in_products + #{value}"
63
+ end
64
+ }
65
+ transition :grab_product => :grab_change
66
+
67
+ # Grab change directly
68
+ transition :waiting => :grab_change do
69
+ guard "LED == red"
70
+ end
71
+
72
+
73
+ # Grab the change and be done
74
+ transition :grab_change => :done do
75
+ precon "pocket_money + change <= #{max_money}"
76
+ action "pocket_money := pocket_money + change"
77
+ end
78
+ end
79
+
80
+ graph :CoinReader do
81
+ states :reading
82
+
83
+ var read_value: (0..coins.values.max), init: 0
84
+
85
+ coins.each { |coin, value|
86
+ transition :reading => :reading do
87
+ guard "User == insert_#{coin} && CoinReadFails == No"
88
+ action "read_value := #{value}"
89
+ end
90
+ }
91
+
92
+ transition :reading => :reading do
93
+ guard coins.keys.map { |coin| "User != insert_#{coin}" }.join(" && ")
94
+ action "read_value := 0"
95
+ end
96
+
97
+ end
98
+
99
+ graph :Controller do
100
+ var budget: (0..max_money), init: 0
101
+
102
+ dispense_states = products.keys.map { |product| :"dispense_#{product}" }
103
+
104
+ states :accepting, *dispense_states, :rejected, :done, init: :accepting
105
+
106
+ transition :accepting => :accepting do
107
+ precon "budget + read_value <= #{max_money}"
108
+ guard "read_value > 0"
109
+ action "budget := budget + read_value"
110
+ end
111
+
112
+ products.each { |product, value|
113
+ transition :accepting => :"dispense_#{product}" do
114
+ guard "User == press_#{product} && budget >= #{value}"
115
+ action "budget := budget - #{value}"
116
+ end
117
+ transition :"dispense_#{product}" => :done
118
+
119
+ transition :accepting => :rejected do
120
+ guard "User == press_#{product} && budget < #{value}"
121
+ end
122
+ }
123
+
124
+ transition :accepting => :accepting do
125
+ guard "CoinDispenser == dispensed_change"
126
+ action "budget := 0"
127
+ end
128
+ transition :done => :accepting do
129
+ guard "CoinDispenser == dispensed_change"
130
+ action "budget := 0"
131
+ end
132
+
133
+
134
+ end
135
+
136
+ graph :LED do
137
+ states :idle, :red, :green, init: :idle
138
+
139
+ transition :idle => :green do
140
+ guard "Controller == done"
141
+ end
142
+ transition :idle => :red do
143
+ guard "Controller == rejected"
144
+ end
145
+
146
+ end
147
+
148
+ graph :Dispenser do
149
+ dispensed_states = products.keys.map { |product| :"dispensed_#{product}" }
150
+ states :empty, *dispensed_states, init: :empty
151
+
152
+ # Dispense the product the Controller tells us to
153
+ products.each { |product, value|
154
+ transition :empty => :"dispensed_#{product}" do
155
+ guard "Controller == dispense_#{product}"
156
+ end
157
+ }
158
+ end
159
+
160
+ graph :CoinDispenser do
161
+ states :idle, :dispensed_change, init: :idle
162
+ var change: (0..max_money), init: 0
163
+
164
+ # Eject the change money
165
+ transition :idle => :dispensed_change do
166
+ guard "Controller == rejected || Controller == done"
167
+ action "change := budget"
168
+ end
169
+
170
+ end
171
+
172
+ specify "The vending machine" do
173
+
174
+ it "allows the user to buy something" => :"EF value_in_products > 0"
175
+ it "always completes the transaction" => :"F User == done"
176
+
177
+ products.each { |product, value|
178
+ assuming "the product can be bough" => :"pocket_money >= #{value}" do
179
+ it "allows the user to buy #{product}" => :"EF Dispenser == dispensed_#{product}"
180
+ end
181
+ }
182
+
183
+ end
184
+
185
+ # hazard "The user looses money" => :"User == done && pocket_money + value_in_products < #{start_money}"
186
+ # hazard "The machine looses money" => :"User == done && pocket_money + value_in_products > #{start_money}"
187
+
188
+ end
@@ -3,6 +3,7 @@ Dir[File.join(__dir__, '*.rb')].sort.each { |file| require file }
3
3
 
4
4
  require 'thor'
5
5
  require 'plantuml_builder'
6
+ require 'json'
6
7
 
7
8
  module PgVerify
8
9
  module Cli
@@ -13,12 +14,24 @@ module PgVerify
13
14
  method_option :only, :type => :array, repeatable: true
14
15
  method_option :hide, :type => :array, repeatable: true
15
16
  method_option :script, :type => :string
17
+ method_option :"json-file", :type => :string
18
+ method_option :"yaml-file", :type => :string
19
+ # Hide labels or parts of labels
20
+ method_option :"hide-labels", :type => :boolean, default: false
21
+ method_option :"hide-precons", :type => :boolean, default: false
22
+ method_option :"hide-guards", :type => :boolean, default: false
23
+ method_option :"hide-actions", :type => :boolean, default: false
16
24
  def puml()
17
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
18
- models = Interpret::PgScript.new.interpret(script_file)
25
+ render_options = {
26
+ render_labels: !options[:"hide-labels"],
27
+ render_precons: !options[:"hide-precons"],
28
+ render_guards: !options[:"hide-guards"],
29
+ render_actions: !options[:"hide-actions"]
30
+ }
31
+ models = CliUtils.load_models(options)
19
32
  models.each { |model|
20
33
  components = self.class.select_components(options[:only], options[:hide], model)
21
- puml = Transform::PumlTransformation.new.transform_graph(model, only: components)
34
+ puml = Transform::PumlTransformation.new(render_options).transform_graph(model, only: components)
22
35
  puts puml
23
36
  }
24
37
  end
@@ -27,16 +40,28 @@ module PgVerify
27
40
  method_option :only, :type => :array, repeatable: true
28
41
  method_option :hide, :type => :array, repeatable: true
29
42
  method_option :script, :type => :string
43
+ method_option :"json-file", :type => :string
44
+ method_option :"yaml-file", :type => :string
45
+ # Hide labels or parts of labels
46
+ method_option :"hide-labels", :type => :boolean, default: false
47
+ method_option :"hide-precons", :type => :boolean, default: false
48
+ method_option :"hide-guards", :type => :boolean, default: false
49
+ method_option :"hide-actions", :type => :boolean, default: false
30
50
  def png()
31
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
32
- models = Interpret::PgScript.new.interpret(script_file)
51
+ render_options = {
52
+ render_labels: !options[:"hide-labels"],
53
+ render_precons: !options[:"hide-precons"],
54
+ render_guards: !options[:"hide-guards"],
55
+ render_actions: !options[:"hide-actions"]
56
+ }
57
+
58
+ models = CliUtils.load_models(options)
33
59
 
34
60
  models.each { |model|
35
61
  components = self.class.select_components(options[:only], options[:hide], model)
36
- puml = Transform::PumlTransformation.new.transform_graph(model, only: components)
62
+ puml = Transform::PumlTransformation.new(render_options).transform_graph(model, only: components)
37
63
  png = PlantumlBuilder::Formats::PNG.new(puml).load
38
- out_name = File.basename(script_file, '.*')
39
- out_name += "-" + model.name.to_s.gsub(/\W+/, '_').downcase + ".png"
64
+ out_name = model.name.to_s.gsub(/\W+/, '_').downcase + ".png"
40
65
  out_path = File.expand_path(out_name, Settings.outdir)
41
66
  FileUtils.mkdir_p(Settings.outdir)
42
67
  File.binwrite(out_path, png)
@@ -46,9 +71,10 @@ module PgVerify
46
71
 
47
72
  desc "yaml", "Shows the model in YAML format"
48
73
  method_option :script, :type => :string
74
+ method_option :"json-file", :type => :string
75
+ method_option :"yaml-file", :type => :string
49
76
  def yaml()
50
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
51
- models = Interpret::PgScript.new.interpret(script_file)
77
+ models = CliUtils.load_models(options)
52
78
 
53
79
  models.each { |model|
54
80
  hash = Transform::HashTransformation.new.transform_graph(model)
@@ -58,9 +84,10 @@ module PgVerify
58
84
 
59
85
  desc "json", "Shows the model in Json format"
60
86
  method_option :script, :type => :string
87
+ method_option :"json-file", :type => :string
88
+ method_option :"yaml-file", :type => :string
61
89
  def json()
62
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
63
- models = Interpret::PgScript.new.interpret(script_file)
90
+ models = CliUtils.load_models(options)
64
91
 
65
92
  models.each { |model|
66
93
  hash = Transform::HashTransformation.new.transform_graph(model)
@@ -70,9 +97,10 @@ module PgVerify
70
97
 
71
98
  desc "nusmv", "Shows the model in NuSMV format"
72
99
  method_option :script, :type => :string
100
+ method_option :"json-file", :type => :string
101
+ method_option :"yaml-file", :type => :string
73
102
  def nusmv()
74
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
75
- models = Interpret::PgScript.new.interpret(script_file)
103
+ models = CliUtils.load_models(options)
76
104
 
77
105
  models.each { |model|
78
106
  nusmv = Transform::NuSmvTransformation.new.transform_graph(model)
@@ -107,11 +135,16 @@ module PgVerify
107
135
 
108
136
  desc "test", "Test the model specifications"
109
137
  method_option :script, :type => :string
138
+ method_option :"json-file", :type => :string
139
+ method_option :"yaml-file", :type => :string
110
140
  def test()
111
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
112
- models = Interpret::PgScript.new.interpret(script_file)
141
+ models = CliUtils.load_models(options)
113
142
 
114
143
  models.each { |model|
144
+ Shell::LoadingPrompt.while_loading("Checking for deadlocks") {
145
+ NuSMV::Runner.new().run_check!(model); "ok"
146
+ }
147
+
115
148
  results = Shell::LoadingPrompt.while_loading("Running specifications") {
116
149
  NuSMV::Runner.new().run_specs(model)
117
150
  }
@@ -121,8 +154,8 @@ module PgVerify
121
154
  puts "[ #{stat_string} ] #{result.spec.text}"
122
155
  puts " #{result.spec.expression.to_s.c_blue}"
123
156
  unless result.success
124
- puts "Here is the trace:".c_red
125
- trace_s = result.trace.to_s.indented(str: " >> ".c_red)
157
+ puts " Here is a counter example:".c_red
158
+ trace_s = result.trace.to_s.indented(str: " >> ".c_red)
126
159
  puts trace_s + "\n"
127
160
  end
128
161
  }
@@ -131,11 +164,16 @@ module PgVerify
131
164
 
132
165
  desc "dcca", "Run the automatic DCCA for hazards of the model"
133
166
  method_option :script, :type => :string
167
+ method_option :"json-file", :type => :string
168
+ method_option :"yaml-file", :type => :string
134
169
  def dcca()
135
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
136
- models = Interpret::PgScript.new.interpret(script_file)
170
+ models = CliUtils.load_models(options)
137
171
 
138
172
  models.each { |model|
173
+ Shell::LoadingPrompt.while_loading("Checking for deadlocks") {
174
+ NuSMV::Runner.new().run_check!(model); "ok"
175
+ }
176
+
139
177
  dcca = Model::DCCA.new(model, NuSMV::Runner.new)
140
178
  result = Shell::LoadingPrompt.while_loading("Calculating DCCA for #{model.name.to_s.c_string}") {
141
179
  dcca.perform()
@@ -143,7 +181,8 @@ module PgVerify
143
181
  result.each { |hazard, crit_sets|
144
182
  s = crit_sets.length == 1 ? "" : "s"
145
183
  message = "Hazard #{hazard.text.to_s.c_string} (#{hazard.expression.to_s.c_blue}) "
146
- message += "has #{crit_sets.length.to_s.c_num} minimal critical cut set#{s}:"
184
+ message += "has #{crit_sets.length.to_s.c_num} minimal critical fault set#{s}:" if crit_sets.length > 0
185
+ message += "has no minimal critical fault sets, meaning it is safe!".c_success if crit_sets.length == 0
147
186
  puts message
148
187
  crit_sets.each { |set|
149
188
  puts "\t{ #{set.map(&:to_s).map(&:c_blue).join(', ')} }"
@@ -154,16 +193,33 @@ module PgVerify
154
193
 
155
194
  desc "simulate", "Simulate the model and save each step as an image"
156
195
  method_option :script, :type => :string
196
+ method_option :"json-file", :type => :string
197
+ method_option :"yaml-file", :type => :string
157
198
  method_option :steps, :type => :numeric, default: 10
158
199
  method_option :force, :type => :numeric, default: 10
159
200
  method_option :random, :type => :boolean, default: false
160
201
  method_option :png, :type => :boolean, default: false
202
+ # Hide labels or parts of labels
203
+ method_option :"hide-labels", :type => :boolean, default: false
204
+ method_option :"hide-precons", :type => :boolean, default: false
205
+ method_option :"hide-guards", :type => :boolean, default: false
206
+ method_option :"hide-actions", :type => :boolean, default: false
161
207
  def simulate()
162
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
163
- models = Interpret::PgScript.new.interpret(script_file)
208
+ render_options = {
209
+ render_labels: !options[:"hide-labels"],
210
+ render_precons: !options[:"hide-precons"],
211
+ render_guards: !options[:"hide-guards"],
212
+ render_actions: !options[:"hide-actions"]
213
+ }
214
+
215
+ models = CliUtils.load_models(options)
164
216
  runner = NuSMV::Runner.new
165
217
 
166
218
  models.each { |model|
219
+ Shell::LoadingPrompt.while_loading("Checking for deadlocks") {
220
+ NuSMV::Runner.new().run_check!(model); "ok"
221
+ }
222
+
167
223
  trace = Shell::LoadingPrompt.while_loading("Simulating model #{model.name.to_s.c_string}") {
168
224
  runner.run_simulation(model, options[:steps], random: options[:random])
169
225
  }
@@ -181,7 +237,7 @@ module PgVerify
181
237
  Shell::LoadingPrompt.while_loading("Rendering states") { |printer|
182
238
  trace.states.each_with_index { |variable_state, index|
183
239
  printer.printl("Step #{index + 1}/#{trace.states.length}")
184
- puml = Transform::PumlTransformation.new.transform_graph(model, variable_state: variable_state)
240
+ puml = Transform::PumlTransformation.new(render_options).transform_graph(model, variable_state: variable_state)
185
241
  png = PlantumlBuilder::Formats::PNG.new(puml).load
186
242
  out_path = File.expand_path("step-#{index}.png", out_dir)
187
243
  File.binwrite(out_path, png)
@@ -0,0 +1,61 @@
1
+ module PgVerify
2
+ class CliUtils
3
+ def self.load_models(options)
4
+ dsl_script_file = options[:script]
5
+ json_file = options[:"json-file"]
6
+ yaml_file = options[:"yaml-file"]
7
+ default_script_file = Settings.ruby_dsl.default_script_name
8
+
9
+ unless dsl_script_file.nil?
10
+ raise NoSuchFileError.new(dsl_script_file) unless File.file?(dsl_script_file)
11
+ return Interpret::PgScript.new.interpret(dsl_script_file)
12
+ end
13
+
14
+ unless json_file.nil?
15
+ raise NoSuchFileError.new(json_file) unless File.file?(json_file)
16
+ json_string = File.read(json_file)
17
+ array = JSON.load(json_string)
18
+ array = [ array ] unless array.is_a?(Array)
19
+ return array.map { |hash| Transform::HashTransformation.new.parse_graph(hash) }
20
+ end
21
+
22
+ unless yaml_file.nil?
23
+ raise NoSuchFileError.new(yaml_file) unless File.file?(yaml_file)
24
+ array = YAML.load_file(yaml_file)
25
+ array = [ array ] unless array.is_a?(Array)
26
+ return array.map { |hash| Transform::HashTransformation.new.parse_graph(hash) }
27
+ end
28
+
29
+ raise NoDefaultFileError.new(default_script_file) unless File.file?(default_script_file)
30
+ return Interpret::PgScript.new.interpret(default_script_file)
31
+ end
32
+
33
+ end
34
+
35
+ class NoSuchFileError < PgVerify::Core::Error
36
+ def initialize(path)
37
+ @path = path
38
+ end
39
+
40
+ def formatted()
41
+ title = "No such file!"
42
+ body = "There is no file at #{@path.c_file}!"
43
+ hint = "Make sure to specify another file or specify another path"
44
+ return title, body, hint
45
+ end
46
+ end
47
+
48
+ class NoDefaultFileError < PgVerify::Core::Error
49
+ def initialize(default_path)
50
+ @default_path = default_path
51
+ end
52
+ def formatted()
53
+ title = "Nothing to interpret!"
54
+ body = "You didn't specify a file to interpret and there is no DSL script\n"
55
+ body += "at the default location: #{@default_path.c_file} (#{File.expand_path(@default_path).c_sidenote})"
56
+ hint = "Make sure to specify another file or specify another path"
57
+ return title, body, hint
58
+ end
59
+ end
60
+
61
+ end
@@ -44,7 +44,7 @@ module PgVerify
44
44
  range = hash[name]
45
45
  raise "Variable name must be different to the component that owns it" if name.to_sym == @name
46
46
  raise InvalidDSL_var.new("Name '#{name}' is not a symbol") unless name.is_a?(Symbol)
47
- raise InvalidDSL_var.new("Range '#{range}' is not a range or array") unless range.is_a?(Range) && range.first.is_a?(Integer)
47
+ raise InvalidDSL_var.new("Range '#{range}' is not a range or array") unless (range.is_a?(Range) || range.is_a?(Array)) && range.first.is_a?(Integer)
48
48
  sloc = @parent_graph.parent_script.find_source_location()
49
49
  variable = Model::Variable.new(name, range, @name, sloc)
50
50
 
@@ -117,7 +117,7 @@ module PgVerify
117
117
  def formatted()
118
118
  title = "No such state #{@state} in component #{@component.name}"
119
119
  body = "The component #{@component.name.to_s.c_cmp} does not contain a state called #{@state.to_s.c_state}.\n"
120
- body += "Available states are: #{@component.states.map(&:to_s).map(&:c_state).join(', ')}"
120
+ body += "Available states are: #{@component.states&.map(&:to_s)&.map(&:c_state)&.join(', ')}"
121
121
  hint = "Make sure to define states before defining transitions."
122
122
  return title, body, hint
123
123
  end
@@ -15,6 +15,7 @@ module PgVerify
15
15
  end
16
16
 
17
17
  def interpret(file, validate: true)
18
+ raise "Not a file path string: '#{file}'::#{file.class}" unless file.is_a?(String)
18
19
  file = File.expand_path(file)
19
20
  raise NoSuchScriptError.new(file) unless File.file?(file)
20
21
  @script_file ||= file
@@ -20,6 +20,8 @@ module PgVerify
20
20
  TYPE_CTL = :ctl
21
21
 
22
22
  TYPES = [ TYPE_GUARD, TYPE_ACTION, TYPE_TERM, TYPE_PL, TYPE_TL, TYPE_LTL, TYPE_CTL ]
23
+ LTL_KEYWORDS = "GFXRU".chars
24
+ CTL_KEYWORDS = [ "A", "E" ].product([ "G", "F", "X", "U" ]).map { |a, e| "#{a}#{e}" }
23
25
 
24
26
  attr_accessor :expression_string
25
27
  attr_accessor :source_location
@@ -36,11 +38,18 @@ module PgVerify
36
38
 
37
39
  def word_tokens()
38
40
  words = expression_string.scan(/[a-zA-Z_][a-zA-Z0-9_]*/).flatten.compact
39
- words = words.reject { |w| w.match(/\A[GFXRU]+\z/) }
41
+ words = words.reject { |w| LTL_KEYWORDS.include?(w) }
42
+ words = words.reject { |w| CTL_KEYWORDS.include?(w) }
40
43
  words = words.reject { |w| w == "TRUE" || w == "FALSE" }
41
44
  return words.map(&:to_sym)
42
45
  end
43
46
 
47
+ def predict_type()
48
+ return @type unless @type == :tl
49
+ tokens = self.tokenize()
50
+ return CTL_KEYWORDS.any? { |kw| tokens.include?(kw) } ? :ctl : :ltl
51
+ end
52
+
44
53
  # Splits the expression string into an array of tokens. e.g:
45
54
  # "(a == b) && 3 >= 2" becomes [ "(", "a", "==", "b", ")", "&&", "3", ">=", "2" ]
46
55
  # Note that this split method very much a hack at the moment
@@ -56,10 +65,6 @@ module PgVerify
56
65
  }.flatten.reject(&:blank?)
57
66
  end
58
67
 
59
- def used_variables()
60
- return expression_string.scan(/[a-zA-Z_][a-zA-Z0-9_]*/).flatten.compact.map(&:to_sym)
61
- end
62
-
63
68
  def to_s()
64
69
  @expression_string
65
70
  end
@@ -5,12 +5,14 @@ module PgVerify
5
5
 
6
6
  attr_accessor :model
7
7
  attr_accessor :states
8
+ attr_accessor :loop_index
8
9
 
9
- def initialize(model, states)
10
+ def initialize(model, states, loop_index: -1)
10
11
  @model, @states = model, states
12
+ @loop_index = loop_index
11
13
  end
12
14
 
13
- def to_s()
15
+ def to_s(include_steps: true)
14
16
  return "No states in trace" if @states.empty?
15
17
  # Get all variables (TODO: Bring into sensible order)
16
18
  vars = @states.first.keys
@@ -18,10 +20,17 @@ module PgVerify
18
20
 
19
21
  parts = vars.map { |var|
20
22
  var_string = state_vars.varname?(var) ? var.to_s.c_state.c_bold : var.to_s.c_string
21
- var_string + "\n" + @states.each_with_index.map{ |state, index| value_str(var, state[var], index) }.join("\n")
23
+ var_string + "\n" + @states.each_with_index.map{ |state, index| value_str(var, state[var], index) }.join("\n")
22
24
  }
23
25
  str = "Step".c_num.c_bold + "\n" + (0...@states.length).map { |i| "#{i + 1}" } .join("\n")
26
+ str = "" unless include_steps
24
27
  parts.each { |part| str = str.line_combine(part, separator: " ") }
28
+
29
+ unless @loop_index.nil? || @loop_index < 0
30
+ loop_str = (0...@states.length).map { |i| i == @loop_index + 1 ? "<- loop".c_blue : "" } .join("\n")
31
+ str = str.line_combine(loop_str, separator: " ")
32
+ str += "\n" + "Loop starts at #{@loop_index + 1}".c_sidenote
33
+ end
25
34
 
26
35
  return str
27
36
  end
@@ -30,7 +39,9 @@ module PgVerify
30
39
  return value.c_green if [ "On", "Yes", "Active" ].include?(value)
31
40
  return value.c_red if [ "Off", "No", "Idle" ].include?(value)
32
41
 
33
- settings_color = Settings.trace.colors[value.to_s]
42
+
43
+ settings_color = Settings.trace.colors.find { |key, val| File.fnmatch?(key.to_s, value) }
44
+ settings_color = settings_color[1] unless settings_color.blank?
34
45
  return value.send(:"c_#{settings_color}") unless settings_color.blank?
35
46
 
36
47
  return "#{value}"
@@ -27,8 +27,8 @@ module PgVerify
27
27
  title = "Unknown token"
28
28
 
29
29
  body = []
30
- body << @expression.source_location.to_s.c_sidenote unless @expression.source_location.blank?
31
- body << @expression.source_location.render_code_block() unless @expression.source_location.blank?
30
+ body << @expression.source_location.to_s.c_sidenote unless @expression.source_location.nil?
31
+ body << @expression.source_location.render_code_block() unless @expression.source_location.nil?
32
32
  body << ""
33
33
  body << "The expression '#{@expression.to_s.c_expression}' uses token #{@token.to_s.c_string}."
34
34
  body << "This is neither a known variable, nor literal."
@@ -114,6 +114,25 @@ module PgVerify
114
114
  end
115
115
  end
116
116
 
117
+ class DeadlockInFSMError < PgVerify::Core::Error
118
+ def initialize(program_graph, deadlock_state)
119
+ @program_graph, @deadlock_state = program_graph, deadlock_state
120
+ end
121
+ def formatted()
122
+ trace = Model::Trace.new(@program_graph, [@deadlock_state])
123
+
124
+ title = "Deadlock in program graph #{@program_graph.name}"
125
+ body = "The following state has no successor:\n\n"
126
+ body += trace.to_s(include_steps: false).indented(str: ">> ".c_error)
127
+
128
+ hint = []
129
+ hint << "This indicates a range violation:"
130
+ hint << "Once your graph is in the mentioned state (which is reachable),"
131
+ hint << "all outgoing transitions are blocked due to variable range violations."
132
+ return title, body, hint.join("\n")
133
+ end
134
+ end
135
+
117
136
  end
118
137
  end
119
138
  end