https://crystal-lang.org/2021/12/29/crystal-i.html
* Home
* Forum
* Blog
* Sponsors
* Community
* Conference
* Team
* Docs
* GitHub
* Home
* Forum
* Blog
* Sponsors
* Community
* Conference
* Team
* Docs
* GitHub
menu
[beta-zilia] Beta Ziliani 29 Dec 2021
Crystal's interpreter - A very special holiday present
Share
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:
1. 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.
2. 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 #/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 #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 vs. time crystal i
). 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:
1. 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.
2. 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 #/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 #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 vs. time crystal i
). 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.
Please enable JavaScript to view the comments powered by Disqus.
comments powered by Disqus
Crystal is licensed under the Apache License, Version 2.0
Crystal language, born & raised at Manas