loading...

Advent of Code 2019 - Day 5 (in Rust!)

bretthancox profile image bretthancox ・8 min read

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:

  1. 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.
  2. The Opcodes from Day 2 continue to apply, but with some additions that break from the [instruction, parameter1, parameter2, parameter3] setup:
    1. 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]
    2. 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.
  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.
    1. For Opcode of 1 or 2, you either sum or multiply the literal content of par 1 and par 2
    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:
      1. [4, 4, 99, 2, 3] results in printing 3
      2. [104, 4, 99, 2, 3] results in printing 4
  4. The Opcode will be provided in reverse. Example: 101103 - in this case we can break it into ABCDE:
    1. A is the mode for parameter 3
    2. B is the mode for parameter 2
    3. C is the mode for parameter 1
    4. DE is the Opcode, now provided as two digits
  5. 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:

  1. 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 a stop opcode or not.
  2. 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:

  1. Implement PartialEq for the new ExtendedOpcode type (not used in the code itself, but needed for testing. assert! needs PartialEq for any types it is comparing).
  2. A new function for the Intcode type.
  3. A new function for the ExtendedOpcode 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:

  1. work_on_intcode is the "command and control" function, maintaining a while loop until a 99 opcode is found (which sets the self.continue_loop to false, exiting the loop). It calls the get_opcode_method function and uses the output of that function to decide which subsequent action to take.
  2. get_opcode_method extracts the long-form opcode, and calls the split_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 new ExtendedOpcode is created and the actual opcode value is put into a match to apply the correct Opcode enum type.
    1. The splitting of the long-form opcode means a 99 appears as [9, 9], so to protect me against future 9-prefixed opcodes, I put a sub-match to check that the second value is also a 9, ensuring I have found a 99 opcode.
  3. Now, with the opcode known, I have the day 5.1 functions in place (all functions are named, but they are placeholders for now):
    1. add_and_place first determines the values to work with, depending on whether the mode for each is Position or Immediate, then adds the two integers and writes them to the location of the third integer. It moves the opcode_pointer forward 4 places.
    2. multiply_and_place is the same as the add function, but it multiplies.
    3. take_input makes use of the text_io crate. Add #[macro_use] extern crate text_io; to the main.rs or lib.rs file for your project and then use let 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 the opcode_pointer by two as there are only two values associated.
    4. 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 the opcode_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 unwrapped 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 by:

bretthancox profile

bretthancox

@bretthancox

Amateur, tinkerer, programming enthusiast

Discussion

pic
Editor guide