Advent of Code 2019 - Day 5 (in Rust!)
bretthancox
Posted on January 5, 2020
Introduction
The thought of revisiting my Clojure version of the Intcode "computer" horrified me. I will be continuing with the Advent of Code, but personal time is limited, so I'll be taking my sweet time. Since I am not competing on time, and my prior implementation was painful to revise, I wanted to write it again.
Time to break out Rust and only rely on the Rust book (with help from the wider internets for some things like user input).
Day 5.1
Rehashing my description from the Clojure version:
Part 1 of Day 5 requires that the Intcode computer from Day 2 be updated to accommodate some new features:
- The Opcode from Day 2 now comprises 5 digits. This is true even if only one digit is provided - you must infer the rest to be 0.
- The Opcodes from Day 2 continue to apply, but with some additions that break from the [instruction, parameter1, parameter2, parameter3] setup:
- Opcode 3 means take a value from human input (or simulate it) and write it to the address provided in the parameter x, where (as we will see in a moment) x is either the address to write it, or a pointer to the address to write it: [3, x]
- Opcode 4 means read the value from an address and print it to the screen. It simulates "diagnostic codes" being printed out. This is like the reverse of Opcode 3.
- The Opcode will provide parameter "modes". The modes are binary. For each Opcode, there are between 1 and 3 parameters, thus there are between 1 and 3 modes. In Day 2 the values in the parameters are always pointers to somewhere else in the Intcode. If a parameter mode is 0, that behavior is unchanged. If the mode is 1, use the literal content of the parameter.
- For Opcode of 1 or 2, you either sum or multiply the literal content of par 1 and par 2
- For Opcode of 3 or 4 (or any other write to/read from operation), a 0 means read/write to the location defined in the parameter (i.e. a pointer), while a 1 means read/write to the parameter itself. Specifically:
- [4, 4, 99, 2, 3] results in printing 3
- [104, 4, 99, 2, 3] results in printing 4
- The Opcode will be provided in reverse. Example: 101103 - in this case we can break it into ABCDE:
- A is the mode for parameter 3
- B is the mode for parameter 2
- C is the mode for parameter 1
- DE is the Opcode, now provided as two digits
- The new two-digit Opcode is really a single digit Opcode with an unnecessary prefix, so in 01, only the 1 has value.
Now, the Rust
So, Rust requires some prep. What types will I need to make?
For me, I needed two custom types:
- The Intcode type, containing the Intcode as supplied and maintaining it through edits, the opcode pointer that tells the "computer" where the current opcode is located, and a boolean that tells the
while
loop we'll be using whether we found astop
opcode or not. - An ExtendedOpcode type that contains the components of the new, longer opcodes.
#[derive(Debug, Clone)]
pub struct Intcode {
intcode: Vec<isize>,
opcode_pointer: usize,
continue_loop: bool,
}
#[derive(Debug, Clone)]
pub struct ExtendedOpcode {
opcode: Opcode,
mode_1: Modes,
mode_2: Modes,
mode_3: Modes,
}
Now, what the heck are the Opcode
and Modes
types?
Those are a pair of enums I used to make my code more readable. Matching integers is entirely possible, but the two enums provide some much more human-friendly labelling:
#[derive(Debug, Clone, PartialEq)]
enum Opcode {
Stop,
Add,
Multiply,
Input,
Output,
JumpIfTrue,
JumpIfFalse,
LessThan,
Equals,
OtherValue,
}
#[derive(Debug, Clone, PartialEq)]
enum Modes {
Position,
Immediate,
}
The remaining setup before we start writing the actual functions that perform the "computer" work are:
- Implement
PartialEq
for the newExtendedOpcode
type (not used in the code itself, but needed for testing.assert!
needsPartialEq
for any types it is comparing). - A
new
function for theIntcode
type. - A
new
function for theExtendedOpcode
type.
impl PartialEq for ExtendedOpcode {
fn eq(&self, comparator: &Self) -> bool {
self.opcode == comparator.opcode
&&
self.mode_1 == comparator.mode_1
&&
self.mode_2 == comparator.mode_2
&&
self.mode_3 == comparator.mode_3
}
}
impl ExtendedOpcode {
fn new(opcode: Opcode) -> Self {
Self {
opcode: opcode,
mode_1: Modes::Position,
mode_2: Modes::Position,
mode_3: Modes::Position,
}
}
}
impl Intcode {
fn new(intcode_vec: Vec<isize>) -> Self {
Self {
intcode: intcode_vec,
opcode_pointer: 0,
continue_loop: true,
}
}
}
In this next section you will find the "computer" logic. Here's the breakdown:
-
work_on_intcode
is the "command and control" function, maintaining awhile
loop until a99
opcode is found (which sets theself.continue_loop
tofalse
, exiting the loop). It calls theget_opcode_method
function and uses the output of that function to decide which subsequent action to take. -
get_opcode_method
extracts the long-form opcode, and calls thesplit_opcodes
function which splits the opcode into pieces, reverses the order, and converts back to integers. Then, adjusting the approach based on the length of the opcode, a newExtendedOpcode
is created and the actual opcode value is put into amatch
to apply the correctOpcode
enum type.- The splitting of the long-form opcode means a
99
appears as[9, 9]
, so to protect me against future9
-prefixed opcodes, I put a sub-match
to check that the second value is also a9
, ensuring I have found a99
opcode.
- The splitting of the long-form opcode means a
- Now, with the opcode known, I have the day 5.1 functions in place (all functions are named, but they are placeholders for now):
-
add_and_place
first determines the values to work with, depending on whether the mode for each isPosition
orImmediate
, then adds the two integers and writes them to the location of the third integer. It moves theopcode_pointer
forward 4 places. -
multiply_and_place
is the same as theadd
function, but it multiplies. -
take_input
makes use of the text_io crate. Add#[macro_use] extern crate text_io;
to themain.rs
orlib.rs
file for your project and then uselet user_input: isize = read!();
or similar. The macro will attempt to coerce the type. The rest of the function, similar to the previous two, determines where it is writing to and puts the user input in that location. It increments theopcode_pointer
by two as there are only two values associated. -
print_output
effectively does the opposite of three; it reads from a location defined by the opcode modes, and then prints to screen. It then moves theopcode_pointer
2 places.
-
impl Intcode {
fn split_opcodes(&self, opcode_full: isize) -> Vec<isize> {
let unsplit_opcode = opcode_full.to_string();
let split_opcode = unsplit_opcode.trim().split_terminator("").skip(1).collect::<Vec<&str>>(); //split_terminator("").skip(1) splits the string, but prevents the resulting vector having "" at start and end
//println!("{:?}", split_opcode);
let rev_split_opcode = split_opcode.iter().rev().map(|o| o.parse::<isize>().unwrap()).collect::<Vec<isize>>();
rev_split_opcode
}
fn get_opcode_method(&self) -> ExtendedOpcode {
let opcode_full = self.intcode[self.opcode_pointer];
let opcode_split = self.split_opcodes(opcode_full);
//println!("{:?}", opcode_split);
let mut extended_opcode: ExtendedOpcode = ExtendedOpcode::new(Opcode::OtherValue);
if opcode_split.len() > 2 && opcode_split[2] == 1 {
extended_opcode.mode_1 = Modes::Immediate;
}
if opcode_split.len() > 3 && opcode_split[3] == 1 {
extended_opcode.mode_2 = Modes::Immediate;
}
if opcode_split.len() > 4 && opcode_split[4] == 1 {
extended_opcode.mode_3 = Modes::Immediate;
}
match opcode_split[0] {
9 => match opcode_split[1] {
9 => extended_opcode.opcode = Opcode::Stop,
_ => extended_opcode.opcode = Opcode::OtherValue,
}
1 => extended_opcode.opcode = Opcode::Add,
2 => extended_opcode.opcode = Opcode::Multiply,
3 => extended_opcode.opcode = Opcode::Input,
4 => extended_opcode.opcode = Opcode::Output,
5 => extended_opcode.opcode = Opcode::JumpIfTrue,
6 => extended_opcode.opcode = Opcode::JumpIfFalse,
7 => extended_opcode.opcode = Opcode::LessThan,
8 => extended_opcode.opcode = Opcode::Equals,
_ => extended_opcode.opcode = Opcode::OtherValue,
}
//println!("{:?}", self.intcode);
extended_opcode
}
fn add_and_place(&mut self, extended_opcode: ExtendedOpcode) {
let value_1 = match extended_opcode.mode_1 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 1 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 1 ],
};
let value_2 = match extended_opcode.mode_2 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 2 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 2 ],
};
let write_to = match extended_opcode.mode_3 {
Modes::Position => self.intcode[ self.opcode_pointer + 3 ] as usize,
Modes::Immediate => self.opcode_pointer + 3 as usize,
};
self.intcode[write_to] = value_1 + value_2;
self.opcode_pointer += 4;
}
fn multiply_and_place(&mut self, extended_opcode: ExtendedOpcode) {
let value_1 = match extended_opcode.mode_1 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 1 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 1 ],
};
let value_2 = match extended_opcode.mode_2 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 2 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 2 ],
};
let write_to = match extended_opcode.mode_3 {
Modes::Position => self.intcode[ self.opcode_pointer + 3 ] as usize,
Modes::Immediate => self.opcode_pointer + 3 as usize,
};
self.intcode[write_to] = value_1 * value_2;
self.opcode_pointer += 4;
}
fn take_input(&mut self, extended_opcode: ExtendedOpcode) {
let user_input: isize = read!();
let write_to = match extended_opcode.mode_1 {
Modes::Position => self.intcode[ self.opcode_pointer + 1 ] as usize,
Modes::Immediate => self.opcode_pointer + 1 as usize,
};
self.intcode[write_to] = user_input;
self.opcode_pointer += 2;
}
fn print_output(&mut self, extended_opcode: ExtendedOpcode) {
let read_from = match extended_opcode.mode_1 {
Modes::Position => self.intcode[ self.opcode_pointer + 1 ] as usize,
Modes::Immediate => self.opcode_pointer + 1 as usize,
};
let val_to_output = self.intcode[read_from];
println!("Diagnostic code: {:?}", val_to_output);
self.opcode_pointer += 2;
}
fn jump_if_true(&self, _extended_opcode: ExtendedOpcode) {
println!("I do nothing, yet.");
}
fn jump_if_false(&self, _extended_opcode: ExtendedOpcode) {
println!("I do nothing, yet.");
}
fn less_than(&self, _extended_opcode: ExtendedOpcode) {
println!("I do nothing, yet.");
}
fn equals(&self, _extended_opcode: ExtendedOpcode) {
println!("I do nothing, yet.");
}
fn work_on_intcode(&mut self) -> Result<(), &str> {
while self.continue_loop {
let extended_opcode = self.get_opcode_method();
match extended_opcode.opcode {
Opcode::Stop => {
println!("Finished.");
self.continue_loop = false;
},
Opcode::Add => self.add_and_place(extended_opcode),
Opcode::Multiply => self.multiply_and_place(extended_opcode),
Opcode::Input => self.take_input(extended_opcode),
Opcode::Output => self.print_output(extended_opcode),
Opcode::JumpIfTrue => self.jump_if_true(extended_opcode),
Opcode::JumpIfFalse => self.jump_if_false(extended_opcode),
Opcode::LessThan => self.less_than(extended_opcode),
Opcode::Equals => self.equals(extended_opcode),
Opcode::OtherValue => return Err("A value outside Opcode range was found."),
_ => return Err("No idea where this came from..."),
}
}
Ok(())
}
}
With all of this in place, running it is pretty simple. My convenience file_open
function just pulls in the contents of a file as a string and returns a Result
containing (hopefully) the string, hence the unwrap
to extract it.
The splitting of the string is a little long because it must be split, the result collected into a vector, that vector then iterated over to parse each &str
into an integer of type isize
. The parse returns a result, so it is unwrap
ped and the integers are then collected into another vector.
pub fn day_five() {
let file_input = file_open();
let input_string = file_input.unwrap();
let int_vec = input_string.split(",").collect::<Vec<&str>>().iter().map(|o| o.parse::<isize>().unwrap()).collect::<Vec<isize>>();
let mut intcode_obj = Intcode::new(int_vec);
let opcode_results = intcode_obj.work_on_intcode();
}
Day 5.2
Adding the functions was simple due to the way I set up my types. Just add the functions and it just works - almost.
The opcode_pointer
in jump_if_true
and jump_if_false
has to be coerced to a usize
value because it is being used to access the vector by index. Rust does not allow negative index access, so an isize
type would be problematic. A simple as usize
makes the change.
fn jump_if_true(&mut self, extended_opcode: ExtendedOpcode) {
let value_1 = match extended_opcode.mode_1 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 1 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 1 ],
};
let value_2 = match extended_opcode.mode_2 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 2 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 2 ],
};
if value_1 != 0 {
self.opcode_pointer = value_2 as usize;
} else {
self.opcode_pointer += 3;
}
}
fn jump_if_false(&mut self, extended_opcode: ExtendedOpcode) {
let value_1 = match extended_opcode.mode_1 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 1 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 1 ],
};
let value_2 = match extended_opcode.mode_2 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 2 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 2 ],
};
if value_1 == 0 {
self.opcode_pointer = value_2 as usize;
} else {
self.opcode_pointer += 3;
}
}
fn less_than(&mut self, extended_opcode: ExtendedOpcode) {
let value_1 = match extended_opcode.mode_1 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 1 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 1 ],
};
let value_2 = match extended_opcode.mode_2 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 2 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 2 ],
};
let write_to = match extended_opcode.mode_3 {
Modes::Position => self.intcode[ self.opcode_pointer + 3 ] as usize,
Modes::Immediate => self.opcode_pointer + 3 as usize,
};
if value_1 < value_2 {
self.intcode[write_to] = 1;
} else {
self.intcode[write_to] = 0;
}
self.opcode_pointer += 4;
}
fn equals(&mut self, extended_opcode: ExtendedOpcode) {
let value_1 = match extended_opcode.mode_1 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 1 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 1 ],
};
let value_2 = match extended_opcode.mode_2 {
Modes::Position => self.intcode[ self.intcode[ self.opcode_pointer + 2 ] as usize ],
Modes::Immediate => self.intcode[ self.opcode_pointer + 2 ],
};
let write_to = match extended_opcode.mode_3 {
Modes::Position => self.intcode[ self.opcode_pointer + 3 ] as usize,
Modes::Immediate => self.opcode_pointer + 3 as usize,
};
if value_1 == value_2 {
self.intcode[write_to] = 1;
} else {
self.intcode[write_to] = 0;
}
self.opcode_pointer += 4;
}
Posted on January 5, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.