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/proof.rb ADDED
@@ -0,0 +1,471 @@
1
+ require_relative 'parser.rb'
2
+ require_relative 'commands.rb'
3
+ require_relative 'utilities.rb'
4
+
5
+ class Proof
6
+ def self.process_file input, options = {}
7
+ process input, :file, options
8
+ end
9
+
10
+ def self.process_text input, options = {}
11
+ process input, :text, options
12
+ end
13
+
14
+ def self.process input, type, options
15
+ addons = {printer: Printer.new, tracker: Tracker.new}
16
+ addons[:tracker].begin_assume nil if options[:marker]
17
+ manager = DerivationManager.new addons[:printer], options
18
+ proof = Proof.new manager, {marker: options[:marker]}
19
+ begin
20
+ status = include proof, input, type, nil, addons
21
+ finalize proof, status, addons, {marker: options[:marker]}
22
+ rescue ProofException => e
23
+ print_exception e, addons[:printer]
24
+ raise
25
+ end
26
+ end
27
+
28
+ def self.include proof, input, type, include_line, addons
29
+ printer, tracker = addons.values_at :printer, :tracker
30
+ case type
31
+ when :file
32
+ dirname = File.dirname input
33
+ filename = File.basename input
34
+ begin
35
+ input = File.read input
36
+ rescue
37
+ exception = (include_line ? ProofException : BaseException)
38
+ raise exception, "could not open file \"#{input}\""
39
+ end
40
+ when :text
41
+ filename = ''
42
+ else raise
43
+ end
44
+ printer.begin_file filename
45
+ tracker.begin_file filename, include_line
46
+ status = :ok
47
+ Parser.new.parse_each(input) {|sentence, action, content, reasons, label,
48
+ fileline|
49
+ next if action == :empty
50
+ printer.line fileline
51
+ if action == :exit
52
+ printer.exit
53
+ status = :exited
54
+ break
55
+ end
56
+ if content.is_a? Content
57
+ content = process_content content, proof.theses[-1]
58
+ end
59
+ # puts "content for line #{fileline} is: #{content.inspect}"
60
+ id = {label: label, filename: filename, fileline: fileline}
61
+ case action
62
+ when :include then
63
+ # include relative to path of current proof file
64
+ content = File.expand_path content, dirname if type == :file
65
+ status = include proof, content, :file, fileline, addons
66
+ break if status != :ok
67
+ when :suppose then proof.suppose content, id
68
+ when :now then proof.now
69
+ when :end then proof.end_block
70
+ when :assume
71
+ proof.assume content, id
72
+ tracker.assume fileline
73
+ when :axiom
74
+ proof.axiom content, id
75
+ tracker.axiom
76
+ when :derive then proof.derive content, reasons, id
77
+ when :so then proof.so content, reasons, id
78
+ when :so_assume
79
+ proof.so_assume content, id
80
+ tracker.assume fileline
81
+ when :proof then proof.proof content, id
82
+ when :begin_assume
83
+ proof.begin_assume
84
+ tracker.begin_assume fileline
85
+ when :end_assume
86
+ proof.end_assume
87
+ tracker.end_assume fileline
88
+ when :marker
89
+ proof.marker
90
+ tracker.end_assume fileline
91
+ else raise "unrecognized action #{action}"
92
+ end
93
+ }
94
+ printer.end_file
95
+ tracker.end_file
96
+ status
97
+ end
98
+
99
+ def self.finalize proof, status, addons, options
100
+ printer, tracker = addons.values_at :printer, :tracker
101
+
102
+ if not proof.scopes.empty?
103
+ message = case proof.scopes.last
104
+ when :suppose then 'active supposition'
105
+ when :now then 'active "now" block'
106
+ when Array then 'active "proof" block'
107
+ when :assume then 'active assume block'
108
+ end
109
+ elsif options[:marker] and proof.assuming?
110
+ message = '-m option used without marker'
111
+ end
112
+ raise EndException, message if message
113
+
114
+ printer.accepted status
115
+ if not tracker.assumptions.empty?
116
+ printer.assumptions tracker.assumptions, options[:marker]
117
+ else
118
+ printer.axioms tracker.axioms
119
+ printer.conclude status
120
+ end
121
+ end
122
+
123
+ def self.print_exception e, printer
124
+ case e
125
+ when ParseException
126
+ if e.line_number
127
+ printer.line e.line_number
128
+ printer.parse_error e.message
129
+ else
130
+ printer.file_message e.message
131
+ end
132
+ when DeriveException # already printed
133
+ when BaseException then printer.base_error e.message
134
+ when EndException then printer.end_error e.message
135
+ else printer.line_error e.message
136
+ end
137
+ end
138
+
139
+ def self.process_content content, thesis
140
+ if not thesis
141
+ return content unless content.uses.include? 'thesis'
142
+ raise ProofException, 'thesis used outside of proof block'
143
+ elsif content.uses.include? 'thesis'
144
+ if content.schema
145
+ raise ProofException, 'cannot use thesis in schema'
146
+ elsif not content.binds.empty?
147
+ raise ProofException, "cannot use thesis in binding"
148
+ elsif not content.tie_ins.empty?
149
+ raise ProofException, "cannot use thesis in tie-in"
150
+ end
151
+ begin # content was just a Tree
152
+ tree = substitute content.sentence, {'thesis' => thesis.sentence}
153
+ rescue SubstituteException => e
154
+ message = "cannot quantify variable #{e}: conflicts with thesis"
155
+ raise ProofException, message
156
+ end
157
+ return Content.new tree
158
+ else
159
+ conflict = (thesis.uses & content.binds).first
160
+ return content unless conflict
161
+ raise ProofException, "cannot bind variable #{conflict}: part of thesis"
162
+ end
163
+ end
164
+
165
+ private_class_method :process, :include, :finalize, :print_exception,
166
+ :process_content
167
+ end
168
+
169
+ class DerivationManager
170
+ def initialize printer, options
171
+ @printer = printer
172
+ @fix, @reduce, @wait_on_unknown = options.values_at :fix, :reduce, :wait
173
+ end
174
+
175
+ def derive proof, content, line_numbers, question_mark
176
+ result, checked = proof.check content, line_numbers
177
+ if question_mark
178
+ if result == :valid
179
+ @printer.line_message 'derivation is already valid, no need for ?'
180
+ else
181
+ @printer.line_message 'trying to resolve ?'
182
+ results = proof.resolve_question_mark content, line_numbers
183
+ if results.empty?
184
+ @printer.line_message 'could not resolve ?'
185
+ else
186
+ to = results.collect {|i| proof.citation_string [i]}.join "\n "
187
+ @printer.line_message_block "resolved ? into:\n #{to}"
188
+ @printer.message 'proof incomplete due to ?'
189
+ end
190
+ end
191
+ raise DeriveException
192
+ end
193
+ if result == :valid
194
+ if @reduce
195
+ reduction = proof.reduce content, line_numbers
196
+ if reduction
197
+ from_lines, to_sets = reduction
198
+ from = proof.citation_string from_lines
199
+ to = to_sets.collect {|set| proof.citation_string set}.join "\n "
200
+ message = "citation can be reduced from:\n #{from}\nto:\n #{to}"
201
+ @printer.line_message_block message
202
+ end
203
+ end
204
+ else
205
+ message = case result
206
+ when :invalid then 'invalid derivation'
207
+ when :unknown then 'could not determine validity of derivation'
208
+ else raise
209
+ end
210
+ @printer.line_message message + " \"#{content.sentence}\""
211
+ if @fix
212
+ @printer.option_message '-f', 'looking for a fix'
213
+ result = proof.fix content, line_numbers
214
+ if result
215
+ @printer.option_message '-f', 'found a fix, reducing'
216
+ result = proof.reduce_fix result, content, line_numbers
217
+ to = proof.citation_string result
218
+ @printer.option_message '-f',
219
+ "derivation will work if you add:\n #{to}"
220
+ else
221
+ @printer.option_message '-f', 'no fix found'
222
+ end
223
+ elsif result == :unknown and @wait_on_unknown
224
+ @printer.option_message '-w', 'checking validity without work limit'
225
+ @printer.option_message '-w',
226
+ '(may never finish, press ctrl-c to abort)'
227
+ result = proof.check_wait_forever checked
228
+ message = case result
229
+ when :invalid then 'invalid derivation'
230
+ # use ceil rather than round to avoid "1.0 times the work limit"
231
+ when Numeric then "valid derivation, but took #{result.ceil 1} " \
232
+ "times the work limit"
233
+ when :unknown then 'eprover gave up'
234
+ else raise result.inspect
235
+ end
236
+ @printer.option_message '-w', message
237
+ @printer.message 'proof unsuccessful'
238
+ end
239
+ raise DeriveException
240
+ end
241
+ end
242
+ end
243
+
244
+ class Printer
245
+ # maintains the invariant: if @stack is empty, @wrapper is clear
246
+
247
+ def initialize
248
+ @wrapper = WordWrapper.new
249
+ @stack = []
250
+ end
251
+
252
+ def accepted status
253
+ raise if not @stack.empty?
254
+ case status
255
+ when :exited then @wrapper.puts 'all lines accepted prior to exit'
256
+ when :ok then @wrapper.puts 'all lines accepted'
257
+ else raise
258
+ end
259
+ end
260
+
261
+ def assumptions assumptions, marker
262
+ raise if not @stack.empty?
263
+ # assumption locations
264
+ assumptions.each {|filename, assumptions|
265
+ if assumptions.size == 1 and assumptions[0].is_a? Numeric
266
+ @wrapper.print "#{filename}: assumption on line "
267
+ else
268
+ @wrapper.print "#{filename}: assumptions on lines "
269
+ end
270
+ values = assumptions.collect {|x| x.is_a?(Numeric) ? x : x.join('-')}
271
+ @wrapper.puts values.join ', '
272
+ }
273
+ # assumption counts
274
+ blocks, lines = 0, 0
275
+ assumptions.values.each {|assumptions|
276
+ assumptions.each {|x|
277
+ if x.is_a? Numeric
278
+ lines += 1
279
+ elsif x.first != :start and x.last != :end
280
+ blocks += 1
281
+ end
282
+ }
283
+ }
284
+ counts = []
285
+ counts << 'marker' if marker
286
+ counts << "#{blocks} assume block" if blocks == 1
287
+ counts << "#{blocks} assume blocks" if blocks > 1
288
+ counts << "#{lines} assumption" if lines == 1
289
+ counts << "#{lines} assumptions" if lines > 1
290
+ raise if counts.empty?
291
+ @wrapper.puts "proof incomplete due to #{counts.join ' and '}"
292
+ end
293
+
294
+ def axioms axioms
295
+ raise if not @stack.empty?
296
+ axioms.each {|filename, axiom_count|
297
+ s = (axiom_count == 1 ? 'axiom' : 'axioms')
298
+ @wrapper.puts "#{axiom_count} #{s} in #{filename}"
299
+ }
300
+ end
301
+
302
+ def base_error message
303
+ raise if not @stack.empty?
304
+ @wrapper.puts "error: #{message}"
305
+ end
306
+
307
+ def begin_file filename
308
+ @wrapper.puts if not @wrapper.clear?
309
+ @stack << {file: filename, line: nil}
310
+ end
311
+
312
+ def conclude status
313
+ raise if not @stack.empty?
314
+ case status
315
+ when :exited then @wrapper.puts 'proof incomplete due to exit'
316
+ when :ok then @wrapper.puts 'proof successful!'
317
+ else raise
318
+ end
319
+ end
320
+
321
+ def end_error message
322
+ raise if not @stack.empty?
323
+ @wrapper.puts "error at end of input: #{message}"
324
+ end
325
+
326
+ def end_file
327
+ raise if @stack.empty?
328
+ if not @stack[-1][:line]
329
+ @wrapper.puts "#{@stack[-1][:file]}: no lines to process"
330
+ elsif not @wrapper.clear?
331
+ @wrapper.puts
332
+ end
333
+ @stack.pop
334
+ end
335
+
336
+ def exit
337
+ line = current_line
338
+ @wrapper.puts if not @wrapper.clear?
339
+ @wrapper.puts "exit at line #{line}: skipping remaining lines"
340
+ end
341
+
342
+ def file_message message
343
+ raise if @stack.empty?
344
+ @wrapper.puts if not @wrapper.clear?
345
+ @wrapper.puts "#{@stack[-1][:file]}: #{message}"
346
+ end
347
+
348
+ def line i
349
+ raise if @stack.empty?
350
+ @wrapper.print "#{@stack[-1][:file]}: processing line" if @wrapper.clear?
351
+ @wrapper.print " #{i}"
352
+ @stack[-1][:line] = i
353
+ end
354
+
355
+ def line_error message
356
+ line = current_line
357
+ @wrapper.puts if not @wrapper.clear?
358
+ @wrapper.puts "error at line #{line}: #{message}"
359
+ end
360
+
361
+ def line_message message
362
+ line = current_line
363
+ @wrapper.puts if not @wrapper.clear?
364
+ @wrapper.puts "line #{line}: #{message}"
365
+ end
366
+
367
+ def line_message_block message
368
+ line = current_line
369
+ @wrapper.puts if not @wrapper.clear?
370
+ @wrapper.puts
371
+ @wrapper.puts "line #{line}: #{message}"
372
+ @wrapper.puts
373
+ end
374
+
375
+ def message message
376
+ @wrapper.puts if not @wrapper.clear?
377
+ @wrapper.puts message
378
+ end
379
+
380
+ def option_message option, message
381
+ line = current_line
382
+ @wrapper.puts if not @wrapper.clear?
383
+ @wrapper.puts "line #{line}: #{option} option: #{message}"
384
+ end
385
+
386
+ def parse_error message
387
+ line = current_line
388
+ @wrapper.puts if not @wrapper.clear?
389
+ @wrapper.puts "parse failed at line #{line}: #{message}"
390
+ end
391
+
392
+ private #####################################################################
393
+
394
+ def current_line
395
+ raise if @stack.empty?
396
+ @stack[-1][:line] or raise
397
+ end
398
+ end
399
+
400
+ class Tracker
401
+ # keeps track of assumptions and axioms
402
+
403
+ attr_reader :assumptions, :axioms
404
+
405
+ def initialize
406
+ @assumptions = Hash.new {|hash, key| hash[key] = []}
407
+ @axioms = Hash.new 0
408
+
409
+ @file_stack = []
410
+ @assume_stack = []
411
+ end
412
+
413
+ def assume fileline
414
+ @assumptions[current_file] << fileline if @assume_stack.empty?
415
+ end
416
+
417
+ def axiom
418
+ @axioms[current_file] += 1
419
+ end
420
+
421
+ def begin_assume fileline
422
+ if not @assume_stack.empty?
423
+ @assume_stack << :nested
424
+ elsif not @file_stack.empty?
425
+ @assume_stack << [current_file, fileline]
426
+ else
427
+ @assume_stack << [nil, nil]
428
+ end
429
+ end
430
+
431
+ def begin_file filename, include_line
432
+ @file_stack << [filename, include_line]
433
+ end
434
+
435
+ def current_file
436
+ @file_stack.last[0]
437
+ end
438
+
439
+ def end_file
440
+ raise if @file_stack.empty? # nothing to end
441
+ filename, line = @file_stack.pop
442
+ return if @assume_stack.empty?
443
+ if @assume_stack[0][0] == filename # assume began in or under this file
444
+ @assumptions[filename] << [@assume_stack[0][1], :end]
445
+ # make the assume begin at the include line in the file above
446
+ @assume_stack[0] = [current_file, line] unless @file_stack.empty?
447
+ else # assume began before this file
448
+ @assumptions[filename] << [:start, :end]
449
+ end
450
+ end
451
+
452
+ def end_assume fileline
453
+ raise if @assume_stack.empty? # nothing to end
454
+ if @assume_stack.last == :nested
455
+ @assume_stack.pop
456
+ else
457
+ assume_filename, assume_line = @assume_stack.pop
458
+ @file_stack.each_index.reverse_each {|i|
459
+ stack_filename = @file_stack[i][0]
460
+ # include line in this file
461
+ include_line = (@file_stack[i+1] ? @file_stack[i+1][1] : fileline)
462
+ if stack_filename != assume_filename # assume began before here
463
+ @assumptions[stack_filename] << [:start, include_line]
464
+ else # assume began here
465
+ @assumptions[stack_filename] << [assume_line, include_line]
466
+ break
467
+ end
468
+ }
469
+ end
470
+ end
471
+ end
data/src/schema.rb ADDED
@@ -0,0 +1,170 @@
1
+ module SchemaModule
2
+ extend self
3
+
4
+ def check_schema_format metas, condition, pattern
5
+ if condition
6
+ return false unless check_schema_format_condition condition, metas
7
+ end
8
+ check_schema_format_pattern pattern, metas
9
+ end
10
+
11
+ def find_constraints pattern, instance, variables
12
+ # find constraints for meta variables, and make sure everything else matches
13
+ if pattern.operator.is_a? Array # list of quantified variables
14
+ return nil unless instance.operator.is_a? Array
15
+ return nil unless pattern.operator.size == instance.operator.size
16
+ constraints = []
17
+ pattern.operator.zip(instance.operator) {|v1, v2|
18
+ if variables.include? v1 # meta variable generates a constraint
19
+ constraints << [v1, Tree.new(v2, [])]
20
+ else
21
+ return nil unless v1 == v2 # others must match exactly
22
+ end
23
+ }
24
+ constraints
25
+ elsif variables.include? pattern.operator
26
+ [[pattern.operator, instance]]
27
+ elsif pattern.operator == :substitution
28
+ [[pattern, instance]]
29
+ else
30
+ return nil if pattern.operator != instance.operator
31
+ return nil if pattern.subtrees.size != instance.subtrees.size
32
+ constraints = []
33
+ pattern.subtrees.zip(instance.subtrees) {|s1, s2|
34
+ result = find_constraints s1, s2, variables
35
+ return nil if not result
36
+ constraints.concat result
37
+ }
38
+ constraints
39
+ end
40
+ end
41
+
42
+ def instantiate_schema schema, instance
43
+ # puts "schema: #{schema}"
44
+ # puts "instance: #{instance}"
45
+
46
+ constraints = find_constraints schema.pattern, instance, schema.metas
47
+ raise ProofException if not constraints
48
+ # puts "constraints are:"
49
+ # constraints.each {|constraint| puts "#{constraint[0]} = #{constraint[1]}"}
50
+ # puts
51
+
52
+ resolved = resolve_constraints constraints
53
+ raise ProofException if not resolved
54
+ # puts "resolved:"
55
+ # p resolved
56
+
57
+ # every meta must have an assignment
58
+ raise ProofException unless (schema.metas - resolved.keys).empty?
59
+
60
+ return tree_for_true if not schema.condition
61
+ apply_resolved schema.condition, resolved
62
+ end
63
+
64
+ def resolve_constraints constraints
65
+ absolutes = constraints.select {|target, x| target.is_a? String}
66
+ relatives = constraints - absolutes
67
+
68
+ # puts "absolutes:"
69
+ # absolutes.each {|constraint| puts "#{constraint[0]} = #{constraint[1]}"}
70
+ # puts "relatives:"
71
+ # relatives.each {|constraint| puts "#{constraint[0]} = #{constraint[1]}"}
72
+ # puts
73
+
74
+ substitution = {}
75
+ absolutes.each {|variable, x|
76
+ if substitution[variable]
77
+ return nil unless substitution[variable].eql? x
78
+ else
79
+ substitution[variable] = x
80
+ end
81
+ }
82
+
83
+ relatives.each {|relative_substitution, x|
84
+ variable = relative_substitution.subtrees[0].operator
85
+ map = relative_substitution.subtrees[1]
86
+
87
+ # multi-substitute not supported yet
88
+ return nil if map.subtrees.size > 2
89
+
90
+ list = map.subtrees[1]
91
+ left = list.subtrees[1]
92
+ right = list.subtrees[2]
93
+
94
+ # schema must include all relative variables in absolute form
95
+ if not substitution[variable]
96
+ raise ProofException, "could not resolve schema variable #{variable}"
97
+ end
98
+
99
+ left = substitute(left, substitution).operator
100
+ return nil unless left.is_a? String
101
+ right = substitute right, substitution
102
+
103
+ # puts "substituted was #{substitution[variable]}"
104
+ substituted = substitute substitution[variable], {left => right}
105
+ # puts "x is #{x}"
106
+ # puts "substituted is #{substituted}"
107
+ # puts "for variable #{variable}"
108
+ # puts "left = #{left}"
109
+ # puts "right = #{right}"
110
+ return nil unless substituted.eql? x
111
+ }
112
+
113
+ substitution
114
+ end
115
+
116
+ private #######################################################################
117
+
118
+ def apply_resolved requirement, resolved
119
+ case requirement.operator
120
+ when :and, :or, :not, :implies, :iff
121
+ subtrees = requirement.subtrees.collect {|subtree|
122
+ apply_resolved subtree, resolved
123
+ }
124
+ Tree.new requirement.operator, subtrees
125
+ when :predicate
126
+ raise unless requirement.subtrees[0].operator == 'free'
127
+ variable = resolved[requirement.subtrees[1].operator].operator
128
+ formula = resolved[requirement.subtrees[2].operator]
129
+ raise ProofException unless variable.is_a? String
130
+ # puts "variable = #{variable}"
131
+ # puts "formula = #{formula}"
132
+ # puts "formula free vars = #{formula.free_variables}"
133
+ if formula.free_variables.include? variable
134
+ tree_for_true
135
+ else
136
+ tree_for_false
137
+ end
138
+ else raise
139
+ end
140
+ end
141
+
142
+ def check_schema_format_condition tree, metas
143
+ case tree.operator
144
+ when :and, :or, :not, :implies, :iff
145
+ tree.subtrees.all? {|subtree|
146
+ check_schema_format_condition subtree, metas
147
+ }
148
+ when :predicate
149
+ return false unless tree.subtrees.size == 3
150
+ return false unless tree.subtrees[0].operator == 'free'
151
+ return false unless tree.subtrees[1].subtrees.empty?
152
+ return false unless tree.subtrees[2].subtrees.empty?
153
+ return false unless metas.include? tree.subtrees[1].operator
154
+ return false unless metas.include? tree.subtrees[2].operator
155
+ true
156
+ else
157
+ return false
158
+ end
159
+ end
160
+
161
+ def check_schema_format_pattern tree, metas
162
+ case tree.operator
163
+ when :substitution
164
+ metas.include? tree.subtrees[0].operator
165
+ else
166
+ tree.subtrees.all? {|subtree| check_schema_format_pattern subtree, metas}
167
+ end
168
+ end
169
+
170
+ end