oakproof 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/src/commands.rb ADDED
@@ -0,0 +1,349 @@
1
+ require_relative 'bindings.rb'
2
+ require_relative 'external prover.rb'
3
+ require_relative 'schema.rb'
4
+
5
+ class Proof
6
+ class Line < Content
7
+ attr_reader :label, :filename, :fileline
8
+
9
+ def initialize content, type, id
10
+ super content # set instance variables from content
11
+ types = [:assumption, :axiom, :derivation, :supposition]
12
+ raise "unknown type #{type.inspect}" unless types.include? type
13
+ @type = type
14
+ @label, @filename, @fileline = id[:label], id[:filename], id[:fileline]
15
+ end
16
+
17
+ def supposition?
18
+ @type == :supposition
19
+ end
20
+ end
21
+ end
22
+
23
+ ### start of Proof class #################################################
24
+
25
+ class Proof
26
+ attr_reader :scopes, :theses
27
+
28
+ def initialize manager, options = {}
29
+ @lines = []
30
+ @active_suppositions = []
31
+ @active_contexts = [[]]
32
+ @scopes = []
33
+ @theses = []
34
+ @label_stack = [[]]
35
+ @line_numbers_by_label = {}
36
+ @inactive_labels = Set[]
37
+ @bindings = Bindings.new
38
+ @manager = manager
39
+ @marker = :waiting if options[:marker]
40
+ end
41
+
42
+ def assume content, id = nil
43
+ if content.schema and not @active_suppositions.empty?
44
+ raise ProofException, '"assume schema" not allowed in supposition'
45
+ end
46
+ check_admission content
47
+ add content, :assumption, id
48
+ end
49
+
50
+ def assuming?
51
+ @scopes.include? :assume or @marker == :waiting
52
+ end
53
+
54
+ def axiom content, id = nil
55
+ message = case @scopes.last
56
+ when :suppose then 'axiom not allowed in supposition'
57
+ when :now then 'axiom not allowed in "now" block'
58
+ when Array then 'axiom not allowed in "proof" block'
59
+ end
60
+ raise ProofException, message if message
61
+ check_admission content
62
+ add content, :axiom, id
63
+ end
64
+
65
+ def begin_assume
66
+ @scopes << :assume
67
+ end
68
+
69
+ def check_admission content
70
+ @bindings.check_admission content
71
+ return
72
+
73
+ =begin
74
+ constants = sentence.free_variables - @active_bindings.keys
75
+ intersection = constants & sentence.bound_variables
76
+ if not intersection.empty?
77
+ name = intersection[0] # pick one for the error message
78
+ raise ProofException, "#{name} appears as both a variable and a constant"
79
+ end
80
+
81
+ # clear binding if :for_all or :for_some occurs in sentence?
82
+ =end
83
+ end
84
+
85
+ def derive content, reasons = [], id = nil
86
+ return assume content, id if assuming?
87
+ line_numbers, question_mark = process_reasons reasons
88
+ derive_internal content, line_numbers, question_mark, id
89
+ end
90
+
91
+ def end_assume
92
+ if not @scopes.include? :assume
93
+ raise ProofException, 'no assume block to end'
94
+ elsif @scopes[-1] == :suppose
95
+ raise ProofException, 'assume block interleaves supposition'
96
+ elsif @scopes[-1] == :now
97
+ raise ProofException, 'assume block interleaves "now" block'
98
+ elsif @scopes[-1].is_a? Array
99
+ raise ProofException, 'assume block interleaves "proof" block'
100
+ elsif @scopes[-1] != :assume
101
+ raise
102
+ end
103
+ @scopes.pop
104
+ end
105
+
106
+ def end_block
107
+ raise ProofException, 'nothing to end' if @scopes.empty?
108
+ if @scopes[-1] == :assume
109
+ raise ProofException, '"begin assume" must end with "end assume"'
110
+ end
111
+ last_scope = @scopes.pop
112
+ last_context = @active_contexts.pop
113
+ last_thesis = @theses.pop
114
+ if last_scope.is_a? Array # end of proof block
115
+ @label_stack[-1].each {|label|
116
+ @line_numbers_by_label.delete label
117
+ @inactive_labels << label
118
+ }
119
+ @label_stack.pop
120
+
121
+ @bindings.end_block
122
+
123
+ content, id = last_scope
124
+ if assuming? then
125
+ assume content, id
126
+ else
127
+ derive_internal content, last_context, false, id
128
+ end
129
+ else
130
+ if last_scope == :suppose
131
+ @active_suppositions.pop
132
+ @bindings.end_block
133
+ end
134
+ @active_contexts.last.concat last_context
135
+ end
136
+ end
137
+
138
+ def marker
139
+ case @marker
140
+ when :waiting then @marker = :seen
141
+ when :seen then raise ProofException, 'duplicate marker'
142
+ when nil then raise ProofException, 'marker used without -m option'
143
+ else raise
144
+ end
145
+ end
146
+
147
+ def now
148
+ @scopes << :now
149
+ @active_contexts << []
150
+ @theses << @theses[-1] # inherit thesis if it exists
151
+ end
152
+
153
+ def proof content, id = nil
154
+ @scopes << [content, id]
155
+ @active_contexts << []
156
+ @theses << content
157
+ @label_stack << []
158
+
159
+ @bindings.begin_block
160
+ end
161
+
162
+ def so content, reasons = [], id = nil
163
+ return so_assume content, id if assuming?
164
+ if @active_contexts[-1].empty?
165
+ raise ProofException, 'nothing for "so" to use'
166
+ end
167
+ line_numbers, question_mark = process_reasons reasons
168
+ line_numbers.concat @active_contexts[-1]
169
+ @active_contexts[-1] = []
170
+ derive_internal content, line_numbers, question_mark, id
171
+ end
172
+
173
+ def so_assume content, id = nil
174
+ if @active_contexts[-1].empty?
175
+ raise ProofException, 'nothing for "so" to use'
176
+ end
177
+ @active_contexts[-1] = []
178
+ assume content, id
179
+ end
180
+
181
+ def suppose content, id = nil
182
+ @scopes << :suppose
183
+ @active_contexts << []
184
+ @theses << @theses[-1] # inherit thesis if it exists
185
+ check_admission content
186
+ @active_suppositions << @lines.size
187
+ add content, :supposition, id
188
+ end
189
+
190
+ def check content, line_numbers
191
+ to_check = @bindings.to_check content, @lines.values_at(*line_numbers)
192
+ schema, tree = to_check.values_at :schema, :tree
193
+
194
+ if schema
195
+ begin
196
+ tree = SchemaModule.instantiate_schema schema, tree
197
+ rescue ProofException => e
198
+ raise unless e.message == 'ProofException' # re-raise original message
199
+ raise ProofException, 'could not instantiate schema'
200
+ end
201
+ end
202
+
203
+ # puts 'checking tree:'
204
+ # tree.pretty_print
205
+
206
+ result = ExternalProver.valid_e? tree
207
+
208
+ if schema and result != :valid
209
+ raise ProofException, 'could not instantiate schema'
210
+ end
211
+
212
+ [result, tree]
213
+ end
214
+
215
+ def check_wait_forever tree
216
+ ExternalProver.valid_e? tree, true
217
+ end
218
+
219
+ def reduce content, line_numbers
220
+ # find the minimal subsets of the line numbers which make the content valid
221
+ labeled = line_numbers.select {|i| @lines[i].label}
222
+ unlabeled = line_numbers - labeled
223
+ minimal = find_minimal_subsets(labeled) {|subset|
224
+ begin
225
+ # note: order and duplicates do not matter to check
226
+ result, checked = check content, unlabeled + subset
227
+ {:valid => true, :invalid => false}[result]
228
+ rescue ProofException # e.g. "could not instantiate schema"
229
+ false
230
+ end
231
+ }
232
+ return nil if minimal == [labeled]
233
+ [labeled, minimal]
234
+ end
235
+
236
+ def fix content, line_numbers
237
+ # look for additional line numbers that make the content valid
238
+ originals = line_numbers
239
+ lines_with_label_not_schema = @line_numbers_by_label.values.select {|i|
240
+ not @lines[i].schema
241
+ }
242
+ additions = lines_with_label_not_schema - originals
243
+ return nil if additions.empty?
244
+ seek_valid_subset(additions) {|subset|
245
+ result, checked = check content, originals + subset
246
+ result
247
+ }
248
+ end
249
+
250
+ def reduce_fix fix_line_numbers, content, original_line_numbers
251
+ # look for a subset of the fix line numbers that makes the content valid
252
+ reduce_subset(fix_line_numbers) {|subset|
253
+ result, checked = check content, original_line_numbers + subset
254
+ result == :valid
255
+ }
256
+ end
257
+
258
+ def resolve_question_mark content, line_numbers
259
+ # look for a single additional line number that makes the content valid
260
+ originals = line_numbers
261
+ lines_with_label_not_schema = @line_numbers_by_label.values.select {|i|
262
+ not @lines[i].schema
263
+ }
264
+ additions = lines_with_label_not_schema - originals
265
+ return [] if additions.empty?
266
+ find_valid_elements(additions) {|subset|
267
+ result, checked = check content, originals + subset
268
+ result
269
+ }
270
+ end
271
+
272
+ def citation_string line_numbers
273
+ line_numbers.collect {|i|
274
+ @lines[i].label or "line #{@lines[i].fileline}"
275
+ }.join ', '
276
+ end
277
+
278
+ private #####################################################################
279
+
280
+ def add content, type, id
281
+ line = Line.new content, type, id
282
+ @bindings.begin_block if type == :axiom or type == :supposition
283
+ @bindings.admit line
284
+ label = id[:label]
285
+ if @line_numbers_by_label[label]
286
+ raise ProofException, "label #{label} already in scope"
287
+ end
288
+ @lines << line
289
+ n = @lines.size - 1
290
+ if label
291
+ @label_stack[-1] << label
292
+ @line_numbers_by_label[label] = n
293
+ elsif content.tie_ins.empty? or not content.binds.empty?
294
+ @active_contexts[-1] << n
295
+ end
296
+ n
297
+ end
298
+
299
+ def derive_internal content, line_numbers, question_mark, id
300
+ check_admission content
301
+ @manager.derive self, content, line_numbers, question_mark
302
+ add content, :derivation, id
303
+ end
304
+
305
+ def process_reasons reasons
306
+ line_numbers, question_mark = [], false, false
307
+ reasons.each {|reason|
308
+ if reason == :question_mark
309
+ raise ProofException, 'cannot use ? twice in "by"' if question_mark
310
+ question_mark = true
311
+ else
312
+ i = @line_numbers_by_label[reason]
313
+ if not i
314
+ if @inactive_labels.include? reason
315
+ raise ProofException, "label #{reason} is no longer in scope"
316
+ else
317
+ raise ProofException, "unknown label #{reason}"
318
+ end
319
+ end
320
+ if @lines[i].supposition? and not @active_suppositions.include? i
321
+ raise ProofException, "supposition cited outside itself"
322
+ end
323
+ line_numbers << i
324
+ end
325
+ }
326
+ [line_numbers, question_mark]
327
+ end
328
+ end
329
+
330
+ ### end of Proof class ###################################################
331
+
332
+ class ProofException < StandardError; end
333
+
334
+ class ParseException < ProofException
335
+ attr_reader :line_number
336
+
337
+ def initialize message, line_number = nil
338
+ @line_number = line_number
339
+ super message
340
+ end
341
+ end
342
+
343
+ class DeriveException < ProofException; end
344
+
345
+ class BaseException < ProofException; end
346
+
347
+ class EndException < ProofException; end
348
+
349
+ class SubstituteException < ProofException; end
@@ -0,0 +1,189 @@
1
+ require 'tempfile'
2
+
3
+ module ExternalProver
4
+ extend self
5
+
6
+ def valid_e? tree, wait_forever = false
7
+ tb_insert_limit = 100000
8
+ valid_tptp?(tree) {|file_path|
9
+ settings = []
10
+ settings << "--tb-insert-limit=#{tb_insert_limit}" unless wait_forever
11
+ settings << '--detsort-rw --detsort-new' # make it deterministic
12
+ settings << '--print-statistics' if wait_forever
13
+
14
+ command = %{"#{find_e}" #{settings.join ' '} --auto --tptp3-format } +
15
+ %{-s #{file_path} 2>&1}
16
+ output = `#{command}`
17
+
18
+ if output.include? '# Proof found!'
19
+ next :valid unless wait_forever
20
+ work = output.match(/# Termbank termtop insertions\s+: (\d+)/).to_a[1]
21
+ next :valid if (Integer work) <= tb_insert_limit
22
+ next Float(work) / tb_insert_limit
23
+ end
24
+ next :invalid if output.include? '# No proof found!'
25
+ next :unknown if output.include? '# Failure: User resource limit exceeded!'
26
+ next :unknown if output.include? '# Failure: Out of unprocessed clauses!'
27
+
28
+ message = "unexpected output when calling eprover:\n #{output.strip}\n"
29
+ if $?.exitstatus == 127 or # command not found
30
+ output.include? 'Unknown Option' or # happens with version < 2.0
31
+ output.include? 'not recognized as an internal or external command'
32
+ message << "check that E Theorem Prover version 2.3 exists at:\n"
33
+ message << " #{find_e}"
34
+ raise ProofException, message
35
+ end
36
+ raise message # no idea what happened, so treat it as a bug
37
+ }
38
+ end
39
+
40
+ private #######################################################################
41
+
42
+ def find_e
43
+ # https://stackoverflow.com/a/171011/
44
+ if (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
45
+ filename = 'eprover-2.3-windows.exe'
46
+ elsif (/darwin/ =~ RUBY_PLATFORM) != nil
47
+ filename = 'eprover-2.3-mac'
48
+ else
49
+ filename = 'eprover-2.3-linux'
50
+ end
51
+ src_dir = File.dirname __FILE__
52
+ File.expand_path "../eprover/#{filename}", src_dir
53
+ end
54
+
55
+ def make_booleans_explicit tree, booleanize_now = true
56
+ case tree.operator
57
+ when :for_all, :for_some
58
+ body = make_booleans_explicit tree.subtrees[1], true
59
+ Tree.new tree.operator, [tree.subtrees[0], body]
60
+ when :not, :and, :or, :implies, :iff
61
+ subtrees = tree.subtrees.collect {|subtree|
62
+ make_booleans_explicit subtree, true
63
+ }
64
+ Tree.new tree.operator, subtrees
65
+ when :equals
66
+ subtrees = tree.subtrees.collect {|subtree|
67
+ make_booleans_explicit subtree, false
68
+ }
69
+ Tree.new tree.operator, subtrees
70
+ when :predicate
71
+ subtrees = tree.subtrees.collect {|subtree|
72
+ make_booleans_explicit subtree, false
73
+ }
74
+ tree = Tree.new :predicate, subtrees
75
+ return tree if not booleanize_now
76
+ Tree.new :predicate, [Tree.new('boolean', []), tree]
77
+ when String
78
+ return tree if not booleanize_now
79
+ Tree.new :predicate, [Tree.new('boolean', []), tree]
80
+ else
81
+ raise "unexpected operator #{tree.operator.inspect}"
82
+ end
83
+ end
84
+
85
+ def rename_for_tptp_internal tree, used, replace = {}
86
+ case tree.operator
87
+ when :for_all, :for_some
88
+ variables = tree.subtrees[0].operator
89
+ old_replacements = variables.collect {|variable| replace[variable]}
90
+ new_replacements = new_names used, variables.size, 'V'
91
+ variables.zip(new_replacements) {|v, r| replace[v] = r}
92
+ used.concat new_replacements
93
+ variables_tree = Tree.new new_replacements, []
94
+ body = rename_for_tptp_internal tree.subtrees[1], used, replace
95
+ variables.zip(old_replacements) {|v, r| replace[v] = r}
96
+ tree = Tree.new tree.operator, [variables_tree, body]
97
+ when :not, :and, :or, :implies, :iff, :equals
98
+ subtrees = tree.subtrees.collect {|subtree|
99
+ rename_for_tptp_internal subtree, used, replace
100
+ }
101
+ Tree.new tree.operator, subtrees
102
+ when :predicate
103
+ # assume that predicate names don't need replacing, e.g. because they
104
+ # were first-orderized
105
+ subtrees = tree.subtrees[1..-1].collect {|subtree|
106
+ rename_for_tptp_internal subtree, used, replace
107
+ }
108
+ Tree.new tree.operator, [tree.subtrees[0], *subtrees]
109
+ when String
110
+ if not replace[tree.operator]
111
+ replace[tree.operator] = new_name used, 'c'
112
+ used << replace[tree.operator]
113
+ end
114
+ Tree.new replace[tree.operator], []
115
+ else
116
+ raise "unexpected operator #{tree.operator.inspect}"
117
+ end
118
+ end
119
+
120
+ def rename_for_tptp tree
121
+ used = strings_from tree
122
+ rename_for_tptp_internal tree, used
123
+ end
124
+
125
+ def strings_from tree
126
+ if tree.operator.is_a? String
127
+ [tree.operator]
128
+ elsif tree.operator.is_a? Array
129
+ tree.operator
130
+ else
131
+ tree.subtrees.collect {|subtree| strings_from subtree}.flatten.uniq
132
+ end
133
+ end
134
+
135
+ def tptp_from_internal tree
136
+ subtrees = tree.subtrees.collect {|subtree| tptp_from_internal subtree}
137
+ case tree.operator
138
+ when :for_all
139
+ "(! [#{subtrees[0]}] : #{subtrees[1]})"
140
+ when :for_some
141
+ "(? [#{subtrees[0]}] : #{subtrees[1]})"
142
+ when :not
143
+ "~(#{subtrees[0]})"
144
+ when :and
145
+ "(#{subtrees.join ' & '})"
146
+ when :or
147
+ "(#{subtrees.join ' | '})"
148
+ when :implies
149
+ "(#{subtrees[0]} => #{subtrees[1]})"
150
+ when :iff
151
+ "(#{subtrees[0]} <=> #{subtrees[1]})"
152
+ when :equals
153
+ "(#{subtrees[0]} = #{subtrees[1]})"
154
+ when :predicate
155
+ "#{subtrees[0]}(#{subtrees[1..-1].join ','})"
156
+ when String
157
+ tree.operator
158
+ when Array
159
+ tree.operator.join ','
160
+ else
161
+ raise "unexpected operator #{tree.operator.inspect}"
162
+ end
163
+ end
164
+
165
+ def tptp_from tree
166
+ tree = replace_for_at_most_one tree
167
+ tree = replace_empty_quantifiers tree
168
+ tree = first_orderize tree, true
169
+ tree = rename_for_tptp tree
170
+ tree = make_booleans_explicit tree
171
+ # puts "tree for tptp_from_internal:"
172
+ # p tree
173
+ tptp_from_internal tree
174
+ end
175
+
176
+ def valid_tptp? tree
177
+ input = tptp_from tree
178
+ input = "fof(query,conjecture,#{input})."
179
+ # puts "\ntptp:"
180
+ # puts input
181
+ file = Tempfile.new ''
182
+ file.write input
183
+ file.close # ensures that input is written
184
+ result = yield file.path
185
+ file.unlink # best practice
186
+ result
187
+ end
188
+
189
+ end