🏡 index : ~doyle/aoc.git

#!/usr/bin/env nix-shell

(*
#!nix-shell --pure -i ocaml -p ocaml
*)

module RegisterMap = Map.Make (Char)

type input = {registers: int RegisterMap.t; program: int list}

let input =
  let initial_state = {registers= RegisterMap.empty; program= []} in
  let parse_register input state =
    match String.split_on_char ' ' input with
    | ["Register"; register; value] ->
        { state with
          registers=
            RegisterMap.add register.[0] (int_of_string value) state.registers
        }
    | _ ->
        failwith (Printf.sprintf "bad registers: %s" input)
  in
  let parse_program input state =
    match String.split_on_char ' ' input with
    | ["Program:"; program] ->
        { state with
          program= program |> String.split_on_char ',' |> List.map int_of_string
        }
    | _ ->
        failwith (Printf.sprintf "bad program: %s" input)
  in
  let rec read_input reading_registers state =
    try
      match (read_line (), reading_registers) with
      | "", _ ->
          read_input false state
      | v, true ->
          parse_register v state |> read_input true
      | v, false ->
          parse_program v state |> read_input false
    with End_of_file -> state
  in
  read_input true initial_state

type instruction = ADV | BXL | BST | JNZ | BXC | OUT | BDV | CDV

let instruction_of = function
  | 0 ->
      ADV
  | 1 ->
      BXL
  | 2 ->
      BST
  | 3 ->
      JNZ
  | 4 ->
      BXC
  | 5 ->
      OUT
  | 6 ->
      BDV
  | 7 ->
      CDV
  | _ ->
      failwith "invalid instruction"

let read_register registers = function
  | (0 | 1 | 2 | 3) as x ->
      x
  | 4 ->
      RegisterMap.find 'A' registers
  | 5 ->
      RegisterMap.find 'B' registers
  | 6 ->
      RegisterMap.find 'C' registers
  | _ ->
      failwith "bad register"

let rec pow2 exp = if exp = 0 then 1 else 2 * pow2 (exp - 1)

let take_after n arr =
  let rec aux n arr =
    match (n, arr) with
    | 0, x :: xs ->
        x :: aux 0 xs
    | n, x :: xs ->
        aux (n - 1) xs
    | _, [] ->
        []
  in
  aux n arr

let rec run_program program registers =
  let execute instr operand xs =
    match instruction_of instr with
    | (ADV | BDV | CDV) as instr ->
        let numerator, raw_denominator =
          (RegisterMap.find 'A' registers, read_register registers operand)
        in
        let output_register =
          match instr with
          | ADV ->
              'A'
          | BDV ->
              'B'
          | CDV ->
              'C'
          | _ ->
              failwith "unreachable"
        in
        RegisterMap.add output_register
          (numerator / pow2 raw_denominator)
          registers
        |> run_program xs
    | BXL ->
        let base = RegisterMap.find 'B' registers in
        RegisterMap.add 'B' (base lxor operand) registers |> run_program xs
    | BST ->
        let base = read_register registers operand in
        RegisterMap.add 'B' (base land 0b111) registers |> run_program xs
    | JNZ ->
        let xs =
          if RegisterMap.find 'A' registers = 0 then xs
          else take_after operand input.program
        in
        run_program xs registers
    | BXC ->
        let b, c =
          (RegisterMap.find 'B' registers, RegisterMap.find 'C' registers)
        in
        RegisterMap.add 'B' (b lxor c) registers |> run_program xs
    | OUT ->
        let ret = read_register registers operand |> ( land ) 0b111 in
        Some (ret, xs, registers)
  in
  match program with
  | instr :: operand :: xs ->
      execute instr operand xs
  | instr :: xs ->
      failwith "bad opcodes"
  | [] ->
      None

let part1 =
  let rec aux program registers =
    match run_program program registers with
    | Some (ret, program, registers) ->
        ret :: aux program registers
    | None ->
        []
  in
  aux input.program input.registers
  |> List.map string_of_int |> String.concat "," |> print_endline

let part2 =
  let build_register value = RegisterMap.add 'A' value input.registers in
  let check_value expected_output value =
    match run_program input.program (build_register value) with
    | Some (value, _, _) ->
        value = expected_output
    | None ->
        false
  in
  let try_digit acc x =
    let possible_values = List.init 8 (fun i -> (acc lsl 3) lor i) in
    List.find_opt (check_value x) possible_values
  in
  let rec build_solution acc = function
    | [] ->
        Some acc
    | x :: xs ->
        try_digit acc x |> Fun.flip Option.bind (Fun.flip build_solution xs)
  in
  input.program |> List.rev |> build_solution 0 |> Option.get |> print_int
  |> print_newline