SexpBuilder¶ ↑
SexpBuilder is an alternative to SexpProcessor which allows you to match and rewrite S-expressions based on recursive descent SexpPaths. You probably want to read github.com/adamsanderson/sexp_path before you proceed.
SexpBuilder works on any S-expressions, but all of these examples uses Ruby’s S-expressions as given from ParseTree and RubyParser.
Synopsis¶ ↑
# Inherit SexpBuilder: class Andand < SexpBuilder ## Rules # # Rules are simply snippets of SexpPath which can refer to each other # and itself. They are basically method definition, so they can take # arguments too. # This matches foo.andand: rule :andand_base do s(:call, # a method call _ % :receiver, # the receiver :andand, # the method name s(:arglist)) # the arguments end # This matches foo.andand.bar rule :andand_call do s(:call, # a method call andand_base, # foo.andand _ % :name, # the method name _ % :args) # the arguments end # This matches foo.andand.bar { |args| block } rule :andand_iter do s(:iter, # a block andand_call, # the method call _ % :blockargs, # the arguments passed to the block _ % :block) # content of the block end ## Rewriters # # Rewriters take one or more rules and defines replacements when they # match. The data-object from SexpPath is given as an argument. # This will rewrite: # # foo.andand.bar => (tmp = foo) && tmp.bar # foo.andand.bar { } => (tmp = foo) && tmp.bar { } # rewrite :andand_call, :andand_iter do |data| # get a tmpvar (see below for definition) tmp = tmpvar # tmp = foo assign = s(:lasgn, tmp, process(data[:receiver])) # tmp.bar call = s(:call, s(:lasgn, tmp), data[:name], process(data[:args])) # tmp.bar { } if data[:block] call = s(:iter, call, process(data[:blockargs]), process(data[:block])) end # (tmp = foo) && tmp.bar s(:and, assign, call) end ## Other methods def initialize @tmp = 0 super # don't forget to call super! end # Generates a random variable. def tmpvar "__andand_#{@tmp += 1}".to_sym end end # instantiate a new processor processor = Andand.new # foo.andand.bar example = s(:call, s(:call, s(:call, nil, :foo, s(:arglist)), :andand, s(:arglist)), :bar, s(:arglist)) # process it result = processor.process(example) pp result # s(:and, # s(:lasgn, :__andand_1, s(:call, nil, :foo, s(:arglist))), # s(:call, s(:lasgn, :__andand_1), :bar, s(:arglist))) # BONUS: turn it into Ruby with Ruby2Ruby require 'ruby2ruby' ruby = Ruby2Ruby.new.process(result) puts ruby # (__andand_1 = foo and (__andand_1).bar)
More¶ ↑
SexpBuilder has four different concepts:
-
Matchers
-
Rules
-
Rewriters
-
Contexts
Matchers¶ ↑
A matcher is a bit of Ruby code which can be used in your rules. The expression it should match is passed in, and it should return a true-ish value if it matches. The matcher will be evaluated under the instantiated processor, so you can use other instance methods and instance variables too.
class Example < SexpBuilder matcher :five_arguments do |exp| self # => the instance of Example exp.length == 6 # the first will always be :arglist end rule :magic_call do s(:call, # a method call nil, # no receiver :MAGIC!, # method name five_arguments) # our matcher end end
Rules¶ ↑
You’ve heard it before, but let’s repeat: Rules are simply snippets of SexpPath which can refer to each other and itself. They are basically method definition, so they can take arguments too.
The rule will be evaluated under a special scope, but if you really need it you can access the instantiated processor using ‘instance`. You should however move any specific Ruby code into a matcher and let the rules simply contain other rules and matchers.
class Example < SexpBuilder # Matches any number. rule :number do |capture_as| # Doesn't make very much sense to take an argument here, # it's just an example s(:lit, _ % capture_as) end # Matches a sequence of plusses: 1 + 2 + 3 rule :plus_sequence do s(:call, # a method call number(:number) | # the receiver can be a number plus_sequence, # or a sequence :+, s(:arglist, number(:number) | # the argument can be a number plus_sequence # or a sequence end end
Rewriters¶ ↑
Rewriters take one or more rules and defines replacements when they match. The data-object from SexpPath is given as an argument. If you want some of the sub-expressions matched too, you’ll have to call process yourself.
class Example < SexpBuilder # We want to rewrite the plus_sequence above rewrite :plus_sequence do |data| # sum the numbers sum = data[:number].inject { |all, one| all + one } # return a new number s(:lit, sum) end rewrite :something_else do |data| # process the block in case it also needs to be rewritten block = process(data[:block]) do_funky_stuff(block) end end
Contexts¶ ↑
Contexts allows you to group a set of rewriters together. It will inherit the parents rules and matchers.
class Example < SexpBuilder # Matches a class definition rule :class_def do s(:class, _ % :name, _ % :parent, _ % :content) end rewrite :class_def do |data| # NOTICE: we use process_class to enter the class-context. content = process_class(data[:content]) do_funky_stuff(content) end # Only for stuff inside a class: context :class do rule :method_definition do s(:defn, _ % :name, _ % :args, _ % :content) end rewrite :method_definition do |data| # this will continue processing in the class-context. # use process_main to enter the main context again. content = process(data[:content]) do_funky_stuff(content) end end end
If you subclass your processor, it will also enter a new context:
class ModuleContext < Example # use process_module to enter this context. end
By default it takes the last part of the name, removes “Context” or “Builder” at the end and turns it into snake case. If needed, you can easily override this yourself (remember to turn it into a writable method name though):
def Example.context_name(mod) "context#{rand(10)}" end
License¶ ↑
See COPYING for legal information. It’s a MIT license which allows you to do pretty much what you want with it, and please do!