From c873b7282e4718a611d303e031131325f32be97d Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Tue, 7 Jan 2025 09:26:28 -0500 Subject: [PATCH] Year 2015 Day 4 Part 1 --- .gitignore | 2 + README.md | 1 + advent.ipkg | 19 +----- src/Years/Y2015.md | 7 ++ src/Years/Y2015/Day4.md | 148 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 src/Years/Y2015/Day4.md diff --git a/.gitignore b/.gitignore index 47ba336..28e34b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ build/ inputs/ *.*~ +support/*.o +support/advent-support diff --git a/README.md b/README.md index 1fb5198..5d485c3 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,4 @@ The goal of this project is to get all 500 currently available stars in the form - [Day 1](src/Years/Y2015/Day1.md) - [Day 2](src/Years/Y2015/Day2.md) - [Day 3](src/Years/Y2015/Day3.md) + - [Day 4](src/Years/Y2015/Day4.md) diff --git a/advent.ipkg b/advent.ipkg index 3f138d0..0673908 100644 --- a/advent.ipkg +++ b/advent.ipkg @@ -20,6 +20,7 @@ depends = base , elab-util , ansi , if-unsolved-implicit + , c-ffi -- modules to install modules = Runner @@ -35,21 +36,3 @@ executable = "advent" sourcedir = "src" -- builddir = -- outputdir = - --- script to run before building --- prebuild = - --- script to run after building --- postbuild = - --- script to run after building, before installing --- preinstall = - --- script to run after installing --- postinstall = - --- script to run before cleaning --- preclean = - --- script to run after cleaning --- postclean = diff --git a/src/Years/Y2015.md b/src/Years/Y2015.md index 27c379d..848935a 100644 --- a/src/Years/Y2015.md +++ b/src/Years/Y2015.md @@ -9,6 +9,7 @@ import Runner import Years.Y2015.Day1 import Years.Y2015.Day2 import Years.Y2015.Day3 +import Years.Y2015.Day4 --> # Days @@ -37,6 +38,12 @@ y2015 = MkYear 2015 [ , day3 ``` +## [Day 4](Y2015/Day3.md) + +```idris + , day4 +``` + ```idris ] ``` diff --git a/src/Years/Y2015/Day4.md b/src/Years/Y2015/Day4.md new file mode 100644 index 0000000..cd1538b --- /dev/null +++ b/src/Years/Y2015/Day4.md @@ -0,0 +1,148 @@ +# Day 4 + +This day introduces us to a little bit of FFI, linking to openssl to use it's `MD5` functionality. + + + +```idris +import Data.String +import Data.Bits +import System.FFI +import Prim.Array +``` + +## FFI + +We will be using openssl's `MD5` function to do the hashing, so we will need to provide a couple of foreign function definitions. + +First, we'll provide one for the C standard library's `strlen`. We could get this value from within idris, but we'll do it through FFI to flesh out the example a bit more. + +The format of the specifier after `%foreign` is "language:function name,library name". Since `strlen` is the c standard library, we use `libc` as the library name. + +Idris will automatically convert primitive types like `String`s and `Int`s to their C equivalents at the FFI boundary. + +`PrimIO` is a special, basic type of IO action based on linear types, we won't be getting into the specifics right now. + +By convention, primitive FFI function names start with `prim__` in idris. + +```idris +%foreign "C:strlen,libc" +prim__strlen : String -> PrimIO Int +``` + +Now we need one for the `MD5` function from openssl, the openssl objectfile calls itself `libcrypto`, so we'll use that as the library name. + +The `MD5` function actually returns a pointer to the hash value, but since the 3rd argument is the pointer it expects to write the hash to, we already have that value and we'll ignore it by having our `prim__md5` return unit. + +The second argument is the length of the input string in bytes. + +```idris +%foreign "C:MD5,libcrypto" +prim__md5 : String -> Int -> Ptr Bits8 -> PrimIO () +``` + +Now we glue these parts together in our higher level `md5'` function. + +We use `malloc` from `System.FFI` to allocate a buffer for `prim__md5` to write to. This returns an `AnyPtr`, when we need a `Ptr Bits8`, so we'll need to cast it, using `prim__castPtr` from the prelude. + +We then use our `prim__strlen` function to get the length of our input string from within C, using `primIO` from the prelude to "lift" the resulting `PrimIO Int` into our `io` context. + +We then feed the string, the calculated length, and our casted buffer into our `prim__md5` function, then use `prim__getArrayBits8` from the [c-ffi](https://github.com/joelberkeley/c-ffi/) library to extract each of the bytes, passing them into a pure idris helper function to convert into hex representation. + +Since we are working with C FFI and performing a little bit of manual memory management here, we must remember to `free` our pointer, and then we can splice together our output string and return. + +```idris +md5' : HasIO io => String -> io String +md5' str = do + buffer <- malloc 16 + len <- primIO $ prim__strlen str + raw_md5 <- primIO $ prim__md5 str len (prim__castPtr buffer) + let bytes = map (\x => prim__getArrayBits8 (prim__castPtr buffer) x) [0..15] + free buffer + pure . joinBy "" . map byteToHex $ bytes + where + hexDigit : Bits8 -> Char + hexDigit n = + if n < 10 + then chr (cast n + ord '0') + else chr (cast (n - 10) + ord 'a') + byteToHex : Bits8 -> String + byteToHex n = + pack [hexDigit (n `shiftR` 4), hexDigit (n .&. 0xF)] +``` + +Now, because foreign functions return a `PrimIO` by default, our `md'` still requires some sort of `HasIO` (like IO). This is undesirable here, after all, MD5 is a hash function, so it really _ought_ to behave like a pure function. We know from taking a careful look at what FFI functions we are invoking that we aren't altering any global state. + +Idris has an escape hatch for these types of situations, `unsafePerformIO`. This function is, as the name suggests, _quite_ unsafe, so it shouldn't be used if you don't fully understand the implications, and only sparingly at that, but doing FFI to C is an intrinsically unsafe operation, so it's morally okay to use here, as long as we are careful to not violate referential transparency or type safety. Haskell programmers should note that `unsafePerformIO` is _much_ easier to use correctly in Idris, largely as a result of Idris being strict by default. + +Finally, we'll construct our top level `md5` function, wrapping `md5'` in `unsafePerformIO` + +```idris +md5 : String -> String +md5 str = unsafePerformIO $ md5' str +``` +## Solver functions + +### Count the leading zeros in a string + +There is a more clever way to do this making use of a "view" strings have for interacting with them as if they were lazy lists, but we've already covered so much here that we'll save that one for another time and just do it the straight forward way, having a top level function that accepts a string, turns it into a list of chars, and then pass it into a tail recursive helper function to actually count the zeros. + +```idris +countZeros : String -> Nat +countZeros str = countZeros' (unpack str) 0 + where + countZeros' : (xs : List Char) -> (acc : Nat) -> Nat + countZeros' [] acc = acc + countZeros' ('0' :: xs) acc = countZeros' xs (acc + 1) + countZeros' (x :: xs) acc = acc +``` + +### Count the number of zeros for a specific input + +Concatenate on our number to our secret key, hash it, and count the zeros + +```idris +checkZeros : (key : String) -> (number : Nat) -> Nat +checkZeros key number = + let hash = md5 (key ++ show number) + in countZeros hash +``` + +### Find the first input with a specific number of zeros + +Search numbers starting at 1 and counting up in a tail recursive helper function. This function is going to be effectively impossible to prove totality for, so we have removed our normal `%default total` annotation for this file + +```idris +findFirst : (zeros : Nat) -> (key : String) -> Nat +findFirst zeros key = findFirst' zeros key 1 + where + findFirst' : (zeros : Nat) -> (key : String) -> (current : Nat) -> Nat + findFirst' zeros key current = + if checkZeros key current >= zeros + then current + else findFirst' zeros key (current + 1) +``` + +## Part Functions + +### Part 1 + +```idris +part1 : Eff (PartEff String) (Nat, ()) +part1 = do + input <- map trim $ askAt "input" + let number = findFirst 5 input + pure (number, ()) +``` + +