Day 19: Aplenty
We get the first computer exercise! Parsing the input is already non-trivial. We have a list of condition statements (quite similar to Scheme cond). As a result of each condition, we may accept, reject or forward to another condition. Then also a list of Part with field x, m, a and s.
abstract type Instr end
struct Accept <: Instr end
struct Reject <: Instr end
struct Forward <: Instr
label::Symbol
end
struct Condition
attribute::Symbol
operator::Symbol
value::Integer
action::Instr
end
struct Cond
clauses::Vector{Condition}
default::Instr
end
@kwdef struct Part
x::Int
m::Int
a::Int
s::Int
end
struct Input
conds::IdDict{Symbol,Cond}
parts::Vector{Part}
end
The parser:
input_p = let
enclosed(a, b) = p -> token(a) >>> p >> skip(token(b))
sep_end_by(a, sep) = some_p(a >> skip(sep))
symbol(re::Regex) = token(re) >> fmap(m -> Symbol(m.match))
curly = enclosed("{", "}")
action_p = token("R") >>> pure_p(Reject()) |
token("A") >>> pure_p(Accept()) |
symbol(r"[a-z]+") >> fmap(Forward)
operator_p = symbol(r"[><]")
condition_p = sequence(
symbol(r"[xmas]"),
operator_p, integer, token(":") >>> action_p) >>
fmap(splat(Condition))
cond_p = sequence(
symbol(r"[a-z]+"),
curly(sequence(sep_end_by(condition_p, token(",")), action_p)) >>
fmap(splat(Cond)))
attribute_p = sequence(symbol(r"[xmas]"), token("=") >>> integer) >>
fmap(Tuple{Symbol,Int})
item_p = curly(sep_by_p(attribute_p, token(","))) >> fmap(x -> Part(; x...))
sequence(
many_p(cond_p) >>
fmap(IdDict{Symbol,Cond}),
many_p(item_p)) >> fmap(splat(Input))
end
Julia metaprogramming
I wanted to solve the first part using Julia's metaprogramming capabilities.
function compile(cond::Cond)
compile_cond(p::Symbol) = function (c::Condition, else_clause)
getattr(x::Symbol, attr::Symbol) = Expr(:., x, QuoteNode(attr))
Expr(:if,
Expr(:call, c.operator, getattr(p, c.attribute), c.value),
c.action,
else_clause)
end
Expr(:->, :p, foldr(compile_cond(:p), cond.clauses; init=cond.default))
end
function part1(inp)
conds = IdDict(k => eval(compile(v)) for (k, v) in inp.conds)
run(instr::Forward, part::Part) = run(Base.invokelatest(conds[instr.label], part), part)
run(::Accept, part::Part) = part.x + part.m + part.a + part.s
run(::Reject, ::Part) = 0
inp.parts .|> (p -> run(Forward(:in), p)) |> sum
end
This works, but it should be noted that Julia's compiler is very slow. So this takes a second to run. For part 2 of course, all of this goes out of the window.
Ranges again!
I've tried storing the ranges for the parts into a struct PartRange ... but it turned out to be very hard to manipulate such a struct using run-time information (unless I would generate the code again, just like in Part 1).
Other than that, the structure is very similar to Part 1: define how to apply a single condition clause, then foldr to obtain the same operation for the entire cond. The difference is that here I'm using ordinary higher level functions to compose the larger function, so everything lives in run-time.
apply(condition::Condition, else_clause) = function (result::Vector{Tuple{Instr,PartRange}}, pr::PartRange)
key = condition.attribute
rng = pr[key]
if condition.value in rng
if condition.operator === :<
push!(result, (condition.action, update(pr, key, rng.start:(condition.value-1))))
else_clause(result, update(pr, key, condition.value:rng.stop))
else
push!(result, (condition.action, update(pr, key, condition.value+1:rng.stop)))
else_clause(result, update(pr, key, rng.start:condition.value))
end
else
if (condition.operator === :< && rng.stop < condition.value) ||
(condition.operator === :> && rng.start > condition.value)
push!(result, (condition.action, pr))
else
else_clause(result, pr)
end
end
end
apply(instr::Instr) = function (result::Vector{Tuple{Instr,PartRange}}, pr::PartRange)
push!(result, (instr, pr))
end
apply(cond::Cond) = foldr(apply, cond.clauses; init=apply(cond.default))
function part2(inp)
accepted = PartRange[]
handle(i::Forward, pr::PartRange) = apply(inp.conds[i.label])(Tuple{Instr,PartRange}[], pr)
handle(::Accept, pr::PartRange) = begin
push!(accepted, pr)
[]
end
handle(::Reject, ::PartRange) = []
x = [(Forward(:in), PartRange(i => 1:4000 for i in [:x, :m, :a, :s]))]
while !isempty(x)
x = reduce(vcat, x .|> splat(handle))
end
accepted .|> (pr -> pr |> values .|> length |> prod) |> sum
end