fabiokung-sexp_processor 3.0.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.
@@ -0,0 +1,407 @@
1
+
2
+ $TESTING = false unless defined? $TESTING
3
+
4
+ require 'sexp'
5
+
6
+ ##
7
+ # SexpProcessor provides a uniform interface to process Sexps.
8
+ #
9
+ # In order to create your own SexpProcessor subclass you'll need
10
+ # to call super in the initialize method, then set any of the
11
+ # Sexp flags you want to be different from the defaults.
12
+ #
13
+ # SexpProcessor uses a Sexp's type to determine which process method
14
+ # to call in the subclass. For Sexp <code>s(:lit, 1)</code>
15
+ # SexpProcessor will call #process_lit, if it is defined.
16
+ #
17
+ # You can also specify a default method to call for any Sexp types
18
+ # without a process_<type> method or use the default processor provided to
19
+ # skip over them.
20
+ #
21
+ # Here is a simple example:
22
+ #
23
+ # class MyProcessor < SexpProcessor
24
+ # def initialize
25
+ # super
26
+ # self.strict = false
27
+ # end
28
+ #
29
+ # def process_lit(exp)
30
+ # val = exp.shift
31
+ # return val
32
+ # end
33
+ # end
34
+
35
+ class SexpProcessor
36
+
37
+ VERSION = '3.0.1'
38
+
39
+ ##
40
+ # Automatically shifts off the Sexp type before handing the
41
+ # Sexp to process_<type>
42
+
43
+ attr_accessor :auto_shift_type
44
+
45
+ ##
46
+ # Return a stack of contexts. Most recent node is first.
47
+
48
+ attr_reader :context
49
+
50
+ ##
51
+ # A Hash of Sexp types and Regexp.
52
+ #
53
+ # Print a debug message if the Sexp type matches the Hash key
54
+ # and the Sexp's #inspect output matches the Regexp.
55
+
56
+ attr_accessor :debug
57
+
58
+ ##
59
+ # A default method to call if a process_<type> method is not found
60
+ # for the Sexp type.
61
+
62
+ attr_accessor :default_method
63
+
64
+ ##
65
+ # Expected result class
66
+
67
+ attr_accessor :expected
68
+
69
+ ##
70
+ # Raise an exception if the Sexp is not empty after processing
71
+
72
+ attr_accessor :require_empty
73
+
74
+ ##
75
+ # Raise an exception if no process_<type> method is found for a Sexp.
76
+
77
+ attr_accessor :strict
78
+
79
+ ##
80
+ # An array that specifies node types that are unsupported by this
81
+ # processor. SexpProcessor will raise UnsupportedNodeError if you try
82
+ # to process one of those node types.
83
+
84
+ attr_accessor :unsupported
85
+
86
+ ##
87
+ # Emit a warning when the method in #default_method is called.
88
+
89
+ attr_accessor :warn_on_default
90
+
91
+ ##
92
+ # A scoped environment to make you happy.
93
+
94
+ attr_reader :env
95
+
96
+ ##
97
+ # Creates a new SexpProcessor. Use super to invoke this
98
+ # initializer from SexpProcessor subclasses, then use the
99
+ # attributes above to customize the functionality of the
100
+ # SexpProcessor
101
+
102
+ def initialize
103
+ @default_method = nil
104
+ @warn_on_default = true
105
+ @auto_shift_type = false
106
+ @strict = false
107
+ @unsupported = [:alloca, :cfunc, :cref, :ifunc, :last, :memo,
108
+ :newline, :opt_n, :method]
109
+ @unsupported_checked = false
110
+ @debug = {}
111
+ @expected = Sexp
112
+ @require_empty = true
113
+ @exceptions = {}
114
+
115
+ # we do this on an instance basis so we can subclass it for
116
+ # different processors.
117
+ @processors = {}
118
+ @rewriters = {}
119
+ @context = []
120
+
121
+ public_methods.each do |name|
122
+ case name
123
+ when /^process_(.*)/ then
124
+ @processors[$1.intern] = name.intern
125
+ when /^rewrite_(.*)/ then
126
+ @rewriters[$1.intern] = name.intern
127
+ end
128
+ end
129
+ end
130
+
131
+ def assert_empty(meth, exp, exp_orig)
132
+ unless exp.empty? then
133
+ msg = "exp not empty after #{self.class}.#{meth} on #{exp.inspect}"
134
+ msg += " from #{exp_orig.inspect}" if $DEBUG
135
+ raise NotEmptyError, msg
136
+ end
137
+ end
138
+
139
+ def rewrite(exp)
140
+ type = exp.first
141
+
142
+ self.context.unshift type
143
+
144
+ exp.map! { |sub| Array === sub ? rewrite(sub) : sub }
145
+
146
+ self.context.shift
147
+
148
+ begin
149
+ meth = @rewriters[type]
150
+ exp = self.send(meth, exp) if meth
151
+ break unless Sexp === exp
152
+ old_type, type = type, exp.first
153
+ end until old_type == type
154
+
155
+ exp
156
+ end
157
+
158
+ ##
159
+ # Default Sexp processor. Invokes process_<type> methods matching
160
+ # the Sexp type given. Performs additional checks as specified by
161
+ # the initializer.
162
+
163
+ def process(exp)
164
+ return nil if exp.nil?
165
+ exp = self.rewrite(exp) if self.context.empty?
166
+
167
+ unless @unsupported_checked then
168
+ m = public_methods.grep(/^process_/) { |o| o.to_s.sub(/^process_/, '').intern }
169
+ supported = m - (m - @unsupported)
170
+
171
+ raise UnsupportedNodeError, "#{supported.inspect} shouldn't be in @unsupported" unless supported.empty?
172
+
173
+ @unsupported_checked = true
174
+ end
175
+
176
+ result = self.expected.new
177
+
178
+ type = exp.first
179
+ raise "type should be a Symbol, not: #{exp.first.inspect}" unless
180
+ Symbol === type
181
+
182
+ self.context.unshift type
183
+
184
+ if @debug.has_key? type then
185
+ str = exp.inspect
186
+ puts "// DEBUG: #{str}" if str =~ @debug[type]
187
+ end
188
+
189
+ exp_orig = nil
190
+ exp_orig = exp.deep_clone if $DEBUG or
191
+ @debug.has_key? type or @exceptions.has_key?(type)
192
+
193
+ raise UnsupportedNodeError, "'#{type}' is not a supported node type" if
194
+ @unsupported.include? type
195
+
196
+ if @debug.has_key? type then
197
+ str = exp.inspect
198
+ puts "// DEBUG (rewritten): #{str}" if str =~ @debug[type]
199
+ end
200
+
201
+ # now do a pass with the real processor (or generic)
202
+ meth = @processors[type] || @default_method
203
+ if meth then
204
+
205
+ if @warn_on_default and meth == @default_method then
206
+ warn "WARNING: Using default method #{meth} for #{type}"
207
+ end
208
+
209
+ exp.shift if @auto_shift_type and meth != @default_method
210
+
211
+ result = error_handler(type, exp_orig) do
212
+ self.send(meth, exp)
213
+ end
214
+
215
+ raise SexpTypeError, "Result must be a #{@expected}, was #{result.class}:#{result.inspect}" unless @expected === result
216
+
217
+ self.assert_empty(meth, exp, exp_orig) if @require_empty
218
+ else
219
+ unless @strict then
220
+ until exp.empty? do
221
+ sub_exp = exp.shift
222
+ sub_result = nil
223
+ if Array === sub_exp then
224
+ sub_result = error_handler(type, exp_orig) do
225
+ process(sub_exp)
226
+ end
227
+ raise "Result is a bad type" unless Array === sub_exp
228
+ raise "Result does not have a type in front: #{sub_exp.inspect}" unless Symbol === sub_exp.first unless sub_exp.empty?
229
+ else
230
+ sub_result = sub_exp
231
+ end
232
+ result << sub_result
233
+ end
234
+
235
+ # NOTE: this is costly, but we are in the generic processor
236
+ # so we shouldn't hit it too much with RubyToC stuff at least.
237
+ #if Sexp === exp and not exp.sexp_type.nil? then
238
+ begin
239
+ result.sexp_type = exp.sexp_type
240
+ rescue Exception
241
+ # nothing to do, on purpose
242
+ end
243
+ else
244
+ msg = "Bug! Unknown node-type #{type.inspect} to #{self.class}"
245
+ msg += " in #{exp_orig.inspect} from #{caller.inspect}" if $DEBUG
246
+ raise UnknownNodeError, msg
247
+ end
248
+ end
249
+
250
+ self.context.shift
251
+ result
252
+ end
253
+
254
+ ##
255
+ # Raises unless the Sexp type for +list+ matches +typ+
256
+
257
+ def assert_type(list, typ)
258
+ raise SexpTypeError, "Expected type #{typ.inspect} in #{list.inspect}" if
259
+ not Array === list or list.first != typ
260
+ end
261
+
262
+ def error_handler(type, exp=nil) # :nodoc:
263
+ begin
264
+ return yield
265
+ rescue StandardError => err
266
+ if @exceptions.has_key? type then
267
+ return @exceptions[type].call(self, exp, err)
268
+ else
269
+ warn "#{err.class} Exception thrown while processing #{type} for sexp #{exp.inspect} #{caller.inspect}" if $DEBUG
270
+ raise
271
+ end
272
+ end
273
+ end
274
+
275
+ ##
276
+ # Registers an error handler for +node+
277
+
278
+ def on_error_in(node_type, &block)
279
+ @exceptions[node_type] = block
280
+ end
281
+
282
+ ##
283
+ # A fairly generic processor for a dummy node. Dummy nodes are used
284
+ # when your processor is doing a complicated rewrite that replaces
285
+ # the current sexp with multiple sexps.
286
+ #
287
+ # Bogus Example:
288
+ #
289
+ # def process_something(exp)
290
+ # return s(:dummy, process(exp), s(:extra, 42))
291
+ # end
292
+
293
+ def process_dummy(exp)
294
+ result = @expected.new(:dummy) rescue @expected.new
295
+
296
+ until exp.empty? do
297
+ result << self.process(exp.shift)
298
+ end
299
+
300
+ result
301
+ end
302
+
303
+ ##
304
+ # Add a scope level to the current env. Eg:
305
+ #
306
+ # def process_defn exp
307
+ # name = exp.shift
308
+ # args = process(exp.shift)
309
+ # scope do
310
+ # body = process(exp.shift)
311
+ # # ...
312
+ # end
313
+ # end
314
+ #
315
+ # env[:x] = 42
316
+ # scope do
317
+ # env[:x] # => 42
318
+ # env[:y] = 24
319
+ # end
320
+ # env[:y] # => nil
321
+
322
+ def scope &block
323
+ env.scope(&block)
324
+ end
325
+
326
+ ##
327
+ # I really hate this here, but I hate subdirs in my lib dir more...
328
+ # I guess it is kinda like shaving... I'll split this out when it
329
+ # itches too much...
330
+
331
+ class Environment
332
+ def initialize
333
+ @env = []
334
+ @env.unshift({})
335
+ end
336
+
337
+ def all
338
+ @env.reverse.inject { |env, scope| env.merge scope }
339
+ end
340
+
341
+ def depth
342
+ @env.length
343
+ end
344
+
345
+ # TODO: depth_of
346
+
347
+ def [] name
348
+ hash = @env.find { |closure| closure.has_key? name }
349
+ hash[name] if hash
350
+ end
351
+
352
+ def []= name, val
353
+ hash = @env.find { |closure| closure.has_key? name } || @env.first
354
+ hash[name] = val
355
+ end
356
+
357
+ def scope
358
+ @env.unshift({})
359
+ begin
360
+ yield
361
+ ensure
362
+ @env.shift
363
+ raise "You went too far unextending env" if @env.empty?
364
+ end
365
+ end
366
+ end
367
+ end
368
+
369
+ class Object
370
+
371
+ ##
372
+ # deep_clone is the usual Marshalling hack to make a deep copy.
373
+ # It is rather slow, so use it sparingly. Helps with debugging
374
+ # SexpProcessors since you usually shift off sexps.
375
+
376
+ def deep_clone
377
+ Marshal.load(Marshal.dump(self))
378
+ end
379
+ end
380
+
381
+ ##
382
+ # SexpProcessor base exception class.
383
+
384
+ class SexpProcessorError < StandardError; end
385
+
386
+ ##
387
+ # Raised by SexpProcessor if it sees a node type listed in its
388
+ # unsupported list.
389
+
390
+ class UnsupportedNodeError < SexpProcessorError; end
391
+
392
+ ##
393
+ # Raised by SexpProcessor if it is in strict mode and sees a node for
394
+ # which there is no processor available.
395
+
396
+ class UnknownNodeError < SexpProcessorError; end
397
+
398
+ ##
399
+ # Raised by SexpProcessor if a processor did not process every node in
400
+ # a sexp and @require_empty is true.
401
+
402
+ class NotEmptyError < SexpProcessorError; end
403
+
404
+ ##
405
+ # Raised if assert_type encounters an unexpected sexp type.
406
+
407
+ class SexpTypeError < SexpProcessorError; end
@@ -0,0 +1,70 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ $TESTING = true
4
+
5
+ require 'composite_sexp_processor'
6
+ require 'test/unit'
7
+
8
+ class FakeProcessor1 < SexpProcessor # ZenTest SKIP
9
+
10
+ def initialize
11
+ super
12
+ self.warn_on_default = false
13
+ self.default_method = :default_processor
14
+ self.expected = Array
15
+ end
16
+
17
+ def default_processor(exp)
18
+ result = []
19
+ result << exp.shift
20
+ until exp.empty? do
21
+ result << exp.shift.to_s + " woot"
22
+ end
23
+ result
24
+ end
25
+ end
26
+
27
+ class TestCompositeSexpProcessor < Test::Unit::TestCase
28
+
29
+ def setup
30
+ @p = CompositeSexpProcessor.new
31
+ end
32
+
33
+ def test_process_default
34
+ data = [1, 2, 3]
35
+ result = @p.process(data.dup)
36
+ assert_equal(data.dup, result)
37
+ end
38
+
39
+ def test_process_fake1
40
+ data = [:x, 1, 2, 3]
41
+ @p << FakeProcessor1.new
42
+ result = @p.process(data.dup)
43
+ assert_equal [:x, "1 woot", "2 woot", "3 woot"], result
44
+ end
45
+
46
+ def test_process_fake1_twice
47
+ data = [:x, 1, 2, 3]
48
+ @p << FakeProcessor1.new
49
+ @p << FakeProcessor1.new
50
+ result = @p.process(data.dup)
51
+ assert_equal [:x, "1 woot woot", "2 woot woot", "3 woot woot"], result
52
+ end
53
+
54
+ def test_processors
55
+ # everything is tested by test_append
56
+ end
57
+
58
+ def test_append
59
+ assert_equal([], @p.processors)
60
+
61
+ assert_raises(ArgumentError) do
62
+ @p << 42
63
+ end
64
+
65
+ fp1 = FakeProcessor1.new
66
+ @p << fp1
67
+ assert_equal([fp1], @p.processors)
68
+ end
69
+
70
+ end