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 #-}