oakproof 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/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
|