I've been doing most of the research on the compilation front, so I'll inject a few points here.
- Compilation to an intermediate bytecode would certainly be an improvement. The major refactoring I undertook last fall to change the interpreter from recursive to iterative was an effort toward this end. Once the actual bytecodes are identified, and the code for them written, compilation would not be terribly difficult. Anders can attest to that; the bytecodes would be mostly a transformation of the AST, which is fairly rich already.
- YARV bytecodes are unlikely to be the most optimal way to do what we need to do. Because YARV is based on C Ruby code, rather than a clean implementation of Ruby, they are in many cases tied to the way things are done in C Ruby. In addition, the optimized versions of those bytecodes frequently are directly tied to particular sequences of C Ruby function calls, which we would obviously not have. They are by no means a purely generic set of bytecodes, and although we may eventually be able to interpret or compile them to Java bytecode, they will probably not be the best route to take for our intermediate bytecodes. This isn't to say we can't learn from ko1's approach, but given the constraints of his design it's hardly going to be the best general-purpose approach.
- There are alternatives to an intermediate bytecode, as well.
Given that we are not C, and we have the ability to create very lightweight "executable" objects, I have taken an initial approach to the interpreter redesign where instructions carry their own code. In other words, you don't look up the appropriate piece of code based on a bytecode...you execute() the instruction directly.
In the current interpreter, this is combined with an "instruction stack" which maintains current context. Each instruction that executes may manipulate the runtime environment (setting variables, defining classes) and may also manipulate the instruction stack. Progressing deeper into the AST results in the stack deepening; executing instructions results in it shrinking. This was the simplest way to directly convert our working recursive interpreter into an iterative one. It is, in its current form, almost purely iterative, while still retaining almost identical code to the original recursive interpreter. Given a few full days of work, I could probably have continuations and basic tail-call optimization working, with green threading close behind.
The next phase of this transition is to make the instructions know not only how to execute, but what instruction should come next. Currently, executing an instruction manipulates the environment and the instruction stack, but does not return any result. In the next version of the interpreter, the instructions themselves will know what comes next in the code, and will do their environment manipulation and return the next instruction to run. This allows us to eliminate the "instruction stack", since each call to an instruction returns a follow-up instruction. In this way, the "current instruction" is simply a cursor into the AST, knowing where all results of this instruction will need to branch as well as the code necessary to perform this instruction's task.
There has been some debate in the team as to whether this approach or an intermediate bytecode have any benefits over each other. The "directed graph" approach would minimize the overhead of looking up instructions based on bytecode, but that could be a simple switch statement too. The graph approach also gives us the potential to define optimized "instructions" without defining new bytecodes and code to go with them. However, the bytecode approach may in some cases be simpler, and may be more easily transitioned to Java bytecodes. I believe the graph will be an easier solution in the short term...and easier is very important when we must make such changes all at once.
- Compilation to Java bytecodes would seem to be simple at first glance, but the newer interpreter design has revolved around being able to support green/m:n threading and continuations, two features not available in "vanilla" Java code. Since we do not have control over the stack or the thread scheduler in a JVM, we can never allow Ruby code to deepen the Java stack. We have discussed a number of ways to support Java bytecode compilation, with the most promising idea being a trampoline-style method that, while pure Java bytecode, repeatedly returns control to a higher "thread" while incrementing its own program counter. This would allow optimizing the bytecode by performing multiple AST operations in a single shot while still supporting green threading and continuations.
I have started some preliminary work on all these items. The big difference between the current work and all previous work is that now the stack must not deepen, and cross-frame events must be passed back to the "thread" to handle at that level, rather than recursing. Since we're focused on JavaOne right now, this work will probably only get a little of my time, but when there's code to show off I'll certainly post it here.
--
Charles Oliver Nutter @ headius.blogspot.com
JRuby Developer @ jruby.sourceforge.net
Application Architect @
www.ventera.com
- [Jruby-devel] JRuby-future. Ola Bini
- RE: [Jruby-devel] JRuby-future. Guy Korland
- Re: [Jruby-devel] JRuby-future. Anders Bengtsson
- Re: [Jruby-devel] JRuby-future. Ola Bini
- Re: [Jruby-devel] JRuby-future. Anders Bengtsson
- Re: [Jruby-devel] JRuby-future. Charles O Nutter