Your program

Your program will contain both an application and a library:

  • The library allows other programs to embed your virtual machine
  • The application lets you run programs written for the virtual machine from the command line.

You are given an archive file which contains (in a vm project):

  • Cargo.toml: the initial configuration file
  • src/main.rs: the main program for the application, which loads a binary file with machine code and executes it
  • src/lib.rs: the entry point for the interpreter library which contains your implementation of the virtual machine
  • src/tests/: a directory with many tests, ranging from individual instructions tests to complex tests
  • src/examples/: some examples for the virtual machines that you can run when your interpreter is complete

⚠ The project uses Rust edition 2024 (released on Feb. 20, 2025, with Rust 1.85). Make sure your compiler is up-to-date by executing rustup update if needed.

Tests and examples are accompanied by their disassembled counterpart to help you understand what happens (*.bin is the program for the virtual machine, *.dis is the disassembly).

Start by adding the vm Cargo project to your repository and ensure that you can build the program even though it doesn't do anything useful yet and will contain many warnings:

$ cargo build

You can see the tests fail (hopefully this is a temporary situation) by running:

$ cargo test

Program structure

At any time, make sure that the program and the tests compile, even if they don't pass succesfully yet. In particular, you are not allowed to rename the Machine and Error types, although you will need to modify them to implement this assignment. Similarly, the already documented method must be kept without modifying their signature because they will be used in automated tests.

❎ After creating a new interpreter through interpreter::Machine::new(), the following methods must be implemented:

  • step_on(): takes a descriptor implementing Write (for the out and out number instructions), and execute just one instruction
  • step(): similar to step_on(), but writes on the standard output
  • run_on(): takes a Write-implementing descriptor and runs until the program terminates
  • run(): similar to run_on(), but writes on the standard output
  • memory() and regs(): return a reference on the current memory and registers content
  • set_reg(): set the value of a register

Do not hesitate to add values to the Error enumeration to ease debugging. Also, you can implement additional functions to Machine if it helps dividing the work.

As far as Machine::new() is concerned, you might be interested in looking at slice::copy_from_slice().

Writing things to the user

For the out and out_number opcodes, you will have to write things to a file descriptor (respectively a character and a number). This can be done with the write!() macro, which lets you write into any object whose type implements the Write trait.

Suggested work program

Several tests are provided in the tests directory:

  • assignment.rs contains all the examples shown in the specification. You should try to concentrate on this one first and implement instructions in the same order as in the specification (and the test) until you pass this test. You can run only this test by using cargo test --test assignment.
  • basic_operations.rs checks that all instructions are implemented correctly. For example, it will attempt to read and write past the virtual machine memory, or use an invalid register, and check that you do not allow it.
  • complex_execution.rs will load binary images and execute them using your virtual machine.

How to debug more easily

In order to ease debugging, you can use two existing crates, log and pretty_env_logger.

log provides you with a set of macros letting you formatting debugging information with different severities:

  • log::info!(…) is for regular information
  • log::debug!(…) is for data you'd like to see when debugging
  • log::trace!(…) is for more verbose cases

See the documentation for a complete information.

pretty_env_logger is a back-end for log which gives you nice colored messages and is configured through environment variables. You can initialize at the beginning of your main program by calling pretty_env_logger::init(). Then, you can set an environment variable to determine the severities you want to see:

$ RUST_LOG=debug cargo run mytest.bin

You'll then see all messages with severity debug and above. Once again, the documentation is online.

💡 Note on the Result type

You might notice a redefinition of the Result type:

#![allow(unused)]
fn main() {
type Result<T, E = Error> = std::result::Result<T, E>;
}

This defines a local Result type whose second generic parameter has a default value: your own Error type. It means that you can write Result<T> instead of Result<T, Error> for the return type of your functions. Also, a user of your library will be able to reference such a type as interpreter:::Result<T> instead of interpreter:::Result<T, interpreter::Error>.

This kind of shortcut is very common in Rust. For example, the std::io module defines:

#![allow(unused)]
fn main() {
type Result<T, E = std::io::Error> = std::result::Result<T, E>;
}

so that you can use std::io::Result<usize> for an I/O operation which returns a number of bytes instead of std::io::Result<usize, std::io::Error>.

Similarly, the std::fmt module goes even further and defines

#![allow(unused)]
fn main() {
type Result<T = (), E = std::fmt::Error> = std::result::Result<T, E>;
}

so that you can use std::fmt::Result (without generic parameters) in a formatting operation instead of std::fmt::Result<(), std::fmt::Error>.