1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! # The Halting Problem
//!
//! The input is parsed into a 2 dimensional array covering each possible combination of state
//! and tape value at the cursor. Each transition is then computed via a lookup into this array.
//!
//! To speed things up by about ten times, multiple transitions are then precomputed to allow
//! skipping forward multiple steps at a time.
//!
//! For each of the 6 * 256 = 1536 combinations of a tape with 4 values to the left and 3 values
//! to the right of the cursor, the turing machine is computed one steps at a time until the cursor
//! leaves the area to the left or to the right. For example:
//!
//! ```none
//!     State: A            => State: B
//!       1 0 1 0 [1] 0 0 1     [0] 0 1 0 0 1 1 0
//!
//!     State: B
//!       t t t t [0] 0 1 0
//! ```
//!
//! In this example the tape then advances four bits to the left, loading the four values of `t`
//! then the next batch lookup is performed.
use crate::util::parse::*;
use std::array::from_fn;

pub struct Input {
    state: usize,
    steps: u32,
    rules: Vec<[Rule; 2]>,
}

struct Rule {
    next_state: usize,
    next_tape: usize,
    advance: bool,
}

impl Rule {
    fn parse(block: &[&[u8]]) -> Self {
        let next_tape = (block[0][22] - b'0') as usize;
        let advance = block[1][27] == b'r';
        let next_state = (block[2][26] - b'A') as usize;
        Rule { next_state, next_tape, advance }
    }
}

struct Skip {
    next_state: usize,
    next_tape: usize,
    steps: u32,
    ones: i32,
    advance: bool,
}

/// Parse the input into 12 rules for each possible combination of state and value at the cursor.
pub fn parse(input: &str) -> Input {
    let lines: Vec<_> = input.lines().map(str::as_bytes).collect();

    let state = (lines[0][15] - b'A') as usize;
    let steps = input.unsigned();
    let rules: Vec<_> = lines[3..]
        .chunks(10)
        .map(|chunk| [Rule::parse(&chunk[2..5]), Rule::parse(&chunk[6..9])])
        .collect();

    Input { state, steps, rules }
}

pub fn part1(input: &Input) -> i32 {
    // Precompute state transitions in larger amount in order to skip forward several transitions
    // at a time. 100 max_steps is a safety valve in case some state transitions do not halt
    // although this is unlikely for the inputs that are provided.
    let table: Vec<[Skip; 256]> = (0..input.rules.len())
        .map(|state| from_fn(|tape| turing(&input.rules, state, tape, 100)))
        .collect();

    let mut state = input.state;
    let mut remaining = input.steps;
    let mut tape = 0;
    let mut checksum = 0;
    let mut left = Vec::new();
    let mut right = Vec::new();

    loop {
        // Lookup the next batch state transition.
        let Skip { next_state, next_tape, steps, ones, advance } = table[state][tape];

        // Handle any remaining transitions less than the batch size one step at a time.
        if steps > remaining {
            let Skip { ones, .. } = turing(&input.rules, state, tape, remaining);
            break checksum + ones;
        }

        state = next_state;
        tape = next_tape;
        remaining -= steps;
        checksum += ones;

        // Use a vector to simulate an empty tape. In practise the cursor doesn't move more than
        // a few thousand steps in any direction, so this approach is as fast as a fixed size
        // array, but much more robust.
        if advance {
            left.push(tape & 0xf0);
            tape = ((tape & 0xf) << 4) | right.pop().unwrap_or(0);
        } else {
            right.push(tape & 0xf);
            tape = (tape >> 4) | left.pop().unwrap_or(0);
        }
    }
}

pub fn part2(_input: &Input) -> &'static str {
    "n/a"
}

// Precompute state transitions up to some maximum value of steps.
fn turing(rules: &[[Rule; 2]], mut state: usize, mut tape: usize, max_steps: u32) -> Skip {
    let mut mask = 0b00001000;
    let mut steps = 0;
    let mut ones = 0;

    // `0`` means the cursor has advanced to the next nibble on the right.
    // `128` means that the cursor is on the left edge of the high nibble.
    while 0 < mask && mask < 128 && steps < max_steps {
        let current = ((tape & mask) != 0) as usize;
        let Rule { next_state, next_tape, advance } = rules[state][current];

        if next_tape == 1 {
            tape |= mask;
        } else {
            tape &= !mask;
        }

        if advance {
            mask >>= 1;
        } else {
            mask <<= 1;
        }

        state = next_state;
        steps += 1;
        // Count the total numbers of ones by summing the number of ones written minus the
        // number of ones erased.
        ones += next_tape as i32 - current as i32;
    }

    Skip { next_state: state, next_tape: tape, steps, ones, advance: mask == 0 }
}