-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathday24.cr
131 lines (111 loc) · 3.52 KB
/
day24.cr
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
BITS = 1
alias Vars = Array(Tuple(String, Int64))
alias Circuit = Array(Tuple(Array(String), String))
def parse_input(raw : String) : Tuple(Vars, Circuit)
raw_vars, raw_circuit = raw.split("\n\n").map { |r| r.lines }
vars = raw_vars.map do |r|
var, raw_value = r.split(": ")
value = raw_value.to_i64
{var, value}
end
circuit = raw_circuit.map do |r|
raw_expr, res = r.split(" -> ")
expr = raw_expr.split(' ')
{expr, res}
end
{vars, circuit}
end
# This is a lot more complicated than it'd have to be. My hope was that using
# the Z3 solver for part 1 (instead of just parsing and interpreting the graph
# directly) would pay off in part 2, but as it turns out expressing the
# additions as Z3 constraints would have been a fair bit more involved than just
# scanning the connections for some simple rules (see `is_wrong`).
def translate_to_z3(vars : Vars, circuit : Circuit) : String
[
*vars.map { |v| "(declare-const #{v[0]} (_ BitVec #{BITS}))" },
*circuit.map { |c| "(declare-const #{c[1]} (_ BitVec #{BITS}))" },
*vars.map { |v| "(assert (= #{v[0]} ((_ int2bv #{BITS}) #{v[1]})))" },
*circuit.map { |c| "(assert (= #{c[1]} (bv#{c[0][1].downcase} #{c[0][0]} #{c[0][2]})))" },
"(check-sat)",
"(get-model)",
].join("\n")
end
def translate_to_dot(vars : Vars, circuit : Circuit) : String
[
"digraph {",
*circuit.flat_map do |c|
[
" #{c[0][0]} -> #{c[1]};",
" #{c[0][2]} -> #{c[1]};",
]
end,
"}",
].join("\n")
end
def solve(z3_src : String) : Array(Tuple(String, Int64))
z3_proc = Process.new("z3", ["-smt2", "-in"], input: :pipe, output: :pipe)
z3_proc.input.puts(z3_src)
z3_proc.input.close
z3_output = z3_proc.output.gets_to_end.gsub("\n", "")
output_vars = [] of Tuple(String, Int64)
z3_output.scan(/\(define-fun\s+(\w+)\s+\(\)\s+\(_\s+BitVec\s+\d+\)\s+#[xb](\d+)\)/).each do |match|
output_vars << {match[1], match[2].to_i64}
end
output_vars.sort!
output_vars
end
def extract_int(prefix : String, values : Array(Tuple(String, Int64))) : Int
values
.select { |v| v[0].starts_with?(prefix) }
.map { |v| v[1] }
.reverse
.reduce(0_i64) { |acc, b| (acc << 1) | b }
end
def is_x(s : String) : Bool
s.starts_with?("x")
end
def is_y(s : String) : Bool
s.starts_with?("y")
end
def is_z(s : String) : Bool
s.starts_with?("z")
end
def is_coord(s : String) : Bool
is_x(s) || is_y(s) || is_z(s)
end
def is_wrong(conn : Tuple(Array(String), String), circuit : Circuit) : Bool
expr, res = conn
lhs, op, rhs = expr
childs = circuit.select { |c| c[0].includes?(res) }
[
op == "XOR" && !is_coord(lhs) && !is_coord(rhs) && !is_coord(res),
op != "XOR" && is_z(res) && res != "z45",
childs.map do |c|
child_op = c[0][1]
(op == "AND" && ![lhs, rhs].includes?("x00") && child_op != "OR") || (op == "XOR" && child_op == "OR")
end.any?
].any?
end
if ARGV.size == 0
puts "Usage: day24 [--dump-dot] [--dump-z3] <path to input>"
exit 1
end
flags = ARGV.select { |a| a.starts_with?("--") }.to_s
positionals = ARGV.select { |a| !flags.includes?(a) }
path = positionals[0]
raw = File.read(path)
vars, circuit = parse_input(raw)
if flags.includes?("--dump-dot")
puts translate_to_dot(vars, circuit)
elsif flags.includes?("--dump-z3")
puts translate_to_z3(vars, circuit)
else
part1 = extract_int("z", solve(translate_to_z3(vars, circuit)))
puts "Part 1: #{part1}"
part2 = circuit
.select { |c| is_wrong(c, circuit) }
.map { |c| c[1] }
.sort
.join(",")
puts "Part 2: #{part2}"
end