pg-verify 0.1.0 → 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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +3 -20
  3. data/README.md +1 -1
  4. data/data/banner.txt +5 -0
  5. data/data/project-template/README.md +81 -0
  6. data/doc/examples/vending_machine/checkpoint_1.rb +29 -0
  7. data/doc/examples/vending_machine/checkpoint_2.rb +47 -0
  8. data/doc/examples/vending_machine/checkpoint_3.rb +68 -0
  9. data/doc/examples/vending_machine/checkpoint_4.rb +202 -0
  10. data/integration_tests/ruby_dsl/001_states.rb +2 -1
  11. data/integration_tests/ruby_dsl/002_transitions.rb +2 -1
  12. data/integration_tests/ruby_dsl/017_ctl_specifications.rb +19 -0
  13. data/integration_tests/ruby_dsl/018_non_atomic_hazard.rb +17 -0
  14. data/integration_tests/ruby_dsl/019_multiple_actions.rb +21 -0
  15. data/integration_tests/ruby_dsl/020_vending_machine.rb +188 -0
  16. data/lib/pg-verify/cli/cli.rb +94 -24
  17. data/lib/pg-verify/cli/cli_utils.rb +61 -0
  18. data/lib/pg-verify/interpret/component_context.rb +1 -1
  19. data/lib/pg-verify/interpret/interpret.rb +1 -1
  20. data/lib/pg-verify/interpret/pg_script.rb +1 -0
  21. data/lib/pg-verify/model/parsed_expression.rb +10 -5
  22. data/lib/pg-verify/model/simulation/trace.rb +15 -4
  23. data/lib/pg-verify/model/validation/errors.rb +21 -2
  24. data/lib/pg-verify/nusmv/runner.rb +69 -17
  25. data/lib/pg-verify/puml/puml.rb +1 -0
  26. data/lib/pg-verify/transform/hash_transformation.rb +46 -14
  27. data/lib/pg-verify/transform/nusmv_transformation.rb +4 -3
  28. data/lib/pg-verify/transform/puml_transformation.rb +27 -8
  29. data/lib/pg-verify/version.rb +1 -1
  30. data/pg-verify.gemspec +0 -1
  31. data/vscript.rb +64 -0
  32. metadata +14 -18
  33. data/data/project-template/program-graph.rb.resource +0 -103
  34. /data/{data/project-template/.pg-verify.yml → lib/pg-verify/puml/runner.rb} +0 -0
@@ -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)
@@ -93,13 +121,30 @@ module PgVerify
93
121
 
94
122
  class BaseCommand < Thor
95
123
 
124
+ desc "version", "Print version information"
125
+ def version()
126
+ banner = File.read(File.join(PgVerify.root, "data", "banner.txt"))
127
+ banner = banner.gsub("0.1.0", PgVerify::VERSION)
128
+ colors = [ "red", "orange", "yellow", "orange", "red" ]
129
+ banner = banner.split("\n").each_with_index.map { |l, i| l.send(:"c_#{colors[i]}") }.join("\n")
130
+ puts banner
131
+ puts
132
+ puts "You are running #{'pg-verify'.c_string} version #{PgVerify::VERSION.to_s.c_num}"
133
+ puts "Installation root: #{PgVerify.root.to_s.c_file}"
134
+ end
135
+
96
136
  desc "test", "Test the model specifications"
97
137
  method_option :script, :type => :string
138
+ method_option :"json-file", :type => :string
139
+ method_option :"yaml-file", :type => :string
98
140
  def test()
99
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
100
- models = Interpret::PgScript.new.interpret(script_file)
141
+ models = CliUtils.load_models(options)
101
142
 
102
143
  models.each { |model|
144
+ Shell::LoadingPrompt.while_loading("Checking for deadlocks") {
145
+ NuSMV::Runner.new().run_check!(model); "ok"
146
+ }
147
+
103
148
  results = Shell::LoadingPrompt.while_loading("Running specifications") {
104
149
  NuSMV::Runner.new().run_specs(model)
105
150
  }
@@ -109,8 +154,8 @@ module PgVerify
109
154
  puts "[ #{stat_string} ] #{result.spec.text}"
110
155
  puts " #{result.spec.expression.to_s.c_blue}"
111
156
  unless result.success
112
- puts "Here is the trace:".c_red
113
- 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)
114
159
  puts trace_s + "\n"
