Task 21

Thorn Thaler - <

2025-11-06

1 Setup

1.1 Libraries

library(httr)
library(xml2)
library(magrittr)
library(dplyr)
library(purrr)
library(stringr)
library(kableExtra)
library(bit64)

1.2 Retrieve Data from AoC

session_cookie <- set_cookies(session = keyring::key_get("AoC-GitHub-Cookie"))
base_url <- paste0("https://adventofcode.com/", params$year, "/day/", params$task_nr)
puzzle <- GET(base_url,
              session_cookie) %>% 
  content(encoding = "UTF-8") %>% 
  xml_find_all("///article") %>% 
  lapply(as.character)

parse_puzzle_data <- function(text_block = readClipboard()) {
  if (length(text_block) == 1L) {
    text_block <- text_block %>% 
      str_split("\n") %>% 
      extract2(1L) %>% 
      keep(nzchar)
  }
  ip <- str_extract(text_block[[1L]], "\\d+") %>% 
    as.integer()
  ops <- text_block[-1L] %>% 
    str_split(" ") %>% 
    do.call(rbind, .) %>% 
    set_colnames(c("op", "A", "B", "C")) %>% 
    as_tibble() %>% 
    mutate(across(A:C, as.integer))
  list(ip = ip, ops = ops)
}

puzzle_data <- local({
  GET(paste0(base_url, "/input"),
      session_cookie) %>% 
    content(encoding = "UTF-8") %>% 
    parse_puzzle_data()
})

2 Puzzle Day 21

2.1 Part 1

2.1.1 Description

— Day 21: Chronal Conversion —

You should have been watching where you were going, because as you wander the new North Pole base, you trip and fall into a very deep hole!

Just kidding. You’re falling through time again.

If you keep up your current pace, you should have resolved all of the temporal anomalies by the next time the device activates. Since you have very little interest in browsing history in 500-year increments for the rest of your life, you need to find a way to get back to your present time.

After a little research, you discover two important facts about the behavior of the device:

First, you discover that the device is hard-wired to always send you back in time in 500-year increments. Changing this is probably not feasible.

Second, you discover the activation system (your puzzle input) for the time travel module. Currently, it appears to run forever without halting.

If you can cause the activation system to halt at a specific moment, maybe you can make the device send you so far back in time that you cause an integer underflow in time itself and wrap around back to your current time!

The device executes the program as specified in manual section one and manual section two.

Your goal is to figure out how the program works and cause it to halt. You can only control register 0; every other register begins at 0 as usual.

Because time travel is a dangerous activity, the activation system begins with a few instructions which verify that bitwise AND (via bani) does a numeric operation and not an operation as if the inputs were interpreted as strings. If the test fails, it enters an infinite loop re-running the test instead of allowing the program to execute normally. If the test passes, the program continues, and assumes that all other bitwise operations (banr, bori, and borr) also interpret their inputs as numbers. (Clearly, the Elves who wrote this system were worried that someone might introduce a bug while trying to emulate this system with a scripting language.)

What is the lowest non-negative integer value for register 0 that causes the program to halt after executing the fewest instructions? (Executing the same instruction multiple times counts as multiple instructions executed.)

2.1.2 Solution

First, we deparse the op-codes by identifying jumps and loops:

Instruction Flow Jumps Pseudo code
1 seti 123 0 5


x5=123
2 bani 5 456 5
<--+

x5=x5&456
3 eqri 5 72 5
   |

x5=(x5==72)?1:0
4 addr 5 1 1
   |

x1+=x5
5 seti 0 0 1
---+
jump if x5 != 72

6 seti 0 2 5


x5=0
7 bori 5 65536 4
<--------------+

x4=x5|65536
8 seti 3935295 1 5
               |

x5=3935295
9 bani 4 255 2
<----------+   |

x2=x4&255
10 addr 5 2 5
           |   |

x5+=x2
11 bani 5 16777215 5
           |   |

x5=x5&16777215
12 muli 5 65899 5
           |   |

x5*=65899
13 bani 5 16777215 5
           |   |

x5=x5&16777215
14 gtir 256 4 2
           |   |

x2=x4<256?1:0
15 addr 2 1 1
           |   |

x1+=x2
16 addi 1 1 1
           |   |

x1++
17 seti 27 1 1
---+       |   |
jump if x4 < 256

18 seti 0 5 2
   |       |   |

x2=0
19 addi 2 1 3
<--|-------|---|---+

