Ballerina is an event-driven, parallel programming language for networked applications.
We’ve been developing the Ballerina language, the compiler, and the runtime environment for about a year now. The whole system is still evolving, and we are continuously introducing improvements and new features.
A language runtime usually consists of a memory manager, a garbage collector, an interpreter (if the language does not get compiled to machine codes), built-in libraries, etc. Ballerina is a compiled language: the compiler transforms the source code to Ballerina bytecode, which is a compact and optimized representation of the Ballerina source interpreted by a bytecode interpreter.
Here in this post, I focused only on the Ballerina interpreter, and it’s evolution so far. We started with the tree (or AST — abstract syntax tree) based interpreter as a proof-of-concept implementation of the language runtime. After that, it went through many iterations, and we’ve done three complete rewrites up to now.
Abstract Syntax Tree (AST) interpreter
Ballerina compiler transforms the source code into an in-memory tree structure. The compiler ensures the syntactic and semantic correctness of the source code during this transformation. This tree structure is called abstract syntax tree (AST). It is the abstract representation of the source program. Then the Ballerina tree interpreter executes the program by traversing the AST. We’ve used the visitor pattern to model the tree traversal as it is the most common technique.
Even before the very first public release of Ballerina, we hit the limitations of this interpreter. It was nearly impossible to implement non-blocking or asynchronous I/O with this design. Once a thread starts executing a Ballerina program by following the tree structure, we cannot release the thread until the interpreter runs the complete program, even if the thread blocks on I/O. E.g., making an outbound call to an HTTP endpoint.
Then we looked at solutions where we can release the thread back to the thread-pool once it performs a blocking operation and resume the execution when the blocking operation is completed.
Linked AST interpreter
We worked on another variation of the AST interpreter where we linked tree nodes based on the execution order. This model allowed us to release a thread and resume once the blocking operation is completed. But the complexity of this implementation was much higher than the previous variation which made it hard to maintain this interpreter with the language improvements.
A compiler performs many time-consuming tasks before it hands over the program to the interpreter. Both the previous interpreter variations did not allow us to persist the compiled program, thus affected the startup time. We began to look for solutions which would solve above mentions problems. We discussed Compiling Ballerina into Java bytecode, but it would make Ballerina a JVM-based language and also converting Ballerina constructs to Java, or Java bytecode could be challenging.
Another feasible approach is to compile source code into Ballerina bytecode which can be interpreted by a bytecode interpreter.
Ballerina bytecode is the instruction set that the Ballerina bytecode interpreter understands. It is also the intermediate representation to which a source program is transformed by the Ballerina compiler.
Following example shows the Ballerina source code and the generated bytecode in a side-by-side view.
If you are familiar with Java bytecode, you may find above bytecode similar to Java counterparts. But there is a big difference in the way we interpret these instructions. I’ve explained differences in the next section.
Ballerina Virtual Machine (BVM)
Ballerina virtual machine (BVM) is a software process which executes Ballerina programs. BVM is a combination of all the following components.
- Instruction set (Ballerina bytecode)
- Bytecode interpreter — This is a virtual CPU which performs the instruction cycle. i.e fetch-decode-execute
- Storage for instructions and operands
- Function call stack
- Instruction pointer which points the next instruction to be executed.
BVM does not define a memory manager or a garbage collector (at least for now). We’ve implemented the first version of the BVM in Java. Therefore it gets a memory manager and a garbage collector for free.
We designed BVM architecture, and the instruction set by following the register-based virtual machine architecture. There are two main VM architectures: Register-based and Stack-based. JVM is based on the stack-based architecture, while the LUA VM and Dalvik VM are based on the register-based architecture. You can find a plenty of articles which explains these two architectures. Here, BVM uses a virtual set of registers and not actual CPU registers. These VM architectures differ from each other in how they store and load operands of the instructions.
Ballerina bytecode interpreter
The bytecode interpreter in BVM is responsible for executing instructions. Instruction is an encoded behavior. It contains an opcode and one or more operands. Here is a sample instruction.
iadd 0 1 2 — Add integers in 0th and 1st registers and store the result in the 2nd register.
We haven’t made a proper language performance comparison yet. Therefore at this point, we cannot make any statements about the performance of Ballerina. However, some simple tests showed that Ballerina performance is on par with Python. We will update you once we’ve got detailed stats.
We are considering LLVM compiler infrastructure to generate machine code for Ballerina programs to gain better performance. That’s all we can say for now. But if you have any suggestions, please do let us know.
You can engage with the Ballerina community in following ways.