mbt-gen 0.0.3 → 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 -1958
  3. data/lib/progress.rb +53 -5
  4. data/lib/solver-lib.rb +572 -433
  5. metadata +6 -3
data/lib/solver-lib.rb CHANGED
@@ -1,433 +1,572 @@
1
- #!/usr/bin/ruby
2
-
3
- require 'open3'
4
-
5
-
6
- class SolverSession
7
-
8
- def initialize(z3in, z3outAndErr, logFileName)
9
- @z3in = z3in
10
- @z3outAndErr = z3outAndErr
11
- @protocol_filename = logFileName
12
- @protocol = File.open(@protocol_filename, "w")
13
- @assertCounter = -1
14
- @associations = {}
15
- @consts = {}
16
- end
17
-
18
- def emptyReadBuffer(progress)
19
- empty = false
20
- while !empty do
21
- begin
22
- c = ""
23
- @z3outAndErr.read_nonblock(1, c)
24
- line = c + @z3outAndErr.gets
25
- checkLineForSolverError(line, progress)
26
- @protocol.puts("; Solver Response: #{line}")
27
- rescue Errno::EWOULDBLOCK
28
- # there is nothing to read -> just continue
29
- empty = true
30
- rescue Errno::EAGAIN
31
- # there is nothing to read -> just continue
32
- empty = true
33
- rescue EOFError
34
- # there is nothing to read -> just continue
35
- empty = true
36
- end
37
- end
38
- end
39
-
40
- def declare_const(progress, name, type, info)
41
- info[:type] = type
42
- @consts[name] = info
43
- to_solver(progress, "(declare-const #{name} #{type})")
44
- end
45
-
46
- def to_solver(progress, line)
47
- emptyReadBuffer(progress);
48
- @protocol.puts(line)
49
- begin
50
- @z3in.puts(line)
51
- rescue Errno::EINVAL
52
- progress.print_line "ERROR: could not write the line #{line.inspect} to the solver."
53
- writeProtocolToFile("failure.smt2")
54
- rescue Errno::EPIPE
55
- writeProtocolToFile("failure.smt2")
56
- throw RuntimeError.new("Broken Pipe")
57
- end
58
- end
59
-
60
- def checkLineForSolverError(line, progress)
61
- if line =~ /\(error \"(.*)\"\)/ then
62
- progress.print_line("WARN: Solver reported ERROR: #{$1}")
63
- end
64
- end
65
-
66
-
67
- def from_solver(progress)
68
- result = @z3outAndErr.gets
69
- checkLineForSolverError(result, progress)
70
- @protocol.puts("; Solver Response: #{result}")
71
- return result
72
- end
73
-
74
- def replace_strings(line)
75
- line.gsub(/"([^"\\]|\\.)*"/, "")
76
- end
77
-
78
- def read_multiline_from_solver()
79
- result = ""
80
- for_counting = ""
81
- @protocol.puts
82
- @protocol.puts("; BEGIN Multiline Solver Response:")
83
- line = @z3outAndErr.gets
84
- result += line
85
- for_counting += replace_strings(line)
86
- @protocol.puts("; #{line}")
87
-
88
- num_opening = for_counting.count("(")
89
- num_closing = for_counting.count(")")
90
- while num_opening > num_closing do
91
- line = @z3outAndErr.gets
92
- result += line
93
- for_counting += replace_strings(line)
94
- @protocol.puts("; #{line}")
95
-
96
- num_opening = for_counting.count("(")
97
- num_closing = for_counting.count(")")
98
- end
99
- @protocol.puts("; END Multiline Solver Response")
100
- return result
101
- end
102
-
103
- def writeProtocolToFile(progress, filename)
104
- dir = File.dirname(filename)
105
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
106
- FileUtils.cp(@protocol_filename, filename) unless @protocol_filename == filename
107
- progress.print_line "Solver protocol written to file #{filename}"
108
- end
109
-
110
- def generateNextAssertionIDAndAssociateWith(hash)
111
- nextAssertionID = "a#{@assertCounter += 1}"
112
- @associations[nextAssertionID] = hash
113
- return nextAssertionID
114
- end
115
-
116
- def associateAssertionIDWith(assertionID, hash)
117
- if @associations.has_key?(assertionID) then
118
- raise RuntimeError.new("AssertionID #{assertionID.inspect} already has info associated with it!")
119
- end
120
- @associations[assertionID] = hash
121
- end
122
-
123
- def getAssociationForAssertionID(assertionID)
124
- return @associations[assertionID]
125
- end
126
-
127
- def for_each_const(&block)
128
- @consts.each do |key, value|
129
- block.call(key, value)
130
- end
131
- end
132
-
133
- def get_const_info(name)
134
- @consts[name]
135
- end
136
- end
137
-
138
- class Z3Solver
139
-
140
- def initialize(progress, z3path)
141
- z3version = `#{z3path} --version`.chomp()
142
- if $?.exitstatus == 0 then
143
- progress.print_line "INFO: Using #{z3version} found in z3path=#{z3path}"
144
- @z3in, @z3outAndErr, @z3thr = Open3.popen2e(z3path, '-in', '-smt2')
145
- Process.detach(@z3thr.pid)
146
- else
147
- progress.print_line "WARN: could not find z3 in z3path=#{z3path.inspect}"
148
- `which z3`
149
- if $?.exitstatus == 0 then
150
- z3version = `z3 --version`.chomp()
151
- progress.print_line "However, #{z3version} is availlable on the PATH. Using this one."
152
- @z3in, @z3outAndErr, @z3thr = Open3.popen2e('z3', '-in', '-smt2')
153
- Process.detach(@z3thr.pid)
154
- else
155
- raise RuntimeError.new("ERROR: unable to find Z3. Exiting! (Please provide its location in the config file using the key :z3path)")
156
- end
157
- end
158
- @z3in.sync = true
159
-
160
- @sessionIdCounter = 0
161
- end
162
-
163
- def close()
164
- @z3in.close()
165
- @z3outAndErr.close()
166
- Thread.kill(@z3thr)
167
- end
168
-
169
- def query(progress, options, &block)
170
- unless options[:solverLog]
171
- raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
172
- end
173
- progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
174
- solver = SolverSession.new(@z3in, @z3outAndErr, options[:solverLog])
175
-
176
- block.call(solver)
177
-
178
- solver.to_solver(progress, "(check-sat)")
179
- result = solver.from_solver(progress)
180
- explanation = true
181
- if result.chomp == "sat"
182
- # model is not contradictory -> continue
183
- elsif result.chomp == "unsat"
184
- # model is contradictory -> add a message
185
-
186
- solver.to_solver(progress, "(get-unsat-core)")
187
- unsat_core = solver.from_solver(progress)
188
- explanation = parse_unsat_core(solver, unsat_core)
189
- else
190
- handleSolverError(progress, solver, options, result)
191
- end
192
-
193
- solver.to_solver(progress, "(reset)")
194
- solver.to_solver(progress, "(set-logic ALL)")
195
- solver.to_solver(progress, "(set-option :produce-unsat-cores true)")
196
- return explanation
197
- end
198
-
199
- def display_assertion(assertion, solver)
200
- if assertion =~ /validation_rule_(.*)_instance_(.*)/ then
201
- return "Validation rule #{$1} of structure #{$2.gsub("_", "/")}"
202
- else
203
- info = solver.getAssociationForAssertionID(assertion)
204
- if info then
205
- if info[:assertion_type] == :doc_field_filled then
206
- return "Field #{info[:xpath]} contains value #{info[:value].inspect} in document"
207
- elsif info[:assertion_type] == :doc_field_empty then
208
- return "Field #{info[:xpath]} is empty in document"
209
- elsif info[:assertion_type] == :doc_structure_exists then
210
- return "Structure #{info[:xpath]} exists in document"
211
- elsif info[:assertion_type] == :doc_structure_not_exists then
212
- return "Structure #{info[:xpath]} does not exist in document"
213
- elsif info[:assertion_type] == :constraint then
214
- if info[:constraint_type] == :min then
215
- return "Field #{info[:xpath]} has a minimum value of #{info[:value]}."
216
- elsif info[:constraint_type] == :max then
217
- return "Field #{info[:xpath]} has a maximum value of #{info[:value]}."
218
- elsif info[:constraint_type] == :required then
219
- return "Field #{info[:xpath]} is required."
220
- else
221
- p info
222
- raise RuntimeError.new("encountered unknown :constraint_type #{info[:constraint_type].inspect} for assertionID #{assertion.inspect}")
223
- end
224
- else
225
- raise RuntimeError.new("encountered unknown :assertion_type #{info[:assertion_type].inspect} for assertionID #{assertion.inspect}")
226
- end
227
- else
228
- return assertion
229
- end
230
- end
231
- end
232
-
233
- def parse_unsat_core(solver, unsat_core)
234
- unsat_core = unsat_core.chomp
235
- # TODO: implement
236
- unless unsat_core =~ /\((.*)\)/ then
237
- raise RuntimeError.new("could not parse unsat core: #{unsat_core.inspect}")
238
- end
239
- list = $1.split(" ")
240
-
241
- result = "EXPLANATION:\n"
242
- result += "The following values\n"
243
-
244
- list.filter{|a| a.start_with?("doc-")}.each do |assertion|
245
- result += "- #{display_assertion(assertion, solver)} (#{assertion})\n"
246
- end
247
- result += "\nviolate the following constraints\n"
248
- list.filter{|a| !a.start_with?("doc-")}.each do |assertion|
249
- result += "- #{display_assertion(assertion, solver)} (#{assertion})\n"
250
- end
251
- return result
252
- end
253
-
254
- # main entry point!
255
- def query_model(progress, options, &block)
256
- unless options[:solverLog]
257
- raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
258
- end
259
- progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
260
- solver = SolverSession.new(@z3in, @z3outAndErr, options[:solverLog])
261
-
262
- block.call(solver)
263
-
264
- solver.to_solver(progress, "(check-sat)")
265
- result = solver.from_solver(progress)
266
- if result.chomp == "sat"
267
- # model is not contradictory -> extract model
268
- const_list = []
269
- const_info = {}
270
- solver.for_each_const do |name, info|
271
- const_list << name
272
- const_info[name] = info
273
- end
274
- solver.to_solver(progress, "(get-value (#{const_list.join(" ")}))")
275
- model_str = solver.read_multiline_from_solver()
276
- model = {}
277
- foreach_field_in_model_str(model_str, solver, const_info) do |varname, value, info|
278
- unless info then
279
- progress.print_line "WARN: no info availlable for varname=#{varname.inspect}, value=#{value.inspect}"
280
- end
281
- xpath = info[:xpath]
282
- if model.has_key?(xpath) then
283
- if varname.end_with?("-filled") then
284
- unless model[xpath][:type] == :field then
285
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a field, not a #{model[xpath][:type]}.")
286
- end
287
- unless value == "true" or value == "false" then
288
- raise RuntimeError.new("inconsistent model value filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
289
- end
290
- if value == "true" then
291
- model[xpath][:filled] = true
292
- else
293
- model[xpath][:filled] = false
294
- end
295
- elsif varname.end_with?("-value") then
296
- unless model[xpath][:type] == :field || model[xpath][:type] == :list then
297
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a field or a list, not a #{model[xpath][:type]}.")
298
- end
299
- if model[xpath][:type] == :field then
300
- model[xpath][:value] = convert_solver_value(info[:type], info[:datatype], value, xpath)
301
- elsif model[xpath][:type] == :list then
302
- unless varname =~ /-index-(\d+)-value$/
303
- raise RuntimeError.new("inconsistent model: xpath #{xpath} is a list value and hence its varname should end like -index-X-value, not #{varname.inspect}.")
304
- end
305
- index = Integer($1)
306
- model[xpath][:xpath_element] = info[:xpath_element]
307
- if model[xpath].has_key?(:value) then
308
- val = convert_solver_value(info[:type], info[:datatype], value, xpath)
309
- model[xpath][:value].insert(index, val)
310
- else
311
- val = convert_solver_value(info[:type], info[:datatype], value, xpath)
312
- model[xpath][:value] = [val]
313
- end
314
- end
315
- elsif varname.end_with?("-size") then
316
- unless model[xpath][:type] == :list || model[xpath][:type] == :field then
317
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a list or a field, not a #{model[xpath][:type]}.")
318
- end
319
- if model[xpath][:type] == :field then
320
- model[xpath][:type] = :list
321
- end
322
- model[xpath][:size] = Integer(value)
323
- elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
324
- unless model[xpath][:type] == :struct then
325
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a struct, not a #{model[xpath][:type]}.")
326
- end
327
- model[xpath][:exists] = value
328
- end
329
- else
330
- if varname.end_with?("-filled") then
331
- unless value == "true" or value == "false" then
332
- raise RuntimeError.new("inconsistent model value filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
333
- end
334
- if value == "true" then
335
- model[xpath] = { :type => :field, :filled => true }
336
- else
337
- model[xpath] = { :type => :field, :filled => false }
338
- end
339
- elsif varname.end_with?("-value") then
340
- check_type(info[:type], value, xpath)
341
- model[xpath] = { :type => :field, :value => value }
342
- elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
343
- unless value == "true" or value == "false" then
344
- raise RuntimeError.new("inconsistent model value exists-value of struct #{xpath} should be a Bool, but was #{value.inspect}.")
345
- end
346
- model[xpath] = { :type => :struct, :exists => value }
347
- elsif varname.end_with?("-size") then
348
- model[xpath] = { :type => :list, :size => value }
349
- end
350
- end
351
- end
352
- return model
353
- elsif result.chomp == "unsat"
354
- # model is contradictory -> add a message
355
-
356
- solver.to_solver(progress, "(get-unsat-core)")
357
- unsat_core = solver.from_solver(progress)
358
- solver.writeProtocolToFile(progress, options[:solverLog])
359
- progress.print_line "Solver reported a contradiction! please see #{options[:solverLog]} for further information."
360
- return nil
361
- else
362
- handleSolverError(progress, solver, options, result)
363
- end
364
-
365
- messages = []
366
- solver.to_solver "(reset)"
367
- solver.to_solver "(set-logic ALL)"
368
- solver.to_solver "(set-option :produce-unsat-cores true)"
369
-
370
- return messages
371
- end
372
-
373
- def check_type(type, value, xpath)
374
- if type == "String" then
375
- unless value =~ /\".*\"/ then
376
- raise RuntimeError.new("inconsistent model value: model value for #{xpath} should be a String, but was #{value}.")
377
- end
378
- elsif type == "Bool"
379
- unless value == "true" or value == "false" then
380
- raise RuntimeError.new("inconsistent model value for #{xpath} should be a Bool, but was #{value.inspect}.")
381
- end
382
- elsif type == "Int"
383
- unless value =~ /\d+/ then
384
- raise RuntimeError.new("inconsistent model value: model value for #{xpath} should be a Int, but was #{value}.")
385
- end
386
- else
387
- raise RuntimeError.new("encountered unknown solver type: type of solver-var for #{xpath} is #{info[:type]}.")
388
- end
389
- end
390
-
391
- def convert_solver_value(type, datatype, value, xpath)
392
- check_type(type, value, xpath)
393
- if datatype == :date then
394
- return (Time.at(0).to_date + Integer(value)).to_s
395
- elsif datatype == :timestamp then
396
- return Time.at(Integer(value)).to_s
397
- else
398
- return value
399
- end
400
- end
401
-
402
- def foreach_field_in_model_str(str, solver, const_info, &block)
403
- unless str =~ /^\((.*)\)$/m
404
- raise RuntimeError.new("could not parse model: #{str.inspect}")
405
- end
406
- str = $1
407
-
408
- str.split("\n").each do |line|
409
- unless line =~ /\(([^\ ]*)\ (.*)\)/
410
- raise RuntimeError.new("could not parse model line: #{line.inspect}")
411
- end
412
- varname = $1
413
- value = $2
414
- if value =~ /\(-\ (\d+)\)/ then
415
- simplified = "-#{$1}"
416
- value = simplified
417
- end
418
- info = const_info[varname]
419
-
420
- block.call(varname, value, info)
421
- end
422
-
423
- return nil
424
- end
425
-
426
- def handleSolverError(progress, solver, options, result)
427
- solver.writeProtocolToFile(progress, options[:solverLog])
428
- raise RuntimeError.new("Solver Error: Solver returned '#{result}', for more information see #{options[:solverLog]}")
429
- end
430
-
431
- end
432
-
433
-
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
+