Wednesday, November 7, 2007

Exploring JRuby Syntax Trees With Scala, Part 1

In this post I'm going to show how to use Scala to have access to JRuby syntax trees.

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.