oakproof 0.7.1

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.
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