Blurred Lines: Is Ruby an interpreted language and what does that even mean?

Manuel Grullon
5 min readMar 27, 2018

--

A little over two weeks ago I began my journey at the Flatiron School with 23 other hopeful individuals, learning to write Ruby code and quickly picking up new tools, but somewhere along the way one of my classmates asked me if Ruby was an interpreted or compiled language. I told him that Ruby is an “interpreted language, I think?” and he asked me to elaborate. Why is Ruby interpreted, what does that mean, and what are the pros and cons of interpreted versus compiled languages. Most importantly, how does our code go from code to execution? Not being able to properly answer his questions I decided to look into the matter and see what I could find.

Compiled versus Interpreted

What’s the difference between a compiled and an interpreted language? An interpreted language is ready to run as soon as your done typing. In contrast, a compiled language must be compiled before it can be run. All of your code is checked for structural errors and then encapsulated into a file that is ready to be executed, consisting of machine code. If your code executes 2 + 2, then your machine code consists of the machine specific instructions to add two numbers. If you were to look at the machine code, it wouldn’t look very much like the code your originally wrote, and the machine code created for different machines can look different despite being produced from the same source code.

In interpreted languages, when you run your code, it doesn’t immediately begin to execute. It gets broken down and into byte code and then sent an interpreter to be executed. You can think of byte code as one level before machine code. Your code has been broken down but it’s not machine level instructions yet. That happens as it’s interpreted. A compiled program doesn’t have to wait to be interpreted, it immediately begins to execute as soon as your run it.

Compiled code is generally executed faster than interpreted code, because it doesn’t have to go through this translation step that interpreted code does. Interpreted code is generally more flexible, as compiled code is not as portable. If you write a C++ program, a compiled language, and compile it on your Machine, and then transfer to another machine, there’s no guarantee it’ll work. You may need to compile it again.

Compiled code can be changed at the machine code level. You could make the tiniest of optimizations because you can change exactly what’s happening at run time. This can be important when designing a system of distributed computers all running the same code, and trying to parallelize very specific operations. Many video game engines are written in C and similar languages, because run time performance is very critical in games.

Interpreted languages are much more flexible. It’s easier to share your code as it will run on any machine that can interpret it. In general, interpreted languages are seen as “easier to write and test” although that is subjective.

image source: quora.com

So Ruby is an interpreted language right?

Turns out the answer is it depends on who you ask. Nowadays whether or not a language is interpreted or compiled is not necessarily dependent on the language, but on the implementation and tools used alongside a language. As Rubyist, we’re familiar with the idea of Ruby being a flexible language that lets you choose how you want to approach a problem, with multiple paths that all achieve the same effect. To some people, Ruby is a compiled language because the first tools for writing Ruby included a compiler. To us, it’s easy to see Ruby as an interpreted language because we run Ruby using the MRI, Matz Ruby Interpreter. There are different interpreters and compilers available for Ruby so it really depends on who you ask.

I wrote some code, hit enter, now what?

This next session I tried to follow along a blog written by Star Horne. I tried to invoke the same methods they used with my own example code.

puts "Hello World"

At some point we’ve all written the code above. You save it as a file called test.rb, you go into terminal, type ruby test.rb, and your terminal outputs “Hello World”. How did it do that though? It turns out that we can ask Ruby to show us some of the steps, so lets find out.

2.3.3 :006 > pp Ripper.tokenize("puts 'Hello World'")["puts", " ", "'", "Hello World", "'"]

Ripper is a built in library that allows us to intervene and see some of the intermediate steps between our code being run and what our code ends up doing. The first step is tokenization. Ruby splits everything we've written into to tokens, discrete pieces of stuff to be evaluated. No code execution has happened yet. It basically just split up all the words and spaces and punctuation, and tagged each token with some information. If we put in gibberish like $=va &+ ta2_ the tokenize step would still split this junk up and tokenize it.

2.3.3 :005 > pp Ripper.lex("puts 'Hello World'")[[[1, 0], :on_ident, "puts"],[[1, 4], :on_sp, " "],[[1, 5], :on_tstring_beg, "'"],[[1, 6], :on_tstring_content, "Hello World"],[[1, 17], :on_tstring_end, "'"]]

The next step is to parse all of these tagged tokens and turn it into something that can be read and run by a machine.

[:program,
[[:command,
[:@ident, "puts", [1, 0]],
[:args_add_block,
[[:string_literal,
[:string_content, [:@tstring_content, "Hello World", [1, 6]]]
]],
false]]]]

Wow, what happened to our code? Well, it has been parsed and turned into what is known as an Abstract Syntax Tree. All the spaces and tabs other information that is useful for humans to read but not for the functionality of our program has been stripped away and now our code is ready to be executed. At this point, if we had gibberish, we would start to get errors.

The final step is executing the code. When we type ruby in terminal to run our ruby code, we are invoking the MRI or Matz Ruby Interpreter. It takes our AST and translates it into bytecode to be run by the Ruby Virtual Machine. Let’s take a look at what that looks like.

2.3.3 :012 > puts RubyVM::InstructionSequence.compile("puts 'Hello World'").disassemble== disasm: #<ISeq:<compiled>@<compiled>>================================0000 trace            1                                               (   1)0002 putself0003 putstring        "Hello World"0005 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>0008 leave

This now resembles assembly language, pretty much as low level as we can go, and our program has been translated to individual commands that our processor can run. Line 0003 puts the string “Hello World” onto the stack, and line 0005 invokes the methods puts, with “Hello World” as the argument, and then the program exits. That’s a very quick look at how we go from our ruby file to executed code.

--

--