From 019f09d75f38b7f754bb7dbda632dc3945127ca1 Mon Sep 17 00:00:00 2001 From: Robert Attard Date: Sat, 2 Mar 2024 21:36:23 -0500 Subject: [PATCH] wip use package interface --- gleam.toml | 2 +- manifest.toml | 4 +- src/gladvent/internal/cmd/new.gleam | 4 +- src/gladvent/internal/cmd/run.gleam | 67 +++++--- src/gladvent/internal/file.gleam | 2 +- src/gladvent/internal/runners.gleam | 252 ++++++++++++++++++---------- src/gladvent/moo.gleam | 0 src/gladvent_ffi.erl | 5 + test/parse_test.gleam | 7 +- 9 files changed, 218 insertions(+), 125 deletions(-) delete mode 100644 src/gladvent/moo.gleam diff --git a/gleam.toml b/gleam.toml index ce7fa4a..205f8a0 100644 --- a/gleam.toml +++ b/gleam.toml @@ -3,7 +3,7 @@ version = "0.6.2" repository = { type = "github", user = "TanklesXL", repo = "gladvent" } description = "An Advent Of Code runner for gleam" licences = ["Apache-2.0"] -internal_modules = ["gladvent/internal/*", "aoc_*"] +internal_modules = ["gladvent/internal/*"] gleam = "~> 0.34 or ~> 1.0" [dependencies] diff --git a/manifest.toml b/manifest.toml index 5bbc780..cd24025 100644 --- a/manifest.toml +++ b/manifest.toml @@ -9,7 +9,7 @@ packages = [ { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, { name = "gleam_otp", version = "0.9.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5FADBBEC5ECF3F8B6BE91101D432758503192AE2ADBAD5602158977341489F71" }, { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.35.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5443EEB74708454B65650FEBBB1EF5175057D1DEC62AEA9D7C6D96F41DA79152" }, + { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, { name = "glearray", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "908154F695D330E06A37FAB2C04119E8F315D643206F8F32B6A6C14A8709FFF4" }, { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, { name = "glint", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "61B7E85CBB0CCD2FD8A9C7AE06CA97A80BF6537716F34362A39DF9C74967BBBC" }, @@ -33,4 +33,4 @@ glint = { version = " ~> 0.16.0" } shellout = { version = "~> 1.6" } simplifile = { version = "~> 0.3" } snag = { version = "~> 0.2" } -spinner = { version = "~> 1.1"} +spinner = { version = "~> 1.1" } diff --git a/src/gladvent/internal/cmd/new.gleam b/src/gladvent/internal/cmd/new.gleam index d23c764..2b33ca0 100644 --- a/src/gladvent/internal/cmd/new.gleam +++ b/src/gladvent/internal/cmd/new.gleam @@ -140,11 +140,11 @@ fn do(ctx: Context) -> String { } const gleam_starter = "pub fn pt_1(input: String) { - todo + todo as \"part 1 not implemented\" } pub fn pt_2(input: String) { - todo + todo as \"part 2 not implemented\" } " diff --git a/src/gladvent/internal/cmd/run.gleam b/src/gladvent/internal/cmd/run.gleam index c007998..52610d3 100644 --- a/src/gladvent/internal/cmd/run.gleam +++ b/src/gladvent/internal/cmd/run.gleam @@ -13,9 +13,10 @@ import gladvent/internal/cmd.{Ending, Endless} import glint import glint/flag import gleam -import gladvent/internal/runners.{type RunnerMap} +import gladvent/internal/runners import gleam/dynamic.{type Dynamic} import gleam/option.{type Option, None} +import gleam/package_interface type AsyncResult = gleam.Result(RunResult, String) @@ -23,6 +24,7 @@ type AsyncResult = type RunErr { FailedToReadInput(String) FailedToParseInput(String) + FailedToGetRunner(snag.Snag) Unregistered(Day) Other(String) } @@ -41,12 +43,13 @@ type SolveResult = 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 - FailedToParseInput(err) -> "failed to parse input: " <> err - Other(s) -> s + snag.new("day" <> " " <> int.to_string(day) <> " " <> "unregistered") + FailedToReadInput(input_path) -> + snag.new("failed to read input file: " <> input_path) + FailedToParseInput(err) -> snag.new("failed to parse input: " <> err) + FailedToGetRunner(s) -> snag.layer(s, "failed to get runner") + Other(s) -> snag.new(s) } - |> snag.new } type Direction { @@ -62,11 +65,15 @@ fn string_trim(s: String, dir: Direction, sub: String) -> String { @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 { +fn do( + year: Int, + day: Day, + package: package_interface.Package, + allow_crash: Bool, +) -> RunResult { use #(pt_1, pt_2, parse) <- result.try( - runners - |> map.get(day) - |> result.replace_error(Unregistered(day)), + runners.get_day(package, year, day) + |> result.map_error(FailedToGetRunner), ) let input_path = @@ -251,14 +258,18 @@ 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)) + let assert Ok(allow_crash) = flag.get_bool(input.flags, allow_crash) + use days <- result.then(parse.days(input.args)) + use package <- result.then( + runners.pkg_interface() + |> snag.context("failed to generate package interface"), + ) days |> cmd.exec( timing(input.flags), - do(year, _, runners, allow_crash), + do(year, _, package, allow_crash), collect_async(year, _), ) |> Ok @@ -272,15 +283,29 @@ pub fn run_command() -> glint.Command(Result(List(String))) { 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(allow_crash) = 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 + use package <- result.then( + runners.pkg_interface() + |> snag.context("failed to generate package interface"), + ) + + package.modules + |> map.keys + |> list.filter_map(fn(k) { + k + |> string.split_once("aoc_" <> int.to_string(year) <> "/day_") + |> result.try(fn(day) { + day.1 + |> parse.day + |> result.replace_error(Nil) + }) + }) + |> list.sort(int.compare) |> cmd.exec( timing(input.flags), - do(year, _, runners, allow_crash), + do(year, _, package, allow_crash), collect_async(year, _), ) |> Ok @@ -295,9 +320,3 @@ fn timing(flags: flag.Map) { |> 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 index 12cf2c8..cb7fce6 100644 --- a/src/gladvent/internal/file.gleam +++ b/src/gladvent/internal/file.gleam @@ -4,7 +4,7 @@ import gleam/result pub type IODevice @external(erlang, "gladvent_ffi", "open_file_exclusive") -pub fn open_file_exclusive(s s: String) -> Result(IODevice, FileError) +pub fn open_file_exclusive(s: String) -> Result(IODevice, FileError) @external(erlang, "gladvent_ffi", "write") fn do_write(a: IODevice, b: String) -> Result(Nil, FileError) diff --git a/src/gladvent/internal/runners.gleam b/src/gladvent/internal/runners.gleam index b7d9d44..93019fa 100644 --- a/src/gladvent/internal/runners.gleam +++ b/src/gladvent/internal/runners.gleam @@ -1,5 +1,5 @@ -import gleam/dict.{type Dict as Map} as map -import gleam/erlang/atom.{type Atom} +import gleam/dict as map +import gleam/erlang/atom import gleam/string import snag.{type Result} import gladvent/internal/parse.{type Day} @@ -14,7 +14,7 @@ import gleam/json import gleam/package_interface import spinner import simplifile -import gleam/io +import gleam/bool pub type PartRunner = fn(Dynamic) -> Dynamic @@ -22,102 +22,32 @@ pub type PartRunner = pub type DayRunner = #(PartRunner, PartRunner, Option(fn(String) -> Dynamic)) -pub type RunnerMap = - Map(Day, DayRunner) +const package_interface_path = "build/.gladvent/pkg.json" -@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(fn(a) -> b, Nil) - -fn function_exists( - year: Int, - filename: String, - mod: Atom, - func_name: String, -) -> Result(fn(a) -> b) { - 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, - option.from_result(function_exists(year, filename, module, "parse")), - )), - ) -} - -pub fn build_from_days_dir(year: Int) -> Result(Map(Day, DayRunner)) { - let assert Ok(package_interface) = pkg_interface() - dict.get(package_interface.modules, "aoc_") - 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") -} - -const package_interface_path = "./build/.gladvent/pkg.json" - -pub type PkgInterfaceErr { +type PkgInterfaceErr { FailedToGeneratePackageInterface(String) FailedToReadPackageInterface(simplifile.FileError) FailedToDecodePackageInterface(json.DecodeError) } -pub fn pkg_interface() { +fn package_interface_error_to_snag(e: PkgInterfaceErr) -> snag.Snag { + case e { + FailedToGeneratePackageInterface(s) -> + snag.new(s) + |> snag.layer("failed to generate " <> package_interface_path) + FailedToReadPackageInterface(e) -> + snag.new(string.inspect(e)) + |> snag.layer("failed to read " <> package_interface_path) + FailedToDecodePackageInterface(e) -> + snag.new(string.inspect(e)) + |> snag.layer("failed to decode package interface json") + } +} + +pub fn pkg_interface() -> Result(package_interface.Package) { + use <- snagify_error(mapping: package_interface_error_to_snag) let spinner = - spinner.new("generating package interface") + spinner.new("generating " <> package_interface_path) |> spinner.start() use <- defer(do: fn() { spinner.stop(spinner) }) @@ -132,16 +62,19 @@ pub fn pkg_interface() { |> result.map_error(fn(e) { FailedToGeneratePackageInterface(e.1) }), ) + spinner.set_text(spinner, "reading " <> package_interface_path) use pkg_interface_contents <- result.try( simplifile.read(package_interface_path) |> result.map_error(FailedToReadPackageInterface), ) + + spinner.set_text(spinner, "decoding package interface JSON") use pkg_interface_details <- result.try( json.decode(from: pkg_interface_contents, using: package_interface.decoder) |> result.map_error(FailedToDecodePackageInterface), ) - Ok(io.debug(pkg_interface_details)) + Ok(pkg_interface_details) } fn defer(do b: fn() -> _, after a: fn() -> a) -> a { @@ -149,3 +82,136 @@ fn defer(do b: fn() -> _, after a: fn() -> a) -> a { b() a_out } + +pub type RunnerRetrievalErr { + ModuleNotFound(String) + ParseFunctionInvalid(String) + FunctionNotFound(module: String, function: String) + IncorrectInputParameters( + function: String, + expected: String, + got: List(package_interface.Type), + ) +} + +pub fn runner_retrieval_error_to_snag(e: RunnerRetrievalErr) -> snag.Snag { + case e { + ModuleNotFound(m) -> snag.new("module " <> m <> " not found") + ParseFunctionInvalid(f) -> + snag.new(f) + |> snag.layer("parse function invalid") + FunctionNotFound(m, f) -> + snag.new("module " <> m <> " does not export function " <> f) + IncorrectInputParameters(f, e, g) -> + { + "function '" + <> f + <> "' has parameter(s) " + <> string.inspect(g) + <> ", but should only have one parameter and it must be of type " + <> e + } + |> snag.new + } +} + +fn snagify_error( + do f: fn() -> gleam.Result(out, err), + mapping m: fn(err) -> snag.Snag, +) -> Result(out) { + f() + |> result.map_error(m) +} + +pub fn get_day( + package: package_interface.Package, + year: Int, + day: Day, +) -> Result(DayRunner) { + use <- snagify_error(mapping: runner_retrieval_error_to_snag) + let module_name = + "aoc_" <> int.to_string(year) <> "/day_" <> int.to_string(day) + let module_atom = atom.create_from_string(to_erlang_module_name(module_name)) + + // get the module for the specified year + day + use module <- result.try( + map.get(package.modules, module_name) + |> result.replace_error(ModuleNotFound(module_name)), + ) + + // get the optional parse function + let parse = + module.functions + |> map.get("parse") + + use runner_param_type <- result.try(case parse { + Error(Nil) -> Ok(string) + Ok(package_interface.Function(parameters: [param], return: return, ..)) if param.type_ == string -> + Ok(return) + _ -> + Error(ParseFunctionInvalid( + "parse function must have 1 input parameter of type String", + )) + }) + + // get pt_1 + use pt_1 <- result.try( + map.get(module.functions, "pt_1") + |> result.replace_error(FunctionNotFound(module_name, "pt_1")), + ) + use <- bool.guard( + when: case pt_1.parameters { + [param] -> param.type_ != runner_param_type + _ -> True + }, + return: Error(IncorrectInputParameters( + function: "pt_1", + expected: string.inspect(runner_param_type), + got: list.map(pt_1.parameters, fn(p) { p.type_ }), + )), + ) + + // get pt_2 + use pt_2 <- result.try( + map.get(module.functions, "pt_2") + |> result.replace_error(FunctionNotFound(module_name, "pt_2")), + ) + use <- bool.guard( + when: case pt_2.parameters { + [param] -> param.type_ != runner_param_type + _ -> True + }, + return: Error(IncorrectInputParameters( + function: "pt_2", + expected: string.inspect(runner_param_type), + got: list.map(pt_2.parameters, fn(p) { p.type_ }), + )), + ) + + Ok(#( + function_arity_one(module_atom, atom.create_from_string("pt_1")), + function_arity_one(module_atom, atom.create_from_string("pt_2")), + result.replace(parse, parse_function(module_atom)) + |> option.from_result, + )) +} + +fn to_erlang_module_name(name) { + string.replace(name, "/", "@") +} + +@external(erlang, "gladvent_ffi", "function_arity_one") +fn function_arity_one( + module: atom.Atom, + function: atom.Atom, +) -> fn(Dynamic) -> Dynamic + +@external(erlang, "gladvent_ffi", "parse_function") +fn parse_function(module: atom.Atom) -> fn(String) -> Dynamic + +const string = package_interface.Named( + name: "String", + module: "gleam", + package: "", + parameters: [], +) diff --git a/src/gladvent/moo.gleam b/src/gladvent/moo.gleam deleted file mode 100644 index e69de29..0000000 diff --git a/src/gladvent_ffi.erl b/src/gladvent_ffi.erl index efb6e0c..2860d96 100644 --- a/src/gladvent_ffi.erl +++ b/src/gladvent_ffi.erl @@ -6,6 +6,8 @@ write/2, ensure_dir/1, function_arity_one_exists/2, + function_arity_one/2, + parse_function/1, module_exists/1, close_iodevice/1 ]). @@ -47,3 +49,6 @@ function_arity_one_exists(ModuleName, Fn) -> false -> {error, nil} end. + +function_arity_one(ModuleName,Fn)-> fun ModuleName:Fn/1. +parse_function(ModuleName)-> fun ModuleName:parse/1. diff --git a/test/parse_test.gleam b/test/parse_test.gleam index 508b30d..5204ad3 100644 --- a/test/parse_test.gleam +++ b/test/parse_test.gleam @@ -1,7 +1,6 @@ import gladvent/internal/parse.{day} import gleam/int import gleam/list -import gleam/function.{compose} import gleeunit/should pub fn day_success_test() { @@ -14,5 +13,9 @@ pub fn day_success_test() { } pub fn day_error_test() { - list.each(["", "0", "-1", "26"], compose(day, should.be_error)) + list.each(["", "0", "-1", "26"], fn(x) { + x + |> day + |> should.be_error + }) }