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.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -20
- data/README.md +1 -1
- data/data/banner.txt +5 -0
- 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 +94 -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/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/pg-verify.gemspec +0 -1
- data/vscript.rb +64 -0
- metadata +14 -18
- data/data/project-template/program-graph.rb.resource +0 -103
- /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
|
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)
|
@@ -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
|
-
|
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
|
113
|
-
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)
|
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
|
-
|
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
|
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
|
-
|
151
|
-
|
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
|
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
|