Abstract syntax trees(AST) provide a representation of the source code that can be manipulated by another program. These trees are used by compilers and IDEs to do their work.
The JRuby distribution includes a parser and a nice tree representation for Ruby source code. This AST is used by the JRuby implementation to do its work, and also recently I noticed that the Netbeans Ruby support uses this representation for some tasks.
Calling JRuby Parser is pretty straightforward, just do the following:
package org.langexplr.jrubyasttests;
import org.jruby._
import org.jruby.ast._
object ScalaTestApp {
def main(args : Array[String]) : Unit = {
val rubyCode = "puts \"Hello\""
val r = Ruby.getDefaultInstance
val rootNode = r.parse(rubyCode,"dummy.rb",r.getCurrentContext.getCurrentScope,0).asInstanceOf[RootNode]
...
}
}
The
rootNode
variable contains the root of the syntax tree for the code stored in rubyCode
. In order to explore the syntax tree I wrote a small program that shows a Swing JTreecomponent with the result of parsing a snippet. Creating this program was easy because Node, the base class of all the elements in JRuby AST, provides a
childNodes
method. The model for the AST is the following:
package org.langexpr.jrubyasttest;
import javax.swing.tree.TreeModel
import javax.swing.event.TreeModelListener
import javax.swing.tree.TreePath
import org.jruby._
import org.jruby.ast._
class JRubyTreeModel() extends TreeModel {
var rootNode : Node = null;
def this(rubyCode : String) = {
this()
val r = Ruby.getDefaultInstance
this.rootNode = r.parse(rubyCode,"dummy.rb",r.getCurrentContext.getCurrentScope,0)
}
def getRoot : Object = rootNode
def isLeaf(node: Object ) =
node.asInstanceOf[Node].childNodes.size == 0
def getChildCount( parent : Object) : int =
parent.asInstanceOf[Node].childNodes.size
def getChild(parent : Object , index : int) : Object = {
parent.asInstanceOf[Node].childNodes.get(index)
}
def getIndexOfChild( parent : Object, child : Object) : int = {
parent.asInstanceOf[Node].childNodes.indexOf(child)
}
def valueForPathChanged( path : TreePath, newValue : Object) : unit ={
}
def addTreeModelListener( l : TreeModelListener) : unit = {
}
def removeTreeModelListener( l : TreeModelListener) : unit = {
}
}
With this model now we can create the main program:
package org.langexpr.jrubyasttest;
import javax.swing._
import java.awt.event._
import java.awt.BorderLayout
import java.awt.Dimension
object JRubyAstTest {
val treeVisualizer = new JTree
val input = new JTextArea
def createFrame = {
val theFrame = new JFrame("JRuby AST test")
theFrame.setSize(new Dimension(640,450))
val content = theFrame.getContentPane
content.setLayout(new BorderLayout)
content.add(treeVisualizer,BorderLayout.CENTER)
val bottomPanel = new JPanel
bottomPanel.setLayout(new BorderLayout)
content.add(bottomPanel,BorderLayout.SOUTH)
bottomPanel.add(new JScrollPane(input),BorderLayout.CENTER)
input.setText("x=1\ny = 2\n")
treeVisualizer.setModel(new JRubyTreeModel(input.getText))
val button = new JButton("Parse!")
button.addActionListener( new ActionListener() {
def actionPerformed(ae : ActionEvent) =
{
treeVisualizer.setModel(new JRubyTreeModel(input.getText))
}
})
bottomPanel.add(button,BorderLayout.SOUTH)
theFrame
}
def main(args : Array[String]) : Unit = {
val f = createFrame
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true)
}
}
Now we can visually explore the AST using this little program.
The following code:
def foo(x)
return x*x
end
Is visualized like this:
Examples in this post could also be implemented easily using JRuby but in the second part I'm going to a couple of Scala features that I specially like to manipulate this data structure.