Class: SexpProcessor

Inherits:
Object show all
Defined in:
lib/sexp_processor.rb

Overview

SexpProcessor provides a uniform interface to process Sexps.

In order to create your own SexpProcessor subclass you’ll need to call super in the initialize method, then set any of the Sexp flags you want to be different from the defaults.

SexpProcessor uses a Sexp’s type to determine which process method to call in the subclass. For Sexp s(:lit, 1) SexpProcessor will call #process_lit, if it is defined.

You can also specify a default method to call for any Sexp types without a process_<type> method or use the default processor provided to skip over them.

Here is a simple example:

class MyProcessor < SexpProcessor
  def initialize
    super
    self.strict = false
  end

  def process_lit(exp)
    val = exp.shift
    return val
  end
end

Defined Under Namespace

Classes: Environment

Constant Summary collapse

VERSION =

duh

"4.12.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSexpProcessor

Creates a new SexpProcessor. Use super to invoke this initializer from SexpProcessor subclasses, then use the attributes above to customize the functionality of the SexpProcessor



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/sexp_processor.rb', line 133

def initialize
  @default_method      = nil
  @warn_on_default     = true
  @auto_shift_type     = false
  @strict              = false
  @unsupported         = [:alloca, :cfunc, :cref, :ifunc, :last, :memo,
                          :newline, :opt_n, :method]
  @unsupported_checked = false
  @debug               = {}
  @expected            = Sexp
  @require_empty       = true
  @exceptions          = {}

  # we do this on an instance basis so we can subclass it for
  # different processors.
  @processors = self.class.processors
  @rewriters  = self.class.rewriters
  @context    = []

  if @processors.empty?
    public_methods.each do |name|
      case name
      when /^process_(.*)/ then
        @processors[$1.to_sym] = name.to_sym
      when /^rewrite_(.*)/ then
        @rewriters[$1.to_sym]  = name.to_sym
      end
    end
  end
end

Instance Attribute Details

#auto_shift_typeObject

Automatically shifts off the Sexp type before handing the Sexp to process_<type>



43
44
45
# File 'lib/sexp_processor.rb', line 43

def auto_shift_type
  @auto_shift_type
end

#contextObject (readonly)

Return a stack of contexts. Most recent node is first.



48
49
50
# File 'lib/sexp_processor.rb', line 48

def context
  @context
end

#debugObject

A Hash of Sexp types and Regexp.

Print a debug message if the Sexp type matches the Hash key and the Sexp’s #inspect output matches the Regexp.



56
57
58
# File 'lib/sexp_processor.rb', line 56

def debug
  @debug
end

#default_methodObject

A default method to call if a process_<type> method is not found for the Sexp type.



62
63
64
# File 'lib/sexp_processor.rb', line 62

def default_method
  @default_method
end

#envObject (readonly)

A scoped environment to make you happy.



94
95
96
# File 'lib/sexp_processor.rb', line 94

def env
  @env
end

#expectedObject

Expected result class



67
68
69
# File 'lib/sexp_processor.rb', line 67

def expected
  @expected
end

#require_emptyObject

Raise an exception if the Sexp is not empty after processing



72
73
74
# File 'lib/sexp_processor.rb', line 72

def require_empty
  @require_empty
end

#strictObject

Raise an exception if no process_<type> method is found for a Sexp.



77
78
79
# File 'lib/sexp_processor.rb', line 77

def strict
  @strict
end

#unsupportedObject

An array that specifies node types that are unsupported by this processor. SexpProcessor will raise UnsupportedNodeError if you try to process one of those node types.



84
85
86
# File 'lib/sexp_processor.rb', line 84

def unsupported
  @unsupported
end

#warn_on_defaultObject

Emit a warning when the method in #default_method is called.



89
90
91
# File 'lib/sexp_processor.rb', line 89

def warn_on_default
  @warn_on_default
end

Class Method Details

