adsl 0.0.3 → 0.1.0

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -20
  3. data/README.md +14 -21
  4. data/bin/adsl-verify +8 -8
  5. data/lib/adsl.rb +3 -0
  6. data/lib/adsl/adsl.rb +3 -0
  7. data/lib/adsl/ds/data_store_spec.rb +339 -0
  8. data/lib/adsl/extract/instrumenter.rb +206 -0
  9. data/lib/adsl/extract/meta.rb +33 -0
  10. data/lib/adsl/extract/rails/action_block_builder.rb +233 -0
  11. data/lib/adsl/extract/rails/action_instrumenter.rb +400 -0
  12. data/lib/adsl/extract/rails/action_runner.rb +57 -0
  13. data/lib/adsl/extract/rails/active_record_metaclass_generator.rb +555 -0
  14. data/lib/adsl/extract/rails/callback_chain_simulator.rb +135 -0
  15. data/lib/adsl/extract/rails/invariant_extractor.rb +48 -0
  16. data/lib/adsl/extract/rails/invariant_instrumenter.rb +70 -0
  17. data/lib/adsl/extract/rails/other_meta.rb +57 -0
  18. data/lib/adsl/extract/rails/rails_extractor.rb +211 -0
  19. data/lib/adsl/extract/rails/rails_instrumentation_test_case.rb +34 -0
  20. data/lib/adsl/extract/rails/rails_special_gem_instrumentation.rb +120 -0
  21. data/lib/adsl/extract/rails/rails_test_helper.rb +140 -0
  22. data/lib/adsl/extract/sexp_utils.rb +54 -0
  23. data/lib/adsl/fol/first_order_logic.rb +261 -0
  24. data/lib/adsl/parser/adsl_parser.racc +159 -0
  25. data/lib/{parser → adsl/parser}/adsl_parser.rex +4 -4
  26. data/lib/{parser → adsl/parser}/adsl_parser.rex.rb +6 -6
  27. data/lib/adsl/parser/adsl_parser.tab.rb +1031 -0
  28. data/lib/adsl/parser/ast_nodes.rb +1410 -0
  29. data/lib/adsl/railtie.rb +67 -0
  30. data/lib/adsl/spass/bin.rb +230 -0
  31. data/lib/{spass → adsl/spass}/ruby_extensions.rb +0 -0
  32. data/lib/adsl/spass/spass_ds_extensions.rb +931 -0
  33. data/lib/adsl/spass/spass_translator.rb +393 -0
  34. data/lib/adsl/spass/util.rb +13 -0
  35. data/lib/adsl/util/csv_hash_formatter.rb +94 -0
  36. data/lib/adsl/util/general.rb +228 -0
  37. data/lib/adsl/util/test_helper.rb +71 -0
  38. data/lib/adsl/verification/formula_generators.rb +231 -0
  39. data/lib/adsl/verification/instrumentation_filter.rb +50 -0
  40. data/lib/adsl/verification/invariant.rb +19 -0
  41. data/lib/adsl/verification/rails_verification.rb +33 -0
  42. data/lib/adsl/verification/utils.rb +20 -0
  43. data/lib/adsl/verification/verification_case.rb +13 -0
  44. data/test/integration/rails/rails_branch_verification_test.rb +112 -0
  45. data/test/integration/rails/rails_verification_test.rb +253 -0
  46. data/test/integration/spass/basic_translation_test.rb +563 -0
  47. data/test/integration/spass/control_flow_translation_test.rb +421 -0
  48. data/test/unit/adsl/ds/data_store_spec_test.rb +54 -0
  49. data/test/unit/adsl/extract/instrumenter_test.rb +103 -0
  50. data/test/unit/adsl/extract/meta_test.rb +142 -0
  51. data/test/unit/adsl/extract/rails/action_block_builder_test.rb +178 -0
  52. data/test/unit/adsl/extract/rails/action_instrumenter_test.rb +68 -0
  53. data/test/unit/adsl/extract/rails/active_record_metaclass_generator_test.rb +336 -0
  54. data/test/unit/adsl/extract/rails/callback_chain_simulator_test.rb +76 -0
  55. data/test/unit/adsl/extract/rails/invariant_extractor_test.rb +92 -0
  56. data/test/unit/adsl/extract/rails/rails_extractor_test.rb +1380 -0
  57. data/test/unit/adsl/extract/rails/rails_test_helper_test.rb +25 -0
  58. data/test/unit/adsl/extract/sexp_utils_test.rb +100 -0
  59. data/test/unit/adsl/fol/first_order_logic_test.rb +227 -0
  60. data/test/unit/adsl/parser/action_parser_test.rb +1040 -0
  61. data/test/unit/adsl/parser/ast_nodes_test.rb +359 -0
  62. data/test/unit/adsl/parser/class_parser_test.rb +288 -0
  63. data/test/unit/adsl/parser/general_parser_test.rb +67 -0
  64. data/test/unit/adsl/parser/invariant_parser_test.rb +432 -0
  65. data/test/unit/adsl/parser/parser_util_test.rb +126 -0
  66. data/test/unit/adsl/spass/bin_test.rb +65 -0
  67. data/test/unit/adsl/spass/ruby_extensions_test.rb +39 -0
  68. data/test/unit/adsl/spass/spass_ds_extensions_test.rb +7 -0
  69. data/test/unit/adsl/spass/spass_translator_test.rb +342 -0
  70. data/test/unit/adsl/util/csv_hash_formatter_test.rb +68 -0
  71. data/test/unit/adsl/util/general_test.rb +303 -0
  72. data/test/unit/adsl/util/test_helper_test.rb +120 -0
  73. data/test/unit/adsl/verification/formula_generators_test.rb +200 -0
  74. data/test/unit/adsl/verification/instrumentation_filter_test.rb +39 -0
  75. data/test/unit/adsl/verification/utils_test.rb +39 -0
  76. data/test/unit/adsl/verification/verification_case_test.rb +8 -0
  77. metadata +229 -29
  78. data/lib/ds/data_store_spec.rb +0 -292
  79. data/lib/fol/first_order_logic.rb +0 -260
  80. data/lib/parser/adsl_ast.rb +0 -779
  81. data/lib/parser/adsl_parser.racc +0 -151
  82. data/lib/parser/adsl_parser.tab.rb +0 -976
  83. data/lib/parser/dsdl_parser.rex.rb +0 -196
  84. data/lib/parser/dsdl_parser.tab.rb +0 -976
  85. data/lib/spass/bin.rb +0 -164
  86. data/lib/spass/spass_ds_extensions.rb +0 -870
  87. data/lib/spass/spass_translator.rb +0 -388
  88. data/lib/spass/util.rb +0 -11
  89. data/lib/util/csv_hash_formatter.rb +0 -47
  90. data/lib/util/test_helper.rb +0 -33
  91. data/lib/util/util.rb +0 -114
