Assignment in Ruby - Simple Scoped Assignment
Scope | AST Name | Code | AST |
Local | :lasgn | a=nil | [:lasgn, :a, [:nil]] |
Instance | :iasgn | @a=nil | [:iasgn, :@a, [:nil]] |
Class | :cvasgn | @@a=nil | [:cvasgn, :@@a, [:nil]] |
Global | :gasgn | $a=nil | [:gasgn, :$a, [:nil]] |
Each of these ASTs was generated using ParseTree 1.4.1 and the following command:echo "code" | parse_tree_show -f |
As you can see, each of these has the same structure in the AST. By using different types of nodes for each one, it is simpler for the Ruby VM to determine where to look for a variable and set it.
The code below is a straight through SexpProcessor using the ParseTree gem and the included SexpProcessor as a base. This processor doesn't do anything exciting but by explicitly writing the process_type methods, we have exposed interaction points where we could do things like store a list of assigned variables, output funny messages or generate metrics.
Sexp stands for S-expression. ParseTree represents S-expressions in ruby as nested arrays of arrays. S-expressions are particularly well known for their use in most Lisp like languages to represent code and data. If you would like to know more about S-expressions, Google is your friend.
SexpProcessors have a few rules that you must observe in order to get a correct traversal of the expression. First, you need to understand that the dispatch method process(exp) is initially called for every pair of matched brackets. This is important because in our process_foo methods, we need to call process() on any members of exp that are arrays. If we don't, we essentially prune that piece of the S-expression from our processing. Usually, that is wrong. It is also important to not that process(exp) only takes arrays as input. If you call process(exp.shift) and the shifted element is a literal, you will have an error on your hands. process(exp) will then pass control onto process_foo(exp) where foo is the :literal that appears as the first element of the expression. (In our processor, because of the auto_shift_type in initialize, the first element of exp is shifted off before control passes to process_foo. This results in slightly cleaner, easier to read code in process foo. Instead of
s(exp.shift, exp.shift, process(exp.shift))
, we get the slightly more clear s(:iasgn, exp.shift, process(exp.shift))
. This is obviously a personal preference.)Another basic rule of SexpProcessor is that what comes in should be what comes out, or, in the case of auto_shift_type, what would have come out if auto_shift_type were false. This is best illustrated with an example.
process([:lasgn, :a, [nil]]) should return [:lasgn, :a, [nil]]
with auto_shift_type = false
process([:lasgn, :a, [nil]]) calls process_lasgn([:lasgn, :a, [nil]])
with auto_shift_type = true
process([:lasgn, :a, [nil]]) calls process_lasgn([:a, [nil]])
in either case process_lasgn() must return [:lasgn, :a, [nil]].
Still another rule, in a process_foo(exp) method, exp should be empty before the method returns. Failure to empty the exp is considered bad form (and is usually wrong too) and therefor raises an error. This is the quickest way to catch errors in coding like
# process_lasgn([:a, [nil]])
def process_lasgn(exp)
s(:lasgn, # auto_shift_type = true or this would be s(exp.shift,
exp.shift) # :a
# exp now == [[nil]]
end
This will really help you when you get the structure wrong for the s-expression representing a node.
The final bit of magic to understand before venturing off to write your own processor is s(). s(*args) is shorthand for Sexp.new(*args). It was added to keep things easier to read and as a user of ParseTree & SexpProcessor, I am sure you will appreciate it.
# A Straight Through SexpProcessor exposing
# the process_foo simple assignment methods.
begin require 'rubygems' rescue LoadError end
require 'parse_tree'
require 'sexp_processor'
class PassThroughProcessor < SexpProcessor
def initialize
super
self.auto_shift_type = true
end
def process_lasgn(exp)
s(:lasgn, exp.shift, process(exp.shift))
end
def process_iasgn(exp)
s(:iasgn, exp.shift, process(exp.shift))
end
def process_cvasgn(exp)
s(:cvasgn, exp.shift, process(exp.shift))
end
def process_gasgn(exp)
s(:gasgn, exp.shift, process(exp.shift))
end
end
You would invoke this processor on an unsuspecting class with the following magic incantation.
PassThroughProcessor.new.process(ParseTree.new.parse_tree(klass))
If you are a glutton for punishment, you could run it against PassThroughProcessor. (*Note: As is, this wouldn't create any output at all but the skeleton is all there. Enjoy yourself.*)
I have more than used up the time and space allotted for this article. I hope this was educational and I look forward to seeing you next time with Assignment in Ruby.