Advent of Code

This module provides a command line interface for running the solution to a specific day for a specific year, run as advent $YEAR $DAY.

This runner will automatically locate the appropriate input file and then run the selected day's solution against it. See Runner for the definitions of the Day, Year, and Advent data types.


module Main

import System
import System.Path
import System.File
import System.Directory
import System.Console.GetOpt
import Data.String
import Data.Vect

import Control.Eff
import Derive.Prelude

import Structures.Dependent.FreshList

import Runner
import Util
import Util.Eff

import Years.Y2015;

%language ElabReflection
%default total

Command Line Interface

Since this is a simple application, I am using GetOpt from contrib for argument parsing. Here we define our options list, a data type to describe our flags, and the usage message header.


data Flag = UseExample | Verbose
%runElab derive "Flag" [Eq, Show]

options : List (OptDescr Flag)
options =
  [
    MkOpt ['u'] ["use-example"] (NoArg UseExample) "Use example input instead of main input"
  , MkOpt ['v'] ["verbose"] (NoArg Verbose) "Enable logging to stderr"
  ]

header : String
header = "Usage: advent YEAR DAY [OPTION...]"

Error Type

We will be using one unified error type for the entire Main module, but we must go ahead and define it before we go any further with command line arguments parsing, as we are about to do some things that might fail.


data Error : Type where
  OptionsError : (errs : List String) -> Error
  ArgumentsError : (count : Nat) -> Error
  InvalidYear : (year : String) -> Error
  InvalidDay : (year : Nat) -> (day : String) -> Error
  NoSuchDay : (year : Nat) -> (day : Nat) -> Error
  NoCurrentDir : Error
  InputRead : (path : String) -> (err : FileError) -> Error
  SolveError : Show e => (year, day, part : Nat) -> (err : e) -> Error
%name Error err

A Show implementation for Error is provided, hidden in this document for brevity.

Extract the year and day

After peeling off our options from the arguments using GetOpt, we will be left with a list of non-option arguments, which we must then parse into our year number/day number pair. We perform some pattern matching to make sure the list is the correct length, and then parse the individual integers, throwing an error if anything goes wrong


||| Convert the non-option arguments into a Year/Day pair
argumentsToPair : Has (Except Error) fs =>
  List String -> Eff fs (Nat, Nat)
argumentsToPair [] = throw $ ArgumentsError 0
argumentsToPair [x] = throw $ ArgumentsError 1
argumentsToPair [year, day] = do
  year <- note (InvalidYear year) $ parsePositive year
  day <- note (InvalidDay year day) $ parsePositive day
  pure (year, day)
argumentsToPair xs = throw $ ArgumentsError (length xs)

The Advent

Construct our top-level Advent record from the imported Year modules. The argument to the MkAdvent constructor is a FreshList, with a freshness criteria ensuring that the list is in ascending order and does not include any duplicate Years.


advent : Advent
advent = MkAdvent [
    y2015
  ]

The Hard Part

Now we must glue everything together, parse our arguments, handle any supplied options, locate the relevant year and day, locate the corresponding input file, read it, and then run the selected Day against it.

Because we are building our application around effects, the actual core of the application is the effectful start computation, that the main function will then apply IO based handlers to.

Our reader and writer aren't ready yet, we need to do some setup first, so the top level type signature doesn't include them, so we'll add them to the effects list as they become available.


covering
||| Actual main, as an effectful computation
start : Eff [IO, Except Error] ()
start = do

Parsing the arguments

Feed the augments from System.getArgs into getOpt, making sure to drop the name the binary was invoked with, then check for any errors in the result, throwing our own error if any are encountered.


  -- Read and parse arguments/options
  args <- getArgs
  let opts = getOpt Permute options (drop 1 args)
  when (not . null $ opts.errors) $
    throw (OptionsError opts.errors)

