So You Want to Build a Language VM - Part 06 - The REPL

Starts building a REPL for the Iridium VM

A REPL

REPL stands for Read, Evaluate, and Print Loop. It is also referred to as the interactive interpreter for a language. For example, if you open up Terminal or iTerm, we can look at Python’s REPL:

fletcher$ python
Python 2.7.10 (default, Oct  6 2017, 22:29:07)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 1
>>> 1 + x
2
>>>

It allows you to execute Python code and see the results, with no need to write your code in a .py file. This can be handy if you want to do some quick scripting or testing. Ours will allow us to both execute code and interact with the VM as we expand its functionality.

Because we don’t yet have an assembler or compiler, we’ll have to enter our code as hexadecimal…​but hey, you wanted to learn about low-level stuff, right? =)

Basic Structure

All a REPL is, is an infinite loop that does something like:

  1. Wait for input

  2. Evaluate input

  3. Print response

So, how should we structure this in our code? The most common approach is to make it part of the language interpreter. That’s why if you type python or perl or ruby without a filename, you get a REPL.

Ownership

Does a VM own a REPL? Does a REPL own a VM? Should we create another data structure to manage interactions between the two? All good questions! With the ownership model of Rust, we have to think a bit more about which data structure owns which other data structure. The approach I took, and it seems to work well so far, is to have the REPL manage a VM.

Implementation

First thing, let’s create a new directory in our src/ directory called repl/. This will put the REPL in its own Rust module. Inside repl/, create a file called mod.rs. This is where we’ll define our struct.

REPL Struct

First, we’ll need to import some things:

use std;
use std::io;
use std::io::Write;
use vm::VM;

/// Core structure for the REPL for the Assembler
pub struct REPL {
    command_buffer: Vec<String>,
    // The VM the REPL will use to execute code
    vm: VM,
}

Pretty simple so far! Our impl block with the typical new() function looks like:

impl REPL {
    /// Creates and returns a new assembly REPL
    pub fn new() -> REPL {
        REPL {
            vm: VM::new(),
            command_buffer: vec![]
        }
    }

Command Buffer

Why are we keeping a list of executed commands? So the user can press the up-arrow key and see what they ran. Try it in the Python REPL!

VM

The REPL struct is going to have a VM it uses to execute code. This is because the REPL should be the first thing created and last thing destroyed.

Yet Another Loop…​

Our infinite loop goes in a public function in the impl block for the REPL. In main.rs, we’ll instantiate the REPL then call this function. It looks like:

pub fn run(&mut self) {
    println!("Welcome to Iridium! Let's be productive!");
    loop {
        // This allocates a new String in which to store whatever the user types each iteration.
        // TODO: Figure out how create this outside of the loop and re-use it every iteration
        let mut buffer = String::new();

        // Blocking call until the user types in a command
        let stdin = io::stdin();

        // Annoyingly, `print!` does not automatically flush stdout like `println!` does, so we
        // have to do that there for the user to see our `>>> ` prompt.
        print!(">>> ");
        io::stdout().flush().expect("Unable to flush stdout");

        // Here we'll look at the string the user gave us.
        stdin.read_line(&mut buffer).expect("Unable to read line from user");
        let buffer = buffer.trim();
        match buffer {
            ".quit" => {
                println!("Farewell! Have a great day!");
                std::process::exit(0);
            },
            _ => {
                println!("Invalid input");
            }
        }
    }
}

The loop above is the skeleton for our REPL infinite loop.

Flushing stdout

We want to preface all our output with a >>> like the Python REPL, but we don’t want to add a newline to the end of it, which println! does. print! doesn’t add a newline, but it doesn’t flush stdout either, so the user won’t see the >>>. In the code above, we manually flush it.

main.rs

Now let’s go to src/main.rs and hook it up to our REPL. First, add:

pub mod repl;
Important
If you do not remember how modules work in Rust, check out this.

Then in our main function:

fn main() {
    let mut repl = repl::REPL::new();
    repl.run();
}

Once you have that, you should be able to type cargo run and see it in action. Typing .quit will exit the REPL.

$ cargo run
   Compiling iridium_part_02 v0.1.0 (file:///Users/fletcher/Projects/iridium-book)
warning: field is never used: `command_buffer`
 --> src/repl/mod.rs:8:5
  |
8 |     command_buffer: Vec<String>,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(dead_code)] on by default

warning: field is never used: `vm`
 --> src/repl/mod.rs:9:5
  |
9 |     vm: VM,
  |     ^^^^^^

    Finished dev [unoptimized + debuginfo] target(s) in 0.44s
     Running `target/debug/iridium_part_02`
Welcome to Iridium! Let's be productive!
>>> .quit
Farewell! Have a great day!

Don’t worry about the unused variable warnings. We’ll fix that soon enough. =)

Command History

While we’re here, let’s implement storing a history of what the user types in. We have our Vec already initialized, so its a matter of appending a copy of every input to it, like so:

stdin.read_line(&mut buffer).expect("Unable to read line from user");
let buffer = buffer.trim();

// This is the line we add to store a copy of each command
self.command_buffer.push(buffer.to_string());

match buffer {

How do we use the command buffer? For now, let’s add another command, .history, that will print out the previous commands. Later on, we’ll make it more sophisticated.

History Command

Below the .quit command in src/repl/mod.rs, add this:

".history" => {
    for command in &self.command_buffer {
        println!("{}", command);
    }
},

And now test it out:

$ cargo run
   Compiling iridium_part_02 v0.1.0 (file:///Users/fletcher/Projects/iridium-book)
Welcome to Iridium! Let's be productive!
>>> invalid
Invalid input
>>> .hello
Invalid input
>>> .history
invalid
.hello
.history
>>>

End

That wraps up this post! In the next one, we’ll add entering bytecode and executing it to our REPL.


If you need some assistance with any of the topics in the tutorials, or just devops and application development in general, we offer consulting services. Check it out over here or click Services along the top.