From f7550e8afba28940e699fdad95067b2dd8e523c5 Mon Sep 17 00:00:00 2001
From: Daniel Lin <ephemient@gmail.com>
Date: Sat, 30 Nov 2024 22:31:22 -0500
Subject: [PATCH] Set up Haskell

---
 .github/workflows/hs-bench.yml | 61 +++++++++++++++++++++++++++++
 .github/workflows/hs.yml       | 71 ++++++++++++++++++++++++++++++++++
 README.md                      |  5 +++
 hs/LICENSE                     | 29 ++++++++++++++
 hs/README.md                   | 44 +++++++++++++++++++++
 hs/aoc2024.cabal               | 69 +++++++++++++++++++++++++++++++++
 hs/app/Main.hs                 | 33 ++++++++++++++++
 hs/app/cbits/main.c            | 25 ++++++++++++
 hs/bench/Main.hs               | 26 +++++++++++++
 hs/bench/cbits/main.c          | 25 ++++++++++++
 hs/cabal.project               |  2 +
 hs/src/Common.hs               | 24 ++++++++++++
 hs/test/Main.hs                |  1 +
 13 files changed, 415 insertions(+)
 create mode 100644 .github/workflows/hs-bench.yml
 create mode 100644 .github/workflows/hs.yml
 create mode 100644 hs/LICENSE
 create mode 100644 hs/README.md
 create mode 100644 hs/aoc2024.cabal
 create mode 100644 hs/app/Main.hs
 create mode 100644 hs/app/cbits/main.c
 create mode 100644 hs/bench/Main.hs
 create mode 100644 hs/bench/cbits/main.c
 create mode 100644 hs/cabal.project
 create mode 100644 hs/src/Common.hs
 create mode 100644 hs/test/Main.hs