.expand_dirs_to_files(*dirs) ⇒ Object

Expand an array of directories into a flattened array of paths, eg:

MyProcessor.run MyProcessor.expand_dirs_to_files ARGV


101
102
103
104
105
106
107
108
109
110
111
# File 'lib/sexp_processor.rb', line 101

def self.expand_dirs_to_files *dirs
  extensions = %w[rb rake]

  dirs.flatten.map { |p|
    if File.directory? p then
      Dir[File.join(p, "**", "*.{#{extensions.join ","}}")]
    else
      p
    end
  }.flatten.sort
end

.processorsObject

Cache processor methods per class.



116
117
118
# File 'lib/sexp_processor.rb', line 116

def self.processors
  @processors ||= {}
end

.rewritersObject

Cache rewiter methods per class.



123
124
125
# File 'lib/sexp_processor.rb', line 123

def self.rewriters
  @rewriters ||= {}
end

Instance Method Details

#assert_empty(meth, exp, exp_orig) ⇒ Object

Raise if exp is not empty.



167
168
169
170
171
172
173
# File 'lib/sexp_processor.rb', line 167

def assert_empty meth, exp, exp_orig
  unless exp.empty? then
    msg = "exp not empty after #{self.class}.#{meth} on #{exp.inspect}"
    msg += " from #{exp_orig.inspect}" if $DEBUG
    raise NotEmptyError, msg
  end
end

#assert_type(list, typ) ⇒ Object

Raises unless the Sexp type for list matches typ

Raises:



318
319
320
321
# File 'lib/sexp_processor.rb', line 318

def assert_type list, typ
  raise SexpTypeError, "Expected type #{typ.inspect} in #{list.inspect}" if
    not Array === list or list.first != typ
end

#error_handler(type, exp = nil) ⇒ Object

:nodoc:



323
324
325
326
327
328
329
330
331
332
# File 'lib/sexp_processor.rb', line 323

def error_handler type, exp = nil # :nodoc:
  yield
rescue StandardError => err
  return @exceptions[type].call self, exp, err if @exceptions.key? type

  warn "#{err.class} Exception thrown while processing #{type} for sexp #{exp.inspect} #{caller.inspect}" if
    $DEBUG

  raise
end

#in_context(type) ⇒ Object

Track a stack of contexts that the processor is in, pushing on type yielding, and then removing the context from the stack.



385
386
387
388
389
390
391
392
# File 'lib/sexp_processor.rb', line 385

def in_context type
  self.context.unshift type

  yield

ensure
  self.context.shift
end

#on_error_in(node_type, &block) ⇒ Object

Registers an error handler for node



337
338
339
# File 'lib/sexp_processor.rb', line 337

def on_error_in node_type, &block
  @exceptions[node_type] = block
end

#process(exp) ⇒ Object

Default Sexp processor. Invokes process_<type> methods matching the Sexp type given. Performs additional checks as specified by the initializer.



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/sexp_processor.rb', line 217

