pg-verify 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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