mbt-gen 0.0.4 → 0.0.5

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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mbt-gen.rb +2058 -1976
  3. data/lib/progress.rb +6 -0
  4. data/lib/solver-lib.rb +572 -513
  5. metadata +2 -2
data/lib/solver-lib.rb CHANGED
@@ -1,513 +1,572 @@
1
- #!/usr/bin/ruby
2
-
3
- require 'open3'
4
-
5
-
6
- class SolverSession
7
-
8
- def initialize(solverFactory, logFileName)
9
- @solverFactory = solverFactory
10
- @solver = @solverFactory.new_solver()
11
- @protocol_filename = logFileName
12
- @protocol = File.open(@protocol_filename, "w")
13
- @assertCounter = -1
14
- @associations = {}
15
- @consts = {}
16
- end
17
-
18
- def switch_solver(progress)
19
- @solver.close()
20
- @solver = @solverFactory.new_solver()
21
- # re-feed the session protocol into the new solver to get it up-to-date
22
- @protocol.close()
23
- File.open(@protocol_filename, "r") do |protocol|
24
- protocol.each_line do |line|
25
- to_solver(progress, line)
26
- end
27
- end
28
- @protocol = File.open(@protocol_filename, "a")
29
- end
30
-
31
- def emptyReadBuffer(progress)
32
- empty = false
33
- while !empty do
34
- begin
35
- c = ""
36
- @solver.z3outAndErr.read_nonblock(1, c)
37
- line = c + @solver.z3outAndErr.gets
38
- checkLineForSolverError(line, progress)
39
- @protocol.puts("; Solver Response: #{line}")
40
- rescue Errno::EWOULDBLOCK
41
- # there is nothing to read -> just continue
42
- empty = true
43
- rescue Errno::EAGAIN
44
- # there is nothing to read -> just continue
45
- empty = true
46
- rescue EOFError
47
- # there is nothing to read -> just continue
48
- empty = true
49
- end
50
- end
51
- end
52
-
53
- def declare_const(progress, name, type, info)
54
- info[:type] = type
55
- @consts[name] = info
56
- to_solver(progress, "(declare-const #{name} #{type})")
57
- end
58
-
59
- def to_solver(progress, line)
60
- emptyReadBuffer(progress);
61
- success = false
62
- while (!success) do
63
- begin
64
- @solver.z3in.puts(line)
65
- success = true
66
- rescue Errno::EINVAL => e
67
- progress.print_line("DEBUG: Invalid argument to puts(): #{line.inspect}")
68
- progress.print_line("An Error occured: #{e.message}")
69
- e.backtrace.each { |stline| progress.print_line(stline) }
70
- progress.print_line("=> Switching Solver")
71
- switch_solver(progress)
72
- progress.print_line("=> Switch successful. Trying again.")
73
- rescue Errno::EPIPE => e
74
- progress.print_line("An Error occured: #{e.message}")
75
- e.backtrace.each { |stline| progress.print_line(stline) }
76
- progress.print_line("=> Switching Solver")
77
- switch_solver(progress)
78
- progress.print_line("=> Switch successful. Trying again.")
79
- end
80
- end
81
- @protocol.puts(line)
82
- end
83
-
84
- def checkLineForSolverError(line, progress)
85
- if line =~ /\(error \"(.*)\"\)/ then
86
- progress.print_line("WARN: Solver reported ERROR: #{$1}")
87
- end
88
- end
89
-
90
-
91
- def from_solver(progress)
92
- result = @solver.z3outAndErr.gets(chomp: true)
93
- checkLineForSolverError(result, progress)
94
- @protocol.puts("; Solver Response: #{result}")
95
- return result
96
- end
97
-
98
- def replace_strings(line)
99
- line.gsub(/"[^"]*"/, "")
100
- end
101
-
102
- def read_multiline_from_solver()
103
- result = ""
104
- for_counting = ""
105
- @protocol.puts
106
- @protocol.puts("; BEGIN Multiline Solver Response:")
107
- line = @solver.z3outAndErr.gets(chomp: true)
108
- result += line
109
- for_counting = replace_strings(line)
110
- @protocol.puts("; #{line}")
111
-
112
- num_opening = for_counting.count("(")
113
- num_closing = for_counting.count(")")
114
- while num_opening > num_closing && line != nil do
115
- line = @solver.z3outAndErr.gets(chomp: true)
116
- if line
117
- result += line
118
- for_counting = replace_strings(line)
119
- @protocol.puts("; #{line}")
120
-
121
- num_opening += for_counting.count("(")
122
- num_closing += for_counting.count(")")
123
- end
124
- end
125
- @protocol.puts("; END Multiline Solver Response")
126
- return result
127
- end
128
-
129
- def writeProtocolToFile(progress, filename)
130
- dir = File.dirname(filename)
131
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
132
- FileUtils.cp(@protocol_filename, filename) unless @protocol_filename == filename
133
- progress.print_line "Solver protocol written to file #{filename}"
134
- end
135
-
136
- def generateNextAssertionIDAndAssociateWith(hash)
137
- nextAssertionID = "a#{@assertCounter += 1}"
138
- @associations[nextAssertionID] = hash
139
- return nextAssertionID
140
- end
141
-
142
- def associateAssertionIDWith(assertionID, hash)
143
- if @associations.has_key?(assertionID) then
144
- raise RuntimeError.new("AssertionID #{assertionID.inspect} already has info associated with it!")
145
- end
146
- @associations[assertionID] = hash
147
- end
148
-
149
- def getAssociationForAssertionID(assertionID)
150
- return @associations[assertionID]
151
- end
152
-
153
- def for_each_const(&block)
154
- @consts.each do |key, value|
155
- block.call(key, value)
156
- end
157
- end
158
-
159
- def get_const_info(name)
160
- @consts[name]
161
- end
162
-
163
- def parse_unsat_core(unsat_core)
164
- @solver.parse_unsat_core(unsat_core, self)
165
- end
166
-
167
- def handleSolverError(progress, options, result)
168
- @solver.handleSolverError(progress, self, options, result)
169
- end
170
- end
171
-
172
- class SolverFactory
173
-
174
- attr_reader :z3path, :z3version
175
-
176
- def initialize(progress, z3path)
177
- @progress = progress
178
- z3version = nil
179
- exitstatus = nil
180
- begin
181
- z3version = `#{z3path} --version`.chomp()
182
- exitstatus = $?.exitstatus
183
- rescue Errno::ENOENT
184
- end
185
- if exitstatus == 0 && z3version != nil then
186
- progress.print_line "INFO: Using #{z3version} found in z3path=#{z3path}"
187
- @z3path = z3path
188
- @z3version = z3version
189
- else
190
- progress.print_line "WARN: could not find z3 in z3path=#{z3path.inspect}"
191
- `which z3`
192
- if $?.exitstatus == 0 then
193
- z3version = `z3 --version`.chomp()
194
- progress.print_line "However, #{z3version} is availlable on the PATH. Using this one."
195
- @z3path = 'z3'
196
- @z3version = z3version
197
- else
198
- raise RuntimeError.new("ERROR: unable to find Z3 solver. Exiting! (Please provide its location in the config file using the key :z3path)")
199
- end
200
- end
201
- end
202
-
203
- def new_solver()
204
- Z3Solver.new(@progress, @z3path)
205
- end
206
-
207
- def query(options, &block)
208
- unless options[:solverLog]
209
- raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
210
- end
211
- @progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
212
-
213
- session = SolverSession.new(self, options[:solverLog])
214
- block.call(session)
215
-
216
- session.to_solver(@progress, "(check-sat)")
217
- result = session.from_solver(@progress)
218
- explanation = true
219
- if result == "sat"
220
- # model is not contradictory -> continue
221
- elsif result == "unsat"
222
- # model is contradictory -> add a message
223
-
224
- session.to_solver(@progress, "(get-unsat-core)")
225
- unsat_core = session.from_solver(@progress)
226
- explanation = session.parse_unsat_core(unsat_core)
227
- else
228
- session.handleSolverError(@progress, options, result)
229
- end
230
-
231
- return explanation
232
- end
233
-
234
- # main entry point!
235
- def query_model(options, &block)
236
- unless options[:solverLog]
237
- raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
238
- end
239
- @progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
240
- session = SolverSession.new(self, options[:solverLog])
241
-
242
- block.call(session)
243
- session.to_solver(@progress, "(check-sat)")
244
- result = session.from_solver(@progress)
245
- if result.chomp == "sat"
246
- # model is not contradictory -> extract model
247
- const_list = []
248
- const_info = {}
249
- session.for_each_const do |name, info|
250
- const_list << name
251
- const_info[name] = info
252
- end
253
- session.to_solver(@progress, "(get-value (#{const_list.join(" ")}))")
254
- model_str = session.read_multiline_from_solver()
255
- model = {}
256
- Z3Solver.foreach_field_in_model_str(model_str, const_info) do |varname, value, info|
257
- unless info then
258
- @progress.print_line "WARN: no info availlable for varname=#{varname.inspect}, value=#{value.inspect}"
259
- end
260
- xpath = info[:xpath]
261
- if model.has_key?(xpath) then
262
- if varname.end_with?("-filled") then
263
- unless model[xpath][:type] == :field then
264
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a field, not a #{model[xpath][:type]}.")
265
- end
266
- unless value == "true" or value == "false" then
267
- raise RuntimeError.new("inconsistent model value: filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
268
- end
269
- if value == "true" then
270
- model[xpath][:filled] = true
271
- else
272
- model[xpath][:filled] = false
273
- end
274
- elsif varname.end_with?("-value") then
275
- unless model[xpath][:type] == :field || model[xpath][:type] == :list then
276
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a field or a list, not a #{model[xpath][:type]}.")
277
- end
278
- if model[xpath][:type] == :field then
279
- model[xpath][:value] = convert_solver_value(info[:type], info[:datatype], value, xpath)
280
- elsif model[xpath][:type] == :list then
281
- unless varname =~ /-index-(\d+)-value$/
282
- raise RuntimeError.new("inconsistent model: xpath #{xpath} is a list value and hence its varname should end like -index-X-value, not #{varname.inspect}.")
283
- end
284
- index = Integer($1)
285
- model[xpath][:xpath_element] = info[:xpath_element]
286
- if model[xpath].has_key?(:value) then
287
- val = convert_solver_value(info[:type], info[:datatype], value, xpath)
288
- model[xpath][:value].insert(index, val)
289
- else
290
- val = convert_solver_value(info[:type], info[:datatype], value, xpath)
291
- model[xpath][:value] = [val]
292
- end
293
- end
294
- elsif varname.end_with?("-size") then
295
- unless model[xpath][:type] == :list || model[xpath][:type] == :field then
296
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a list or a field, not a #{model[xpath][:type].inspect}.")
297
- end
298
- if model[xpath][:type] == :field then
299
- model[xpath][:type] = :list
300
- end
301
- model[xpath][:size] = Integer(value)
302
- elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
303
- unless model[xpath][:type] == :struct then
304
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a struct, not a #{model[xpath][:type]}.")
305
- end
306
- model[xpath][:exists] = value
307
- end
308
- else
309
- if varname.end_with?("-filled") then
310
- unless value == "true" or value == "false" then
311
- raise RuntimeError.new("inconsistent model value: filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
312
- end
313
- if value == "true" then
314
- model[xpath] = { :type => :field, :filled => true }
315
- else
316
- model[xpath] = { :type => :field, :filled => false }
317
- end
318
- elsif varname.end_with?("-value") then
319
- check_type(info[:type], value, xpath)
320
- model[xpath] = { :type => :field, :value => value }
321
- elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
322
- unless value == "true" or value == "false" then
323
- raise RuntimeError.new("inconsistent model value: exists-value of struct #{xpath} should be a Bool, but was #{value.inspect}.")
324
- end
325
- model[xpath] = { :type => :struct, :exists => value }
326
- elsif varname.end_with?("-size") then
327
- model[xpath] = { :type => :list, :size => value }
328
- end
329
- end
330
- end
331
- return model
332
- elsif result.chomp == "unsat"
333
- # model is contradictory -> add a message
334
-
335
- session.to_solver(@progress, "(get-unsat-core)")
336
- unsat_core = session.from_solver(@progress)
337
- session.writeProtocolToFile(@progress, options[:solverLog])
338
- @progress.print_line "Solver reported a contradiction! please see #{options[:solverLog]} for further information."
339
- return nil
340
- else
341
- session.handleSolverError(@progress, options, result)
342
- end
343
-
344
- return nil
345
- end
346
-
347
- def convert_solver_value(type, datatype, value, xpath)
348
- check_type(type, value, xpath)
349
- if datatype == :date then
350
- return (Time.at(0).to_date + Integer(value)).to_s
351
- elsif datatype == :timestamp then
352
- return Time.at(Integer(value)).to_s
353
- else
354
- return value
355
- end
356
- end
357
-
358
- def check_type(type, value, xpath)
359
- if type == "String" then
360
- unless value =~ /\".*\"/ then
361
- raise RuntimeError.new("inconsistent model value: model value for #{xpath} should be a String, but was #{value}.")
362
- end
363
- elsif type == "Bool"
364
- unless value == "true" or value == "false" then
365
- raise RuntimeError.new("inconsistent model value for #{xpath} should be a Bool, but was #{value.inspect}.")
366
- end
367
- elsif type == "Int"
368
- unless value =~ /\d+/ then
369
- raise RuntimeError.new("inconsistent model value: model value for #{xpath} should be a Int, but was #{value}.")
370
- end
371
- else
372
- raise RuntimeError.new("encountered unknown solver type: type of solver-var for #{xpath} is #{info[:type]}.")
373
- end
374
- end
375
-
376
- end
377
-
378
- class Z3Solver
379
-
380
- attr_reader :z3in, :z3outAndErr
381
-
382
- def initialize(progress, z3path)
383
- @progress = progress
384
- @z3in, @z3outAndErr, @z3thr = Open3.popen2e(z3path, '-in', '-smt2')
385
- Process.detach(@z3thr.pid)
386
- @z3in.sync = true
387
- @sessionIdCounter = 0
388
- end
389
-
390
- def close()
391
- begin
392
- @z3in.close() unless @z3in.closed?
393
- rescue Errno::EINVAL
394
- end
395
- begin
396
- @z3outAndErr.close() unless @z3outAndErr.closed?
397
- rescue Errno::EINVAL
398
- end
399
- Thread.kill(@z3thr)
400
- end
401
-
402
- def display_assertion(assertion, session)
403
- if assertion =~ /validation_rule_(.*)_instance_(.*)/ then
404
- return "Validation rule #{$1} of structure #{$2.gsub("_", "/")}"
405
- else
406
- info = session.getAssociationForAssertionID(assertion)
407
- if info then
408
- if info[:assertion_type] == :doc_field_filled then
409
- return "Field #{info[:xpath]} contains value #{info[:value].inspect} in document"
410
- elsif info[:assertion_type] == :doc_field_empty then
411
- return "Field #{info[:xpath]} is empty in document"
412
- elsif info[:assertion_type] == :doc_structure_exists then
413
- return "Structure #{info[:xpath]} exists in document"
414
- elsif info[:assertion_type] == :doc_structure_not_exists then
415
- return "Structure #{info[:xpath]} does not exist in document"
416
- elsif info[:assertion_type] == :constraint then
417
- if info[:constraint_type] == :min then
418
- return "Field #{info[:xpath]} has a minimum value of #{info[:value]}."
419
- elsif info[:constraint_type] == :max then
420
- return "Field #{info[:xpath]} has a maximum value of #{info[:value]}."
421
- elsif info[:constraint_type] == :required then
422
- return "Field #{info[:xpath]} is required."
423
- else
424
- p info
425
- raise RuntimeError.new("encountered unknown :constraint_type #{info[:constraint_type].inspect} for assertionID #{assertion.inspect}")
426
- end
427
- else
428
- raise RuntimeError.new("encountered unknown :assertion_type #{info[:assertion_type].inspect} for assertionID #{assertion.inspect}")
429
- end
430
- else
431
- return assertion
432
- end
433
- end
434
- end
435
-
436
- def parse_unsat_core(unsat_core, session)
437
- unless unsat_core =~ /\((.*)\)/ then
438
- raise RuntimeError.new("could not parse unsat core: #{unsat_core.inspect}")
439
- end
440
- list = $1.split(" ")
441
-
442
- result = "EXPLANATION:\n"
443
- result += "The following values\n"
444
-
445
- list.filter{|a| a.start_with?("doc-")}.each do |assertion|
446
- result += "- #{display_assertion(assertion, session)} (#{assertion})\n"
447
- end
448
- result += "\nviolate the following constraints\n"
449
- list.filter{|a| !a.start_with?("doc-")}.each do |assertion|
450
- result += "- #{display_assertion(assertion, session)} (#{assertion})\n"
451
- end
452
- return result
453
- end
454
-
455
- def self.find_matching_bracket(str)
456
- i = 0
457
- open = 0
458
- in_str = false
459
- while open <= 0 do
460
- if str[i] == '(' and !in_str then
461
- open += 1
462
- elsif str[i] == '"' then
463
- in_str = !in_str
464
- end
465
- i += 1
466
- end
467
- while open > 0 do
468
- if str[i] == '(' && !in_str then
469
- open += 1
470
- elsif str[i] == ')' && !in_str then
471
- open -= 1
472
- elsif str[i] == '"' then
473
- in_str = !in_str
474
- end
475
- i += 1
476
- end
477
- str[0,i]
478
- end
479
-
480
- def self.foreach_field_in_model_str(str, const_info, &block)
481
- unless str =~ /^\((.*)\)$/m
482
- raise RuntimeError.new("could not parse model: #{str.inspect}")
483
- end
484
- str = $1
485
-
486
- while str.size() > 0 do
487
- line = Z3Solver.find_matching_bracket(str)
488
- str = str[line.size()..]
489
- unless line =~ /\(([^\ ]*)\ (.*)\)/
490
- raise RuntimeError.new("could not parse model line: #{line.inspect}")
491
- end
492
- varname = $1
493
- value = $2
494
- if value =~ /\(-\ (\d+)\)/ then
495
- simplified = "-#{$1}"
496
- value = simplified
497
- end
498
- info = const_info[varname]
499
-
500
- block.call(varname, value, info)
501
- end
502
-
503
- return nil
504
- end
505
-
506
- def handleSolverError(progress, session, options, result)
507
- session.writeProtocolToFile(progress, options[:solverLog])
508
- raise RuntimeError.new("Solver Error: Solver returned '#{result}', for more information see #{options[:solverLog]}")
509
- end
510
-
511
- end
512
-
513
-
1
+ #!/usr/bin/ruby
2
+
3
+ require 'open3'
4
+
5
+
6
+ class SolverSession
7
+
8
+ def initialize(solverFactory, logFileName)
9
+ @solverFactory = solverFactory
10
+ @solver = @solverFactory.new_solver()
11
+ @protocol_filename = logFileName
12
+ @protocol = File.open(@protocol_filename, "w")
13
+ @assertCounter = -1
14
+ @associations = {}
15
+ @consts = {}
16
+ end
17
+
18
+ def switch_solver(progress)
19
+ @solver.close()
20
+ @solver = @solverFactory.new_solver()
21
+ # re-feed the session protocol into the new solver to get it up-to-date
22
+ @protocol.close()
23
+ File.open(@protocol_filename, "r") do |protocol|
24
+ protocol.each_line do |line|
25
+ to_solver(progress, line)
26
+ end
27
+ end
28
+ @protocol = File.open(@protocol_filename, "a")
29
+ end
30
+
31
+ def emptyReadBuffer(progress)
32
+ empty = false
33
+ while !empty do
34
+ begin
35
+ c = ""
36
+ @solver.z3outAndErr.read_nonblock(1, c)
37
+ line = c + @solver.z3outAndErr.gets
38
+ checkLineForSolverError(line, progress)
39
+ @protocol.puts("; Solver Response: #{line}")
40
+ rescue Errno::EWOULDBLOCK
41
+ # there is nothing to read -> just continue
42
+ empty = true
43
+ rescue Errno::EAGAIN
44
+ # there is nothing to read -> just continue
45
+ empty = true
46
+ rescue EOFError
47
+ # there is nothing to read -> just continue
48
+ empty = true
49
+ end
50
+ end
51
+ end
52
+
53
+ def declare_const(progress, name, type, info)
54
+ info[:type] = type
55
+ @consts[name] = info
56
+ to_solver(progress, "(declare-const #{name} #{type})")
57
+ end
58
+
59
+ def to_solver(progress, line)
60
+ emptyReadBuffer(progress);
61
+ success = false
62
+ while (!success) do
63
+ begin
64
+ @solver.z3in.puts(line)
65
+ success = true
66
+ rescue Errno::EINVAL => e
67
+ progress.print_line("DEBUG: Invalid argument to puts(): #{line.inspect}")
68
+ progress.print_line("An Error occured: #{e.message}")
69
+ e.backtrace.each { |stline| progress.print_line(stline) }
70
+ progress.print_line("=> Switching Solver")
71
+ switch_solver(progress)
72
+ progress.print_line("=> Switch successful. Trying again.")
73
+ rescue Errno::EPIPE => e
74
+ progress.print_line("An Error occured: #{e.message}")
75
+ e.backtrace.each { |stline| progress.print_line(stline) }
76
+ progress.print_line("=> Switching Solver")
77
+ switch_solver(progress)
78
+ progress.print_line("=> Switch successful. Trying again.")
79
+ end
80
+ end
81
+ @protocol.puts(line)
82
+ end
83
+
84
+ def checkLineForSolverError(line, progress)
85
+ if line =~ /\(error \"(.*)\"\)/ then
86
+ progress.print_line("WARN: Solver reported ERROR: #{$1}")
87
+ end
88
+ end
89
+
90
+
91
+ def from_solver(progress)
92
+ result = @solver.z3outAndErr.gets(chomp: true)
93
+ checkLineForSolverError(result, progress)
94
+ @protocol.puts("; Solver Response: #{result}")
95
+ return result
96
+ end
97
+
98
+ def replace_strings(line)
99
+ line.gsub(/"[^"]*"/, "")
100
+ end
101
+
102
+ def read_multiline_from_solver()
103
+ result = ""
104
+ for_counting = ""
105
+ @protocol.puts
106
+ @protocol.puts("; BEGIN Multiline Solver Response:")
107
+ line = @solver.z3outAndErr.gets(chomp: true)
108
+ result += line
109
+ for_counting = replace_strings(line)
110
+ @protocol.puts("; #{line}")
111
+
112
+ num_opening = for_counting.count("(")
113
+ num_closing = for_counting.count(")")
114
+ while num_opening > num_closing && line != nil do
115
+ line = @solver.z3outAndErr.gets(chomp: true)
116
+ if line
117
+ result += "#{line}\n"
118
+ for_counting = replace_strings(line)
119
+ @protocol.puts("; #{line}")
120
+
121
+ num_opening += for_counting.count("(")
122
+ num_closing += for_counting.count(")")
123
+ end
124
+ end
125
+ @protocol.puts("; END Multiline Solver Response")
126
+ return result
127
+ end
128
+
129
+ def writeProtocolToFile(progress, filename)
130
+ dir = File.dirname(filename)
131
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
132
+ FileUtils.cp(@protocol_filename, filename) unless @protocol_filename == filename
133
+ progress.print_line "Solver protocol written to file #{filename}"
134
+ end
135
+
136
+ def generateNextAssertionIDAndAssociateWith(hash)
137
+ nextAssertionID = "a#{@assertCounter += 1}"
138
+ @associations[nextAssertionID] = hash
139
+ return nextAssertionID
140
+ end
141
+
142
+ def associateAssertionIDWith(assertionID, hash)
143
+ if @associations.has_key?(assertionID) then
144
+ raise RuntimeError.new("AssertionID #{assertionID.inspect} already has info associated with it!")
145
+ end
146
+ @associations[assertionID] = hash
147
+ end
148
+
149
+ def getAssociationForAssertionID(assertionID)
150
+ return @associations[assertionID]
151
+ end
152
+
153
+ def for_each_const(&block)
154
+ @consts.each do |key, value|
155
+ block.call(key, value)
156
+ end
157
+ end
158
+
159
+ def get_const_info(name)
160
+ @consts[name]
161
+ end
162
+
163
+ def parse_unsat_core(unsat_core)
164
+ @solver.parse_unsat_core(unsat_core, self)
165
+ end
166
+
167
+ def handleSolverError(progress, options, result)
168
+ ModelUtils.handleSolverError(progress, self, options, result)
169
+ end
170
+ end
171
+
172
+ class SolverFactory
173
+
174
+ attr_reader :z3path, :z3version
175
+
176
+ def initialize(progress, z3path)
177
+ @progress = progress
178
+ z3version = nil
179
+ exitstatus = nil
180
+ begin
181
+ z3version = `#{z3path} --version`.chomp()
182
+ exitstatus = $?.exitstatus
183
+ rescue Errno::ENOENT
184
+ end
185
+ if exitstatus == 0 && z3version != nil then
186
+ progress.print_line "INFO: Using #{z3version} found in z3path=#{z3path}"
187
+ @z3path = z3path
188
+ @z3version = z3version
189
+ else
190
+ progress.print_line "WARN: could not find z3 in z3path=#{z3path.inspect}"
191
+ `which z3`
192
+ if $?.exitstatus == 0 then
193
+ z3version = `z3 --version`.chomp()
194
+ progress.print_line "However, #{z3version} is availlable on the PATH. Using this one."
195
+ @z3path = 'z3'
196
+ @z3version = z3version
197
+ else
198
+ raise RuntimeError.new("ERROR: unable to find Z3 solver. Exiting! (Please provide its location in the config file using the key :z3path)")
199
+ end
200
+ end
201
+ end
202
+
203
+ def new_solver()
204
+ Z3Solver.new(@progress, @z3path)
205
+ end
206
+
207
+ def query(options, &block)
208
+ unless options[:solverLog]
209
+ raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
210
+ end
211
+ @progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
212
+
213
+ session = SolverSession.new(self, options[:solverLog])
214
+ block.call(session)
215
+
216
+ session.to_solver(@progress, "(check-sat)")
217
+ result = session.from_solver(@progress)
218
+ explanation = true
219
+ if result == "sat"
220
+ # model is not contradictory -> continue
221
+ elsif result == "unsat"
222
+ # model is contradictory -> add a message
223
+
224
+ session.to_solver(@progress, "(get-unsat-core)")
225
+ unsat_core = session.from_solver(@progress)
226
+ explanation = session.parse_unsat_core(unsat_core)
227
+ else
228
+ ModelUtils.handleSolverError(@progress, options, result)
229
+ end
230
+
231
+ return explanation
232
+ end
233
+
234
+ def display_assertion(assertion, solver)
235
+ if assertion =~ /validation_rule_(.*)_instance_(.*)/ then
236
+ return "Validation rule #{$1} of structure #{$2.gsub("_", "/")}"
237
+ else
238
+ info = solver.getAssociationForAssertionID(assertion)
239
+ if info then
240
+ if info[:assertion_type] == :doc_field_filled then
241
+ return "Field #{info[:xpath]} contains value #{info[:value].inspect} in document"
242
+ elsif info[:assertion_type] == :doc_field_empty then
243
+ return "Field #{info[:xpath]} is empty in document"
244
+ elsif info[:assertion_type] == :doc_structure_exists then
245
+ return "Structure #{info[:xpath]} exists in document"
246
+ elsif info[:assertion_type] == :doc_structure_not_exists then
247
+ return "Structure #{info[:xpath]} does not exist in document"
248
+ elsif info[:assertion_type] == :constraint then
249
+ if info[:constraint_type] == :min then
250
+ return "Field #{info[:xpath]} has a minimum value of #{info[:value]}."
251
+ elsif info[:constraint_type] == :max then
252
+ return "Field #{info[:xpath]} has a maximum value of #{info[:value]}."
253
+ elsif info[:constraint_type] == :required then
254
+ return "Field #{info[:xpath]} is required."
255
+ elsif info[:constraint_type] == :values then
256
+ return "Field #{info[:xpath]} is an enum field and can only contain the values #{info[:values].inspect}."
257
+ else
258
+ p info
259
+ raise RuntimeError.new("encountered unknown :constraint_type #{info[:constraint_type].inspect} for assertionID #{assertion.inspect}")
260
+ end
261
+ else
262
+ raise RuntimeError.new("encountered unknown :assertion_type #{info[:assertion_type].inspect} for assertionID #{assertion.inspect}")
263
+ end
264
+ else
265
+ return assertion
266
+ end
267
+ end
268
+ end
269
+
270
+ def parse_unsat_core(solver, unsat_core)
271
+ # TODO: implement
272
+ unless unsat_core =~ /\((.*)\)/ then
273
+ raise RuntimeError.new("could not parse unsat core: #{unsat_core.inspect}")
274
+ end
275
+ list = $1.split(" ")
276
+
277
+ result = "EXPLANATION:\n"
278
+ result += "The following values\n"
279
+
280
+ list.filter{|a| a.start_with?("doc-")}.each do |assertion|
281
+ result += "- #{display_assertion(assertion, solver)} (#{assertion})\n"
282
+ end
283
+ result += "\nviolate the following constraints\n"
284
+ list.filter{|a| !a.start_with?("doc-")}.each do |assertion|
285
+ result += "- #{display_assertion(assertion, solver)} (#{assertion})\n"
286
+ end
287
+ return result
288
+ end
289
+
290
+ # main entry point!
291
+ def query_model(options, &block)
292
+ unless options[:solverLog]
293
+ raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
294
+ end
295
+ @progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
296
+ session = SolverSession.new(self, options[:solverLog])
297
+
298
+ block.call(session)
299
+ session.to_solver(@progress, "(check-sat)")
300
+ result = session.from_solver(@progress)
301
+ if result.chomp == "sat"
302
+ # model is not contradictory -> extract model
303
+ const_list = []
304
+ const_info = {}
305
+ session.for_each_const do |name, info|
306
+ const_list << name
307
+ const_info[name] = info
308
+ end
309
+ session.to_solver(@progress, "(get-value (#{const_list.join(" ")}))")
310
+ model_str = session.read_multiline_from_solver()
311
+ model = {}
312
+ ModelUtils.foreach_field_in_model_str(model_str, const_info) do |varname, value, info|
313
+ unless info then
314
+ @progress.print_line "WARN: no info availlable for varname=#{varname.inspect}, value=#{value.inspect}"
315
+ end
316
+ xpath = info[:xpath]
317
+ if model.has_key?(xpath) then
318
+ if varname.end_with?("-filled") then
319
+ unless model[xpath][:type] == :field then
320
+ raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a field, not a #{model[xpath][:type]}.")
321
+ end
322
+ unless value == "true" or value == "false" then
323
+ raise RuntimeError.new("inconsistent model value: filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
324
+ end
325
+ if value == "true" then
326
+ model[xpath][:filled] = true
327
+ else
328
+ model[xpath][:filled] = false
329
+ end
330
+ elsif varname.end_with?("-value") then
331
+ unless model[xpath][:type] == :field || model[xpath][:type] == :list then
332
+ raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a field or a list, not a #{model[xpath][:type]}.")
333
+ end
334
+ if model[xpath][:type] == :field then
335
+ model[xpath][:value] = convert_solver_value(info[:type], info[:datatype], value, xpath)
336
+ elsif model[xpath][:type] == :list then
337
+ unless varname =~ /-index-(\d+)-value$/
338
+ raise RuntimeError.new("inconsistent model: xpath #{xpath} is a list value and hence its varname should end like -index-X-value, not #{varname.inspect}.")
339
+ end
340
+ index = Integer($1)
341
+ model[xpath][:xpath_element] = info[:xpath_element]
342
+ if model[xpath].has_key?(:value) then
343
+ val = convert_solver_value(info[:type], info[:datatype], value, xpath)
344
+ model[xpath][:value].insert(index, val)
345
+ else
346
+ val = convert_solver_value(info[:type], info[:datatype], value, xpath)
347
+ model[xpath][:value] = [val]
348
+ end
349
+ end
350
+ elsif varname.end_with?("-size") then
351
+ unless model[xpath][:type] == :list || model[xpath][:type] == :field then
352
+ raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a list or a field, not a #{model[xpath][:type].inspect}.")
353
+ end
354
+ if model[xpath][:type] == :field then
355
+ model[xpath][:type] = :list
356
+ end
357
+ model[xpath][:size] = Integer(value)
358
+ elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
359
+ unless model[xpath][:type] == :struct then
360
+ raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a struct, not a #{model[xpath][:type]}.")
361
+ end
362
+ model[xpath][:exists] = value
363
+ end
364
+ else
365
+ if varname.end_with?("-filled") then
366
+ unless value == "true" or value == "false" then
367
+ raise RuntimeError.new("inconsistent model value: filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
368
+ end
369
+ if value == "true" then
370
+ model[xpath] = { :type => :field, :filled => true }
371
+ else
372
+ model[xpath] = { :type => :field, :filled => false }
373
+ end
374
+ elsif varname.end_with?("-value") then
375
+ check_type(info[:type], value, xpath)
376
+ model[xpath] = { :type => :field, :value => value }
377
+ elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
378
+ unless value == "true" or value == "false" then
379
+ raise RuntimeError.new("inconsistent model value: exists-value of struct #{xpath} should be a Bool, but was #{value.inspect}.")
380
+ end
381
+ model[xpath] = { :type => :struct, :exists => value }
382
+ elsif varname.end_with?("-size") then
383
+ model[xpath] = { :type => :list, :size => value }
384
+ end
385
+ end
386
+ end
387
+ return model
388
+ elsif result.chomp == "unsat"
389
+ # model is contradictory -> add a message
390
+
391
+ session.to_solver(@progress, "(get-unsat-core)")
392
+ unsat_core = session.from_solver(@progress)
393
+ session.writeProtocolToFile(@progress, options[:solverLog])
394
+ @progress.print_line "Solver reported a contradiction! please see #{options[:solverLog]} for further information."
395
+ return nil
396
+ else
397
+ ModelUtils.handleSolverError(@progress, session, options, result)
398
+ end
399
+
400
+ return nil
401
+ end
402
+
403
+ def convert_solver_value(type, datatype, value, xpath)
404
+ check_type(type, value, xpath)
405
+ if datatype == :date then
406
+ return (Time.at(0).to_date + Integer(value)).to_s
407
+ elsif datatype == :timestamp then
408
+ return Time.at(Integer(value)).to_s
409
+ else
410
+ return value
411
+ end
412
+ end
413
+
414
+ def check_type(type, value, xpath)
415
+ if type == "String" then
416
+ unless value =~ /\".*\"/ then
417
+ raise RuntimeError.new("inconsistent model value: model value for #{xpath} should be a String, but was #{value}.")
418
+ end
419
+ elsif type == "Bool"
420
+ unless value == "true" or value == "false" then
421
+ raise RuntimeError.new("inconsistent model value for #{xpath} should be a Bool, but was #{value.inspect}.")
422
+ end
423
+ elsif type == "Int"
424
+ unless value =~ /\d+/ then
425
+ raise RuntimeError.new("inconsistent model value: model value for #{xpath} should be a Int, but was #{value}.")
426
+ end
427
+ else
428
+ raise RuntimeError.new("encountered unknown solver type: type of solver-var for #{xpath} is #{info[:type]}.")
429
+ end
430
+ end
431
+
432
+ end
433
+
434
+ class Z3Solver
435
+
436
+ attr_reader :z3in, :z3outAndErr
437
+
438
+ def initialize(progress, z3path)
439
+ @progress = progress
440
+ @z3in, @z3outAndErr, @z3thr = Open3.popen2e(z3path, '-in', '-smt2')
441
+ Process.detach(@z3thr.pid)
442
+ @z3in.sync = true
443
+ @sessionIdCounter = 0
444
+ end
445
+
446
+ def close()
447
+ begin
448
+ @z3in.close() unless @z3in.closed?
449
+ rescue Errno::EINVAL
450
+ end
451
+ begin
452
+ @z3outAndErr.close() unless @z3outAndErr.closed?
453
+ rescue Errno::EINVAL
454
+ end
455
+ Thread.kill(@z3thr)
456
+ end
457
+
458
+ def display_assertion(assertion, session)
459
+ if assertion =~ /validation_rule_(.*)_instance_(.*)/ then
460
+ return "Validation rule #{$1} of structure #{$2.gsub("_", "/")}"
461
+ else
462
+ info = session.getAssociationForAssertionID(assertion)
463
+ if info then
464
+ if info[:assertion_type] == :doc_field_filled then
465
+ return "Field #{info[:xpath]} contains value #{info[:value].inspect} in document"
466
+ elsif info[:assertion_type] == :doc_field_empty then
467
+ return "Field #{info[:xpath]} is empty in document"
468
+ elsif info[:assertion_type] == :doc_structure_exists then
469
+ return "Structure #{info[:xpath]} exists in document"
470
+ elsif info[:assertion_type] == :doc_structure_not_exists then
471
+ return "Structure #{info[:xpath]} does not exist in document"
472
+ elsif info[:assertion_type] == :constraint then
473
+ if info[:constraint_type] == :min then
474
+ return "Field #{info[:xpath]} has a minimum value of #{info[:value]}."
475
+ elsif info[:constraint_type] == :max then
476
+ return "Field #{info[:xpath]} has a maximum value of #{info[:value]}."
477
+ elsif info[:constraint_type] == :required then
478
+ return "Field #{info[:xpath]} is required."
479
+ else
480
+ p info
481
+ raise RuntimeError.new("encountered unknown :constraint_type #{info[:constraint_type].inspect} for assertionID #{assertion.inspect}")
482
+ end
483
+ else
484
+ raise RuntimeError.new("encountered unknown :assertion_type #{info[:assertion_type].inspect} for assertionID #{assertion.inspect}")
485
+ end
486
+ else
487
+ return assertion
488
+ end
489
+ end
490
+ end
491
+
492
+ def parse_unsat_core(unsat_core, session)
493
+ unless unsat_core =~ /\((.*)\)/ then
494
+ raise RuntimeError.new("could not parse unsat core: #{unsat_core.inspect}")
495
+ end
496
+ list = $1.split(" ")
497
+
498
+ result = "EXPLANATION:\n"
499
+ result += "The following values\n"
500
+
501
+ list.filter{|a| a.start_with?("doc-")}.each do |assertion|
502
+ result += "- #{display_assertion(assertion, session)} (#{assertion})\n"
503
+ end
504
+ result += "\nviolate the following constraints\n"
505
+ list.filter{|a| !a.start_with?("doc-")}.each do |assertion|
506
+ result += "- #{display_assertion(assertion, session)} (#{assertion})\n"
507
+ end
508
+ return result
509
+ end
510
+
511
+ def self.find_matching_bracket(str)
512
+ i = 0
513
+ open = 0
514
+ in_str = false
515
+ while open <= 0 do
516
+ if str[i] == '(' and !in_str then
517
+ open += 1
518
+ elsif str[i] == '"' then
519
+ in_str = !in_str
520
+ end
521
+ i += 1
522
+ end
523
+ while open > 0 do
524
+ if str[i] == '(' && !in_str then
525
+ open += 1
526
+ elsif str[i] == ')' && !in_str then
527
+ open -= 1
528
+ elsif str[i] == '"' then
529
+ in_str = !in_str
530
+ end
531
+ i += 1
532
+ end
533
+ str[0,i]
534
+ end
535
+
536
+ end
537
+
538
+ class ModelUtils
539
+
540
+ def self.foreach_field_in_model_str(str, const_info, &block)
541
+ unless str =~ /^\((.*)\)$/m
542
+ raise RuntimeError.new("could not parse model: #{str.inspect}")
543
+ end
544
+ str = $1
545
+
546
+ while str.size() > 0 do
547
+ line = Z3Solver.find_matching_bracket(str)
548
+ str = str[line.size()..]
549
+ unless line =~ /\(([^\ ]*)\ (.*)\)/
550
+ raise RuntimeError.new("could not parse model line: #{line.inspect}")
551
+ end
552
+ varname = $1
553
+ value = $2
554
+ if value =~ /\(-\ (\d+)\)/ then
555
+ simplified = "-#{$1}"
556
+ value = simplified
557
+ end
558
+ info = const_info[varname]
559
+
560
+ block.call(varname, value, info)
561
+ end
562
+
563
+ return nil
564
+ end
565
+
566
+ def self.handleSolverError(progress, session, options, result)
567
+ session.writeProtocolToFile(progress, options[:solverLog])
568
+ raise RuntimeError.new("Solver Error: Solver returned '#{result}', for more information see #{options[:solverLog]}")
569
+ end
570
+
571
+ end
572
+