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!