ebd2a1bb4889

Add a few more days
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Wed, 11 Dec 2019 18:48:20 -0500
parents d692b61fbee1
children af91bed748cd f4f8705ccf34
branches/tags (none)
files data/2019/09.txt data/2019/10.txt data/2019/11.txt package.lisp src/2016/days/day-05.lisp src/2018/days/day-10.lisp src/2019/days/day-03.lisp src/2019/days/day-08.lisp src/2019/days/day-09.lisp src/2019/days/day-10.lisp src/2019/days/day-11.lisp src/2019/intcode.lisp src/utils.lisp vendor/make-quickutils.lisp vendor/quickutils.lisp

Changes

--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/2019/09.txt	Wed Dec 11 18:48:20 2019 -0500
@@ -0,0 +1,1 @@
+1102,34463338,34463338,63,1007,63,34463338,63,1005,63,53,1101,3,0,1000,109,988,209,12,9,1000,209,6,209,3,203,0,1008,1000,1,63,1005,63,65,1008,1000,2,63,1005,63,904,1008,1000,0,63,1005,63,58,4,25,104,0,99,4,0,104,0,99,4,17,104,0,99,0,0,1102,35,1,1010,1102,1,33,1013,1101,0,715,1022,1102,1,20,1004,1102,1,24,1012,1101,36,0,1005,1101,0,655,1024,1102,32,1,1014,1101,0,499,1026,1102,1,242,1029,1101,0,25,1002,1101,0,27,1017,1101,708,0,1023,1101,0,21,1016,1101,0,28,1000,1101,0,492,1027,1102,34,1,1015,1102,29,1,1007,1102,247,1,1028,1101,0,39,1011,1102,1,31,1018,1102,1,0,1020,1102,1,37,1006,1101,1,0,1021,1102,26,1,1009,1102,1,38,1008,1101,30,0,1019,1102,1,23,1001,1102,650,1,1025,1101,22,0,1003,109,7,2101,0,-7,63,1008,63,29,63,1005,63,205,1001,64,1,64,1105,1,207,4,187,1002,64,2,64,109,-1,1202,-1,1,63,1008,63,35,63,1005,63,227,1106,0,233,4,213,1001,64,1,64,1002,64,2,64,109,17,2106,0,5,4,239,1105,1,251,1001,64,1,64,1002,64,2,64,109,-1,21108,40,39,-4,1005,1018,271,1001,64,1,64,1106,0,273,4,257,1002,64,2,64,109,-9,1206,8,285,1106,0,291,4,279,1001,64,1,64,1002,64,2,64,109,-13,2108,27,0,63,1005,63,307,1106,0,313,4,297,1001,64,1,64,1002,64,2,64,109,11,2101,0,-5,63,1008,63,37,63,1005,63,339,4,319,1001,64,1,64,1105,1,339,1002,64,2,64,109,13,21101,41,0,-9,1008,1015,41,63,1005,63,365,4,345,1001,64,1,64,1106,0,365,1002,64,2,64,109,-14,1201,-6,0,63,1008,63,22,63,1005,63,385,1106,0,391,4,371,1001,64,1,64,1002,64,2,64,109,-10,1202,3,1,63,1008,63,22,63,1005,63,417,4,397,1001,64,1,64,1105,1,417,1002,64,2,64,109,6,1207,-3,21,63,1005,63,437,1001,64,1,64,1105,1,439,4,423,1002,64,2,64,109,16,21107,42,41,-8,1005,1014,455,1105,1,461,4,445,1001,64,1,64,1002,64,2,64,109,-28,2107,24,7,63,1005,63,481,1001,64,1,64,1106,0,483,4,467,1002,64,2,64,109,33,2106,0,0,1001,64,1,64,1106,0,501,4,489,1002,64,2,64,109,-18,2108,38,-1,63,1005,63,519,4,507,1105,1,523,1001,64,1,64,1002,64,2,64,109,-3,1208,-4,25,63,1005,63,545,4,529,1001,64,1,64,1106,0,545,1002,64,2,64,109,12,21102,43,1,-8,1008,1010,43,63,1005,63,571,4,551,1001,64,1,64,1106,0,571,1002,64,2,64,109,-1,1207,-8,27,63,1005,63,593,4,577,1001,64,1,64,1106,0,593,1002,64,2,64,109,-7,21101,44,0,8,1008,1018,42,63,1005,63,617,1001,64,1,64,1105,1,619,4,599,1002,64,2,64,109,-4,1208,-1,39,63,1005,63,639,1001,64,1,64,1105,1,641,4,625,1002,64,2,64,109,13,2105,1,5,4,647,1106,0,659,1001,64,1,64,1002,64,2,64,109,4,1206,-3,673,4,665,1106,0,677,1001,64,1,64,1002,64,2,64,109,-22,21108,45,45,10,1005,1011,699,4,683,1001,64,1,64,1105,1,699,1002,64,2,64,109,29,2105,1,-7,1001,64,1,64,1105,1,717,4,705,1002,64,2,64,109,-19,21107,46,47,5,1005,1016,739,4,723,1001,64,1,64,1106,0,739,1002,64,2,64,109,-8,2102,1,2,63,1008,63,33,63,1005,63,763,1001,64,1,64,1106,0,765,4,745,1002,64,2,64,109,1,1201,-2,0,63,1008,63,25,63,1005,63,791,4,771,1001,64,1,64,1105,1,791,1002,64,2,64,109,16,1205,0,803,1105,1,809,4,797,1001,64,1,64,1002,64,2,64,109,-8,1205,9,827,4,815,1001,64,1,64,1106,0,827,1002,64,2,64,109,-4,2102,1,-3,63,1008,63,36,63,1005,63,853,4,833,1001,64,1,64,1106,0,853,1002,64,2,64,109,17,21102,47,1,-6,1008,1019,50,63,1005,63,877,1001,64,1,64,1105,1,879,4,859,1002,64,2,64,109,-29,2107,22,5,63,1005,63,897,4,885,1106,0,901,1001,64,1,64,4,64,99,21102,27,1,1,21101,0,915,0,1106,0,922,21201,1,25338,1,204,1,99,109,3,1207,-2,3,63,1005,63,964,21201,-2,-1,1,21101,942,0,0,1105,1,922,22102,1,1,-1,21201,-2,-3,1,21102,957,1,0,1106,0,922,22201,1,-1,-2,1105,1,968,21202,-2,1,-2,109,-3,2106,0,0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/2019/10.txt	Wed Dec 11 18:48:20 2019 -0500
@@ -0,0 +1,26 @@
+##.#..#..###.####...######
+#..#####...###.###..#.###.
+..#.#####....####.#.#...##
+.##..#.#....##..##.#.#....
+#.####...#.###..#.##.#..#.
+..#..#.#######.####...#.##
+#...####.#...#.#####..#.#.
+.#..#.##.#....########..##
+......##.####.#.##....####
+.##.#....#####.####.#.####
+..#.#.#.#....#....##.#....
+....#######..#.##.#.##.###
+###.#######.#..#########..
+###.#.#..#....#..#.##..##.
+#####.#..#.#..###.#.##.###
+.#####.#####....#..###...#
+##.#.......###.##.#.##....
+...#.#.#.###.#.#..##..####
+#....#####.##.###...####.#
+#.##.#.######.##..#####.##
+#.###.##..##.##.#.###..###
+#.####..######...#...#####
+#..#..########.#.#...#..##
+.##..#.####....#..#..#....
+.###.##..#####...###.#.#.#
+.##..######...###..#####.#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/2019/11.txt	Wed Dec 11 18:48:20 2019 -0500
@@ -0,0 +1,1 @@
+3,8,1005,8,298,1106,0,11,0,0,0,104,1,104,0,3,8,1002,8,-1,10,1001,10,1,10,4,10,108,1,8,10,4,10,101,0,8,28,3,8,102,-1,8,10,1001,10,1,10,4,10,1008,8,1,10,4,10,1002,8,1,51,1006,0,37,1006,0,65,1,4,9,10,3,8,1002,8,-1,10,101,1,10,10,4,10,1008,8,0,10,4,10,102,1,8,83,2,3,9,10,1006,0,39,1,1,0,10,1,104,11,10,3,8,102,-1,8,10,1001,10,1,10,4,10,1008,8,0,10,4,10,1002,8,1,120,2,104,13,10,1,1007,18,10,1006,0,19,1,107,2,10,3,8,102,-1,8,10,101,1,10,10,4,10,1008,8,0,10,4,10,1001,8,0,157,3,8,1002,8,-1,10,1001,10,1,10,4,10,1008,8,0,10,4,10,1001,8,0,179,2,108,16,10,2,1108,14,10,1006,0,70,3,8,102,-1,8,10,1001,10,1,10,4,10,108,1,8,10,4,10,101,0,8,211,3,8,1002,8,-1,10,1001,10,1,10,4,10,1008,8,1,10,4,10,101,0,8,234,3,8,102,-1,8,10,1001,10,1,10,4,10,1008,8,0,10,4,10,102,1,8,256,3,8,1002,8,-1,10,101,1,10,10,4,10,1008,8,1,10,4,10,1002,8,1,278,101,1,9,9,1007,9,957,10,1005,10,15,99,109,620,104,0,104,1,21101,387508441896,0,1,21101,0,315,0,1105,1,419,21101,666412880532,0,1,21102,1,326,0,1106,0,419,3,10,104,0,104,1,3,10,104,0,104,0,3,10,104,0,104,1,3,10,104,0,104,1,3,10,104,0,104,0,3,10,104,0,104,1,21101,106341436456,0,1,21101,373,0,0,1106,0,419,21101,46211886299,0,1,21101,384,0,0,1106,0,419,3,10,104,0,104,0,3,10,104,0,104,0,21101,0,838433923860,1,21102,1,407,0,1105,1,419,21102,1,988224946540,1,21102,1,418,0,1106,0,419,99,109,2,21201,-1,0,1,21101,40,0,2,21102,1,450,3,21101,440,0,0,1105,1,483,109,-2,2106,0,0,0,1,0,0,1,109,2,3,10,204,-1,1001,445,446,461,4,0,1001,445,1,445,108,4,445,10,1006,10,477,1101,0,0,445,109,-2,2105,1,0,0,109,4,1201,-1,0,482,1207,-3,0,10,1006,10,500,21101,0,0,-3,21201,-3,0,1,21202,-2,1,2,21101,1,0,3,21102,1,519,0,1105,1,524,109,-4,2106,0,0,109,5,1207,-3,1,10,1006,10,547,2207,-4,-2,10,1006,10,547,22102,1,-4,-4,1106,0,615,21202,-4,1,1,21201,-3,-1,2,21202,-2,2,3,21102,1,566,0,1105,1,524,21201,1,0,-4,21101,0,1,-1,2207,-4,-2,10,1006,10,585,21101,0,0,-1,22202,-2,-1,-2,2107,0,-3,10,1006,10,607,22101,0,-1,1,21102,1,607,0,105,1,482,21202,-2,-1,-2,22201,-4,-2,-4,109,-5,2105,1,0
--- a/package.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/package.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -10,6 +10,7 @@
     :read-numbers
     :read-numbers-from-string
     :read-comma-separated-values
