class SexpProcessor
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
Constants
- VERSION
duh
Attributes
Automatically shifts off the Sexp type before handing the Sexp to process_<type>
Return a stack of contexts. Most recent node is first.
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.
A default method to call if a process_<type> method is not found for the Sexp type.
A scoped environment to make you happy.
Expected result class
Raise an exception if the Sexp is not empty after processing
Raise an exception if no process_<type> method is found for a Sexp.
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.
Emit a warning when the method in default_method
is called.
Public Class Methods
Expand an array of directories into a flattened array of paths, eg:
MyProcessor.run MyProcessor.expand_dirs_to_files ARGV
# 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
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
# 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
Cache processor methods per class.
# File lib/sexp_processor.rb, line 116 def self.processors @processors ||= {} end
Cache rewiter methods per class.
# File lib/sexp_processor.rb, line 123 def self.rewriters @rewriters ||= {} end
Public Instance Methods
Raise if exp
is not empty.
# 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
Raises unless the Sexp type for list
matches typ
# 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.sexp_type != typ end
Track a stack of contexts that the processor is in, pushing on type
yielding, and then removing the context from the stack.
# File lib/sexp_processor.rb, line 385 def in_context type self.context.unshift type yield ensure self.context.shift end
Registers an error handler for node
# File lib/sexp_processor.rb, line 337 def on_error_in node_type, &block @exceptions[node_type] = block end
Default Sexp processor. Invokes process_<type> methods matching the Sexp type given. Performs additional checks as specified by the initializer.
# 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
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
# 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
using rewrite_* method for exp
‘s sexp_type, if one exists.
# 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
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
# File lib/sexp_processor.rb, line 377 def scope &block env.scope(&block) end