Current section
Files
Jump to
Current section
Files
src/cmd/run.gleam
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], " "))
}