+    :read-2d-array
 
     :ensure-string
     :ensure-stream
@@ -37,6 +38,8 @@
     :let-result
     :let-complex
     :queue-thunk
+    :bounds
+    :draw-bitmap
 
     :bytes->hex
     :bytes->integer
--- a/src/2016/days/day-05.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/src/2016/days/day-05.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -26,17 +26,13 @@
     (with part2 = (make-string 8 :initial-element #\_))
     (returning part1 part2)
 
-    ;; Instead create a fresh string of the entire ID every time, we keep
-    ;; a buffer and just replace the numeric portion on each iteration.
-    (with n = (length data))
-    (with buffer = (make-array 1024 :fill-pointer n :element-type '(unsigned-byte 8)))
-    (initially (replace buffer (string->bytes data)))
+    (for i :from 0)
 
-    (for i :from 0)
-    (for id = (string->bytes (princ-to-string i)))
-    (setf (fill-pointer buffer) (+ n (length id)))
-    (replace buffer id :start1 n)
-    (for hash = (md5:md5sum-sequence buffer))
+    ;; Instead create a fresh string of the entire ID every time, we could keep
+    ;; a buffer and just replace the numeric portion on each iteration.
+    ;; Unfortunately CCL's MD5 implementation wants a simple-array, not one with
+    ;; a fill pointer.  Welp.
+    (for hash = (md5:md5sum-string (format nil "~A~D" data i)))
 
     (when (dividesp i 100000)
       (progress i))
--- a/src/2018/days/day-10.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/src/2018/days/day-10.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -1,5 +1,5 @@
 (defpackage :advent/2018/10 #.cl-user::*advent-use*
-  (:shadow :x :y))
+  (:shadow :x :y :bounds))
 (in-package :advent/2018/10)
 
 (defun parse-line (line)
--- a/src/2019/days/day-03.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/src/2019/days/day-03.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -1,18 +1,9 @@
 (defpackage :advent/2019/03 #.cl-user::*advent-use*)
 (in-package :advent/2019/03)
 
-(defun bounds (grid)
-  (multiple-value-bind (bottom top)
-      (losh:extrema #'< (alexandria:hash-table-keys grid) :key #'y)
-    (multiple-value-bind (left right)
-        (losh:extrema #'< (alexandria:hash-table-keys grid) :key #'x)
-      (values (x left)
-              (x right)
-              (y top)
-              (y bottom)))))
-
 (defun print-grid (grid)
-  (multiple-value-bind (left right top bottom) (bounds grid)
+  (multiple-value-bind (left right bottom top)
+      (bounds (alexandria:hash-table-keys grid))
     (iterate
       (for y :from (1+ top) :downto (1- bottom))
       (iterate
--- a/src/2019/days/day-08.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/src/2019/days/day-08.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -23,18 +23,15 @@
                       (#\0 0)))))
 
 (defun decode-image (layers)
-  (destructuring-bind (height width) (array-dimensions (first layers))
-    (let-result (image (make-array (list width height)))
+  (gathering
+    (destructuring-bind (height width) (array-dimensions (first layers))
       (do-range ((y 0 height)
                  (x 0 width))
-        (setf (aref image x y)
-              (pixel-color layers y x))))))
+        (gather (cons (complex x y) (pixel-color layers y x)))))))
 
 (define-problem (2019 8) (stream) (1806)
   (let ((image (read-image stream 25 6)))
-    (netpbm:write-to-file "out/2019-08.pbm" (decode-image image)
-                          :if-exists :supersede
-                          :format :pbm)
+    (draw-bitmap (decode-image image) "out/2019-08.pbm")
     (iterate
       (for layer :in image)
       (finding (* (count-digit layer #\1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/2019/days/day-09.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -0,0 +1,25 @@
+(defpackage :advent/2019/09 #.cl-user::*advent-use*)
+(in-package :advent/2019/09)
+
+(define-problem (2019 9) (data read-numbers) (2955820355 46643)
+  (values
+    (first (gathering
+             (advent/intcode:run data
+                                 :input (constantly 1)
+                                 :output #'gather)))
+    (first (gathering
+             (advent/intcode:run data
+                                 :input (constantly 2)
+                                 :output #'gather)))))
+
+
+#; Scratch --------------------------------------------------------------------
+
+(let ((advent/intcode:*trace* nil))
+  (run '(109 1 204 -1 1001 100 1 100 1008 100 16 101 1006 101 0 99)))
+
+(let ((advent/intcode:*trace* nil))
+  (run '(1102 34915192 34915192 7 4 7 99 0)))
+
+(let ((advent/intcode:*trace* t))
+  (run '(104 1125899906842624 99)))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/2019/days/day-10.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -0,0 +1,87 @@
+(defpackage :advent/2019/10 #.cl-user::*advent-use*)
+(in-package :advent/2019/10)
+
+(defun asteroid-positions (map)
+  "Return a list of the asteroid positions in the 2D input `map`."
+  (destructuring-bind (rows cols) (array-dimensions map)
+    (iterate
+      (for-nested ((row :from 0 :below rows)
+                   (col :from 0 :below cols)))
+      (when (char= #\# (aref map row col))
+        ;; swap axes to match the problem text
+        (collect (complex col row))))))
+
+(defun slope (v)
+  (let-complex (v)
+    (if (zerop vy)
+      nil
+      (/ vx vy))))
+
+(defun slope= (a b)
+  (eql (slope a)
+       (slope b)))
+
+(defun sign= (a b)
+  (and (= (signum (x a)) (signum (x b)))
+       (= (signum (y a)) (signum (y b)))))
+
+(defun colinearp (origin a b)
+  (let ((va (- a origin))
+        (vb (- b origin)))
+    (and (sign= va vb)
+         (slope= va vb))))
+
+(defun group (asteroids origin)
+  (equivalence-classes (curry #'colinearp origin)
+                       (remove origin asteroids)))
+
+(defun count-seen (asteroids pos)
+  (length (group asteroids pos)))
+
+(defun angle (a b)
+  (mod (- (atan (y a) (x a))
+          (atan (y b) (x b)))
+       tau))
+
+(defun distance (a b)
+  (abs (- a b)))
+
+(defun splice (list)
+  "Splice `list` into a circular list."
+  (setf (cdr (last list)) list))
+
+(defun part1 (data)
+  (iterate
+    (with asteroids = (asteroid-positions data))
+    (for pos :in asteroids)
+    (finding pos :maximizing (count-seen asteroids pos) :into (best score))
+    (returning best score)))
+
+(defun part2 (data origin &optional (n 200))
+  (flet ((group-angle (group)
+           (angle #c(0 -1) (- (first group) origin)))
+         (sort-group (group)
+           (sort group #'< :key (curry #'distance origin))))
+    (iterate
+      (with groups = (_ data
+                       asteroid-positions
+                       (group _ origin)
+                       (mapcar #'sort-group _)
+                       (sort _ #'< :key #'group-angle)
+                       (apply #'ring _)))
+      (repeat n)
+      (for pos = (pop (ring-data groups)))
+      (if (null (ring-data groups))
+        ;; We go in the opposite direction than expected because the Y axis is
+        ;; annoyingly flipped in the problem.
+        (ring-cutf groups :prev t)
+        (ring-prevf groups))
+      (returning pos))))
+
+(define-problem (2019 10) (data read-2d-array) (284 404)
+  (multiple-value-bind (origin score) (part1 data)
+    (let ((lucky (part2 data origin)))
+      (values score (+ (* (x lucky) 100) (y lucky))))))
+
+
+#; Scratch --------------------------------------------------------------------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/2019/days/day-11.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -0,0 +1,41 @@
+(defpackage :advent/2019/11 #.cl-user::*advent-use*)
+(in-package :advent/2019/11)
+
+(defun run-robot (program &key (origin 'black))
+  (let ((panels (make-hash-table))
+        (mode '#1=(paint walk . #1#))
+        (pos #c(0 0))
+        (heading #c(0 1)))
+    (labels ((color ()
+               (gethash pos panels 0))
+             (paint (color)
+               (setf (gethash pos panels) color))
+             (turn (direction)
+               (mulf heading
+                     (ecase direction
+                       (0 #c(0 1))
+                       (1 #c(0 -1)))))
+             (walk (direction)
+               (turn direction)
+               (incf pos heading))
+             (exec (input)
+               (ecase (pop mode)
+                 (paint (paint input))
+                 (walk (walk input)))))
+      (paint (ecase origin
+               (black 0)
+               (white 1)))
+      (advent/intcode:run program :input #'color :output #'exec))
+    panels))
+
+(defun draw-panels (panels)
+  (draw-bitmap (alexandria:hash-table-alist panels)
+               "out/2019-11.pbm"
+               :flip-vertically t))
+
+(define-problem (2019 11) (data read-numbers) (1907)
+  (draw-panels (run-robot data :origin 'white))
+  (hash-table-count (run-robot data)))
+
+
+#; Scratch --------------------------------------------------------------------
--- a/src/2019/intcode.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/src/2019/intcode.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -1,20 +1,30 @@
 (defpackage :advent/intcode
   #.cl-user::*advent-use*
   (:shadow :step :trace)
-  (:export :init :step :run :run-machine))
+  (:export :init :step :run :run-machine :*trace*))
 
 (in-package :advent/intcode)
 
+(defparameter *trace* nil)
 (defparameter *trace-lock* (bt:make-lock "intcode trace lock"))
 
+
 ;;;; Data Structures ----------------------------------------------------------
 (defclass* machine ()
   ((pc :type (integer 0) :initform 0)
-   (memory :type (vector integer))
+   (rb :type (integer 0) :initform 0)
+   (memory :type hash-table)
    (input :type function)
    (output :type function)))
 
-(define-with-macro machine pc memory input output)
+(define-with-macro machine pc rb memory input output)
+
+(defun mref (machine address &optional (default 0))
+  (gethash address (memory machine) default))
+
+(defun (setf mref) (new-value machine address &optional (default 0))
+  (setf (gethash address (memory machine) default)
+        new-value))
 
 
 (defclass* operation ()
@@ -22,7 +32,7 @@
    (name :type symbol)
    (size :type (integer 1))
    (parameters :type list)
-   (perform :type function)))
+   (perform :type (or symbol function))))
 
 (defun perform-operation (opcode parameter-modes machine)
   (funcall (perform (gethash opcode *operations*))
@@ -30,12 +40,7 @@
 
 
 ;;;; Opcode Definition --------------------------------------------------------
-(defun retrieve (machine parameter-mode operand)
-  (ecase parameter-mode
-    (0 (aref (memory machine) operand))
-    (1 operand)))
-
-(defmacro define-opcode ((opcode name) parameters &body body)
+(defun retrieve (machine parameter-mode operand &key out)
   ;; Note that (confusingly) output parameters don't use the same addressing
   ;; scheme as input parameters.  For example: in the instruction 00002,1,2,99
   ;; all the parameter modes are 0, which means "look up the value at address
@@ -45,6 +50,16 @@
   ;; 1 (immediate mode).  So we need to handle output parameters specially.
   ;;
   ;; Sigh.
+  (ecase parameter-mode
+    (0 (if out ; position
+         operand
+         (mref machine operand)))
+    (1 operand) ; immediate
+    (2 (if out ; relative
+         (+ (rb machine) operand)
+         (mref machine (+ (rb machine) operand))))))
+
+(defmacro define-opcode ((opcode name) parameters &body body)
   (setf parameters (mapcar (lambda (param)
                              (if (symbolp param)
                                `(,param in)
@@ -66,15 +81,16 @@
                         (for (param kind) :in parameters)
                         (for offset :from 0)
                         (collect
-                          (ecase kind
-                            (in `(,param (retrieve ,machine
-                                                   (pop-mode)
-                                                   (aref memory (+ pc ,offset)))))
-                            (out `(,param (progn
-                                            (pop-mode)
-                                            (aref memory (+ pc ,offset)))))))))
+                          `(,param (retrieve ,machine
+                                             (pop-mode)
+                                             (mref ,machine (+ pc ,offset))
+                                             :out ,(ecase kind
+                                                     (in nil)
+                                                     (out t)))))))
                 (incf pc ,(length parameters))
-                ,@body))))
+                (macrolet ((mem (addr)
+                             `(mref ,',machine ,addr)))
+                  ,@body)))))
          (setf (gethash ,opcode *operations*)
                (make-instance 'operation
                  :opcode ,opcode
@@ -92,13 +108,13 @@
   :halt)
 
 (define-opcode (1 ADD) (x y (dest out))
-  (setf (aref memory dest) (+ x y)))
+  (setf (mem dest) (+ x y)))
 
 (define-opcode (2 MUL) (x y (dest out))
-  (setf (aref memory dest) (* x y)))
+  (setf (mem dest) (* x y)))
 
 (define-opcode (3 INP) ((dest out))
-  (setf (aref memory dest) (funcall input)))
+  (setf (mem dest) (funcall input)))
 
 (define-opcode (4 OUT) (val)
   (funcall output val))
@@ -112,83 +128,105 @@
     (setf pc addr)))
 
 (define-opcode (7 LES) (x y (dest out))
-  (setf (aref memory dest)
+  (setf (mem dest)
         (if (< x y) 1 0)))
 
 (define-opcode (8 EQL) (x y (dest out))
-  (setf (aref memory dest)
+  (setf (mem dest)
         (if (= x y) 1 0)))
 
+(define-opcode (9 ARB) (val)
+  (incf rb val))
+
 
 ;;;; Disassembly --------------------------------------------------------------
 (defun parse-op (n)
   (multiple-value-bind (parameter-modes opcode) (truncate n 100)
     (values opcode parameter-modes)))
 
-(defun disassemble-operation (program address)
+(defun disassemble-operation (machine address)
   (multiple-value-bind (opcode parameter-modes)
-      (parse-op (aref program address))
+      (parse-op (mref machine address))
     (let ((op (gethash opcode *operations*)))
       (if op
         (values
           `(,(name op)
             ,@(iterate
                 (for (param kind) :in (parameters op))
-                (for value :in-vector program :from (1+ address))
+                (for addr :from (1+ address))
+                (for value = (mref machine addr))
                 (for mode = (mod parameter-modes 10))
                 (collect `(,param ,(ecase kind
                                      (in (ecase mode
                                            (0 (vector value))
-                                           (1 value)))
-                                     (out value))))
+                                           (1 value)
+                                           (2 (list :r value))))
+                                     (out (ecase mode
+                                            ((0 1) value)
+                                            (2 (list :r value)))))))
                 (setf parameter-modes (truncate parameter-modes 10))))
           (size op))
-        (values `(data ,(aref program address)) 1)))))
+        (values `(data ,(mref machine address)) 1)))))
 
-(defun disassemble-program (program &key (start 0) (limit nil))
+(defun disassemble-program (machine &key (start 0) (limit nil))
   (iterate
     (when limit
       (if (zerop limit)
         (return)
         (decf limit)))
     (with address = start)
-    (with bound = (length program))
-    (while (< address bound))
-    (for (values instruction size) = (disassemble-operation program address))
+    (with addresses = (-<> (memory machine)
+                        alexandria:hash-table-keys
+                        (sort <> #'<)))
+    (with bound = (1+ (elt addresses (1- (length addresses)))))
+    (flet ((advance (addr)
+             (iterate
+               (until (null addresses))
+               (while (> addr (first addresses)))
+               (pop addresses))))
+      (advance address))
+    (while addresses)
+    (for (values instruction size) = (disassemble-operation machine address))
     (for end = (+ address size))
     (when (> end bound) ; hack to handle trailing data that looks instructionish
-      (setf instruction `(data ,(aref program address))
+      (setf instruction `(data ,(mref machine address))
             size 1
             end (1+ address)))
-    (for bytes = (coerce (subseq program address end) 'list))
-    (format t "~4D | ~{~5D~^ ~} ~36T| ~{~A~^ ~}~%" address bytes instruction)
+    (for bytes = (iterate (for i :from address :below end)
+                          (collect (mref machine i))))
+    (format t "~4D | ~4D | ~{~5D~^ ~} ~42T| ~{~A~^ ~}~%" address (rb machine) bytes instruction)
     (incf address size)))
 
 
 ;;;; Running ------------------------------------------------------------------
+(defun program->hash-table (program &key (test #'eql))
+  (iterate (for x :in-whatever program)
+           (for i :from 0)
+           (collect-hash (i x) :test test)))
+
 (defun init (program &key input output)
   (make-instance 'machine
-    :memory (fresh-vector program)
+    :memory (program->hash-table program)
     :input (or input #'read)
     :output (or output #'print)))
 
-(defun step (machine &key trace)
+(defun step (machine &key (trace *trace*))
   (with-machine (machine)
     (when trace
       (bt:with-lock-held (*trace-lock*)
-        (when (numberp trace)
-          (format t "~D: " trace))
-        (disassemble-program (memory machine) :start pc :limit 1)))
-    (multiple-value-bind (opcode parameter-modes) (parse-op (aref memory pc))
+        (unless (member trace '(t nil))
+          (format t "~A: " trace))
+        (disassemble-program machine :start pc :limit 1)))
+    (multiple-value-bind (opcode parameter-modes) (parse-op (mref machine pc))
       (incf pc)
       (perform-operation opcode parameter-modes machine))))
 
-(defun run-machine (machine &key trace)
+(defun run-machine (machine &key (trace *trace*))
   (iterate
     (case (step machine :trace trace)
-      (:halt (return (aref (memory machine) 0))))))
+      (:halt (return (mref machine 0))))))
 
-(defun run (program &key input output trace)
+(defun run (program &key input output (trace *trace*))
   (run-machine (init program :input input :output output) :trace trace))
 
 ;; #; Scratch --------------------------------------------------------------------
--- a/src/utils.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/src/utils.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -114,6 +114,18 @@
 (defun read-comma-separated-values (stream)
   (str:split #\, (alexandria:read-stream-content-into-string stream)))
 
+(defun read-2d-array (stream)
+  (iterate
+    (with lines = (read-lines stream))
+    (with result = (make-array (list (length lines) (length (first lines)))))
+    (for row :from 0)
+    (for line :in lines)
+    (iterate
+      (for col :from 0)
+      (for char :in-string line)
+      (setf (aref result row col) char))
+    (returning result)))
+
 
 ;;;; Rings --------------------------------------------------------------------
 (declaim (inline ring-prev ring-next ring-data))
@@ -466,11 +478,18 @@
      ,symbol))
 
 (defmacro let-complex (bindings &body body)
-  `(let* (,@(iterate (for (x y val) :in bindings)
-                     (for v = (gensym))
-                     (collect `(,v ,val))
-                     (collect `(,x (realpart ,v)))
-                     (collect `(,y (imagpart ,v)))))
+  `(let* (,@(iterate
+              (for binding :in bindings)
+              (for (x y val) = (etypecase binding
+                                 (symbol (list
+                                           (alexandria:symbolicate binding 'x)
+                                           (alexandria:symbolicate binding 'y)
+                                           binding))
+                                 (cons binding)))
+              (for v = (gensym))
+              (collect `(,v ,val))
+              (collect `(,x (realpart ,v)))
+              (collect `(,y (imagpart ,v)))))
      ,@body))
 
 
@@ -488,6 +507,44 @@
     (returning result)))
 
 
+(defun bounds (coords)
+  "Return the left, right, bottom, and top bounds of `coords`."
+  (multiple-value-bind (bottom top) (losh:extrema #'< coords :key #'y)
+    (multiple-value-bind (left right) (losh:extrema #'< coords :key #'x)
+      (values (x left)
+              (x right)
+              (y bottom)
+              (y top)))))
+
+
+(defun flip-vertically (image-array)
+  (destructuring-bind (width height) (array-dimensions image-array)
+    (dotimes (y (truncate height 2))
+      (dotimes (x width)
+        (rotatef (aref image-array x y)
+                 (aref image-array x (- height y 1)))))))
+
+(defun draw-bitmap (pixels path &key flip-vertically)
+  "Draw `pixels` to `path`.
+
+  `pixels` must be a sequence of `(position . color)` conses.
+
+  "
+  (multiple-value-bind (left right bottom top) (bounds (mapcar #'car pixels))
+    (let ((origin (complex left bottom))
+          (image (make-array (list (1+ (- right left))
+                                   (1+ (- top bottom)))
+                   :initial-element 0)))
+      (iterate
+        (for (pos . color) :in-whatever pixels)
+        (for pixel = (- pos origin))
+        (setf (aref image (x pixel) (y pixel)) color))
+      (when flip-vertically
+        (flip-vertically image))
+      (netpbm:write-to-file path image
+                            :if-exists :supersede
+                            :format :pbm))))
+
 ;;;; A* Search ----------------------------------------------------------------
 (defstruct path
   state
--- a/vendor/make-quickutils.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/vendor/make-quickutils.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -10,6 +10,7 @@
                :deletef
                :ensure-gethash
                :extremum
+               :equivalence-classes
                :flatten-once
                :hash-table-keys
                :hash-table-values
--- a/vendor/quickutils.lisp	Sun Dec 08 14:50:54 2019 -0500
+++ b/vendor/quickutils.lisp	Wed Dec 11 18:48:20 2019 -0500
@@ -2,7 +2,7 @@
 ;;;; See http://quickutil.org for details.
 
 ;;;; To regenerate:
-;;;; (qtlc:save-utils-as "quickutils.lisp" :utilities '(:COMPOSE :COPY-HASH-TABLE :CURRY :DELETEF :ENSURE-GETHASH :EXTREMUM :FLATTEN-ONCE :HASH-TABLE-KEYS :HASH-TABLE-VALUES :ONCE-ONLY :RCURRY :READ-FILE-INTO-STRING :REMOVEF :SYMB :WITH-GENSYMS) :ensure-package T :package "ADVENT.QUICKUTILS")
+;;;; (qtlc:save-utils-as "quickutils.lisp" :utilities '(:COMPOSE :COPY-HASH-TABLE :CURRY :DELETEF :ENSURE-GETHASH :EXTREMUM :EQUIVALENCE-CLASSES :FLATTEN-ONCE :HASH-TABLE-KEYS :HASH-TABLE-VALUES :ONCE-ONLY :RCURRY :READ-FILE-INTO-STRING :REMOVEF :SYMB :WITH-GENSYMS) :ensure-package T :package "ADVENT.QUICKUTILS")
 
 (eval-when (:compile-toplevel :load-toplevel :execute)
   (unless (find-package "ADVENT.QUICKUTILS")
@@ -13,15 +13,13 @@
 (in-package "ADVENT.QUICKUTILS")
 
 (when (boundp '*utilities*)
-  (setf *utilities* (union *utilities* '(:MAKE-GENSYM-LIST :ENSURE-FUNCTION
-                                         :COMPOSE :COPY-HASH-TABLE :CURRY
-                                         :DELETEF :ENSURE-GETHASH :EXTREMUM
-                                         :FLATTEN-ONCE :MAPHASH-KEYS
-                                         :HASH-TABLE-KEYS :MAPHASH-VALUES
-                                         :HASH-TABLE-VALUES :ONCE-ONLY :RCURRY
-                                         :WITH-OPEN-FILE* :WITH-INPUT-FROM-FILE
-                                         :READ-FILE-INTO-STRING :REMOVEF :MKSTR
-                                         :SYMB :STRING-DESIGNATOR :WITH-GENSYMS))))
+  (setf *utilities* (union *utilities* '(:MAKE-GENSYM-LIST :ENSURE-FUNCTION :COMPOSE
+                                         :COPY-HASH-TABLE :CURRY :DELETEF :ENSURE-GETHASH :EXTREMUM
+                                         :EQUIVALENCE-CLASSES :FLATTEN-ONCE :MAPHASH-KEYS
+                                         :HASH-TABLE-KEYS :MAPHASH-VALUES :HASH-TABLE-VALUES
+                                         :ONCE-ONLY :RCURRY :WITH-OPEN-FILE* :WITH-INPUT-FROM-FILE
+                                         :READ-FILE-INTO-STRING :REMOVEF :MKSTR :SYMB
+                                         :STRING-DESIGNATOR :WITH-GENSYMS))))
 (eval-when (:compile-toplevel :load-toplevel :execute)
   (defun make-gensym-list (length &optional (x "G"))
     "Returns a list of `length` gensyms, each generated as if with a call to `make-gensym`,
@@ -182,6 +180,32 @@
                     :end end)))))
   
 
+  (defun equivalence-classes (equiv seq)
+    "Partition the sequence `seq` into a list of equivalence classes
+defined by the equivalence relation `equiv`."
+    (let ((classes nil))
+      (labels ((find-equivalence-class (x)
+                 (member-if (lambda (class)
+                              (funcall equiv x (car class)))
+                            classes))
+               
+               (add-to-class (x)
+                 (let ((class (find-equivalence-class x)))
+                   (if class
+                       (push x (car class))
+                       (push (list x) classes)))))
+        (declare (dynamic-extent (function find-equivalence-class)
+                                 (function add-to-class))
+                 (inline find-equivalence-class
+                         add-to-class))
+        
+        ;; Partition into equivalence classes.
+        (map nil #'add-to-class seq)
+        
+        ;; Return the classes.
+        classes)))
+  
+
   (defun flatten-once (list)
     "Flatten `list` once."
     (loop :for x :in list
@@ -399,8 +423,8 @@
     `(with-gensyms ,names ,@forms))
   
 (eval-when (:compile-toplevel :load-toplevel :execute)
-  (export '(compose copy-hash-table curry deletef ensure-gethash extremum
-            flatten-once hash-table-keys hash-table-values once-only rcurry
-            read-file-into-string removef symb with-gensyms with-unique-names)))
+  (export '(compose copy-hash-table curry deletef ensure-gethash extremum equivalence-classes
+            flatten-once hash-table-keys hash-table-values once-only rcurry read-file-into-string
+            removef symb with-gensyms with-unique-names)))
 
 ;;;; END OF quickutils.lisp ;;;;