115
160
  end
116
161
  }
@@ -119,11 +164,16 @@ module PgVerify
119
164
 
120
165
  desc "dcca", "Run the automatic DCCA for hazards of the model"
121
166
  method_option :script, :type => :string
167
+ method_option :"json-file", :type => :string
168
+ method_option :"yaml-file", :type => :string
122
169
  def dcca()
123
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
124
- models = Interpret::PgScript.new.interpret(script_file)
170
+ models = CliUtils.load_models(options)
125
171
 
126
172
  models.each { |model|
173
+ Shell::LoadingPrompt.while_loading("Checking for deadlocks") {
174
+ NuSMV::Runner.new().run_check!(model); "ok"
175
+ }
176
+
127
177
  dcca = Model::DCCA.new(model, NuSMV::Runner.new)
128
178
  result = Shell::LoadingPrompt.while_loading("Calculating DCCA for #{model.name.to_s.c_string}") {
129
179
  dcca.perform()
@@ -131,7 +181,8 @@ module PgVerify
131
181
  result.each { |hazard, crit_sets|
132
182
  s = crit_sets.length == 1 ? "" : "s"
133
183
  message = "Hazard #{hazard.text.to_s.c_string} (#{hazard.expression.to_s.c_blue}) "
134
- 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
135
186
  puts message
136
187
  crit_sets.each { |set|
137
188
  puts "\t{ #{set.map(&:to_s).map(&:c_blue).join(', ')} }"
@@ -142,16 +193,33 @@ module PgVerify
142
193
 
143
194
  desc "simulate", "Simulate the model and save each step as an image"
144
195
  method_option :script, :type => :string
196
+ method_option :"json-file", :type => :string
197
+ method_option :"yaml-file", :type => :string
145
198
  method_option :steps, :type => :numeric, default: 10
146
199
  method_option :force, :type => :numeric, default: 10
147
200
  method_option :random, :type => :boolean, default: false
148
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
149
207
  def simulate()
150
- script_file = options[:script] || Settings.ruby_dsl.default_script_name
151
- 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)
152
216
  runner = NuSMV::Runner.new
153
217
 
154
218
  models.each { |model|
219
+ Shell::LoadingPrompt.while_loading("Checking for deadlocks") {
220
+ NuSMV::Runner.new().run_check!(model); "ok"
221
+ }
222
+
155
223
  trace = Shell::LoadingPrompt.while_loading("Simulating model #{model.name.to_s.c_string}") {
156
224
  runner.run_simulation(model, options[:steps], random: options[:random])
157
225
  }
@@ -169,7 +237,7 @@ module PgVerify
169
237
  Shell::LoadingPrompt.while_loading("Rendering states") { |printer|
170
238
  trace.states.each_with_index { |variable_state, index|
171
239
  printer.printl("Step #{index + 1}/#{trace.states.length}")
172
- 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)
173
241
  png = PlantumlBuilder::Formats::PNG.new(puml).load
174
242
  out_path = File.expand_path("step-#{index}.png", out_dir)
175
243
  File.binwrite(out_path, png)
@@ -205,6 +273,8 @@ module PgVerify
205
273
  # Copy the actual default config into the project as that
206
274
  # will contain all keys and should be commented
207
275
  FileUtils.cp(File.join(PgVerify.root, "data", "config", "pg-verify.yml"), File.join(target, ".pg-verify.yml"))
276
+ prelude_pg_file = File.join(PgVerify.root, "integration_tests", "ruby_dsl", "016_pressure_tank.rb")
277
+ FileUtils.cp(prelude_pg_file, File.join(target, Settings.ruby_dsl.default_script_name))
208
278
 
209
279
  # Initialize git project
210
280
  Dir.chdir(target) {
@@ -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