diff --git a/.github/workflows/hs-bench.yml b/.github/workflows/hs-bench.yml
new file mode 100644
index 00000000..64ad7d9d
--- /dev/null
+++ b/.github/workflows/hs-bench.yml
@@ -0,0 +1,61 @@
+name: Haskell benchmarks
+
+on:
+  workflow_dispatch:
+
+permissions:
+  contents: write
+
+jobs:
+  get-inputs:
+    uses: ephemient/aoc2024/.github/workflows/get-inputs.yml@main
+    secrets:
+      SESSION: ${{ secrets.SESSION }}
+
+  build:
+    needs: [ get-inputs ]
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          ref: gh-docs
+          path: gh-docs
+      - uses: actions/download-artifact@v4
+        with:
+          name: inputs
+          path: inputs
+      - uses: haskell-actions/setup@v2
+        id: setup
+        with:
+          ghc-version: 9.10.1
+      - run: |
+          cabal configure --enable-tests --enable-benchmarks
+          cabal build all --dry-run
+        working-directory: hs
+      - uses: actions/cache/restore@v4
+        id: cache
+        env:
+          key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
+        with:
+          path: ${{ steps.setup.outputs.cabal-store }}
+          key: ${{ env.key }}-plan-${{ hashFiles('hs/dist-newstyle/cache/plan.json') }}
+          restore-keys: ${{ env.key }}-
+      - run: cabal build all --only-dependencies
+        if: steps.cache.outputs.cache-hit != 'true'
+        working-directory: hs
+      - uses: actions/cache/save@v4
+        if: steps.cache.outputs.cache-hit != 'true'
+        with:
+          path: ${{ steps.setup.outputs.cabal-store }}
+          key: ${{ steps.cache.outputs.cache-primary-key }}
+      - run: cabal bench bench:aoc2024-bench --benchmark-options='-o ${{ github.workspace }}/gh-docs/aoc2024-bench.html'
+        env:
+          AOC2024_DATADIR: ${{ github.workspace }}/inputs
+        working-directory: hs
+      - uses: EndBug/add-and-commit@v9
+        with:
+          cwd: gh-docs
+          add: aoc2024-bench.html
+          message: 'Haskell Criterion ${{ github.sha }}'
diff --git a/.github/workflows/hs.yml b/.github/workflows/hs.yml
new file mode 100644
index 00000000..4de42c63
--- /dev/null
+++ b/.github/workflows/hs.yml
@@ -0,0 +1,71 @@
+name: Haskell CI
+
+on:
+  push:
+    branches: [ main ]
+    paths: [ hs/** ]
+  pull_request:
+    branches: [ main ]
+    paths: [ hs/** ]
+
+  workflow_dispatch:
+
+jobs:
+  get-inputs:
+    uses: ephemient/aoc2024/.github/workflows/get-inputs.yml@main
+    secrets:
+      SESSION: ${{ secrets.SESSION }}
+
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: haskell-actions/setup@v2
+        id: setup
+        with:
+          ghc-version: 9.10.1
+      - run: |
+          cabal configure --enable-tests --enable-benchmarks
+          cabal build all --dry-run
+        working-directory: hs
+      - uses: actions/cache/restore@v4
+        id: cache
+        env:
+          key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
+        with:
+          path: ${{ steps.setup.outputs.cabal-store }}
+          key: ${{ env.key }}-plan-${{ hashFiles('hs/dist-newstyle/cache/plan.json') }}
+          restore-keys: ${{ env.key }}-
+      - run: cabal build all --only-dependencies
+        if: steps.cache.outputs.cache-hit != 'true'
+        working-directory: hs
+      - uses: actions/cache/save@v4
+        if: steps.cache.outputs.cache-hit != 'true'
+        with:
+          path: ${{ steps.setup.outputs.cabal-store }}
+          key: ${{ steps.cache.outputs.cache-primary-key }}
+      - id: build
+        run: |
+          cabal build all
+          echo "exe=$(cabal list-bin aoc2024)" >> $GITHUB_OUTPUT
+        working-directory: hs
+      - run: cabal test all --test-show-details=direct
+        working-directory: hs
+      - run: cabal check
+        working-directory: hs
+      - uses: actions/upload-artifact@v4
+        with:
+          name: aoc2024-hs
+          path: ${{ steps.build.outputs.exe }}
+
+  run:
+    needs: [ get-inputs, build ]
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/download-artifact@v4
+      - run: chmod +x aoc2024-hs/aoc2024
+      - run: aoc2024-hs/aoc2024
+        env:
+          AOC2024_DATADIR: inputs
diff --git a/README.md b/README.md
index 012db731..341ad8f9 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,7 @@
 # [Advent of Code 2024](https://adventofcode.com/2024)
 ### my answers
+
+Development occurs in language-specific directories:
+
+|[Haskell](hs) ![Haskell CI](https://github.com/ephemient/aoc2024/workflows/Haskell%20CI/badge.svg)|
+|--:|
diff --git a/hs/LICENSE b/hs/LICENSE
new file mode 100644
index 00000000..fb4c6d48
--- /dev/null
+++ b/hs/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2024, Daniel Lin
+
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of the copyright holder nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/hs/README.md b/hs/README.md
new file mode 100644
index 00000000..49a1478c
--- /dev/null
+++ b/hs/README.md
@@ -0,0 +1,44 @@
+# [Advent of Code 2024](https://adventofcode.com/2024)
+### my answers in [Haskell](https://www.haskell.org/) ![Haskell CI](https://github.com/ephemient/aoc2024/workflows/Haskell%20CI/badge.svg)
+
+This project builds with [The Haskell Cabal](https://www.haskell.org/cabal/).
+
+Setup:
+
+```sh
+curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
+ghcup install cabal latest
+ghcup install ghc 9.10.1
+cabal configure --with-compiler ghc-9.10 --enable-tests
+```
+
+Run the [Hspec](https://hspec.github.io/) test suite:
+
+```sh
+cabal test test:aoc2024-test
+```
+
+Run [criterion](http://www.serpentine.com/criterion/) benchmarks ([results online](https://ephemient.github.io/aoc2024/aoc2024-bench.html)):
+
+```sh
+cabal bench bench:aoc2024-bench
+```
+
+Print solutions for the inputs provided in local data files:
+
+```sh
+cabal run exe:aoc2024
+```
+
+Generate [Haddock](https://www.haskell.org/haddock/) API documentation:
+
+```sh
+cabal haddock lib:aoc2024
+```
+
+Run [hlint](https://github.com/ndmitchell/hlint) source code suggestions:
+
+```sh
+cabal install hlint
+hlint src test bench
+```
diff --git a/hs/aoc2024.cabal b/hs/aoc2024.cabal
new file mode 100644
index 00000000..092e22ed
--- /dev/null
+++ b/hs/aoc2024.cabal
@@ -0,0 +1,69 @@
+cabal-version:      3.0
+
+name:               aoc2024
+version:            0.1.0.0
+synopsis:
+    Please see the README on GitHub at <https://github.com/ephemient/aoc2024/blob/main/hs/README.md>
+homepage:           https://github.com/ephemient/aoc2024/tree/main/hs
+license:            BSD-3-Clause
+license-file:       LICENSE
+author:             Daniel Lin
+maintainer:         ephemient@gmail.com
+category:           None
+build-type:         Simple
+extra-source-files: README.md
+
+source-repository head
+    type:     git
+    location: https://github.com/ephemient/aoc2024.git
+    subdir:   hs
+
+library
+    other-modules:
+        Common
+    build-depends:
+        base ^>=4.20.0.0,
+        text ^>=2.1.1 
+    ghc-options: -Wall
+    hs-source-dirs:   src
+
+executable aoc2024
+    hs-source-dirs:   app
+    main-is:          Main.hs
+    c-sources:        app/cbits/main.c
+    build-depends:
+        aoc2024,
+        base ^>=4.20.0.0,
+        filepath ^>=1.5.2.0,
+        megaparsec ^>=9.7.0,
+        text ^>=2.1.1
+    ghc-options:      -no-hs-main -threaded -Wall
+    default-language: GHC2024
+
+test-suite aoc2024-test
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          Main.hs
+    build-depends:
+        aoc2024,
+        base ^>=4.20.0.0,
+        hspec ^>=2.11.10,
+        text ^>=2.1.1
+    build-tool-depends:
+        hspec-discover:hspec-discover ^>=2.11.10
+    ghc-options:      -threaded -rtsopts "-with-rtsopts=-N" -Wall
+    default-language: GHC2024
+
+benchmark aoc2024-bench
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   bench
+    main-is:          Main.hs
+    c-sources:        bench/cbits/main.c
+    ghc-options:      -no-hs-main -threaded
+    default-language: GHC2024
+    build-depends:
+        aoc2024,
+        base ^>=4.20.0.0,
+        criterion ^>=1.6.4.0,
+        filepath ^>=1.5.2.0,
+        text ^>=2.1.1
diff --git a/hs/app/Main.hs b/hs/app/Main.hs
new file mode 100644
index 00000000..b80d6771
--- /dev/null
+++ b/hs/app/Main.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE NondecreasingIndentation #-}
+module Main (main) where
+
+import Control.Monad (ap, when)
+import Data.Foldable (find)
+import Data.Function (on)
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import qualified Data.Text.IO as TIO (putStrLn, readFile)
+import Text.Megaparsec (errorBundlePretty)
+import System.Environment (getArgs, lookupEnv)
+import System.FilePath (combine)
+
+getDayInput :: Int -> IO Text
+getDayInput i = do
+    dataDir <- fromMaybe "." . find (not . null) <$> lookupEnv "AOC2024_DATADIR"
+    TIO.readFile . combine dataDir $ "day" ++ show i ++ ".txt"
+
+run :: Int -> (a -> IO ()) -> [Text -> a] -> IO ()
+run = run' `ap` show
+
+run' :: Int -> String -> (a -> IO ()) -> [Text -> a] -> IO ()
+run' day name showIO funcs = do
+    args <- getArgs
+    when (null args || name `elem` args) $ do
+    putStrLn $ "Day " ++ name
+    contents <- getDayInput day
+    mapM_ (showIO . ($ contents)) funcs
+    putStrLn ""
+
+main :: IO ()
+main = do
+    pure ()
diff --git a/hs/app/cbits/main.c b/hs/app/cbits/main.c
new file mode 100644
index 00000000..b17921d3
--- /dev/null
+++ b/hs/app/cbits/main.c
@@ -0,0 +1,25 @@
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include "HsFFI.h"
+#include "Rts.h"
+
+extern StgClosure ZCMain_main_closure;
+
+static int envRtsMsgFunction(const char *s, va_list ap) {
+    const char *trace = getenv("TRACE");
+    if (trace == NULL || trace[0] != '0') {
+        return rtsDebugMsgFn(s, ap);
+    }
+    return 0;
+}
+
+int main(int argc, char *argv[]) {
+    RtsConfig rtsConfig = defaultRtsConfig;
+    rtsConfig.rts_opts_enabled = RtsOptsAll;
+    rtsConfig.rts_opts_suggestions = true;
+    rtsConfig.rts_opts = "-N";
+    rtsConfig.rts_hs_main = true;
+    debugMsgFn = envRtsMsgFunction;
+    return hs_main(argc, argv, &ZCMain_main_closure, rtsConfig);
+}
diff --git a/hs/bench/Main.hs b/hs/bench/Main.hs
new file mode 100644
index 00000000..e87cdd7c
--- /dev/null
+++ b/hs/bench/Main.hs
@@ -0,0 +1,26 @@
+module Main (main) where
+
+import Control.Arrow ((>>>))
+import Criterion.Main (bench, bgroup, defaultMain, env, envWithCleanup, nf)
+import Data.Foldable (find)
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import qualified Data.Text.IO as TIO (readFile)
+import System.Environment.Blank (getEnv, setEnv, unsetEnv)
+import System.FilePath (combine)
+
+setTrace :: String -> IO (Maybe String)
+setTrace value = getEnv "TRACE" <* setEnv "TRACE" value True
+
+unsetTrace :: Maybe String -> IO ()
+unsetTrace = maybe (unsetEnv "TRACE") (setEnv "TRACE" `flip` True)
+
+getDayInput :: Int -> IO Text
+getDayInput i = do
+    dataDir <- fromMaybe "." . find (not . null) <$> getEnv "AOC2024_DATADIR"
+    TIO.readFile . combine dataDir $ "day" ++ show i ++ ".txt"
+
+main :: IO ()
+main = defaultMain
+  [
+  ]
diff --git a/hs/bench/cbits/main.c b/hs/bench/cbits/main.c
new file mode 100644
index 00000000..b17921d3
--- /dev/null
+++ b/hs/bench/cbits/main.c
@@ -0,0 +1,25 @@
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include "HsFFI.h"
+#include "Rts.h"
+
+extern StgClosure ZCMain_main_closure;
+
+static int envRtsMsgFunction(const char *s, va_list ap) {
+    const char *trace = getenv("TRACE");
+    if (trace == NULL || trace[0] != '0') {
+        return rtsDebugMsgFn(s, ap);
+    }
+    return 0;
+}
+
+int main(int argc, char *argv[]) {
+    RtsConfig rtsConfig = defaultRtsConfig;
+    rtsConfig.rts_opts_enabled = RtsOptsAll;
+    rtsConfig.rts_opts_suggestions = true;
+    rtsConfig.rts_opts = "-N";
+    rtsConfig.rts_hs_main = true;
+    debugMsgFn = envRtsMsgFunction;
+    return hs_main(argc, argv, &ZCMain_main_closure, rtsConfig);
+}
diff --git a/hs/cabal.project b/hs/cabal.project
new file mode 100644
index 00000000..84098b61
--- /dev/null
+++ b/hs/cabal.project
@@ -0,0 +1,2 @@
+packages: .
+with-compiler: ghc-9.10
diff --git a/hs/src/Common.hs b/hs/src/Common.hs
new file mode 100644
index 00000000..a188b81e
--- /dev/null
+++ b/hs/src/Common.hs
@@ -0,0 +1,24 @@
+module Common (readEntire, readMany, readSome) where
+
+import Control.Arrow (first)
+import Data.Char (isSpace)
+import Data.List.NonEmpty (NonEmpty((:|)))
+import Data.Text (Text)
+import qualified Data.Text as T (dropWhile, null)
+import Data.Text.Read (Reader)
+
+readEntire :: Reader a -> Text -> Either String a
+readEntire reader input = do
+    (a, t) <- reader input
+    if T.null t then Right a else Left "incomplete read"
+
+readMany :: Reader a -> Reader [a]
+readMany reader = pure . readMany' id where
+    readMany' k input =
+        either (const (k [], input)) (uncurry $ readMany' . (.) k . (:)) .  reader $
+        T.dropWhile isSpace input
+
+readSome :: Reader a -> Reader (NonEmpty a)
+readSome reader input = do
+    (a, input') <- reader input
+    first (a :|) <$> readMany reader input'
diff --git a/hs/test/Main.hs b/hs/test/Main.hs
new file mode 100644
index 00000000..a824f8c3
--- /dev/null
+++ b/hs/test/Main.hs
@@ -0,0 +1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}