Use our argumentsToPair function to extract the selected year and day numbers from the positional arguments, implicitly throwing an error if there are any failures doing so.


  (year, day_n) <- argumentsToPair opts.nonOptions

Handling the arguments and finding the input

Handle the verbosity flag, if it is set, hook our logger up to stderr, otherwise blackhole the logs. Afterwards, use logHandler to introduce the Logger into the effects list.


  -- If the verbose flag is set, hook up the logging writer to stderr
  let verbosity = count (== Verbose) opts.options
  let logHandler : Eff [IO, Except Error, Logger] () -> Eff [IO, Except Error] () =
    handleLog (Warn >+ verbosity)
  -- Add the logging writer to the effect
  logHandler $ do

Find the current directory and append to it to find the input file. If the --use-example/-u flag is set, look for the input file at inputs/examples/$year/$day, otherwise look for it at inputs/real/$year/$day.

After we've located it, attempt to read from it, throwing an InputRead error if any errors occur, then insert the contents into our Reader and inject it into the effects list.


  -- Locate and read in the input file
  Just cwd <- currentDir | _ => throw NoCurrentDir
  let cwd = parse cwd
  let use_example = any (== UseExample) opts.options
  let base_dir = cwd /> "inputs" /> (if use_example then "examples" else "real");
  let input_path = show $ base_dir /> (show year) /> (show day_n)
  info "Reading input from \{input_path}"
  Right contents <- readFile input_path | Left err => throw $ InputRead input_path err
  -- Now add the reader that provides the input to the effect
  runReaderAt "input" contents {fs = PartEff Error} $ do

Try and locate the Day in our Advent record, throwing an error if we are unable to locate it


  -- Attempt to locate the provided day
  Just day <- pure $ locate year day_n advent | _ => throw $ NoSuchDay year day_n

Executing the parts

Run the selected Day's part 1, wrapping any errors that it returns in a SolveError. Collect both the result and the opaque context value, then print out the result.


  -- Run part 1
  part_1 <- lift $ runExcept day.part1
  (part_1, ctx) <- rethrow . mapLeft (SolveError year day_n 1 @{day.showErr1}) $ part_1
  putStrLn "\{show year} day \{show day_n} part 1:"
  putStrLn $ unlines . map ("  " ++) . lines . show @{day.showOut1} $ part_1

Check and see if the selected Day has a part 2, if it does, feed the opaque context value collected from part 1 into it, wrap any errors that it returns in a SolveError, then print out the result, then return, closing out the program.


  -- Run part 2 if we have it
  case day.part2 of
    Nothing => pure ()
    Just part_2_f => do
      part_2 <- lift . runExcept $ part_2_f ctx
      part_2 <- rethrow . mapLeft (SolveError year day_n 2 @{day.showErr2}) $ part_2
      putStrLn "\{show year} day \{show day_n} part 2:"
      putStrLn $ unlines . map ("  " ++) . lines . show @{day.showOut2} $ part_2

Helper functions


  where

Lower logging into the IO component of the effect

Makes use of Logger's handleLoggerIO function to "lower" logging actions into IO within the effect.


    -- Lowers logging into IO within the effect using the given IO function
    handleLog :
      Has Logger fs => Has IO (fs - Logger) => 
      (max_log : Level) -> Eff fs a -> Eff (fs - Logger) a
    handleLog max_log x = 
      handle 
        (\msg, f => 
          do _ <- send $ handleLoggerIO max_log msg 
             f $ ignore msg) x

Handle the effectful action as an IO action

Use runEff to translate the effects in start's effects list into IO actions, and return the resulting IO ().

The IO component of the effects list is handled via id, translating IO actions into themselves, and we construct a helper function to handle errors as IO actions by printing the error and crashing the program.


covering
||| Shim main, which simply executes the effectful computation
main : IO ()
main = runEff start [id, handleError]
  where
  handleError : Except Error a -> IO a
  handleError (Err err) = do
    printLn err
    exitFailure