def process exp
  return nil if exp.nil?

  unless Sexp === exp then
    raise SexpTypeError, "exp must be a Sexp, was #{exp.class}:#{exp.inspect}"
  end

  if self.context.empty? then
    p :rewriting unless debug.empty?
    exp = self.rewrite(exp)
    p :done_rewriting unless debug.empty?
  end

  unless @unsupported_checked then
    m = public_methods.grep(/^process_/) { |o| o.to_s.sub(/^process_/, "").to_sym }
    supported = m - (m - @unsupported)

    raise UnsupportedNodeError, "#{supported.inspect} shouldn't be in @unsupported" unless supported.empty?

    @unsupported_checked = true
  end

  result = self.expected.new

  type = exp.sexp_type
  raise "type should be a Symbol, not: #{exp.first.inspect}" unless
    Symbol === type

  in_context type do
    if @debug.key? type then
      str = exp.inspect
      puts "// DEBUG:(original ): #{str}" if str =~ @debug[type]
    end

    exp_orig = nil
    exp_orig = exp.deep_clone if $DEBUG or
      @debug.key? type or @exceptions.key?(type)

    raise UnsupportedNodeError, "'#{type}' is not a supported node type" if
      @unsupported.include? type

    # now do a pass with the real processor (or generic)
    meth = @processors[type] || @default_method
    if meth then

      if @warn_on_default and meth == @default_method then
        warn "WARNING: Using default method #{meth} for #{type}"
      end

      exp = exp.sexp_body if @auto_shift_type and meth != @default_method # HACK

      result = error_handler(type, exp_orig) {
        self.send meth, exp
      }

      if @debug.key? type then
        str = exp.inspect
        puts "// DEBUG (processed): #{str}" if str =~ @debug[type]
      end

      raise SexpTypeError, "Result of #{type} must be a #{@expected}, was #{result.class}:#{result.inspect}" unless
        @expected === result

      self.assert_empty(meth, exp, exp_orig) if @require_empty
    else
      unless @strict then
        until exp.empty? do
          sub_exp, *exp = exp # HACK
          sub_result = nil
          if Array === sub_exp then
            sub_result = error_handler(type, exp_orig) do
              process(sub_exp)
            end
            raise "Result is a bad type" unless Array === sub_exp
            raise "Result does not have a type in front: #{sub_exp.inspect}" unless
              Symbol === sub_exp.sexp_type unless
              sub_exp.empty?
          else
            sub_result = sub_exp
          end
          # result << sub_result
          result = result.class.new(*result, sub_result) # HACK
        end

        # NOTE: this is costly, but we are in the generic processor
        # so we shouldn't hit it too much with RubyToC stuff at least.
        result.c_type ||= exp.c_type if Sexp === exp and exp.respond_to?(:c_type)
      else
        msg = "Bug! Unknown node-type #{type.inspect} to #{self.class}"
        msg += " in #{exp_orig.inspect} from #{caller.inspect}" if $DEBUG
        raise UnknownNodeError, msg
      end
    end
  end

  result
end

#process_dummy(exp) ⇒ Object

A fairly generic processor for a dummy node. Dummy nodes are used when your processor is doing a complicated rewrite that replaces the current sexp with multiple sexps.

Bogus Example:

def process_something(exp)
  return s(:dummy, process(exp), s(:extra, 42))
end


352
353
354
355
356
# File 'lib/sexp_processor.rb', line 352

def process_dummy exp
  result = @expected.new(:dummy) rescue @expected.new
  result << self.process(exp.shift) until exp.empty?
  result
end

#rewrite(exp) ⇒ Object

Rewrite exp using rewrite_* method for exp‘s sexp_type, if one exists.



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/sexp_processor.rb', line 179

def rewrite exp
  type = exp.sexp_type

  comments = exp.comments

  if @debug.key? type then
    str = exp.inspect
    puts "// DEBUG (original ): #{str}" if str =~ @debug[type]
  end

  in_context type do
    exp = exp.map { |sub| Array === sub ? rewrite(sub) : sub }
  end

  loop do
    meth = @rewriters[type]
    exp  = self.send(meth, exp) if meth
    break unless Sexp === exp

    if @debug.key? type then
      str = exp.inspect
      puts "// DEBUG (rewritten): #{str}" if str =~ @debug[type]
    end

    old_type, type = type, exp.sexp_type
    break if old_type == type
  end

  exp.comments = comments

  exp
end

#scope(&block) ⇒ Object

Add a scope level to the current env. Eg:

def process_defn exp
  name = exp.shift
  args = process(exp.shift)
  scope do
    body = process(exp.shift)
    # ...
  end
end

env[:x] = 42
scope do
  env[:x]       # => 42
  env[:y] = 24
end
env[:y]         # => nil


377
378
379
# File 'lib/sexp_processor.rb', line 377

def scope &block
  env.scope(&block)
end