x3=x2+1
20 muli 3 256 3
   |       |   |   |

x3*=256
21 gtrr 3 4 3
   |       |   |   |

x3=(x3>x4)?1:0
22 addr 3 1 1
   |       |   |   |

x1+=x3
23 addi 1 1 1
   |       |   |   |

x1++
24 seti 25 0 1
---|---+   |   |   |
jump if x3 > x4

25 addi 2 1 2
   |   |   |   |   |

x2++
26 seti 17 7 1
---|---|---|---|---+


27 setr 2 2 4
<--|---+   |   |

x4=x2
28 seti 7 6 1
---|-------+   |


29 eqrr 5 0 2
<--+           |

x2=(x5==x0)?1:0
30 addr 2 1 1
               |

x1+=x2
31 seti 5 4 1
---------------+
jump if x5 != x0

That is the code boils down to the following logic:

  1. Lines 1 - 5 do not do anything useful as x5 is anyways reset in line 6.
  2. Lines 9 - 28: in the first iteration x2 is zero and we setup x5 and x4. Then as long as x4 is greater or equal than 256 we will find the largest x2 such that 256 * x2 <= x4 in a loop.
  3. We use this x2 to update x5 in the next iteration. x4 is also updated and the iteration starts again.
  4. Line 29 is the only possibility to stop the infinite loop. If we set x0 to match (any) generated x5 the program will terminate.

Hence, to solve the first part we have to generate the first x5 and set x0 to this value.

N.B. As we need bit 64 bitwise operations, we include a simple C++ code to avoid the need to work on bit vectors in R for speed reasons.

#include <Rcpp.h>

Rcpp::NumericVector bitwOp64(Rcpp::NumericVector a, Rcpp::NumericVector b, 
                             int64_t(*op)(int64_t, int64_t)) {
  size_t len = a.size();
  std::vector<int64_t> res(len), x(len), y(len);
  // integer64 in R are internally stored as double -> use memcpy to copy bit structure
  // reinterpret_cast would work too but results in a compiler warning
  std::memcpy(&(x[0]), &(a[0]), len * sizeof(double));
  std::memcpy(&(y[0]), &(b[0]), len * sizeof(double));
  for (size_t i = 0; i < len; ++i) {
    res[i] = op(x[i], y[i]);
  }
  Rcpp::NumericVector res_r(len);
  std::memcpy(&(res_r[0]), &(res[0]), len * sizeof(double));
  res_r.attr("class") = "integer64";
  return res_r;
}

// [[Rcpp::export]]
Rcpp::NumericVector bitwAnd64(Rcpp::NumericVector a, Rcpp::NumericVector b) {
  return bitwOp64(a, b, [](int64_t x, int64_t y) { return x & y; });
}

// [[Rcpp::export]]
Rcpp::NumericVector bitwOr64(Rcpp::NumericVector a, Rcpp::NumericVector b) {
  return bitwOp64(a, b, [](int64_t x, int64_t y) { return x | y; });
}
debug_chronal <- function() {
  x4 <- x5 <- as.integer64(0L)
  first_x5 <- NULL
  seen <- new.env(parent = emptyenv())
  while (TRUE) {
    x4 <- bitwOr64(x5, as.integer64(65536L))
    x5 <- as.integer64(3935295)
    while (x4 > 0) {
      x2 <- bitwAnd64(x4, as.integer64(255L))
      x5 <- bitwAnd64(x5 + x2,
                      as.integer64(16777215L)) * 
        as.integer64(65899L)
      x5 <- bitwAnd64(x5, as.integer64(16777215L))
      x4 <- x4 %/% as.integer64(256L)
    }
    if (is.null(first_x5)) {
      first_x5 <- x5
    }
    key <- as.character(x5)
    if (exists(key, seen)) {
      break
    }
    seen[[key]] <- TRUE
    last_x5 <- x5
  }
  list(first_x5 = first_x5,
       last_x5 = last_x5)
}

res <- debug_chronal()
res$first_x5
## integer64
## [1] 16457176

2.2 Part 2

2.2.1 Description

— Part Two —

In order to determine the timing window for your underflow exploit, you also need an upper bound:

What is the lowest non-negative integer value for register 0 that causes the program to halt after executing the most instructions? (The program must actually halt; running forever does not count as halting.)

2.2.2 Solution

To solve the second part, we continue producing x5 until we end get an x5 which we have seen already. The x5 produced just before is our solution.

res$last_x5
## integer64
## [1] 13625951