@@ -0,0 +1,393 @@
1
+ require 'adsl/spass/ruby_extensions'
2
+ require 'adsl/fol/first_order_logic'
3
+
4
+ module ADSL
5
+ module Spass
6
+ module SpassTranslator
7
+
8
+ def replace_conjecture(input, conjecture)
9
+ input.gsub(/list_of_formulae\s*\(\s*conjectures\s*\)\s*\..*?end_of_list\./m, <<-SPASS)
10
+ list_of_formulae(conjectures).
11
+ formula(#{conjecture.resolve_spass}).
12
+ end_of_list.
13
+ SPASS
14
+ end
15
+
16
+ class Predicate
17
+ attr_accessor :name, :arity
18
+
19
+ include FOL
20
+
21
+ def initialize(name, arity)
22
+ @name = name
23
+ @arity = arity
24
+ end
25
+
26
+ def [](*args)
27
+ args = args.flatten
28
+ return "#{@name}(#{ (1..@arity).map{ |i| "${#{i}}"}.join(", ") })".resolve_params(*args)
29
+ end
30
+ end
31
+
32
+ class ContextCommon
33
+ attr_accessor :parent, :level
34
+
35
+ def type_pred(*args)
36
+ return @level == 0 ? 'true' : @type_pred[*args.flatten]
37
+ end
38
+
39
+ def initialize(translation, name, parent)
40
+ @level = parent.nil? ? 0 : parent.level + 1
41
+ @translation = translation
42
+ @parent = parent
43
+
44
+ unless parent.nil?
45
+ @type_pred = translation.create_predicate(name, @level)
46
+
47
+ translation.reserve_names @parent.p_names do |ps|
48
+ translation.create_formula FOL::ForAll.new(ps, :c, FOL::Implies.new(
49
+ type_pred(ps, :c), @parent.type_pred(ps)
50
+ ))
51
+ end
52
+ translation.reserve_names @parent.p_names, @parent.p_names, :c do |p1s, p2s, c|
53
+ translation.create_formula FOL::ForAll.new(p1s, p2s, :c, FOL::Implies.new(
54
+ FOL::And.new(type_pred(p1s, c), type_pred(p2s, c)),
55
+ FOL::PairwiseEqual.new(p1s, p2s)
56
+ ))
57
+ end
58
+ end
59
+ end
60
+
61
+ def same_level_before_formula(parents, c1, c2)
62
+ raise 'To be implemented'
63
+ end
64
+
65
+ def p_names(num = level)
66
+ num.times.map{ |i| "p#{i+1}".to_sym }
67
+ end
68
+
69
+ def self.get_common_context(c1, c2)
70
+ while c1.level > c2.level
71
+ c1 = c1.parent
72
+ end
73
+ while c2.level > c1.level
74
+ c2 = c2.parent
75
+ end
76
+ while c1 != c2
77
+ c1 = c1.parent
78
+ c2 = c2.parent
79
+ end
80
+ return c1
81
+ end
82
+
83
+ def before(c2, c1var, c2var, executed_before)
84
+ c1 = self
85
+ @translation.reserve_names((1..c1.level-1).map{|i| "parent_a#{i}"}) do |context1_names|
86
+ @translation.reserve_names((1..c2.level-1).map{|i| "parent_b#{i}"}) do |context2_names|
87
+ context1_names << c1var
88
+ context2_names << c2var
89
+ common_context = ContextCommon.get_common_context c1, c2
90
+ prereq_formulae = FOL::And.new(c1.type_pred(context1_names), c2.type_pred(context2_names))
91
+
92
+ solution = executed_before
93
+ parent_args = context1_names.first(common_context.level)
94
+ parent_args.pop
95
+ while common_context.parent
96
+ c1_name = context1_names[common_context.level-1]
97
+ c2_name = context2_names[common_context.level-1]
98
+ solution = FOL::And.new(
99
+ FOL::Implies.new(common_context.same_level_before_formula(parent_args, c1_name, c2_name), true),
100
+ FOL::Implies.new(common_context.same_level_before_formula(parent_args, c2_name, c1_name), false),
101
+ FOL::Implies.new(
102
+ FOL::Not.new(
103
+ common_context.same_level_before_formula(parent_args, c1_name, c2_name),
104
+ common_context.same_level_before_formula(parent_args, c2_name, c1_name)
105
+ ),
106
+ solution
107
+ )
108
+ )
109
+ common_context = common_context.parent
110
+ parent_args.pop
111
+ end
112
+ solution = FOL::Implies.new(FOL::And.new(prereq_formulae), solution)
113
+ if context1_names.length > 1 or context2_names.length > 1
114
+ solution = FOL::ForAll.new([context1_names[0..-2], context2_names[0..-2]], solution)
115
+ end
116
+ return solution
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ class FlatContext < ContextCommon
123
+ def initialize(translation, name, parent)
124
+ super
125
+ end
126
+
127
+ def same_level_before_formula(ps, c1, c2)
128
+ false
129
+ end
130
+ end
131
+
132
+ class ChainedContext < ContextCommon
133
+ attr_accessor :before_pred, :just_before, :first, :last
134
+ include FOL
135
+
136
+ def initialize(translation, name, parent)
137
+ super
138
+
139
+ @before_pred = translation.create_predicate "#{@type_pred.name}_before", @type_pred.arity + 1
140
+ @just_before = translation.create_predicate "#{@type_pred.name}_just_before", @type_pred.arity + 1
141
+ @first = translation.create_predicate "#{@type_pred.name}_first", @type_pred.arity
142
+ @last = translation.create_predicate "#{@type_pred.name}_last", @type_pred.arity
143
+
144
+ ps = []
145
+ (@type_pred.arity-1).times{ |i| ps << "p#{i}" }
146
+ translation.create_formula _for_all(ps, :c, _not(@before_pred[ps, :c, :c]))
147
+ translation.create_formula _for_all(ps, :c1, :c2, _implies(@before_pred[ps, :c1, :c2], _and(
148
+ @type_pred[ps, :c1],
149
+ @type_pred[ps, :c2],
150
+ _not(@before_pred[ps, :c2, :c1]),
151
+ _implies(
152
+ _and(@type_pred[ps, :c1], @type_pred[ps, :c2]),
153
+ _or(_equal(:c1, :c2), @before_pred[ps, :c1, :c2], @before_pred[ps, :c2, :c1])
154
+ )
155
+ )))
156
+ translation.create_formula _for_all(ps, :c1, :c2, :c3, _implies(
157
+ _and(@before_pred[ps, :c1, :c2], @before_pred[ps, :c2, :c3]),
158
+ @before_pred[ps, :c1, :c3]
159
+ ))
160
+ translation.create_formula _for_all(ps, :c1, :c2, _equiv(
161
+ @just_before[ps, :c1, :c2],
162
+ _and(
163
+ @before_pred[ps, :c1, :c2],
164
+ _not(_exists(:mid, _and(@before_pred[ps, :c1, :mid], @before_pred[ps, :mid, :c2])))
165
+ )
166
+ ))
167
+ translation.create_formula _for_all(ps, _and(
168
+ _equiv(
169
+ _exists(:c, @type_pred[ps, :c]),
170
+ _exists(:c, @first[ps, :c]),
171
+ _exists(:c, @last[ps, :c])
172
+ ),
173
+ _for_all(ps, :c, _implies(
174
+ @type_pred[ps, :c],
175
+ _one_of(@last[ps, :c], _exists(:next, @just_before[ps, :c, :next]))
176
+ )),
177
+ _for_all(ps, :c, _equiv(@first[ps, :c],
178
+ _and(@type_pred[ps, :c], _not(_exists(:pre, @before_pred[ps, :pre, :c])))
179
+ )),
180
+ _for_all(ps, :c, _equiv(@last[ps, :c],
181
+ _and(@type_pred[ps, :c], _not(_exists(:post, @before_pred[ps, :c, :post])))
182
+ ))
183
+ ))
184
+ end
185
+
186
+ def same_level_before_formula(ps, c1, c2)
187
+ @before_pred[ps, c1, c2]
188
+ end
189
+ end
190
+
191
+ class Translation
192
+ attr_accessor :context, :prev_state, :invariant_state
193
+ attr_reader :existed_initially, :exists_finally, :root_context
194
+ attr_reader :is_object, :is_tuple, :is_either_resolution, :resolved_as_true
195
+ attr_reader :create_obj_stmts, :delete_obj_stmts, :all_contexts, :classes
196
+ attr_reader :conjectures
197
+
198
+ include FOL
199
+
200
+ def initialize
201
+ @classes = []
202
+ @temp_vars = []
203
+ @functions = []
204
+ @predicates = []
205
+ @formulae = [[]]
206
+ @conjectures = []
207
+ @all_contexts = []
208
+ @existed_initially = create_predicate :existed_initially, 1
209
+ @exists_finally = create_predicate :exists_finally, 1
210
+ @is_object = create_predicate :is_object, 1
211
+ @is_tuple = create_predicate :is_tuple, 1
212
+ @is_either_resolution = create_predicate :is_either_resolution, 1
213
+ @root_context = create_context 'root_context', true, nil
214
+ @context = @root_context
215
+ # {class => [[before_stmt, context], [after_stmt, context]]}
216
+ @create_obj_stmts = Hash.new{ |hash, klass| hash[klass] = [] }
217
+ @delete_obj_stmts = Hash.new{ |hash, klass| hash[klass] = [] }
218
+ @prev_state = create_state :init_state
219
+
220
+ @invariant_state = nil
221
+ end
222
+
223
+ def create_state name
224
+ state = create_predicate name, @context.level + 1
225
+ reserve_names([:c_1] * @context.level, :o) do |cs, o|
226
+ create_formula FOL::ForAll.new(cs, o, FOL::Implies.new(
227
+ state[cs, o],
228
+ FOL::And.new(@context.type_pred(cs), FOL::Or.new(@is_object[o], @is_tuple[o]))
229
+ ))
230
+ end
231
+ state
232
+ end
233
+
234
+ def create_context(name, flat, parent)
235
+ context = nil
236
+ if flat
237
+ context = FlatContext.new self, name, parent
238
+ else
239
+ context = ChainedContext.new self, name, parent
240
+ end
241
+ @all_contexts << context
242
+ context
243
+ end
244
+
245
+ def push_formula_frame
246
+ @formulae.push []
247
+ end
248
+
249
+ def pop_formula_frame
250
+ @formulae.pop
251
+ end
252
+
253
+ def create_formula(formula)
254
+ raise ArgumentError, 'Formula not resolveable to Spass' unless formula.class.method_defined? :resolve_spass
255
+ @formulae.last.push formula
256
+ end
257
+
258
+ def create_conjecture(formula)
259
+ raise ArgumentError, 'Formula not resolveable to Spass' unless formula.class.method_defined? :resolve_spass
260
+ @conjectures.push formula
261
+ end
262
+
263
+ def create_function(name, arity)
264
+ function = Predicate.new get_pred_name(name.to_s), arity
265
+ @functions << function
266
+ function
267
+ end
268
+
269
+ def create_predicate(name, arity)
270
+ pred = Predicate.new get_pred_name(name.to_s), arity
271
+ @predicates << pred
272
+ pred
273
+ end
274
+
275
+ def get_pred_name common_name
276
+ registered_names = (@functions + @predicates).map{ |a| a.name }
277
+ prefix = common_name
278
+ prefix = common_name.scan(/^(.+)_\d+$/).first.first if prefix =~ /^.+_\d+$/
279
+ regexp = /^#{ Regexp.escape prefix }(?:_(\d+))?$/
280
+
281
+ already_registered = registered_names.select{ |a| a =~ regexp }
282
+ return common_name if already_registered.empty?
283
+
284
+ rhs_numbers = already_registered.map{ |a| [a, a.scan(regexp).first.first] }
285
+
286
+ rhs_numbers.each do |a|
287
+ a[1] = a[1].nil? ? -1 : a[1].to_i
288
+ end
289
+
290
+ max_name = rhs_numbers.max_by{ |a| a[1] }
291
+ return max_name[0].increment_suffix
292
+ end
293
+
294
+ def _reserve_names(*names)
295
+ result = []
296
+ names.each do |name|
297
+ if name.is_a? Array
298
+ result << _reserve_names(*name)
299
+ else
300
+ while @temp_vars.include? name
301
+ name = name.to_s.increment_suffix.to_sym
302
+ end
303
+ @temp_vars.push name
304
+ result << name
305
+ end
306
+ end
307
+ result
308
+ end
309
+
310
+ def reserve_names(*names)
311
+ result = _reserve_names(*names)
312
+ yield *result
313
+ ensure
314
+ names.flatten.length.times do
315
+ @temp_vars.pop
316
+ end
317
+ end
318
+
319
+ def gen_formula_for_unique_arg(pred, *args)
320
+ individuals = []
321
+ args.each do |arg|
322
+ arg = arg.is_a?(Range) ? arg.to_a : [arg].flatten
323
+ next if arg.empty?
324
+ vars1 = (1..pred.arity).map{ |i| "e#{i}" }
325
+ vars2 = vars1.dup
326
+ as = []
327
+ bs = []
328
+ arg.each do |index|
329
+ a = "a#{index+1}".to_sym
330
+ vars1[index] = a
331
+ b = "b#{index+1}".to_sym
332
+ vars2[index] = b
333
+ as << a
334
+ bs << b
335
+ end
336
+ reserve_names (vars1 | vars2) do
337
+ individuals << _for_all(vars1 | vars2, _implies(_and(pred[vars1], pred[vars2]), _pairwise_equal(as, bs)))
338
+ end
339
+ end
340
+ return true if individuals.empty?
341
+ formula = _and(individuals)
342
+ create_formula formula
343
+ return formula
344
+ end
345
+
346
+ def spass_wrap(with, what)
347
+ return "" if what.length == 0
348
+ return with % what
349
+ end
350
+
351
+ def spass_list_of(what, *content)
352
+ spass_wrap "list_of_#{what.to_s}.%s\nend_of_list.", content.flatten.map{ |c| "\n " + c.to_s }.join("")
353
+ end
354
+
355
+ def to_spass_string
356
+ functions = @functions.map{ |f| "(#{f.name}, #{f.arity})" }.join(", ")
357
+ predicates = @predicates.map{ |p| "(#{p.name}, #{p.arity})" }.join(", ")
358
+ formulae = @formulae.first.map do |f|
359
+ begin
360
+ next "formula(#{f.resolve_spass})."
361
+ rescue => e
362
+ pp f
363
+ raise e
364
+ end
365
+ end
366
+ conjectures = @conjectures.map{ |f| "formula(#{f.resolve_spass})." }
367
+ <<-SPASS
368
+ begin_problem(Blahblah).
369
+ list_of_descriptions.
370
+ name({* *}).
371
+ author({* *}).
372
+ status(satisfiable).
373
+ description( {* *} ).
374
+ end_of_list.
375
+ #{spass_list_of( :symbols,
376
+ spass_wrap("functions[%s].", functions),
377
+ spass_wrap("predicates[%s].", predicates)
378
+ )}
379
+ #{spass_list_of( "formulae(axioms)",
380
+ formulae
381
+ )}
382
+ #{spass_list_of( "formulae(conjectures)",
383
+ conjectures
384
+ )}
385
+ end_problem.
386
+ SPASS
387
+ end
388
+
389
+ end
390
+ end
391
+
392
+ end
393
+ end
@@ -0,0 +1,13 @@
1
+ module ADSL
2
+ module Spass
3
+ module Util
4
+ def replace_conjecture(input, conjecture)
5
+ input.gsub(/list_of_formulae\s*\(\s*conjectures\s*\)\s*\..*?end_of_list\./m, <<-SPASS)
6
+ list_of_formulae(conjectures).
7
+ formula(#{conjecture.resolve_spass}).
8
+ end_of_list.
9
+ SPASS
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,94 @@
1
+ # A writer into CSV format that takes lines in hash format
2
+ # row = {:column1 => value1, :column2 => value2, ... }
3
+ # All rows are buffered together and a csv file is output with
4
+ # the union of individual column sets
5
+ # if the order of columns matter, supply an OrderedHash
6
+ # instance for each row
7
+
8
+ require 'set'
9
+
10
+ module ADSL
11
+ module Util
12
+ class CSVHashFormatter
13
+ def escape_str(obj)
14
+ "\"#{obj.to_s.gsub('"', '""')}\""
15
+ end
16
+
17
+ def initialize(*cols)
18
+ @row_hashes = []
19
+ @columns = []
20
+ cols.each do |col|
21
+ add_column col
22
+ end
23
+ end
24
+
25
+ def prepare_for_csv(row)
26
+ row.keys.each do |col|
27
+ row[col] = row[col].to_s if row[col].is_a? Symbol
28
+ end
29
+ end
30
+
31
+ def add_row(row)
32
+ prepare_for_csv row
33
+ @row_hashes << row
34
+ row.keys.each do |key|
35
+ add_column key unless @columns.include? key
36
+ end
37
+ end
38
+
39
+ def add_column(col)
40
+ raise "Duplicate column name #{col}" if @columns.include? col.to_sym
41
+ @columns << col.to_sym
42
+ end
43
+
44
+ alias_method :<<, :add_row
45
+
46
+ def column_type(col)
47
+ type = nil
48
+ @row_hashes.each do |row|
49
+ next if row[col].nil?
50
+ if row[col].is_a?(Numeric) && type.nil?
51
+ type = Numeric
52
+ elsif row[col].is_a?(String) || row[col].is_a?(Symbol)
53
+ type = String
54
+ end
55
+ end
56
+ type
57
+ end
58
+
59
+ def infer_column_types
60
+ types = {}
61
+ @columns.each do |col|
62
+ types[col] = column_type col
63
+ end
64
+ types
65
+ end
66
+
67
+ def sort!(*columns)
68
+ types = infer_column_types
69
+ @row_hashes.sort_by! do |row|
70
+ columns.map do |col|
71
+ if types[col] == nil
72
+ nil
73
+ elsif types[col] == Numeric
74
+ row[col] || -Float::INFINITY
75
+ else
76
+ row[col] || ''
77
+ end
78
+ end.to_a
79
+ end
80
+ self
81
+ end
82
+
83
+ def to_s
84
+ return '' if @columns.empty?
85
+ output = @columns.map{ |c| escape_str(c) }.join(',') + "\n"
86
+ types = infer_column_types
87
+ @row_hashes.each do |row|
88
+ output += @columns.map{ |c| types[c] == Numeric ? (row[c] || '') : escape_str(row[c] || '') }.join(',') + "\n"
89
+ end
90
+ output
91
+ end
92
+ end
93
+ end
94
+ end