Thursday, March 2, 2006

IRB is GO!

I should post about things not working more often.

Within a few hours after my previous post, where I showed the world how IRB now starts up successfully in JRuby but does not work, I was back at it trying to fix the next few bugs preventing it from working. The first issue was a NullPointerException deep in the interpreter, when executing an "until" block. Our parser, for right or for wrong, was producing an AST "UntilNode" with no body. While this could be correct or incorrect behavior--since the "until" in question actually did have an empty body--we still were not handling it correctly. The interpreter assumed that all "until"s would have bodies, and when a body turned up null...kaboom. A fix to check for null and not attempt to evaluate the body was an easy, if not entirely kosher, way to fix it. Done.

However, nothing could prepare me for what followed.

C:\JRubyWork\jruby3>jruby C:\ruby\bin\irb
irb(#<IRB::WorkSpace:0x5a9c6e
>):001:0> x = 1
=> 1

I expected that the "until" bug would go away...that much was easy. however, I did not expect the variable assignment to work. "Ok," I thought, "that's better progress than I expected, but let's try something more complicated."

irb(#<IRB::WorkSpace:0x5a9c6e>):002:0> puts x
NameError: undefined local variable or method 'x' for
#<IRB::WorkSpace:0x5a9c6e>
from (irb):1:in `method_missing'
...

Ahh, there's the comforting disappointment I was used to. The 'x' variable had been declared and assigned, but for whatever reason, it was not visible in the current scope.

Normally, I would have continued on to fix this scoping issue, which certainly would have involved a complicated dive into JRuby internals, hunting for mishandled scopes, bindings, frames, and wrapper objects. In this case, however, I decided to give IRB's "single IRB" mode a try, which simplifies the logical scoping of the IRB workspace. What follows is a series of annotated IRB sessions running--yes, running successfully--under JRuby.

This first demo shows something basic: a multiline do/end array iteration.

C:\JRubyWork\jruby3>jruby C:\ruby\bin\irb --single-irb
irb(#<irb::workspace:0x175ace6>):001:0> [1, 2, 3].each do |i|
irb(#<irb::workspace:0x175ace6>):002:1* puts i
irb(#<irb::workspace:0x175ace6>):003:1> end
1
2
3
=> [1, 2, 3]

This confirmed several things:
  • method calls were working just fine
  • array instantiation and integer literals were ok
  • multi-line constructs were being handled correctly
It is this last one that surprised me a bit. I had not expected multi-line constructs to work so well and without any problems, but there it was. Playing around a bit more, I discovered some other surprises:
  • line editing was working successfully, and I could arrow-key left and right to correct mistakes
  • command history was also working, so that up and down arrow would retrieve the next and previous lines, respectively
  • tab completion does not work
Excluding the tab completion issue (hitting tab just inserts a "tab" character into the current line), the perfectly working line editing and command history totally blew me away. I have NEVER seen a console-mode Java application do such things so seamlessly, much less one running an interactive shell. It appears that IRB's fallback "StdioInputHandler" is far less "dumb" than I expected. It was making Java do things I didn't know Java could do. Excited, I pressed on.

This next demonstration tests the declaration and instantiation of a multiline class, another area I thought would never work correctly.

irb(#<irb::workspace:0x175ace6>):001:0> class MyClass
irb(#<irb::workspace:0x175ace6>):002:1> def hello
irb(#<irb::workspace:0x175ace6>):003:2> "Hello from IRB!"
irb(#<irb::workspace:0x175ace6>):004:2> end
irb(#<irb::workspace:0x175ace6>):005:1> end
=> nil
irb(#<irb::workspace:0x175ace6>):006:0> x = MyClass.new
=> #<myclass:0x1621fe6>
irb(#<irb::workspace:0x175ace6>):007:0> puts x.hello
Hello from IRB!
=> nil

Once again, JRuby (and IRB) thoroughly surprised me. Defining a class over multiple lines worked perfectly, just as you'd expect from IRB running under C Ruby. At this point, IRB was running so well I began to have some doubts. Could it be that IRB had called out to an external C Ruby process for running the interactive portion of the shell? Such a thing would not be unheard of; Rake launches external Ruby processes to run test cases, though you might never notice such a thing. There was, however, a simple way to confirm that I was actually seeing JRuby at work and not C Ruby: call Java code.

JRuby's greatest strength lies, unsurprisingly, in its ability to neatly tie Ruby and Java code together. For what other purpose would we want Ruby running in the JVM than to take advantage of the wealth of libraries the Java world has to offer? The integration is improving more and more with each release, and has become extremely powerful, usable, and above all very Ruby-like.

This next demonstration shows IRB calling Java code.

irb(#<irb::workspace:0x175ace6>):001:0> require 'java'
=> true
irb(#<irb::workspace:0x175ace6>):002:0> include_class "java.lang.System"
=> ["java.lang.System"]
irb(#<irb::workspace:0x175ace6>):003:0> System.out.println("Hello from Java")
Hello from Java
=> nil

Now some of you may not realize what this means. The ability to interactively script and exercise Java code from within an IRB session has huge potential for testing Java code, debugging JRuby (perhaps that's more exciting to me...oh well), and providing all the interactive goodies that Rubyists have taken for granted with the power and variety of Java's capabilities.

So, a final demonstration is in order.

irb(#<irb::workspace:0x175ace6>):001:0> require 'java'
=> true
irb(#<irb::workspace:0x175ace6>):002:0> include_class "javax.swing.JFrame"
=> ["javax.swing.JFrame"]
irb(#<irb::workspace:0x175ace6>):003:0> include_class "javax.swing.JButton"
=> ["javax.swing.JButton"]
irb(#<irb::workspace:0x175ace6>):004:0> frame = JFrame.new("my frame")
=> javax.swing.JFrame[long desc omitted...]
irb(#<irb::workspace:0x175ace6>):005:0> button = JButton.new("my button")
=> javax.swing.JButton[
long desc omitted...]
irb(#<irb::workspace:0x175ace6>):006:0> frame.contentPane.add(button)
=> javax.swing.JButton[
long desc omitted...]
irb(#<irb::workspace:0x175ace6>):007:0> frame.setSize(200, 100)
=> nil
irb(#<irb::workspace:0x175ace6>):008:0> frame.show
=> nil

The result:

It works! It really works!

So there you have it. With a few small caveats (like --single-irb), IRB is actually up and working in JRuby, far sooner than I expected. This is turning out to be a really good week.

4 comments:

  1. And even greater than your last post. Great stuff! Swing from within irb. Can't believe it! I've really got your stuff another look (last time is quite some time ago) some time soon. Keep it up!

    Jan Prill

    ReplyDelete
  2. Thouroughly enjoyed reading your blog this friday afternoon. Will continue to follow your progress.

    --corbanbrook

    ReplyDelete
  3. Charles, great work.

    I'm build trading systems for banks, so it will be very useful for me to develop web interfaces with rails and still have full access to the enterprise features of java such as JMS.

    ReplyDelete