adsl 0.0.3 → 0.1.0

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