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 filesrc/main.rs: the main program for the application, which loads a binary file with machine code and executes itsrc/lib.rs: the entry point for theinterpreterlibrary which contains your implementation of the virtual machinesrc/tests/: a directory with many tests, ranging from individual instructions tests to complex testssrc/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 updateif 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 implementingWrite(for theoutandout numberinstructions), and execute just one instructionstep(): similar tostep_on(), but writes on the standard outputrun_on(): takes aWrite-implementing descriptor and runs until the program terminatesrun(): similar torun_on(), but writes on the standard outputmemory()andregs(): return a reference on the current memory and registers contentset_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.rscontains 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 usingcargo test --test assignment.basic_operations.rschecks 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.rswill 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 informationlog::debug!(…)is for data you'd like to see when debugginglog::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
ResulttypeYou might notice a redefinition of the
Resulttype:#![allow(unused)] fn main() { type Result<T, E = Error> = std::result::Result<T, E>; }This defines a local
Resulttype whose second generic parameter has a default value: your ownErrortype. It means that you can writeResult<T>instead ofResult<T, Error>for the return type of your functions. Also, a user of your library will be able to reference such a type asinterpreter:::Result<T>instead ofinterpreter:::Result<T, interpreter::Error>.This kind of shortcut is very common in Rust. For example, the
std::iomodule 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 ofstd::io::Result<usize, std::io::Error>.Similarly, the
std::fmtmodule 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 ofstd::fmt::Result<(), std::fmt::Error>.