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.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -3
- data/README.md +1 -1
- data/data/project-template/README.md +81 -0
- data/doc/examples/vending_machine/checkpoint_1.rb +29 -0
- data/doc/examples/vending_machine/checkpoint_2.rb +47 -0
- data/doc/examples/vending_machine/checkpoint_3.rb +68 -0
- data/doc/examples/vending_machine/checkpoint_4.rb +202 -0
- data/integration_tests/ruby_dsl/001_states.rb +2 -1
- data/integration_tests/ruby_dsl/002_transitions.rb +2 -1
- data/integration_tests/ruby_dsl/017_ctl_specifications.rb +19 -0
- data/integration_tests/ruby_dsl/018_non_atomic_hazard.rb +17 -0
- data/integration_tests/ruby_dsl/019_multiple_actions.rb +21 -0
- data/integration_tests/ruby_dsl/020_vending_machine.rb +188 -0
- data/lib/pg-verify/cli/cli.rb +80 -24
- data/lib/pg-verify/cli/cli_utils.rb +61 -0
- data/lib/pg-verify/interpret/component_context.rb +1 -1
- data/lib/pg-verify/interpret/interpret.rb +1 -1
- data/lib/pg-verify/interpret/pg_script.rb +1 -0
- data/lib/pg-verify/model/parsed_expression.rb +10 -5
- data/lib/pg-verify/model/simulation/trace.rb +15 -4
- data/lib/pg-verify/model/validation/errors.rb +21 -2
- data/lib/pg-verify/nusmv/runner.rb +69 -17
- data/lib/pg-verify/puml/puml.rb +1 -0
- data/lib/pg-verify/puml/runner.rb +0 -0
- data/lib/pg-verify/transform/hash_transformation.rb +46 -14
- data/lib/pg-verify/transform/nusmv_transformation.rb +4 -3
- data/lib/pg-verify/transform/puml_transformation.rb +27 -8
- data/lib/pg-verify/version.rb +1 -1
- data/vscript.rb +64 -0
- metadata +13 -2
@@ -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
|
data/lib/pg-verify/cli/cli.rb
CHANGED
@@ -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
|
-
|
18
|
-
|
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
|
-
|
32
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
125
|
-
trace_s = result.trace.to_s.indented(str: "
|
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
|
-
|
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
|
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
|
-
|
163
|
-
|
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
|
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|
|
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
|
-
|
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.
|
31
|
-
body << @expression.source_location.render_code_block() unless @expression.source_location.
|
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
|