The awaited Crystal interpreter has been merged. To use it, you need to compile Crystal with a special flag and, at the time of writing, the official releases (.deb, .rpm, docker images, etc.) are not being compiled with it.
This post doubles as a F.A.Q. for this special feature. Let’s start from the very beginning:
Why Crystal needs an interpreter
While many will find this obvious, it’s useful to point out two specific features that an interpreter might enable:
- In principle an interpreter should start executing code faster, since the codegen phase is skipped (see below), allowing to quickly test the result of some code without needing to recompile. It should be noted that the interpreter is quick for short programs that need to run only once, but compiled mode provides much more performance and should be preferred for most production use cases. Also, while the behavior of interpreted and compiled programs might differ in some aspects (due to the system), we intend to keep the differences down to a minimum, and most programs should behave exactly the same in interpreted and compiled mode.
- An interpreter improves the experience of debugging, being an ad-hoc tool to the language (unlike the generic
lldb
).
What is the current status?
The interpreter is currently on an experimental phase, with lots of missing bits. The reason to merge it at this early stage is to enable a proper discussion of interpreter-related PRs and speed up its development a bit. For those willing to try it out, we welcome interpreter-related issues taking into consideration the already known issues aforelinked.
How does it work?
The original PR answers it:
When running in interpreted mode, semantic analysis is done as usual, but instead of then using LLVM to generate code, we compile code to bytecode (custom bytecode defined in this PR, totally unrelated to LLVM). Then there’s an interpreter that understands this bytecode.
How do I invoke it?
⚠️ This is not set in stone!
Assuming you’ve compiled Crystal passing the flag interpreter=1
to make
, you can invoke the interpreter using two modes right now:
crystal i file.cr
This commands runs a file in interpreted mode, so if write_hello.cr
contains the following:
File.write("/tmp/hello", "Hello from the interpreter!")
puts "done"
Invoking crystal i write_hello.cr
will produce the file /tmp/hello
and print "done"
to the stdout
.
crystal i
This command opens an interactive crystal session (REPL) similar to irb
from Ruby. In this session we can write a command and get its result:
icr:1:0> File.read_lines("/tmp/hello")
=> ["Hello from the interpreter!"]
(Note: the highlighting is not yet part of the interpreter.)
Debugging a program
In any of these two modes, you can use debugger
in your code to debug it at that point. This is similar to Ruby’s pry
. There you can use these commands (similar to pry
also):
step
: go to the next line/instruction, possibly going inside a method.next
: go to the next line/instruction, doesn’t enter into methods.finish
: exit the current method.continue
: resume execution.whereami
: show where the debugger is.
For instance, if we add debugger
between the write
and the puts
in write_hello.cr
, we get the following after interpreting the file:
From: /tmp/write_hello.cr:3:6 <Program>#/tmp/write_hello.cr:
1: File.write("/tmp/hello", "Hello from the interpreter!")
2: debugger
=> 3: puts "done"
pry>
And if we step
, we can see the method form the standard library being called:
pry> step
From: /Users/beta/projects/crystal/crystal/src/kernel.cr:385:1 <Program>#puts:
380:
381: # Prints *objects* to `STDOUT`, each followed by a newline character unless
382: # the object is a `String` and already ends with a newline.
383: #
384: # See also: `IO#puts`.
=> 385: def puts(*objects) : Nil
386: STDOUT.puts *objects
387: end
388:
389: # Inspects *object* to `STDOUT` followed
390: # by a newline. Returns *object*.
pry>
At this point, we might be curious: what are the objects
that we passed to this method? We step
once again to have the variable in scope, and we issue:
pry> objects
{"done"}
How much faster does the interpreter load a program?
We’re definitely missing benchmarks, more so given that not many shards can be successfully interpreted. Testing on a few random files from the standard library and Crystal Patterns, it loads them (and executes them, see below) between 50 and 75% faster than it takes when compiling them (comparing time crystal <file>
vs. time crystal i <file>
). This depends significantly on how much time Crystal takes on the common steps to the compiler and the interpreter, like parsing and semantic analysis.
How fast (or slow) does it run?
As Ary, its creator, showed in Crystal Conf 1.0, it runs sufficiently fast—for an interpreter that is. Of course, it’s not nearly as efficient as a mature interpreter implementation like Ruby’s, yet in our preliminary tests it runs fast enough for the expected use cases. For instance, it can handle millions of integer additions within the second. This said, using the interpreter for processing intensive tasks is definitively discouraged, as that’s the task for a compiled program.
To conclude, merging the PR is the first step to get a working interpreter in Crystal and, more importantly, it’s a testament of the will of the Crystal team to improve the developer experience.
The awaited Crystal interpreter has been merged. To use it, you need to compile Crystal with a special flag and, at the time of writing, the official releases (.deb, .rpm, docker images, etc.) are not being compiled with it.
This post doubles as a F.A.Q. for this special feature. Let’s start from the very beginning:
Why Crystal needs an interpreter
While many will find this obvious, it’s useful to point out two specific features that an interpreter might enable:
lldb
).What is the current status?
The interpreter is currently on an experimental phase, with lots of missing bits. The reason to merge it at this early stage is to enable a proper discussion of interpreter-related PRs and speed up its development a bit. For those willing to try it out, we welcome interpreter-related issues taking into consideration the already known issues aforelinked.
How does it work?
The original PR answers it:
How do I invoke it?
⚠️ This is not set in stone!
Assuming you’ve compiled Crystal passing the flag
interpreter=1
tomake
, you can invoke the interpreter using two modes right now:crystal i file.cr
This commands runs a file in interpreted mode, so if
write_hello.cr
contains the following:Invoking
crystal i write_hello.cr
will produce the file/tmp/hello
and print"done"
to thestdout
.crystal i
This command opens an interactive crystal session (REPL) similar to
irb
from Ruby. In this session we can write a command and get its result:(Note: the highlighting is not yet part of the interpreter.)
Debugging a program
In any of these two modes, you can use
debugger
in your code to debug it at that point. This is similar to Ruby’spry
. There you can use these commands (similar topry
also):step
: go to the next line/instruction, possibly going inside a method.next
: go to the next line/instruction, doesn’t enter into methods.finish
: exit the current method.continue
: resume execution.whereami
: show where the debugger is.For instance, if we add
debugger
between thewrite
and theputs
inwrite_hello.cr
, we get the following after interpreting the file:And if we
step
, we can see the method form the standard library being called:At this point, we might be curious: what are the
objects
that we passed to this method? Westep
once again to have the variable in scope, and we issue:How much faster does the interpreter load a program?
We’re definitely missing benchmarks, more so given that not many shards can be successfully interpreted. Testing on a few random files from the standard library and Crystal Patterns, it loads them (and executes them, see below) between 50 and 75% faster than it takes when compiling them (comparing
time crystal <file>
vs.time crystal i <file>
). This depends significantly on how much time Crystal takes on the common steps to the compiler and the interpreter, like parsing and semantic analysis.How fast (or slow) does it run?
As Ary, its creator, showed in Crystal Conf 1.0, it runs sufficiently fast—for an interpreter that is. Of course, it’s not nearly as efficient as a mature interpreter implementation like Ruby’s, yet in our preliminary tests it runs fast enough for the expected use cases. For instance, it can handle millions of integer additions within the second. This said, using the interpreter for processing intensive tasks is definitively discouraged, as that’s the task for a compiled program.
To conclude, merging the PR is the first step to get a working interpreter in Crystal and, more importantly, it’s a testament of the will of the Crystal team to improve the developer experience.