Year 2015 Day 4 Part 1

This commit is contained in:
Nathan McCarty 2025-01-07 09:26:28 -05:00
parent fdfa15daf8
commit c873b7282e
5 changed files with 159 additions and 18 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
build/
inputs/
*.*~
support/*.o
support/advent-support

View file

@ -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)

View file

@ -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 =

View file

@ -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
]
```

148
src/Years/Y2015/Day4.md Normal file
View file

@ -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
module Years.Y2015.Day4
import Control.Eff
import Runner
-->
```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, ())
```
<!-- idris
public export
day4 : Day
day4 = First 4 part1
-->