From 99ccae276ff5509737545b0cbaabe0b4e8f242a7 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Mon, 13 Nov 2023 17:32:44 -0500 Subject: [PATCH] Update to latest glint and gleam versions (#7) * refactor: update glint and make relevant modifications * testing with glint 0.12.0-rc3 * wip: refactoring commands * Deps * use glint 0.12.0-rc6 * update to gleam 0.32.4 * switch to task.await_forever and task.await --- .github/workflows/main.yml | 2 - .gitignore | 1 + .tool-versions | 2 +- README.md | 22 +- gleam.toml | 7 +- manifest.toml | 29 +-- src/cmd.gleam | 77 ------- src/cmd/new.gleam | 149 ------------- src/cmd/run.gleam | 252 --------------------- src/ffi/file.gleam | 13 -- src/gladvent.gleam | 38 ++-- src/gladvent/internal/cmd.gleam | 105 +++++++++ src/gladvent/internal/cmd/new.gleam | 184 ++++++++++++++++ src/gladvent/internal/cmd/run.gleam | 277 ++++++++++++++++++++++++ src/gladvent/internal/file.gleam | 34 +++ src/{ => gladvent/internal}/parse.gleam | 6 +- src/gladvent/internal/runners.gleam | 86 ++++++++ src/gladvent_ffi.erl | 6 +- src/runners.gleam | 88 -------- test/parse_test.gleam | 2 +- 20 files changed, 745 insertions(+), 635 deletions(-) delete mode 100644 src/cmd.gleam delete mode 100644 src/cmd/new.gleam delete mode 100644 src/cmd/run.gleam delete mode 100644 src/ffi/file.gleam create mode 100644 src/gladvent/internal/cmd.gleam create mode 100644 src/gladvent/internal/cmd/new.gleam create mode 100644 src/gladvent/internal/cmd/run.gleam create mode 100644 src/gladvent/internal/file.gleam rename src/{ => gladvent/internal}/parse.gleam (85%) create mode 100644 src/gladvent/internal/runners.gleam delete mode 100644 src/runners.gleam diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d4601d0..47e5cc1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,11 +7,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - gleam: ["0.25.0"] erlang: ["24.2"] steps: - uses: actions/checkout@v2 - uses: ./.github/actions/test with: - gleam-version: ${{ matrix.gleam }} erlang-version: ${{ matrix.erlang }} diff --git a/.gitignore b/.gitignore index dd829e1..1a77d1f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ logs rebar3.crashdump build src/days +src/aoc_* input diff --git a/.tool-versions b/.tool-versions index 4ae8ce2..c2a7d21 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -gleam 0.25.0 \ No newline at end of file +gleam 0.32.4 \ No newline at end of file diff --git a/README.md b/README.md index f1a63f1..18e63d4 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,14 @@ To add this library to your project run: `gleam add gladvent` and add `import gl ## Using the library -This library provides 2 options to run your advent of code solvers: +This library provides 3 options to run your advent of code solvers: +1. The easiest way: call it via `gleam run -m gladvent [ARGS]`, not requiring a custom `main()` function. 1. The easy way: simply add `gladvent.main()` to the end of your project's `main` function. -2. Create your own `Map(Int, #(fn(String) -> Dynamic, fn(String) -> Dynamic))` and pass it to `gladvent.execute` + +## Multi-year support + +Gladvent now comes with out-of-the-box multi-year support via the `--year` flag when running it. For convenience it defaults to the current year. Therefore, passing the `--year=YEAR`flag to either the`run`, `run all`or`new` commands will use the year specified or the current year if the flag was not provided. ## Available commands @@ -44,10 +48,8 @@ This project provides your application with 2 commands, `new` and `run`: _Note:_ -- due to how `gladvent` works, the `pt_1` and `pt_2` functions only need to return `Dynamic` when directly building a `RunnerMap` and using `gladvent.execute`, when using `gladvent.main` they can return anything. -- the `new` command creates source files in `src/days/` and input files in the `input/` directory. -- the `run` command expects input files to be in the `input/` directory. -- using `gladvent.main` expects gleam day runners to be in `src/days/` +- the `new` command creates source files in `src/aoc_/` and input files in the `input/` directory. +- the `run` command expects input files to be in the `input/` directory, and code to be in `src/aoc_/` - any triggered `assert` will be captured and printed, for example: `error: assert - Assertion pattern match failed in module days/day_1 in function pt_1 at line 2 with value 2` - any message in a `todo` will be captured and printed, for example: `error: todo - test in module days/day_1 in function pt_2 at line 7` @@ -64,10 +66,10 @@ Where X is the day you'd like to add (when using `gladvent.main()`): _Note:_ this method requires all day solutions be in `src/days/` with filenames `day_X.gleam`, each solution module containing `fn pt_1(String) -> Int` and a `fn pt_2(String) -> Int` -1. run `gleam run new X` -2. add your input to `input/day_X.txt` -3. add your code to `src/days/day_X.gleam` -4. run `gleam run run X` +1. run `gleam run -m gladvent run new X` +2. add your input to `input//day_X.txt` +3. add your code to `src/aoc_/day_X.gleam` +4. run `gleam run -m gladvent run X` ## FAQ diff --git a/gleam.toml b/gleam.toml index e295e46..db39773 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,15 +1,18 @@ name = "gladvent" -version = "0.5.1" +version = "0.6.0" repository = { type = "github", user = "TanklesXL", repo = "gladvent" } description = "An Advent Of Code runner for gleam" licences = ["Apache-2.0"] +internal_modules = ["gladvent/internal/*", "aoc_*"] +gleam = ">= 0.32.0" [dependencies] gleam_stdlib = "~> 0.19" gleam_otp = "~> 0.4" gleam_erlang = "~> 0.7" snag = "~> 0.2" -glint = "~> 0.8" +glint = " ~> 0.12" +simplifile = "~> 0.3" [dev-dependencies] gleeunit = "~> 0.6" diff --git a/manifest.toml b/manifest.toml index 7be8a8c..3e92afe 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,19 +2,22 @@ # You typically do not need to edit this file packages = [ - { name = "gleam_erlang", version = "0.17.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A3BB3D4A6AFC2E34CAB1A4960F0CBBC4AA1A052D5023477D16B848D86E69948A" }, - { name = "gleam_otp", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "24B88BF1D5B8DEC2525C00ECB65B96D2FD4DC66D8B2BB4D7AD4D12B2CE2A9988" }, - { name = "gleam_stdlib", version = "0.25.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AD0F89928E0B919C8F8EDF640484633B28DBF88630A9E6AE504617A3E3E5B9A2" }, - { name = "gleeunit", version = "0.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "5F4FBED3E93CDEDB0570D30E9DECB7058C2D327996B78BB2D245C739C7136010" }, - { name = "glint", version = "0.10.0", build_tools = ["gleam"], requirements = ["shellout", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "540D6435A0CE5C3660769B364CFF9BC98724DF3ED942FA80946A47C653B4ED02" }, - { name = "shellout", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "shellout", source = "hex", outer_checksum = "310662075CDCB6E3928B03E3FD371B1DDCD691E1C709E606AB02195E36675D4E" }, - { name = "snag", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "35C63E478782C58236F1050297C2FDF9806A4DD55C6FAF0B6EC5E54BC119342D" }, + { name = "gleam_community_ansi", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8B5A9677BC5A2738712BBAF2BA289B1D8195FDF962BBC769569976AD5E9794E1" }, + { name = "gleam_community_colour", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "036C206886AFB9F153C552700A7A0B4D2864E3BC96A20C77E5F34A013C051BE3" }, + { name = "gleam_erlang", version = "0.23.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "DA7A8E5540948DE10EB01B530869F8FF2FF6CAD8CFDA87626CE6EF63EBBF87CB" }, + { name = "gleam_otp", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "18EF8242A5E54BA92F717C7222F03B3228AEE00D1F286D4C56C3E8C18AA2588E" }, + { name = "gleam_stdlib", version = "0.32.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "ABF00CDCCB66FABBCE351A50060964C4ACE798F95A0D78622C8A7DC838792577" }, + { name = "gleeunit", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "1397E5C4AC4108769EE979939AC39BF7870659C5AFB714630DEEEE16B8272AD5" }, + { name = "glint", version = "0.12.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "799D6C76990EE1265396D7BEB5771AE902C3221AD79BA0B70A4FBADAEBAC1A1A" }, + { name = "simplifile", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "45E2C6C7FD8D931A660CA56880EC75186BB39C84F36951B4EE284F6F95E8F65D" }, + { name = "snag", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "8FD70D8FB3728E08AC425283BB509BB0F012BE1AE218424A597CDE001B0EE589" }, ] [requirements] -gleam_erlang = "~> 0.7" -gleam_otp = "~> 0.4" -gleam_stdlib = "~> 0.19" -gleeunit = "~> 0.6" -glint = "~> 0.8" -snag = "~> 0.2" +gleam_erlang = { version = "~> 0.7" } +gleam_otp = { version = "~> 0.4" } +gleam_stdlib = { version = "~> 0.19" } +gleeunit = { version = "~> 0.6" } +glint = { version = " ~> 0.12" } +simplifile = { version = "~> 0.3" } +snag = { version = "~> 0.2" } diff --git a/src/cmd.gleam b/src/cmd.gleam deleted file mode 100644 index c654cc4..0000000 --- a/src/cmd.gleam +++ /dev/null @@ -1,77 +0,0 @@ -import gleam/iterator -import gleam/result -import parse.{Day} -import gleam/otp/task.{Task} -import gleam/erlang -import gleam/pair -import gleam/list -import gleam/int -import gleam/string - -pub type Timing { - Endless - Ending(Timeout) -} - -pub type Timeout = - Int - -pub fn exec( - days: List(Day), - timing: Timing, - do: fn(Day) -> Result(a, b), - other: fn(String) -> b, - collect: fn(#(Day, Result(a, b))) -> String, -) -> List(String) { - days - |> task_map(do) - |> try_await_many(timing) - |> iterator.from_list() - |> iterator.map(fn(x) { - x - |> pair.map_second(result.map_error(_, other)) - |> pair.map_second(result.flatten) - }) - |> iterator.map(collect) - |> iterator.to_list() -} - -fn now_ms() { - erlang.system_time(erlang.Millisecond) -} - -fn task_map(over l: List(a), with f: fn(a) -> b) -> List(#(a, Task(b))) { - use x <- list.map(l) - #(x, task.async(fn() { f(x) })) -} - -fn try_await_many( - tasks: List(#(x, Task(a))), - timing: Timing, -) -> List(#(x, Result(a, String))) { - case timing { - Endless -> { - use tup <- list.map(tasks) - use t <- pair.map_second(tup) - task.try_await_forever(t) - |> result.map_error(await_err_to_string) - } - - Ending(timeout) -> { - let end = now_ms() + timeout - use tup <- list.map(tasks) - use t <- pair.map_second(tup) - end - now_ms() - |> int.clamp(min: 0, max: timeout) - |> task.try_await(t, _) - |> result.map_error(await_err_to_string) - } - } -} - -fn await_err_to_string(err: task.AwaitError) -> String { - case err { - task.Timeout -> "task timed out" - task.Exit(s) -> "task exited for some reason: " <> string.inspect(s) - } -} diff --git a/src/cmd/new.gleam b/src/cmd/new.gleam deleted file mode 100644 index d9e695a..0000000 --- a/src/cmd/new.gleam +++ /dev/null @@ -1,149 +0,0 @@ -import gleam/int -import gleam/result -import gleam/list -import gleam/string -import snag.{Snag} -import ffi/file -import runners.{days_dir, input_dir} -import gleam/erlang/file as efile -import cmd -import glint.{CommandInput} -import parse.{Day} - -type Err { - FailedToCreateDir(String) - FailedToCreateFile(String) - FileAlreadyExists(String) - Combo(Err, Err) - Other(String) -} - -fn input_path(day: Day) -> String { - string.concat([input_dir, "day_", int.to_string(day), ".txt"]) -} - -fn gleam_src_path(day: Day) -> String { - string.concat([days_dir, "day_", int.to_string(day), ".gleam"]) -} - -fn create_dir(dir: String) -> Result(Nil, Err) { - dir - |> efile.make_directory() - |> handle_dir_open_res(dir) -} - -fn handle_dir_open_res( - res: Result(Nil, efile.Reason), - filename: String, -) -> Result(Nil, Err) { - case res { - Ok(Nil) | Error(efile.Eexist) -> Ok(Nil) - _ -> - filename - |> FailedToCreateDir - |> Error - } -} - -fn create_files(day: Day) -> snag.Result(Nil) { - let input_path = input_path(day) - let gleam_src_path = gleam_src_path(day) - - let create_src_res = - file.open_file_exclusive(gleam_src_path) - |> result.then(file.write(_, gleam_starter)) - |> result.map_error(handle_file_open_failure(_, gleam_src_path)) - - let create_input_res = - file.open_file_exclusive(input_path) - |> result.map_error(handle_file_open_failure(_, input_path)) - - case create_input_res, create_src_res { - Ok(_), Ok(_) -> Ok(Nil) - Error(e1), Ok(_) -> - Error( - ["created ", gleam_src_path, ", but failed to create ", input_path] - |> string.concat - |> snag.layer(to_snag(e1), _), - ) - Ok(_), Error(e2) -> - Error( - ["created ", input_path, ", but failed to create ", gleam_src_path] - |> string.concat - |> snag.layer(to_snag(e2), _), - ) - Error(e1), Error(e2) -> - Error( - Combo(e1, e2) - |> to_snag, - ) - } -} - -fn handle_file_open_failure(reason: efile.Reason, filename: String) -> Err { - case reason { - efile.Eexist -> FileAlreadyExists(filename) - _ -> FailedToCreateFile(filename) - } -} - -fn do(day: Day) -> snag.Result(Nil) { - try _ = - list.try_map([input_dir, days_dir], create_dir) - |> result.map_error(to_snag) - - create_files(day) -} - -const gleam_starter = "pub fn pt_1(input: String) { - todo -} - -pub fn pt_2(input: String) { - todo -} -" - -fn collect(x: #(Day, snag.Result(Nil))) -> String { - let day = int.to_string(x.0) - case - x.1 - |> snag.context("error occurred when initializing day " <> day) - |> result.map_error(snag.pretty_print) - { - Ok(_) -> "initialized day: " <> day - Error(reason) -> reason - } -} - -pub fn new_command() { - glint.Stub( - path: ["new"], - run: run, - flags: [], - description: "Create .gleam and input files", - ) -} - -fn run(input: CommandInput) -> snag.Result(List(String)) { - input.args - |> parse.days - |> snag.context(string.join(["failed to initialize:", ..input.args], " ")) - |> result.map(cmd.exec(_, cmd.Endless, do, snag.new, collect)) -} - -fn to_snag(e: Err) -> Snag { - case e { - FailedToCreateDir(d) -> "failed to create dir: " <> d - FailedToCreateFile(f) -> "failed to create file: " <> f - FileAlreadyExists(f) -> "file already exists: " <> f - Combo(e1, e2) -> - [e1, e2] - |> list.map(to_snag) - |> list.map(snag.line_print) - |> list.filter(fn(s) { s != "" }) - |> string.join(" && ") - Other(s) -> s - } - |> snag.new -} diff --git a/src/cmd/run.gleam b/src/cmd/run.gleam deleted file mode 100644 index d84fa1d..0000000 --- a/src/cmd/run.gleam +++ /dev/null @@ -1,252 +0,0 @@ -import gleam/int -import gleam/list -import gleam/result -import gleam/string -import snag.{Result, Snag} -import gleam/erlang/file -import gleam/erlang -import gleam/erlang/charlist.{Charlist} -import gleam/erlang/atom -import parse.{Day} -import gleam/map -import cmd.{Ending, Endless} -import glint.{CommandInput} -import glint/flag -import gleam -import runners.{RunnerMap} -import gleam/dynamic.{Dynamic} -import gleam/option.{None, Option} - -type SolveErr { - Undef - RunFailed(String) -} - -type Err { - FailedToReadInput(String) - Unregistered(Day) - Other(String) -} - -fn err_to_snag(err: Err) -> Snag { - case err { - Unregistered(day) -> - "day" <> " " <> int.to_string(day) <> " " <> "unregistered" - FailedToReadInput(input_path) -> "failed to read input file: " <> input_path - Other(s) -> s - } - |> snag.new -} - -type RunResult = - gleam.Result(Dynamic, SolveErr) - -type Direction { - // Leading - // Trailing - Both -} - -fn string_trim(s: String, dir: Direction, sub: String) -> String { - do_trim(s, dir, charlist.from_string(sub)) -} - -external fn do_trim(String, Direction, Charlist) -> String = - "string" "trim" - -fn do( - day: Day, - runners: RunnerMap, - allow_crash: Bool, -) -> gleam.Result(#(RunResult, RunResult), Err) { - try #(pt_1, pt_2) = - map.get(runners, day) - |> result.replace_error(Unregistered(day)) - - let input_path = string.join(["input/day_", int.to_string(day), ".txt"], "") - - try input = - input_path - |> file.read() - |> result.map(string_trim(_, Both, "\n")) - |> result.replace_error(FailedToReadInput(input_path)) - - case allow_crash { - True -> Ok(#(Ok(pt_1(input)), Ok(pt_2(input)))) - False -> { - let pt_1 = - fn() { pt_1(input) } - |> erlang.rescue - |> result.map_error(run_err_to_string) - let pt_2 = - fn() { pt_2(input) } - |> erlang.rescue - |> result.map_error(run_err_to_string) - Ok(#(pt_1, pt_2)) - } - } -} - -fn crash_to_dyn(err: erlang.Crash) -> dynamic.Dynamic { - case err { - erlang.Errored(dyn) | erlang.Exited(dyn) | erlang.Thrown(dyn) -> dyn - } -} - -type GleamErr { - GleamErr( - gleam_error: atom.Atom, - module: String, - function: String, - line: Int, - message: String, - value: Option(Dynamic), - ) -} - -fn decode_gleam_err() { - dynamic.decode6( - GleamErr, - dynamic.field(atom.create_from_string("gleam_error"), atom.from_dynamic), - dynamic.field(atom.create_from_string("module"), dynamic.string), - dynamic.field(atom.create_from_string("function"), dynamic.string), - dynamic.field(atom.create_from_string("line"), dynamic.int), - dynamic.field(atom.create_from_string("message"), dynamic.string), - dynamic.any([ - dynamic.field( - atom.create_from_string("value"), - dynamic.optional(dynamic.dynamic), - ), - fn(_) { Ok(None) }, - ]), - ) -} - -fn gleam_err_to_string(g: GleamErr) -> String { - string.join( - [ - "error:", - atom.to_string(g.gleam_error), - "-", - g.message, - "in module", - g.module, - "in function", - g.function, - "at line", - int.to_string(g.line), - g.value - |> option.map(fn(val) { "with value " <> string.inspect(val) }) - |> option.unwrap(""), - ], - " ", - ) -} - -fn run_err_to_string(err: erlang.Crash) -> SolveErr { - let dyn = crash_to_dyn(err) - decode_gleam_err()(dyn) - |> result.map(gleam_err_to_string) - |> result.lazy_unwrap(fn() { - "run failed for some reason: " <> string.inspect(err) - }) - |> RunFailed -} - -fn run_res_to_string(res: RunResult) -> String { - case res { - Ok(res) -> string.inspect(res) - Error(err) -> - case err { - Undef -> "function undefined" - RunFailed(s) -> s - } - } -} - -fn collect(x: #(Day, gleam.Result(#(RunResult, RunResult), Err))) -> String { - let day = int.to_string(x.0) - case x.1 { - Ok(#(res_1, res_2)) -> - "Ran day " <> day <> ":\n" <> " Part 1: " <> run_res_to_string(res_1) <> "\n" <> " Part 2: " <> run_res_to_string( - res_2, - ) - - Error(err) -> - err - |> err_to_snag - |> snag.layer(string.append("failed to run day ", day)) - |> snag.pretty_print() - } -} - -// ----- CLI ----- - -fn timeout_flag() { - flag.int("timeout", 0, "Run with specified timeout") -} - -fn allow_crash_flag() { - flag.bool("allow-crash", False, "Don't catch exceptions thrown by runners") -} - -pub fn run_command(runners: RunnerMap) -> glint.Stub(Result(List(String))) { - glint.Stub( - path: ["run"], - run: run(_, runners, False), - flags: [timeout_flag(), allow_crash_flag()], - description: "Run the specified days", - ) -} - -pub fn run_all_command(runners: RunnerMap) -> glint.Stub(Result(List(String))) { - glint.Stub( - path: ["run", "all"], - run: run(_, runners, True), - flags: [timeout_flag(), allow_crash_flag()], - description: "Run all registered days", - ) -} - -fn run( - input: CommandInput, - runners: RunnerMap, - run_all: Bool, -) -> Result(List(String)) { - assert Ok(flag.I(timeout)) = flag.get(input.flags, timeout_flag().0) - assert Ok(flag.B(allow_crash)) = flag.get(input.flags, allow_crash_flag().0) - - try timing = case timeout { - 0 -> Ok(Endless) - _ if timeout < 0 -> invalid_timeout_err(timeout) - _ -> Ok(Ending(timeout)) - } - - try days = case run_all { - True -> - runners - |> map.keys() - |> list.sort(by: int.compare) - |> Ok - - False -> - input.args - |> parse.days() - |> wrap_failed_to_parse_err(input.args) - } - - days - |> cmd.exec(timing, do(_, runners, allow_crash), Other, collect) - |> Ok -} - -fn invalid_timeout_err(timeout: Int) -> Result(a) { - ["invalid timeout value ", "'", int.to_string(timeout), "'"] - |> string.concat() - |> snag.error() - |> snag.context("timeout must be greater than or equal to 1 ms") -} - -fn wrap_failed_to_parse_err(res: Result(a), args: List(String)) -> Result(a) { - snag.context(res, string.join(["failed to parse arguments:", ..args], " ")) -} diff --git a/src/ffi/file.gleam b/src/ffi/file.gleam deleted file mode 100644 index ed4e35d..0000000 --- a/src/ffi/file.gleam +++ /dev/null @@ -1,13 +0,0 @@ -import gleam/erlang/file.{Reason} - -pub external type IODevice - -pub external fn open_file_exclusive(s: String) -> Result(IODevice, Reason) = - "gladvent_ffi" "open_file_exclusive" - -external fn do_write(IODevice, String) -> Result(Nil, Reason) = - "gladvent_ffi" "write" - -pub fn write(iod: IODevice, s: String) -> Result(Nil, Reason) { - do_write(iod, s) -} diff --git a/src/gladvent.gleam b/src/gladvent.gleam index 7d96d5b..eb26713 100644 --- a/src/gladvent.gleam +++ b/src/gladvent.gleam @@ -1,9 +1,9 @@ import gleam/string import gleam/io import gleam/erlang.{start_arguments as args} -import runners.{RunnerMap} -import cmd/run -import cmd/new +import gladvent/internal/cmd/run +import gladvent/internal/cmd/new +import gladvent/internal/cmd import glint import snag @@ -11,34 +11,26 @@ import snag /// run either the 'run' or 'new' command as specified /// pub fn main() { - case runners.build_from_days_dir() { - Ok(runners) -> execute(given: runners) - Error(err) -> print_snag_and_halt(err) - } -} - -/// Given the daily runners, create the command tree and run the specified command -/// -pub fn execute(given runners: RunnerMap) { let commands = glint.new() - |> glint.with_pretty_help(glint.default_pretty_help) - |> glint.add_command_from_stub(new.new_command()) - |> glint.add_command_from_stub(run.run_command(runners)) - |> glint.add_command_from_stub(run.run_all_command(runners)) + |> glint.global_flag(cmd.year, cmd.year_flag()) + |> glint.with_pretty_help(glint.default_pretty_help()) + |> glint.add(["new"], new.new_command()) + |> glint.add(["run"], run.run_command()) + |> glint.add(["run", "all"], run.run_all_command()) - case glint.execute(commands, args()) { - Ok(glint.Out(Ok(output))) -> - output + use out <- glint.run_and_handle(commands, args()) + case out { + Ok(out) -> + out |> string.join("\n\n") |> io.println - Ok(glint.Help(help)) -> io.println(help) - Ok(glint.Out(Error(err))) | Error(err) -> print_snag_and_halt(err) + Error(err) -> print_snag_and_halt(err) } } -external fn exit(Int) -> Nil = - "erlang" "halt" +@external(erlang, "erlang", "halt") +fn exit(a: Int) -> Nil fn print_snag_and_halt(err: snag.Snag) -> Nil { err diff --git a/src/gladvent/internal/cmd.gleam b/src/gladvent/internal/cmd.gleam new file mode 100644 index 0000000..4d248b2 --- /dev/null +++ b/src/gladvent/internal/cmd.gleam @@ -0,0 +1,105 @@ +import gleam/result +import gladvent/internal/parse.{type Day} +import gleam/otp/task.{type Task} +import gleam/erlang +import gleam/pair +import gleam/list +import gleam/int +import gleam/string +import glint/flag +import snag + +pub fn input_dir(year) { + input_root <> int.to_string(year) <> "/" +} + +pub const input_root = "input/" + +pub const src_root = "src/" + +pub fn src_dir(year) { + src_root <> "aoc_" <> int.to_string(year) <> "/" +} + +pub type Timing { + Endless + Ending(Timeout) +} + +pub type Timeout = + Int + +pub type Year = + Int + +pub fn exec( + days: List(Day), + timing: Timing, + do: fn(Day) -> a, + collect: fn(#(Day, Result(a, String))) -> c, +) -> List(c) { + days + |> task_map(do) + |> try_await_many(timing) + |> list.map(collect) +} + +fn now_ms() { + erlang.system_time(erlang.Millisecond) +} + +fn task_map(over l: List(a), with f: fn(a) -> b) -> List(#(a, Task(b))) { + use x <- list.map(l) + #(x, task.async(fn() { f(x) })) +} + +fn try_await_many( + tasks: List(#(x, Task(a))), + timing: Timing, +) -> List(#(x, Result(a, String))) { + case timing { + Endless -> { + use tup <- list.map(tasks) + use t <- pair.map_second(tup) + Ok(task.await_forever(t)) + } + + Ending(timeout) -> { + let end = now_ms() + timeout + use tup <- list.map(tasks) + use t <- pair.map_second(tup) + let res = + end - now_ms() + |> int.clamp(min: 0, max: timeout) + |> task.try_await(t, _) + use err <- result.map_error(res) + case err { + task.Timeout -> "task timed out" + task.Exit(s) -> "task exited for some reason: " <> string.inspect(s) + } + } + } +} + +@external(erlang, "erlang", "localtime") +fn date() -> #(#(Int, Int, Int), #(Int, Int, Int)) + +fn current_year() -> Int { + { date().0 }.0 +} + +pub const year = "year" + +pub fn year_flag() { + flag.int() + |> flag.default(current_year()) + |> flag.constraint(fn(year) { + case year < 2015 { + True -> + snag.error( + "advent of code did not exist prior to 2015, did you mistype?", + ) + False -> Ok(Nil) + } + }) +} diff --git a/src/gladvent/internal/cmd/new.gleam b/src/gladvent/internal/cmd/new.gleam new file mode 100644 index 0000000..d2f43a4 --- /dev/null +++ b/src/gladvent/internal/cmd/new.gleam @@ -0,0 +1,184 @@ +import gleam/int +import gleam/result +import gleam/list +import gleam/string +import gladvent/internal/file +import simplifile as efile +import gladvent/internal/cmd +import glint +import glint/flag +import gladvent/internal/parse.{type Day} +import gleam/pair + +type Context { + Context(year: Int, day: Day) +} + +fn create_src_dir(ctx: Context) { + ctx.year + |> cmd.src_dir() + |> create_dir +} + +fn create_src_file(ctx: Context) { + let gleam_src_path = gleam_src_path(ctx.year, ctx.day) + + gleam_src_path + |> file.do_with_file(file.write(_, gleam_starter)) + |> result.flatten + |> result.map_error(handle_file_open_failure(_, gleam_src_path)) + |> result.replace(gleam_src_path) +} + +fn create_input_root(_ctx: Context) { + cmd.input_root + |> create_dir +} + +fn create_input_dir(ctx: Context) { + ctx.year + |> cmd.input_dir + |> create_dir +} + +fn create_input_file(ctx: Context) { + let input_path = input_path(ctx.year, ctx.day) + + file.do_with_file(input_path, fn(_) { Nil }) + |> result.map_error(handle_file_open_failure(_, input_path)) + |> result.replace(input_path) +} + +type Err { + FailedToCreateDir(String) + FailedToCreateFile(String) + FileAlreadyExists(String) +} + +fn err_to_string(e: Err) -> String { + case e { + FailedToCreateDir(d) -> "failed to create dir: " <> d + FailedToCreateFile(f) -> "failed to create file: " <> f + FileAlreadyExists(f) -> "file already exists: " <> f + } +} + +fn input_path(year: Int, day: Day) -> String { + cmd.input_dir(year) <> int.to_string(day) <> ".txt" +} + +fn gleam_src_path(year: Int, day: Day) -> String { + cmd.src_dir(year) <> "day_" <> int.to_string(day) <> ".gleam" +} + +fn create_dir(dir: String) -> Result(String, Err) { + efile.create_directory(dir) + |> handle_dir_open_res(dir) +} + +fn handle_dir_open_res( + res: Result(_, efile.FileError), + filename: String, +) -> Result(String, Err) { + case res { + Ok(_) -> Ok(filename) + Error(efile.Eexist) -> Ok("") + _ -> + filename + |> FailedToCreateDir + |> Error + } +} + +fn handle_file_open_failure(reason: efile.FileError, filename: String) -> Err { + case reason { + efile.Eexist -> FileAlreadyExists(filename) + _ -> FailedToCreateFile(filename) + } +} + +fn do(ctx: Context) -> String { + let seq = [ + create_input_root, + create_input_dir, + create_input_file, + create_src_dir, + create_src_file, + ] + + let successes = fn(good) { + case good { + "" -> "" + _ -> "created:" <> good + } + } + + let errors = fn(errs) { + case errs { + "" -> "" + _ -> "errors:" <> errs + } + } + + let newline_tab = fn(a, b) { a <> "\n\t" <> b } + + let #(good, bad) = + { + use acc, f <- list.fold(seq, #("", "")) + case f(ctx) { + Ok("") -> acc + Ok(o) -> pair.map_first(acc, newline_tab(_, o)) + Error(err) -> pair.map_second(acc, newline_tab(_, err_to_string(err))) + } + } + |> pair.map_first(successes) + |> pair.map_second(errors) + + [good, bad] + |> list.filter(fn(s) { s != "" }) + |> string.join("\n\n") +} + +const gleam_starter = "pub fn pt_1(input: String) { + todo +} + +pub fn pt_2(input: String) { + todo +} +" + +import gleam + +fn collect_async(year: Int, x: #(Day, Result(String, String))) -> String { + fn(res) { + case res { + Ok(res) -> res + Error(err) -> err + } + } + |> pair.map_second(x, _) + |> collect(year, _) +} + +fn collect(year: Int, x: #(Day, String)) -> String { + let day = int.to_string(x.0) + let year = int.to_string(year) + + "initialized " <> year <> " day " <> day <> "\n" <> x.1 +} + +pub fn new_command() { + { + use input <- glint.command() + use days <- result.map(parse.days(input.args)) + let assert Ok(year) = flag.get_int(input.flags, cmd.year) + cmd.exec( + days, + cmd.Endless, + fn(day) { do(Context(year, day)) }, + collect_async(year, _), + ) + } + |> glint.description("Create .gleam and input files") +} diff --git a/src/gladvent/internal/cmd/run.gleam b/src/gladvent/internal/cmd/run.gleam new file mode 100644 index 0000000..784348e --- /dev/null +++ b/src/gladvent/internal/cmd/run.gleam @@ -0,0 +1,277 @@ +import gleam/int +import gleam/list +import gleam/result +import gleam/string +import snag.{type Result, type Snag} +import simplifile +import gleam/erlang +import gleam/erlang/charlist.{type Charlist} +import gleam/erlang/atom +import gladvent/internal/parse.{type Day} +import gleam/map +import gladvent/internal/cmd.{Ending, Endless} +import glint +import glint/flag +import gleam +import gladvent/internal/runners.{type RunnerMap} +import gleam/dynamic.{type Dynamic} +import gleam/option.{type Option, None} + +type AsyncResult = + gleam.Result(RunResult, String) + +type RunErr { + FailedToReadInput(String) + Unregistered(Day) + Other(String) +} + +type RunResult = + gleam.Result(#(SolveResult, SolveResult), RunErr) + +type SolveErr { + Undef + RunFailed(String) +} + +type SolveResult = + gleam.Result(Dynamic, SolveErr) + +fn run_err_to_snag(err: RunErr) -> Snag { + case err { + Unregistered(day) -> + "day" <> " " <> int.to_string(day) <> " " <> "unregistered" + FailedToReadInput(input_path) -> "failed to read input file: " <> input_path + Other(s) -> s + } + |> snag.new +} + +type Direction { + // Leading + // Trailing + Both +} + +fn string_trim(s: String, dir: Direction, sub: String) -> String { + do_trim(s, dir, charlist.from_string(sub)) +} + +@external(erlang, "string", "trim") +fn do_trim(a: String, b: Direction, c: Charlist) -> String + +fn do(year: Int, day: Day, runners: RunnerMap, allow_crash: Bool) -> RunResult { + use #(pt_1, pt_2) <- result.then( + runners + |> map.get(day) + |> result.replace_error(Unregistered(day)), + ) + + let input_path = + "input/" <> int.to_string(year) <> "/" <> int.to_string(day) <> ".txt" + + use input <- result.then( + input_path + |> simplifile.read() + |> result.map(string_trim(_, Both, "\n")) + |> result.replace_error(FailedToReadInput(input_path)), + ) + + case allow_crash { + True -> Ok(#(Ok(pt_1(input)), Ok(pt_2(input)))) + False -> { + let pt_1 = + fn() { pt_1(input) } + |> erlang.rescue + |> result.map_error(crash_to_string) + let pt_2 = + fn() { pt_2(input) } + |> erlang.rescue + |> result.map_error(crash_to_string) + Ok(#(pt_1, pt_2)) + } + } +} + +fn crash_to_dyn(err: erlang.Crash) -> dynamic.Dynamic { + case err { + erlang.Errored(dyn) | erlang.Exited(dyn) | erlang.Thrown(dyn) -> dyn + } +} + +type GleamErr { + GleamErr( + gleam_error: atom.Atom, + module: String, + function: String, + line: Int, + message: String, + value: Option(Dynamic), + ) +} + +fn decode_gleam_err() { + dynamic.decode6( + GleamErr, + dynamic.field(atom.create_from_string("gleam_error"), atom.from_dynamic), + dynamic.field(atom.create_from_string("module"), dynamic.string), + dynamic.field(atom.create_from_string("function"), dynamic.string), + dynamic.field(atom.create_from_string("line"), dynamic.int), + dynamic.field(atom.create_from_string("message"), dynamic.string), + dynamic.any([ + dynamic.field( + atom.create_from_string("value"), + dynamic.optional(dynamic.dynamic), + ), + fn(_) { Ok(None) }, + ]), + ) +} + +fn gleam_err_to_string(g: GleamErr) -> String { + string.join( + [ + "error:", + atom.to_string(g.gleam_error), + "-", + g.message, + "in module", + g.module, + "in function", + g.function, + "at line", + int.to_string(g.line), + g.value + |> option.map(fn(val) { "with value " <> string.inspect(val) }) + |> option.unwrap(""), + ], + " ", + ) +} + +fn crash_to_string(err: erlang.Crash) -> SolveErr { + crash_to_dyn(err) + |> decode_gleam_err() + |> result.map(gleam_err_to_string) + |> result.lazy_unwrap(fn() { + "run failed for some reason: " <> string.inspect(err) + }) + |> RunFailed +} + +fn solve_err_to_string(solve_err: SolveErr) -> String { + case solve_err { + Undef -> "function undefined" + RunFailed(s) -> s + } +} + +fn solve_res_to_string(res: SolveResult) -> String { + case res { + Ok(res) -> string.inspect(res) + Error(err) -> solve_err_to_string(err) + } +} + +import gleam/pair + +fn collect_async(year: Int, x: #(Day, AsyncResult)) -> String { + x + |> pair.map_second(result.map_error(_, Other)) + |> pair.map_second(result.flatten) + |> collect(year, _) +} + +fn collect(year: Int, x: #(Day, RunResult)) -> String { + let day = int.to_string(x.0) + + case x.1 { + Ok(#(res_1, res_2)) -> + "Ran " <> int.to_string(year) <> " day " <> day <> ":\n" <> " Part 1: " <> solve_res_to_string( + res_1, + ) <> "\n" <> " Part 2: " <> solve_res_to_string(res_2) + + Error(err) -> + err + |> run_err_to_snag + |> snag.layer("Failed to run " <> int.to_string(year) <> " day " <> day) + |> snag.pretty_print() + } +} + +// ----- CLI ----- + +const timeout = "timeout" + +const allow_crash = "allow-crash" + +fn timeout_flag() { + flag.int() + |> flag.constraint(fn(i) { + case i > 0 { + True -> Ok(Nil) + False -> snag.error("timeout value must greater than zero") + } + }) + |> flag.description("Run with specified timeout") +} + +fn allow_crash_flag() { + flag.bool() + |> flag.default(False) + |> flag.description("Don't catch exceptions thrown by runners") +} + +pub fn run_command() -> glint.Command(Result(List(String))) { + { + use input <- glint.command() + let assert Ok(year) = flag.get_int(input.flags, cmd.year) + use runners <- result.then(runners.build_from_days_dir(year)) + use allow_crash <- result.try(flag.get_bool(input.flags, allow_crash)) + use days <- result.then(parse.days(input.args)) + + days + |> cmd.exec( + timing(input.flags), + do(year, _, runners, allow_crash), + collect_async(year, _), + ) + |> Ok + } + |> glint.flag(timeout, timeout_flag()) + |> glint.flag(allow_crash, allow_crash_flag()) + |> glint.description("Run the specified days") +} + +pub fn run_all_command() -> glint.Command(Result(List(String))) { + { + use input <- glint.command() + use allow_crash <- result.then(flag.get_bool(input.flags, allow_crash)) + let assert Ok(year) = flag.get_int(input.flags, cmd.year) + use runners <- result.then(runners.build_from_days_dir(year)) + + runners + |> all_days + |> cmd.exec( + timing(input.flags), + do(year, _, runners, allow_crash), + collect_async(year, _), + ) + |> Ok + } + |> glint.flag(timeout, timeout_flag()) + |> glint.flag(allow_crash, allow_crash_flag()) + |> glint.description("Run all registered days") +} + +fn timing(flags: flag.Map) { + flag.get_int(flags, timeout) + |> result.map(Ending) + |> result.unwrap(Endless) +} + +fn all_days(runners) { + runners + |> map.keys() + |> list.sort(by: int.compare) +} diff --git a/src/gladvent/internal/file.gleam b/src/gladvent/internal/file.gleam new file mode 100644 index 0000000..12cf2c8 --- /dev/null +++ b/src/gladvent/internal/file.gleam @@ -0,0 +1,34 @@ +import simplifile.{type FileError} +import gleam/result + +pub type IODevice + +@external(erlang, "gladvent_ffi", "open_file_exclusive") +pub fn open_file_exclusive(s s: String) -> Result(IODevice, FileError) + +@external(erlang, "gladvent_ffi", "write") +fn do_write(a: IODevice, b: String) -> Result(Nil, FileError) + +pub fn write(iod: IODevice, s: String) -> Result(Nil, FileError) { + do_write(iod, s) +} + +@external(erlang, "gladvent_ffi", "close_iodevice") +fn close_iodevice(a: IODevice) -> Result(Nil, FileError) + +pub fn do_with_file( + filename: String, + f: fn(IODevice) -> a, +) -> Result(a, FileError) { + use file <- result.map(open_file_exclusive(filename)) + use <- defer(do: fn() { + let assert Ok(Nil) = close_iodevice(file) + }) + f(file) +} + +fn defer(do later: fn() -> _, after now: fn() -> a) -> a { + let res = now() + later() + res +} diff --git a/src/parse.gleam b/src/gladvent/internal/parse.gleam similarity index 85% rename from src/parse.gleam rename to src/gladvent/internal/parse.gleam index 90b26f0..ecb923f 100644 --- a/src/parse.gleam +++ b/src/gladvent/internal/parse.gleam @@ -1,4 +1,4 @@ -import snag.{Result} +import snag.{type Result} import gleam/int import gleam/list import gleam/result @@ -13,12 +13,12 @@ pub fn int(s: String) -> Result(Int) { } pub fn day(s: String) -> Result(Day) { - try i = int(s) + use i <- result.then(int(s)) case i > 0 && i < 26 { True -> Ok(i) False -> - "invalid day value " <> "'" <> s <> "'" + { "invalid day value " <> "'" <> s <> "'" } |> snag.error |> snag.context("day must be an integer from 1 to 25") } diff --git a/src/gladvent/internal/runners.gleam b/src/gladvent/internal/runners.gleam new file mode 100644 index 0000000..3ef91f5 --- /dev/null +++ b/src/gladvent/internal/runners.gleam @@ -0,0 +1,86 @@ +import gleam/map.{type Map} +import gleam/erlang/atom.{type Atom} +import gleam/string +import snag.{type Result} +import gladvent/internal/parse.{type Day} +import gleam/list +import gleam/result +import gleam +import gleam/dynamic.{type Dynamic} +import gleam/int + +pub type PartRunner = + fn(String) -> Dynamic + +pub type DayRunner = + #(PartRunner, PartRunner) + +pub type RunnerMap = + Map(Day, DayRunner) + +@external(erlang, "gladvent_ffi", "find_files") +fn find_files(matching matching: String, in in: String) -> List(String) + +type Module = + Atom + +fn to_module_name(file: String) -> String { + file + |> string.replace(".gleam", "") + |> string.replace(".erl", "") + |> string.replace("/", "@") +} + +@external(erlang, "gladvent_ffi", "module_exists") +fn module_exists(a: Module) -> Bool + +@external(erlang, "gladvent_ffi", "function_arity_one_exists") +fn do_function_exists(a: Module, b: Atom) -> gleam.Result(PartRunner, Nil) + +fn function_exists( + year: Int, + filename: String, + mod: Atom, + func_name: String, +) -> Result(PartRunner) { + case module_exists(mod) { + False -> + ["module ", filename, " not found"] + |> string.concat + |> snag.error + True -> + func_name + |> atom.create_from_string + |> do_function_exists(mod, _) + |> result.replace_error(snag.new( + "module " <> "src/" <> int.to_string(year) <> "/" <> filename <> " does not export a function \"" <> func_name <> "/1\"", + )) + |> snag.context("function missing") + } +} + +fn get_runner(year: Int, filename: String) -> Result(#(Day, DayRunner)) { + use day <- result.then( + string.replace(filename, "day_", "") + |> string.replace(".gleam", "") + |> parse.day + |> snag.context(string.append("cannot create runner for ", filename)), + ) + + let module = + { "aoc_" <> int.to_string(year) <> "/" <> filename } + |> to_module_name + |> atom.create_from_string + + use pt_1 <- result.then(function_exists(year, filename, module, "pt_1")) + use pt_2 <- result.then(function_exists(year, filename, module, "pt_2")) + + Ok(#(day, #(pt_1, pt_2))) +} + +pub fn build_from_days_dir(year: Int) -> Result(Map(Day, DayRunner)) { + find_files(matching: "day_*.gleam", in: "src/aoc_" <> int.to_string(year)) + |> list.try_map(get_runner(year, _)) + |> result.map(map.from_list) + |> snag.context("failed to generate runners list from filesystem") +} diff --git a/src/gladvent_ffi.erl b/src/gladvent_ffi.erl index 1acd0ea..efb6e0c 100644 --- a/src/gladvent_ffi.erl +++ b/src/gladvent_ffi.erl @@ -6,9 +6,13 @@ write/2, ensure_dir/1, function_arity_one_exists/2, - module_exists/1 + module_exists/1, + close_iodevice/1 ]). +close_iodevice(IoDevice) -> + to_gleam_result(file:close(IoDevice)). + find_files(Pattern, In) -> Results = filelib:wildcard(binary_to_list(Pattern), binary_to_list(In)), lists:map(fun list_to_binary/1, Results). diff --git a/src/runners.gleam b/src/runners.gleam deleted file mode 100644 index f3f5911..0000000 --- a/src/runners.gleam +++ /dev/null @@ -1,88 +0,0 @@ -import gleam/map.{Map} -import gleam/erlang/atom.{Atom} -import gleam/string -import snag.{Result} -import parse.{Day} -import gleam/list -import gleam/result -import gleam -import gleam/dynamic.{Dynamic} - -pub const input_dir = "input/" - -pub const days_dir = "src/days/" - -pub type PartRunner = - fn(String) -> Dynamic - -pub type DayRunner = - #(PartRunner, PartRunner) - -pub type RunnerMap = - Map(Day, DayRunner) - -external fn find_files(matching: String, in: String) -> List(String) = - "gladvent_ffi" "find_files" - -type Module = - Atom - -fn to_module_name(file: String) -> String { - file - |> string.replace(".gleam", "") - |> string.replace(".erl", "") - |> string.replace("/", "@") -} - -external fn module_exists(Module) -> Bool = - "gladvent_ffi" "module_exists" - -external fn do_function_exists(Module, Atom) -> gleam.Result(PartRunner, Nil) = - "gladvent_ffi" "function_arity_one_exists" - -fn function_exists( - filename: String, - mod: Atom, - func_name: String, -) -> Result(PartRunner) { - case module_exists(mod) { - False -> - ["module ", filename, " not found"] - |> string.concat - |> snag.error - True -> - func_name - |> atom.create_from_string - |> do_function_exists(mod, _) - |> result.replace_error(snag.new( - "module " <> days_dir <> filename <> " does not export a function \"" <> func_name <> "/1\"", - )) - |> snag.context("function missing") - } -} - -fn get_runner(filename: String) -> Result(#(Day, DayRunner)) { - try day = - string.replace(filename, "day_", "") - |> string.replace(".gleam", "") - |> parse.day - |> snag.context(string.append("cannot create runner for ", filename)) - - let module = - filename - |> string.append("days/", _) - |> to_module_name - |> atom.create_from_string - - try pt_1 = function_exists(filename, module, "pt_1") - try pt_2 = function_exists(filename, module, "pt_2") - - Ok(#(day, #(pt_1, pt_2))) -} - -pub fn build_from_days_dir() -> Result(Map(Day, DayRunner)) { - find_files(matching: "day_*.gleam", in: days_dir) - |> list.try_map(get_runner) - |> result.map(map.from_list) - |> snag.context("failed to generate runners list from filesystem") -} diff --git a/test/parse_test.gleam b/test/parse_test.gleam index d0a655b..508b30d 100644 --- a/test/parse_test.gleam +++ b/test/parse_test.gleam @@ -1,4 +1,4 @@ -import parse.{day} +import gladvent/internal/parse.{day} import gleam/int import gleam/list import gleam/function.{compose}