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.
- checksums.yaml +7 -0
- data/eprover/eprover-2.3-linux +0 -0
- data/eprover/eprover-2.3-mac +0 -0
- data/eprover/eprover-2.3-windows.exe +0 -0
- data/eprover/eprover.txt +5 -0
- data/oak +3 -0
- data/src/bindings.rb +464 -0
- data/src/commands.rb +349 -0
- data/src/external prover.rb +189 -0
- data/src/grammar rules.rb +439 -0
- data/src/grammar.rb +218 -0
- data/src/oak.rb +56 -0
- data/src/parser.rb +1089 -0
- data/src/proof.rb +471 -0
- data/src/schema.rb +170 -0
- data/src/utilities.rb +361 -0
- metadata +58 -0
data/src/utilities.rb
ADDED
@@ -0,0 +1,361 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
|
3
|
+
class WordWrapper
|
4
|
+
def initialize
|
5
|
+
@indent = 2
|
6
|
+
@width = IO.console.winsize[1] - 1 # subtract 1 for portability
|
7
|
+
@position = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def clear?
|
11
|
+
@position == 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def print s
|
15
|
+
s.to_s.each_line {|line|
|
16
|
+
if line.end_with? "\n"
|
17
|
+
puts line.chomp
|
18
|
+
else
|
19
|
+
word_wrap line
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def puts s = ''
|
25
|
+
print s
|
26
|
+
$stdout.puts
|
27
|
+
@position = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def word_wrap s
|
31
|
+
if @position == :wrapping # waiting to go to next line and indent
|
32
|
+
s = s.lstrip
|
33
|
+
return if s.empty?
|
34
|
+
$stdout.puts
|
35
|
+
$stdout.print ' ' * @indent
|
36
|
+
@position = @indent
|
37
|
+
word_wrap s
|
38
|
+
elsif s.length <= @width-@position # fits on this line
|
39
|
+
$stdout.print s
|
40
|
+
@position += s.length
|
41
|
+
else # wrap to next line
|
42
|
+
i = s.rindex ' ', @width-@position
|
43
|
+
if i # break it at the space
|
44
|
+
$stdout.print s[0...i].rstrip
|
45
|
+
s = s[i..-1]
|
46
|
+
elsif @position <= @indent # no choice but to truncate it
|
47
|
+
$stdout.print s[0...@width-@position]
|
48
|
+
s = s[@width-@position..-1]
|
49
|
+
end
|
50
|
+
@position = :wrapping
|
51
|
+
word_wrap s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def bind_variables variables, body, quantifier
|
57
|
+
if body
|
58
|
+
to_bind = variables & body.free_variables # only bind those that need it
|
59
|
+
else
|
60
|
+
raise unless quantifier == :for_some
|
61
|
+
to_bind = variables
|
62
|
+
end
|
63
|
+
return body if to_bind.empty?
|
64
|
+
variables_tree = Tree.new to_bind, []
|
65
|
+
Tree.new quantifier, [variables_tree, body].compact
|
66
|
+
end
|
67
|
+
|
68
|
+
def conjuncts trees
|
69
|
+
# seems to have no effect on the prover, and is easier to read!
|
70
|
+
trees.collect {|tree|
|
71
|
+
next [tree] unless tree.operator == :and
|
72
|
+
conjuncts tree.subtrees
|
73
|
+
}.flatten
|
74
|
+
end
|
75
|
+
|
76
|
+
def conjunction_tree trees
|
77
|
+
trees = conjuncts trees
|
78
|
+
return nil if trees.empty?
|
79
|
+
return trees[0] if trees.size == 1
|
80
|
+
Tree.new :and, trees
|
81
|
+
end
|
82
|
+
|
83
|
+
def contains_quantifiers? tree
|
84
|
+
Tree::Quantifiers.any? {|quantifier| tree.contains? quantifier}
|
85
|
+
end
|
86
|
+
|
87
|
+
def equal_up_to_variable_names? tree1, tree2, refs1 = {}, refs2 = {}, level = 0
|
88
|
+
# check whether tree1 and tree2 are equal up to variable renaming
|
89
|
+
return false unless tree1.operator.class == tree2.operator.class
|
90
|
+
if tree1.operator.is_a? Symbol
|
91
|
+
return false unless tree1.operator == tree2.operator
|
92
|
+
return false unless tree1.subtrees.size == tree2.subtrees.size
|
93
|
+
pairs = [tree1.subtrees, tree2.subtrees].transpose
|
94
|
+
case tree1.operator
|
95
|
+
when *Tree::Quantifiers
|
96
|
+
last1, last2 = {}, {}
|
97
|
+
zipped = tree1.subtrees[0].operator.zip tree2.subtrees[0].operator
|
98
|
+
zipped.each_with_index {|(var1, var2), i|
|
99
|
+
refs1[var1], last1[var1] = [level,i], refs1[var1]
|
100
|
+
refs2[var2], last2[var2] = [level,i], refs2[var2]
|
101
|
+
}
|
102
|
+
result = pairs[1..-1].all? {|subtree1, subtree2|
|
103
|
+
equal_up_to_variable_names? subtree1, subtree2, refs1, refs2, level+1
|
104
|
+
}
|
105
|
+
zipped.each_with_index {|(var1, var2), i|
|
106
|
+
last1[var1] ? (refs1[var1] = last1[var1]) : (refs1.delete var1)
|
107
|
+
last2[var2] ? (refs2[var2] = last2[var2]) : (refs2.delete var2)
|
108
|
+
}
|
109
|
+
result
|
110
|
+
when :not, :and, :or, :implies, :iff, :equals, :predicate
|
111
|
+
pairs.all? {|subtree1, subtree2|
|
112
|
+
equal_up_to_variable_names? subtree1, subtree2, refs1, refs2, level+1
|
113
|
+
}
|
114
|
+
else raise
|
115
|
+
end
|
116
|
+
elsif tree1.operator.is_a? String
|
117
|
+
if refs1[tree1.operator] and refs2[tree2.operator]
|
118
|
+
refs1[tree1.operator] == refs2[tree2.operator] # both variables
|
119
|
+
elsif refs1[tree1.operator] or refs2[tree2.operator]
|
120
|
+
false # one is a constant, the other is a variable
|
121
|
+
else
|
122
|
+
tree1.operator == tree2.operator # both constants
|
123
|
+
end
|
124
|
+
else
|
125
|
+
raise
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def first_orderize tree, by_arity = false
|
130
|
+
subtrees = tree.subtrees.collect {|subtree| first_orderize subtree, by_arity}
|
131
|
+
if tree.operator == :predicate
|
132
|
+
i = (by_arity ? subtrees.size.to_s : '')
|
133
|
+
Tree.new :predicate, [Tree.new("apply#{i}", []), *subtrees]
|
134
|
+
else
|
135
|
+
Tree.new tree.operator, subtrees
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def implication_tree antecedents, consequent
|
140
|
+
antecedent = conjunction_tree antecedents.compact
|
141
|
+
return consequent if not antecedent
|
142
|
+
Tree.new :implies, [antecedent, consequent]
|
143
|
+
end
|
144
|
+
|
145
|
+
def new_name names, prefix = 'x'
|
146
|
+
# make a guess, for speed!
|
147
|
+
name = "#{prefix}_#{names.size}"
|
148
|
+
return name if not names.include? name
|
149
|
+
# otherwise, find the lowest available
|
150
|
+
options = (0..names.size).collect {|i| "#{prefix}_#{i}"}
|
151
|
+
available = options - names
|
152
|
+
raise if available.empty?
|
153
|
+
available.first
|
154
|
+
end
|
155
|
+
|
156
|
+
def new_names used, count, prefix = 'x'
|
157
|
+
used = used.dup
|
158
|
+
count.times.collect {
|
159
|
+
name = new_name used, prefix
|
160
|
+
used << name
|
161
|
+
name
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
def replace_empty_quantifiers tree
|
166
|
+
# we assume that the domain is not empty, and replace
|
167
|
+
# each occurrence of "there is an x" with a tautology
|
168
|
+
if tree.operator == :for_some and not tree.subtrees[1]
|
169
|
+
tree_for_true
|
170
|
+
else
|
171
|
+
subtrees = tree.subtrees.collect {|subtree|
|
172
|
+
replace_empty_quantifiers subtree
|
173
|
+
}
|
174
|
+
Tree.new tree.operator, subtrees
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def replace_for_at_most_one tree
|
179
|
+
# for at most one a,b,c, p[a,b,c]
|
180
|
+
# becomes
|
181
|
+
# for some a',b',c',
|
182
|
+
# for all a,b,c,
|
183
|
+
# p[a,b,c] implies (a=a' and b=b' and c=c')
|
184
|
+
subtrees = tree.subtrees.collect {|subtree| replace_for_at_most_one subtree}
|
185
|
+
if tree.operator == :for_at_most_one
|
186
|
+
variables, body = tree.subtrees[0].operator, tree.subtrees[1]
|
187
|
+
used = tree.free_variables + variables
|
188
|
+
primes = new_names used, variables.size, 'at_most_one'
|
189
|
+
equalities = conjunction_tree variables.each_index.collect {|i|
|
190
|
+
Tree.new :equals, [Tree.new(variables[i],[]), Tree.new(primes[i],[])]
|
191
|
+
}
|
192
|
+
tree = body ? Tree.new(:implies, [body, equalities]) : equalities
|
193
|
+
tree = Tree.new :for_all, [Tree.new(variables,[]), tree]
|
194
|
+
Tree.new :for_some, [Tree.new(primes,[]), tree]
|
195
|
+
else
|
196
|
+
Tree.new tree.operator, subtrees
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def self_implication? tree
|
201
|
+
tree.operator == :implies and equal_up_to_variable_names? *tree.subtrees
|
202
|
+
end
|
203
|
+
|
204
|
+
def substitute tree, substitution, bound = [] # faster than Set for some reason
|
205
|
+
# performs substitution on tree, where substitution keys are strings and
|
206
|
+
# values are trees. does not substitute again within the values.
|
207
|
+
case tree.operator
|
208
|
+
when *Tree::Quantifiers
|
209
|
+
return tree if tree.subtrees.size == 1 # empty quantifier body
|
210
|
+
variables = tree.subtrees[0].operator
|
211
|
+
subtree = substitute tree.subtrees[1], substitution, (bound + variables)
|
212
|
+
if subtree.equal? tree.subtrees[1]
|
213
|
+
tree
|
214
|
+
else
|
215
|
+
Tree.new tree.operator, [tree.subtrees[0], subtree]
|
216
|
+
end
|
217
|
+
when :and, :or, :not, :implies, :iff, :equals, :predicate
|
218
|
+
subtrees = tree.subtrees.collect {|subtree|
|
219
|
+
substitute subtree, substitution, bound
|
220
|
+
}
|
221
|
+
if subtrees.each_index.all? {|i| subtrees[i].equal? tree.subtrees[i]}
|
222
|
+
tree
|
223
|
+
else
|
224
|
+
Tree.new tree.operator, subtrees
|
225
|
+
end
|
226
|
+
when String
|
227
|
+
return tree if not substitution.has_key? tree.operator
|
228
|
+
return tree if bound.include? tree.operator
|
229
|
+
replacement = substitution[tree.operator]
|
230
|
+
conflict = (bound & replacement.free_variables).first
|
231
|
+
# error if substitution contains quantified variable
|
232
|
+
raise SubstituteException, conflict if conflict
|
233
|
+
replacement
|
234
|
+
else
|
235
|
+
raise "unexpected operator #{tree.operator.inspect}"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def tree_for_false
|
240
|
+
Tree.new :and, [
|
241
|
+
Tree.new('false', []),
|
242
|
+
Tree.new(:not, [Tree.new('false', [])])
|
243
|
+
]
|
244
|
+
end
|
245
|
+
|
246
|
+
def tree_for_true
|
247
|
+
Tree.new :or, [
|
248
|
+
Tree.new('true', []),
|
249
|
+
Tree.new(:not, [Tree.new('true', [])])
|
250
|
+
]
|
251
|
+
end
|
252
|
+
|
253
|
+
def find_minimal_subsets_from_results array, results = {}
|
254
|
+
# find the minimal subsets of array which are true in results, assuming that
|
255
|
+
# if a given set is false, all of its subsets are false
|
256
|
+
set = array.to_set
|
257
|
+
raise unless results.has_key? set
|
258
|
+
result = results[set]
|
259
|
+
return [] if result == false
|
260
|
+
subarrays = array.combination array.size-1
|
261
|
+
minimal = subarrays.collect {|subarray|
|
262
|
+
find_minimal_subsets_from_results subarray, results
|
263
|
+
}.flatten(1).uniq
|
264
|
+
(minimal.empty? and result == true) ? [array] : minimal
|
265
|
+
end
|
266
|
+
|
267
|
+
def find_minimal_subsets input_array
|
268
|
+
# find the minimal subsets for which yield returns true, assuming that if it
|
269
|
+
# returns false for a given set, it will return false for all of its subsets,
|
270
|
+
# and that order and duplicates do not matter
|
271
|
+
input_array = input_array.uniq
|
272
|
+
pending, results = [input_array], {}
|
273
|
+
until pending.empty?
|
274
|
+
array = pending.shift
|
275
|
+
set = array.to_set
|
276
|
+
next if results.has_key? set
|
277
|
+
if results.any? {|key, result| result == false and set.subset? key}
|
278
|
+
results[set] = false
|
279
|
+
else
|
280
|
+
results[set] = yield array
|
281
|
+
end
|
282
|
+
next if results[set] == false
|
283
|
+
array.combination(array.size-1).each {|subarray| pending << subarray}
|
284
|
+
end
|
285
|
+
find_minimal_subsets_from_results input_array, results
|
286
|
+
end
|
287
|
+
|
288
|
+
def find_valid_elements array
|
289
|
+
# find all elements for which the block returns :valid, assuming that if it
|
290
|
+
# returns :invalid on a subset, then no element in that subset is valid
|
291
|
+
result = yield array
|
292
|
+
return [] if result == :invalid
|
293
|
+
if array.size == 1
|
294
|
+
case result
|
295
|
+
when :valid then return array
|
296
|
+
when :unknown then return []
|
297
|
+
else raise
|
298
|
+
end
|
299
|
+
end
|
300
|
+
part1, part2 = array[0...array.size/2], array[array.size/2..-1]
|
301
|
+
results1 = find_valid_elements(part1) {|subset| yield subset}
|
302
|
+
results2 = find_valid_elements(part2) {|subset| yield subset}
|
303
|
+
results1 + results2
|
304
|
+
end
|
305
|
+
|
306
|
+
def seek_valid_subset array
|
307
|
+
# try to find a subset for which the block returns :valid, assuming that if it
|
308
|
+
# returns :invalid on a subset, then no subset of that subset is valid. if
|
309
|
+
# unsuccessful, guarantees that there is no valid single-element subset.
|
310
|
+
queue = [array]
|
311
|
+
until queue.empty?
|
312
|
+
subset = queue.shift
|
313
|
+
result = yield subset
|
314
|
+
return subset if result == :valid
|
315
|
+
next if result == :invalid
|
316
|
+
next if subset.size == 1
|
317
|
+
queue << subset[0...subset.size/2]
|
318
|
+
queue << subset[subset.size/2..-1]
|
319
|
+
end
|
320
|
+
nil
|
321
|
+
end
|
322
|
+
|
323
|
+
def reduce_subset array
|
324
|
+
# assuming that the block is truthy for the array, finds a subset for which
|
325
|
+
# the block is still truthy, such that no element can be removed from the
|
326
|
+
# subset without losing the truthiness of the block.
|
327
|
+
targets = 1 # start by assuming there is one target
|
328
|
+
target_ratio = 1 / Math::E
|
329
|
+
hits, tries = 0, 0
|
330
|
+
while true
|
331
|
+
sample_fraction = 1 / Math::E ** (1.0 / targets)
|
332
|
+
sample_size = (sample_fraction * array.size).round
|
333
|
+
# print "hits, tries = #{hits},#{tries} targets = #{targets} "
|
334
|
+
# puts "array size = #{array.size} sample size = #{sample_size}"
|
335
|
+
if sample_size >= array.size - 1
|
336
|
+
sample = array.combination(array.size-1).find {|sample|
|
337
|
+
tries += 1
|
338
|
+
if yield sample
|
339
|
+
hits += 1
|
340
|
+
targets -= 1 if hits / tries.to_f > target_ratio and targets > 1
|
341
|
+
sample
|
342
|
+
else
|
343
|
+
targets += 1 if hits / tries.to_f < target_ratio
|
344
|
+
nil
|
345
|
+
end
|
346
|
+
}
|
347
|
+
return array if not sample
|
348
|
+
array = sample
|
349
|
+
else
|
350
|
+
sample = array.sample sample_size
|
351
|
+
tries += 1
|
352
|
+
if yield sample
|
353
|
+
array = sample
|
354
|
+
hits += 1
|
355
|
+
targets -= 1 if hits / tries.to_f > target_ratio and targets > 1
|
356
|
+
else
|
357
|
+
targets += 1 if hits / tries.to_f < target_ratio
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oakproof
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tim Smith
|
8
|
+
autorequire:
|
9
|
+
bindir: "."
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-21 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
executables:
|
16
|
+
- oak
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- "./oak"
|
21
|
+
- eprover/eprover-2.3-linux
|
22
|
+
- eprover/eprover-2.3-mac
|
23
|
+
- eprover/eprover-2.3-windows.exe
|
24
|
+
- eprover/eprover.txt
|
25
|
+
- src/bindings.rb
|
26
|
+
- src/commands.rb
|
27
|
+
- src/external prover.rb
|
28
|
+
- src/grammar rules.rb
|
29
|
+
- src/grammar.rb
|
30
|
+
- src/oak.rb
|
31
|
+
- src/parser.rb
|
32
|
+
- src/proof.rb
|
33
|
+
- src/schema.rb
|
34
|
+
- src/utilities.rb
|
35
|
+
homepage: http://oakproof.org/
|
36
|
+
licenses:
|
37
|
+
- AGPL-3.0
|
38
|
+
metadata: {}
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubygems_version: 3.4.22
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: A proof checker focused on simplicity, readability, and ease of use.
|
58
|
+
test_files: []
|