This documentation explains a solution for Day 3 of Advent of Code 2024, which involves parsing and processing a program with special instructions.
The key insight for this problem is recognizing that we need to parse a text file containing a mix of random characters and specific instruction patterns. These instructions need to be identified, extracted, and executed according to certain rules.
The solution involves:
The input consists of a text file with seemingly random characters interspersed with specific instruction patterns:
// Example of the input format
// mul(2,4) - multiply instruction
// do() and don't() - control flow instructions
The first task is to recognize and extract these patterns from the noise.
// This shows a small sample of the input to understand the format
// xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))
We need to represent our program and the instructions it contains:
#[derive(Debug, Copy, Clone)]
pub enum Instruction {
MUL(u32, u32), // Multiply two numbers
DONT, // Stop execution
DO, // Resume execution
}
struct Program {
instructions: Vec<Instruction>,
}
This representation allows us to:
For parsing, we use the Nom library, which provides parser combinators allowing us to build complex parsers from simpler ones:
impl FromStr for Program {
type Err = ();
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
let mut instructions = vec![];
while let Ok((remaining, (_, instruction))) =
many_till(anychar, alt((parse_mul, parse_do, parse_dont)))(s)
{
instructions.push(instruction);
s = remaining;
}
Ok(Self { instructions })
}
}
The key insight here is using many_till
to skip characters until we find a valid instruction pattern, then process that instruction and continue. This effectively ignores all the noise in the input.
We define specialized parsers for each instruction type:
fn parse_mul(i: &str) -> IResult<&str, Instruction> {
delimited(
tag("mul("),
map(
separated_pair(
nom::character::complete::u32,
char(','),
nom::character::complete::u32,
),
|(x, y)| Instruction::MUL(x, y),
),
tag(")"),
)(i)
}
fn parse_do(i: &str) -> IResult<&str, Instruction> {
value(Instruction::DO, tag("do()"))(i)
}
fn parse_dont(i: &str) -> IResult<&str, Instruction> {
value(Instruction::DONT, tag("don't()"))(i)
}
These parsers recognize specific patterns in the input text:
mul(x,y)
- A multiplication instruction with two numeric parametersdo()
- A control flow instruction to resume executiondon't()
- A control flow instruction to pause executionOur CPU executes the instructions according to certain rules:
#[derive(Debug)]
struct Cpu {
run_state: bool, // Whether we're currently executing instructions
use_enhanced: bool, // Whether we respect control flow instructions
}
impl Cpu {
fn use_simple_instructions() -> Cpu {
Cpu {
run_state: true,
use_enhanced: false,
}
}
fn use_enhanced_instructions() -> Cpu {
Cpu {
run_state: true,
use_enhanced: true,
}
}
}
The CPU has two operation modes:
MUL
instructions, ignoring control flowDO
and DON'T
instructions for controlling executionThe CPU executes the program by processing each instruction and accumulating results:
fn run(&mut self, pgm: &Program) -> u32 {
pgm.instructions
.iter()
.filter_map(|&i| self.run_instruction(i))
.sum::<u32>()
}
fn run_instruction(&mut self, instruction: Instruction) -> Option<u32> {
match instruction {
Instruction::MUL(x, y) if self.run_state => Some(x * y),
Instruction::DONT if self.use_enhanced => {
self.run_state = false;
None
}
Instruction::DO if self.use_enhanced => {
self.run_state = true;
None
}
_ => None,
}
}
The insight here is:
MUL
instructionsMUL
when run_state
is trueDO
and DON'T
toggle the run_state
flag in enhanced modeThe main function ties everything together:
fn main() {
let input = std::fs::read_to_string("src/bin/day3/input.txt").unwrap();
let pgm = input
.parse::<Program>()
.map_err(|e| panic!("{e:?}"))
.unwrap();
let t = Instant::now();
let sum = Cpu::use_simple_instructions().run(&pgm);
println!("part1: {} - {:?}", sum, t.elapsed());
assert_eq!(185797128, sum);
let t = Instant::now();
let sum = Cpu::use_enhanced_instructions().run(&pgm);
println!("part1: {} - {:?}", sum, t.elapsed());
assert_eq!(89798695, sum)
}
This:
Parser Combinators: Using Nom makes the parsing elegant and maintainable compared to regex or manual string manipulation.
Filtering During Execution: The filter_map
pattern efficiently collects only the results of instructions that produce values, avoiding the need for extra checks or intermediate collections.
Enum for Instructions: Using an enum with parameters keeps the instruction set extensible while maintaining type safety.
State Machine: The CPU behaves like a simple state machine, with the run_state
flag determining whether instructions get executed.
Mode Selection: Parameterizing the behavior with use_enhanced
makes it easy to switch between part 1 and part 2 without duplicating code.
This implementation demonstrates several important programming principles:
Separation of Concerns: Parsing, instruction representation, and execution are kept separate.
Declarative Parsing: Nom allows us to describe what to parse rather than how to parse it.
State Management: The CPU’s state is clearly defined and transitions are explicit.
Functional Programming: The use of iterators and combinators leads to concise, readable code.
The solution handles both parts of the challenge efficiently, processing a complex input format into a simple computational model.