You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
aoc-racket/day07.rkt

138 lines
6.4 KiB
Racket

9 years ago
#lang scribble/lp2
@(require scribble/manual aoc-racket/helper)
@aoc-title[7]
9 years ago
@defmodule[aoc-racket/day07]
9 years ago
9 years ago
@link["http://adventofcode.com/day/7"]{The puzzle}. Our @link-rp["day07-input.txt"]{input} describes an electrical circuit, with each line of the file describing the signal provided to a particular wire.
9 years ago
9 years ago
@chunk[<day07>
<day07-setup>
<day07-ops>
<day07-q1>
<day07-q2>
<day07-test>]
9 years ago
9 years ago
@section{What's the signal on wire @tt{a}?}
9 years ago
The first question we should ask is  how do we model a wire? We're told that it's a thing with inputs that can be evaluated to get a value. So it sounds a lot like a function. Thus, what we'll do is convert our wire descriptions into functions, and then run the function called @racket[a].
In other languages, creating functions from text strings would be a difficult trick. But this facility is built into Racket with @racket[define-syntax]. Essentially our program will run in two phases: in the syntax-transformation phase, we'll read in the list of wire descriptions and expand them into code that represents functions. In the second phase, the program including our new functions, created via syntax transformation will compile & run as usual.
9 years ago
The @racket[convert-input-to-wire-functions] transformer takes the input strings and first converts each into a @italic{datum}  that is, a fragment of Racket code. So an input string like this:
9 years ago
9 years ago
@racket["bn RSHIFT 2 -> bo"]
9 years ago
9 years ago
becomes a datum like this:
9 years ago
9 years ago
@racket[(wire bn RSHIFT 2 -> bo)]
9 years ago
9 years ago
Next, this transformer converts the datums into @italic{syntax}, a process that adds contextual information (for instance, the meanings of identifiers) so the code can be evaluated.
Then the @racket[wire] transformer moves the arguments around to define functions, by matching the three definition patterns that appear in the input. Thus, syntax like this:
9 years ago
9 years ago
@racket[(wire bn RSHIFT 2 -> bo)]
9 years ago
9 years ago
becomes:
9 years ago
9 years ago
@racket[(define (bo) (RSHIFT (evaluate-arg bn) (evaluate-arg 2)))]
9 years ago
9 years ago
@racket[evaluate-arg] lets us handle the fact that some of the arguments for our wires are other wires, and some arguments are numbers. Rather than detect these differences during the syntax-transformation phase, we'll just wrap every input argument with @racket[evaluate-arg], which will do the right thing in the next phase.
9 years ago
(@racket[wire-value-cache] is just a performance enhancement, so that wire values don't have to be computed multiple times.)
9 years ago
One gotcha when using syntax transformers is that identifiers introduced by a transformer can silently override others (in the same way that identifiers defined inside a @racket[let] will override those with the same name outside the @racket[let]). For instance, one of the wires in our input is named @tt{if}. When our syntax transformer defines the @tt{if} function, it will override the usual meaning of @racket[if]. There are plenty of elegant ways to prevent these name collisions. (The most important of which is called @italic{syntax hygiene}, and permeates the design of Racket's syntax-transformation system.) But because this is a puzzle, we'll take the cheap way out: we won't use @racket[if] elsewhere in our code, and instead use @racket[cond].
9 years ago
9 years ago
@chunk[<day07-setup>
9 years ago
(require racket rackunit
9 years ago
(for-syntax racket/file racket/string))
9 years ago
(provide (all-defined-out))
9 years ago
(define-syntax (convert-input-to-wire-functions stx)
(syntax-case stx ()
[(_)
9 years ago
(let* ([input-strings (file->lines "day07-input.txt")]
9 years ago
[wire-strings (map (λ(str) (format "(wire ~a)" str)) input-strings)]
[wire-datums (map (compose1 read open-input-string) wire-strings)])
(datum->syntax stx `(begin ,@wire-datums)))]))
(define-syntax (wire stx)
(syntax-case stx (->)
[(_ arg -> wire-name)
#'(define (wire-name) (evaluate-arg arg))]
[(_ 16bit-op arg -> wire-name)
#'(define (wire-name) (16bit-op (evaluate-arg arg)))]
[(_ arg1 16bit-op arg2 -> wire-name)
#'(define (wire-name) (16bit-op (evaluate-arg arg1) (evaluate-arg arg2)))]
[(_ expr) #'(begin expr)]
[else #'(void)]))
(convert-input-to-wire-functions)
(define wire-value-cache (make-hash))
(define (evaluate-arg x)
(cond
[(procedure? x) (hash-ref! wire-value-cache x (thunk* (x)))]
[else x]))
]
9 years ago
We also need to implement our 16-bit math operations. As we saw above, our syntax transformers are generating code that looks like, for instance, @racket[(RSHIFT (evaluate-arg bn) (evaluate-arg 2))]. This code won't work unless we've defined an @racket[RSHIFT] function too.
9 years ago
9 years ago
These next definitions use @racket[define-syntax-rule] as a shortcut, which is another syntax transformer. (Thanks to @link["https://jeapostrophe.github.io"]{Jay McCarthy} for the 16-bit operations.)
9 years ago
9 years ago
@chunk[<day07-ops>
9 years ago
(define (16bitize x)
(define 16bit-max (expt 2 16))
(define r (modulo x 16bit-max))
(cond
[(negative? r) (16bitize (+ 16bit-max r))]
[else r]))
(define-syntax-rule (define-16bit id proc)
(define id (compose1 16bitize proc)))
(define-16bit AND bitwise-and)
(define-16bit OR bitwise-ior)
(define-16bit LSHIFT arithmetic-shift)
(define-16bit RSHIFT (λ(x y) (arithmetic-shift x (- y))))
(define-16bit NOT bitwise-not)]
After that, we just evaluate wire function @racket[a] to get our answer.
9 years ago
@chunk[<day07-q1>
9 years ago
(define (q1) (a))]
9 years ago
@section{What's the signal on wire @tt{a} if wire @tt{b} is overridden with @tt{a}'s original value?}
9 years ago
Having done the heavy lifting, this is easy. We'll redefine wire function @racket[b] to produce the new value, and then check the value of @racket[a] again.
9 years ago
Ordinarily, as a safety measure, Racket won't let you redefine functions. But we can circumvent this limitation by setting @racket[compile-enforce-module-constants] to @racket[#f]. We'll also need to reset our cache, since this change will affect the other wires too.
9 years ago
9 years ago
@chunk[<day07-q2>
9 years ago
(compile-enforce-module-constants #f)
(define (q2)
(define first-a-val (a))
(set! b (thunk* first-a-val))
(set! wire-value-cache (make-hash))
(a))
]
9 years ago
@section{Testing Day 7}
9 years ago
9 years ago
@chunk[<day07-test>
9 years ago
(module+ test
(check-equal? (q1) 46065)
(check-equal? (q2) 14134))]