From d5963532328ce5f1c9f266bf7e760b7d18a60c15 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Mon, 23 Jun 2025 16:28:11 +0100 Subject: various updates --- README | 3 +- nihil.config/CMakeLists.txt | 14 +- nihil.config/error.ccm | 5 +- nihil.config/nihil.config.ccm | 1 + nihil.config/option.ccm | 26 +++- nihil.config/read.ccm | 63 +++++++++ nihil.config/store.ccm | 137 ++++++------------- nihil.config/string.ccm | 31 +++-- nihil.config/tests/CMakeLists.txt | 15 +++ nihil.config/tests/string.cc | 34 +++++ nihil.config/write.ccm | 42 ++++++ nihil.ucl/error.ccm | 5 +- nihil.ucl/map.ccm | 3 +- nihil.ucl/nihil.ucl.ccm | 1 + nihil.ucl/parser.ccm | 8 +- nihil.ucl/type.ccm | 6 +- nihil/CMakeLists.txt | 8 +- nihil/exec.ccm | 21 +-- nihil/format_filesystem.ccm | 35 +++++ nihil/generic_error.ccm | 15 ++- nihil/monad.ccm | 271 ++++++++++++++++++++++++++++++++++++++ nihil/nihil.ccm | 5 + nihil/open_file.ccm | 35 +++++ nihil/process.ccm | 55 ++++++-- nihil/read_file.ccm | 49 +++++++ nihil/spawn.ccm | 9 +- nihil/tabulate.ccm | 13 +- nihil/tests/CMakeLists.txt | 4 +- nihil/tests/generic_error.cc | 23 +++- nihil/tests/monad.cc | 68 ++++++++++ nihil/usage_error.ccm | 5 +- nihil/write_file.ccm | 79 +++++++++++ 32 files changed, 914 insertions(+), 175 deletions(-) create mode 100644 nihil.config/read.ccm create mode 100644 nihil.config/tests/CMakeLists.txt create mode 100644 nihil.config/tests/string.cc create mode 100644 nihil.config/write.ccm create mode 100644 nihil/format_filesystem.ccm create mode 100644 nihil/monad.ccm create mode 100644 nihil/open_file.ccm create mode 100644 nihil/read_file.ccm create mode 100644 nihil/tests/monad.cc create mode 100644 nihil/write_file.ccm diff --git a/README b/README index 5d4d2ab..bf4ca68 100644 --- a/README +++ b/README @@ -11,7 +11,8 @@ license all of nihil is in the public domain, with the exception of: -- modules/generator.ccm +- nihil/generator.ccm (BSL) +- nihil/monad.ccm (MIT) requirements ------------ diff --git a/nihil.config/CMakeLists.txt b/nihil.config/CMakeLists.txt index d96b116..ed2bba3 100644 --- a/nihil.config/CMakeLists.txt +++ b/nihil.config/CMakeLists.txt @@ -1,11 +1,21 @@ # This source code is released into the public domain. add_library(nihil.config STATIC) +target_link_libraries(nihil.config PUBLIC nihil nihil.ucl) target_sources(nihil.config PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + nihil.config.ccm error.ccm + read.ccm store.ccm + write.ccm + option.ccm - string.ccm) -target_link_libraries(nihil.config PUBLIC nihil nihil.ucl) + string.ccm +) + +if(NIHIL_TESTS) + add_subdirectory(tests) + enable_testing() +endif() diff --git a/nihil.config/error.ccm b/nihil.config/error.ccm index 0da91cb..4e7131a 100644 --- a/nihil.config/error.ccm +++ b/nihil.config/error.ccm @@ -17,10 +17,7 @@ namespace nihil::config { * Exception thrown when an issue occurs with the configuration. */ export struct error : generic_error { - template - error(std::format_string fmt, Args &&...args) - : generic_error(fmt, std::forward(args)...) - {} + error(std::string what) : generic_error(std::move(what)) {} }; } // namespace nihil::config diff --git a/nihil.config/nihil.config.ccm b/nihil.config/nihil.config.ccm index 0b12885..4ada81c 100644 --- a/nihil.config/nihil.config.ccm +++ b/nihil.config/nihil.config.ccm @@ -10,3 +10,4 @@ export import :error; export import :option; export import :store; export import :string; +export import :write; diff --git a/nihil.config/option.ccm b/nihil.config/option.ccm index 207eb65..1be542e 100644 --- a/nihil.config/option.ccm +++ b/nihil.config/option.ccm @@ -4,13 +4,14 @@ module; +#include #include #include export module nihil.config:option; -import nihil; +import nihil.ucl; import :error; namespace nihil::config { @@ -52,10 +53,16 @@ export struct option } /* - * Add this option to a UCL object. This is used when writing the + * Return this object as a UCL object. This is used when writing the * configuration file. */ - virtual void add_to_ucl(ucl_object_t *) const = 0; + virtual auto to_ucl() const -> ucl::object = 0; + + /* + * Set this object from a UCL object. This is used when reading the + * configuration file. + */ + virtual auto from_ucl(ucl::object const &) -> void = 0; // Not copyable or movable. option(option const &) = delete; @@ -69,6 +76,11 @@ protected: { } + auto is_default(bool b) -> void + { + _is_default = b; + } + /* * Get or set this option as a string. */ @@ -81,4 +93,12 @@ private: bool _is_default = true; }; +/* + * Make options printable. This is mostly useful for testing. + */ +export auto operator<<(std::ostream &strm, option const &opt) -> std::ostream & +{ + return strm << "<" << opt.name() << "=" << opt.string() << ">"; +} + } // namespace nihil diff --git a/nihil.config/read.ccm b/nihil.config/read.ccm new file mode 100644 index 0000000..8d6c202 --- /dev/null +++ b/nihil.config/read.ccm @@ -0,0 +1,63 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +export module nihil.config:read; + +import nihil; +import nihil.ucl; + +import :error; +import :store; + +namespace nihil::config { + +/* + * Load the configuration from a file. Throws config::error on failure. + */ +export auto read_from(std::filesystem::path const &filename) -> void +{ + // TODO: nihil.ucl should have a way to load UCL from a filename. + + std::string config_text; + auto err = read_file(filename, std::back_inserter(config_text)); + if (!err) { + // Ignore ENOENT, it simply means we haven't created the + // config file yet, so default values will be used. + if (err.error() == std::errc::no_such_file_or_directory) + return; + throw error(std::format("{}: {}", + filename.string(), + err.error().message())); + } + + // Parse the UCL. + try { + auto uclconfig = ucl::parse(config_text); + + for (auto &&[key, value] : uclconfig) { + auto &opt = store::get().fetch(key); + opt.from_ucl(value); + } + } catch (unknown_option const &) { + // This is probably an old option which has been removed; + // ignore it, and we'll remove the bad option next time + // we write the config. + } catch (error const &err) { + // Include the filename in any other config errors. + throw error(std::format("{}: {}", filename.string(), + err.what())); + } catch (ucl::error const &err) { + throw error(std::format("{}: {}", filename.string(), + err.what())); + } +} + +} // namespace nihil::config diff --git a/nihil.config/store.ccm b/nihil.config/store.ccm index 7ed4ccb..e0eebc0 100644 --- a/nihil.config/store.ccm +++ b/nihil.config/store.ccm @@ -9,12 +9,12 @@ module; */ #include +#include #include #include export module nihil.config:store; -import nihil; import :error; import :option; @@ -22,15 +22,22 @@ namespace nihil::config { // Exception thrown on an attempt to fetch an undefined option. export struct unknown_option final : error { - std::string varname; - - unknown_option(std::string_view varname_) - : error("unknown configuration variable '{}'", varname_) - , varname(varname_) + unknown_option(std::string_view option_name) + : error(std::format("unknown configuration variable '{}'", + option_name)) + , _option_name(option_name) {} + + auto option_name(this unknown_option const &self) -> std::string_view + { + return self._option_name; + } + +private: + std::string _option_name; }; -export struct store final { +struct store final { /* * Get the global config store. */ @@ -41,52 +48,6 @@ export struct store final { return *instance; } - /* - * Initialise the global config store. - */ -#if 0 - void init(context const &ctx) { - std::string config_text; - - // Load the configuration text. - auto config_path = ctx.dbdir / "config.ucl"; - try { - read_file(config_path, std::back_inserter(config_text)); - } catch (io_error const &exc) { - // Ignore ENOENT, it simply means we haven't created the - // config file yet, so default values will be used. - if (exc.error == std::errc::no_such_file_or_directory) - return; - throw; - } - - // Parse the UCL. - - std::string err; - auto uclconfig = ucl::Ucl::parse(config_text, err); - - if (!uclconfig) - throw error("{0}: {1}", config_path, err); - - auto const &cfg = get(); - for (auto const &uclvalue : uclconfig) { - auto &value = cfg.fetch(uclvalue.key()); - - switch (uclvalue.type()) { - case UCL_INT: - value.integer(uclvalue.int_value()); - break; - case UCL_STRING: - value.string(uclvalue.string_value()); - break; - default: - throw error( - "INTERNAL ERROR: unknown value type {0}", - static_cast(uclvalue.type())); - } - } - } -#endif /* * Register a new value with the config store. @@ -96,12 +57,26 @@ export struct store final { auto [it, okay] = self.options.insert( std::pair{object->name(), object}); - if (okay) - return; + if (!okay) + throw error(std::format( + "INTERNAL ERROR: attempt to register " + "duplicate config value '{0}'", + object->name())); + } - throw error("INTERNAL ERROR: attempt to register " - "duplicate config value '{0}'", - object->name()); + /* + * Remove a value from the config store. + */ + auto unregister_option(this store &self, option *object) -> void + { + auto it = self.options.find(object->name()); + if (it == self.options.end()) + throw error(std::format( + "INTERNAL ERROR: attempt to unregister " + "non-existent config value '{}'", + object->name())); + + self.options.erase(it); } /* @@ -125,44 +100,6 @@ export struct store final { co_yield *it.second; } - /* - * Write all config values (except defaults) to disk. - */ -#if 0 - void store::write_all(this store const &self, context const &ctx) { - // The UCL C++ API doesn't seem to support creating new objects - // from scratch, so we use the C API here. We should probably - // provider a better wrapper for this. - - auto ucl = ::ucl_object_typed_new(UCL_OBJECT); - auto ucl_guard = guard([ucl] { ::ucl_object_unref(ucl); }); - - // Add all the options to the UCL object. - for (auto const &option : self.fetch_all()) { - if (option.is_default) - continue; - - option.add_to_ucl(ucl); - } - - // Dump the UCL object to a string. - auto *ucl_c_text = reinterpret_cast( - ::ucl_object_emit(ucl, UCL_EMIT_CONFIG)); - //NOLINTNEXTLINE(cppcoreguidelines-no-malloc) - auto ucl_text_guard = guard([ucl_c_text] { ::free(ucl_c_text); }); - std::string ucl_text(ucl_c_text); - - // Write the object to a file. - auto config_path = ctx.dbdir / "config.ucl"; - - try { - safe_write_file(config_path, ucl_text); - } catch (io_error const &exc) { - throw error("{}", exc.what()); - } - } -#endif - // Not movable or copyable. store(store const &) = delete; store(store &&) = delete; @@ -180,4 +117,12 @@ private: store() = default; }; +/* + * The public API. + */ +export auto get_option(std::string_view option_name) -> option & +{ + return store::get().fetch(option_name); +} + } // namespace nihil::config diff --git a/nihil.config/string.ccm b/nihil.config/string.ccm index f3273c3..57770ae 100644 --- a/nihil.config/string.ccm +++ b/nihil.config/string.ccm @@ -4,20 +4,19 @@ module; -#include +#include #include -#include - export module nihil.config:string; import nihil; +import nihil.ucl; import :option; import :store; namespace nihil::config { -struct string final : option +export struct string final : option { string(std::string &storage, std::string_view name, @@ -28,6 +27,11 @@ struct string final : option store::get().register_option(this); } + ~string() + { + store::get().unregister_option(this); + } + auto get_string() const -> std::string override { return _storage; @@ -38,13 +42,20 @@ struct string final : option _storage = new_value; } - auto add_to_ucl(ucl_object_t *ucl) const -> void override + auto to_ucl() const -> ucl::object override + { + return ucl::string(_storage); + } + + auto from_ucl(ucl::object const &uclobj) -> void override { - auto ucl_value = ucl_object_fromstring_common( - _storage.data(), _storage.size(), - UCL_STRING_RAW); - ucl_object_insert_key(ucl, ucl_value, - name().data(), name().size(), true); + try { + _storage = object_cast(uclobj).value(); + is_default(false); + } catch (ucl::type_mismatch const &exc) { + throw error(std::format("'{}': expected string, not {}", + name(), str(exc.actual_type()))); + } } private: diff --git a/nihil.config/tests/CMakeLists.txt b/nihil.config/tests/CMakeLists.txt new file mode 100644 index 0000000..1805f7f --- /dev/null +++ b/nihil.config/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +# This source code is released into the public domain. + +add_executable(nihil.config.test + string.cc +) + +target_link_libraries(nihil.config.test PRIVATE + nihil.config + Catch2::Catch2WithMain) + +find_package(Catch2 REQUIRED) + +include(CTest) +include(Catch) +catch_discover_tests(nihil.config.test) diff --git a/nihil.config/tests/string.cc b/nihil.config/tests/string.cc new file mode 100644 index 0000000..7e95190 --- /dev/null +++ b/nihil.config/tests/string.cc @@ -0,0 +1,34 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +import nihil.config; + +TEST_CASE("nihil.config: string option", "[nihil][nihil.config]") +{ + std::string storage; + + REQUIRE_THROWS_AS(nihil::config::get_option("test_option"), + nihil::config::unknown_option); + + { + auto string_option = nihil::config::string( + storage, "test_option", "This is a test option"); + + auto &opt = nihil::config::get_option("test_option"); + REQUIRE(opt.name() == "test_option"); + REQUIRE(opt.description() == "This is a test option"); + REQUIRE(opt.is_default() == true); + REQUIRE(opt.string() == ""); + + opt.string("testing"); + REQUIRE(storage == "testing"); + } + + REQUIRE_THROWS_AS(nihil::config::get_option("test_option"), + nihil::config::unknown_option); +} diff --git a/nihil.config/write.ccm b/nihil.config/write.ccm new file mode 100644 index 0000000..947c7ee --- /dev/null +++ b/nihil.config/write.ccm @@ -0,0 +1,42 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include + +export module nihil.config:write; + +import nihil.ucl; +import :store; + +namespace nihil::config { + +/* + * Write all config values (except defaults) to disk. + */ +auto write_to(std::filesystem::path const &filename) -> void +try { + auto uclconfig = ucl::map(); + + // Add all the options to the UCL object. + for (auto const &option : store::get().all()) { + if (option.is_default()) + continue; + + uclconfig.insert({option.name(), option.to_ucl()}); + } + + auto ucl_text = std::format("{:c}", uclconfig); + auto ret = safe_write_file(filename, ucl_text); + if (!ret) + throw error(std::format("{}: {}", filename.string(), + ret.error().message())); +} catch (ucl::error const &exc) { + throw error(std::format("{}: {}", filename.string(), exc.what())); +} + +}; diff --git a/nihil.ucl/error.ccm b/nihil.ucl/error.ccm index 4eda774..c6a0f2d 100644 --- a/nihil.ucl/error.ccm +++ b/nihil.ucl/error.ccm @@ -17,10 +17,7 @@ namespace nihil::ucl { * Exception thrown when an issue occurs with UCL. */ export struct error : generic_error { - template - error(std::format_string fmt, Args &&...args) - : generic_error(fmt, std::forward(args)...) - {} + error(std::string what) : generic_error(std::move(what)) {} }; } // namespace nihil::ucl diff --git a/nihil.ucl/map.ccm b/nihil.ucl/map.ccm index c140885..434659b 100644 --- a/nihil.ucl/map.ccm +++ b/nihil.ucl/map.ccm @@ -7,6 +7,7 @@ module; #include #include #include +#include #include #include #include @@ -22,7 +23,7 @@ namespace nihil::ucl { // Exception thrown when map::operator[] does not find the key. export struct key_not_found : error { key_not_found(std::string_view key) - : error("key '{}' not found in map", key) + : error(std::format("key '{}' not found in map", key)) , _key(key) {} diff --git a/nihil.ucl/nihil.ucl.ccm b/nihil.ucl/nihil.ucl.ccm index 66e2c2b..9c2ea88 100644 --- a/nihil.ucl/nihil.ucl.ccm +++ b/nihil.ucl/nihil.ucl.ccm @@ -7,6 +7,7 @@ module; export module nihil.ucl; export import :emit; +export import :error; export import :object; export import :object_cast; export import :parser; diff --git a/nihil.ucl/parser.ccm b/nihil.ucl/parser.ccm index 8e715d0..9b87773 100644 --- a/nihil.ucl/parser.ccm +++ b/nihil.ucl/parser.ccm @@ -25,10 +25,7 @@ namespace nihil::ucl { * Exception thrown when an issue occurs parsing UCL. */ export struct parse_error : error { - template - parse_error(std::format_string fmt, Args &&...args) - : error(fmt, std::forward(args)...) - {} + parse_error(std::string what) : error(std::move(what)) {} }; // UCL parser flags. @@ -124,8 +121,7 @@ export struct parser { auto ret = ::ucl_parser_add_chunk(self._parser, dptr, std::ranges::size(data)); if (ret == false) - throw parse_error("{}", - ::ucl_parser_get_error(self._parser)); + throw parse_error(::ucl_parser_get_error(self._parser)); } auto add(this parser &self, std::ranges::range auto &&data) diff --git a/nihil.ucl/type.ccm b/nihil.ucl/type.ccm index 5050e7a..bf6c6bc 100644 --- a/nihil.ucl/type.ccm +++ b/nihil.ucl/type.ccm @@ -5,6 +5,7 @@ module; #include +#include #include #include @@ -69,8 +70,9 @@ concept datatype = requires(T o) { // Exception thrown when a type assertion fails. export struct type_mismatch : error { type_mismatch(object_type expected_type, object_type actual_type) - : error("UCL type mismatch: expected type '{}' != actual type '{}'", - str(expected_type), str(actual_type)) + : error(std::format("UCL type mismatch: expected type '{}' " + "!= actual type '{}'", + str(expected_type), str(actual_type))) , _expected_type(expected_type) , _actual_type(actual_type) {} diff --git a/nihil/CMakeLists.txt b/nihil/CMakeLists.txt index 72f7e22..6406294 100644 --- a/nihil/CMakeLists.txt +++ b/nihil/CMakeLists.txt @@ -10,16 +10,22 @@ target_sources(nihil PUBLIC exec.ccm fd.ccm find_in_path.ccm + format_filesystem.ccm generator.ccm generic_error.ccm getenv.ccm guard.ccm + monad.ccm next_word.ccm + open_file.ccm process.ccm + read_file.ccm skipws.ccm spawn.ccm tabulate.ccm - usage_error.ccm) + usage_error.ccm + write_file.ccm +) if(NIHIL_TESTS) add_subdirectory(tests) diff --git a/nihil/exec.ccm b/nihil/exec.ccm index 5718a04..f91efdf 100644 --- a/nihil/exec.ccm +++ b/nihil/exec.ccm @@ -31,9 +31,8 @@ namespace nihil { * Generic error, what() should be descriptive. */ export struct exec_error : generic_error { - template - exec_error(std::format_string fmt, Args &&...args) - : generic_error(fmt, std::forward(args)...) + exec_error(std::string what) + : generic_error(std::move(what)) {} }; @@ -41,12 +40,18 @@ export struct exec_error : generic_error { * We tried to execute a path or filename and the file was not found. */ export struct executable_not_found : exec_error { - std::string executable; - - executable_not_found(std::string_view executable_) - : exec_error("{}: command not found", executable_) - , executable(executable_) + executable_not_found(std::string_view filename) + : exec_error(std::format("{}: command not found", filename)) + , _filename(filename) {} + + auto filename(this executable_not_found const &self) -> std::string_view + { + return self._filename; + } + +private: + std::string _filename; }; /* diff --git a/nihil/format_filesystem.ccm b/nihil/format_filesystem.ccm new file mode 100644 index 0000000..11d8675 --- /dev/null +++ b/nihil/format_filesystem.ccm @@ -0,0 +1,35 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include + +export module nihil:format_filesystem; + +/* + * std::formatter for path was only added in C++26; LLVM 19 doesn't have it. + * This is a basic implementation that doesn't support any format flags. + */ + +#ifndef __cpp_lib_format_path +export template<> +struct std::formatter +{ + template + constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator + { + return ctx.begin(); + } + + template + auto format(std::filesystem::path const &path, FmtContext& ctx) const + -> FmtContext::iterator + { + return std::ranges::copy(path.native(), ctx.out()).out; + } +}; +#endif // !__cpp_lib_format_path diff --git a/nihil/generic_error.ccm b/nihil/generic_error.ccm index a582519..4322236 100644 --- a/nihil/generic_error.ccm +++ b/nihil/generic_error.ccm @@ -9,6 +9,8 @@ module; export module nihil:generic_error; +import :format_filesystem; + namespace nihil { /* @@ -17,9 +19,16 @@ namespace nihil { */ export struct generic_error : std::runtime_error { - template - generic_error(std::format_string fmt, Args &&...args) - : std::runtime_error(std::format(fmt, std::forward(args)...)) + generic_error(char const *what) + : std::runtime_error(what) + {} + + generic_error(std::string_view what) + : std::runtime_error(std::string(what).c_str()) + {} + + generic_error(std::string const &what) + : std::runtime_error(what.c_str()) {} }; diff --git a/nihil/monad.ccm b/nihil/monad.ccm new file mode 100644 index 0000000..f6f48eb --- /dev/null +++ b/nihil/monad.ccm @@ -0,0 +1,271 @@ +/* + * From https://github.com/toby-allsopp/coroutine_monad + * + * Copyright (c) 2017 Toby Allsopp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +module; + +#include +#include +#include +#include +#include + +export module nihil:monad; + +namespace nihil { + +/********************************************************************** + * return_object_holder + */ + +// An object that starts out unitialized. Initialized by a call to emplace. +template +using deferred = std::optional; + +template +struct return_object_holder { + // The staging object that is returned (by copy/move) to the caller of + // the coroutine. + deferred stage; + return_object_holder*& p; + + // When constructed, we assign a pointer to ourselves to the supplied + // reference to pointer. + return_object_holder(return_object_holder*& p) + : stage{} + , p(p) + { + p = this; + } + + // Copying doesn't make any sense (which copy should the pointer refer + // to?). + return_object_holder(return_object_holder const&) = delete; + + // To move, we just update the pointer to point at the new object. + return_object_holder(return_object_holder&& other) + : stage(std::move(other.stage)) + , p(other.p) + { + p = this; + } + + // Assignment doesn't make sense. + void operator=(return_object_holder const&) = delete; + void operator=(return_object_holder&&) = delete; + + // A non-trivial destructor is required until + // https://bugs.llvm.org//show_bug.cgi?id=28593 is fixed. + ~return_object_holder() {} + + // Construct the staging value; arguments are perfect forwarded to T's + // constructor. + template + void emplace(Args&&... args) + { + stage.emplace(std::forward(args)...); + } + + // We assume that we will be converted only once, so we can move from + // the staging object. We also assume that `emplace` has been called + // at least once. + operator T() + { + return std::move(*stage); + } +}; + +template +auto make_return_object_holder(return_object_holder*& p) +{ + return return_object_holder{p}; +} + +/********************************************************************** + * std::optional + */ + +template +struct optional_promise { + return_object_holder>* data; + + auto get_return_object() + { + return make_return_object_holder(data); + } + + auto initial_suspend() noexcept -> std::suspend_never + { + return {}; + } + + auto final_suspend() noexcept -> std::suspend_never + { + return {}; + } + + void return_value(T x) + { + data->emplace(std::move(x)); + } + + void unhandled_exception() + { + std::rethrow_exception(std::current_exception()); + } +}; + +} // namespace nihil + +// This makes std::optional useable as a coroutine return type. Strictly, +// this specilaization should depend on a user-defined type, otherwise this is +// undefined behaviour. As this is purely for demonstration purposes, let's +// live dangerously. + +export template +struct std::coroutine_traits, Args...> { + using promise_type = nihil::optional_promise; +}; + +namespace nihil { + +template +struct optional_awaitable { + std::optional o; + + auto await_ready() + { + return o.has_value(); + } + + auto await_resume() + { + return *o; + } + + template + void await_suspend(std::coroutine_handle> h) + { + h.promise().data->emplace(std::nullopt); + h.destroy(); + } +}; + +} // namespace nihil + +namespace std { + +export template +auto operator co_await(std::optional o) { + return nihil::optional_awaitable{std::move(o)}; +} + +} // namespace std + +/********************************************************************** + * std::expected + */ + +namespace nihil { + +template +struct expected_promise { + return_object_holder>* data; + + auto get_return_object() + { + return make_return_object_holder(data); + } + + auto initial_suspend() noexcept -> std::suspend_never + { + return {}; + } + + auto final_suspend() noexcept -> std::suspend_never + { + return {}; + } + + void return_value(T o) + { + data->emplace(std::move(o)); + } + + void return_value(std::unexpected err) + { + data->emplace(std::move(err)); + } + + void unhandled_exception() + { + std::rethrow_exception(std::current_exception()); + } +}; + +} // namespace nihil + +// This makes std::expected useable as a coroutine return type. Strictly, +// this specilaization should depend on a user-defined type, otherwise this is +// undefined behaviour. As this is purely for demonstration purposes, let's +// live dangerously. + +export template +struct std::coroutine_traits, Args...> { + using promise_type = nihil::expected_promise; +}; + +namespace nihil { + +template +struct expected_awaitable { + std::expected o; + + auto await_ready() + { + return o.has_value(); + } + + auto await_resume() + { + return *o; + } + + template + void await_suspend(std::coroutine_handle

h) + { + h.promise().data->emplace(std::unexpected(o.error())); + h.destroy(); + } +}; + +} // namespace nihil + +namespace std { + +export template +auto operator co_await(std::expected o) { + return nihil::expected_awaitable{std::move(o)}; +} + +} // namespace std diff --git a/nihil/nihil.ccm b/nihil/nihil.ccm index 21790c8..718bc08 100644 --- a/nihil/nihil.ccm +++ b/nihil/nihil.ccm @@ -12,13 +12,18 @@ export import :ctype; export import :exec; export import :fd; export import :find_in_path; +export import :format_filesystem; export import :generator; export import :generic_error; export import :getenv; export import :guard; +export import :monad; export import :next_word; +export import :open_file; export import :process; +export import :read_file; export import :skipws; export import :spawn; export import :tabulate; export import :usage_error; +export import :write_file; diff --git a/nihil/open_file.ccm b/nihil/open_file.ccm new file mode 100644 index 0000000..38fedbd --- /dev/null +++ b/nihil/open_file.ccm @@ -0,0 +1,35 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include + +#include +#include + +export module nihil:open_file; + +import :fd; + +namespace nihil { + +/* + * Open the given file and return an fd for it. + */ +auto open_file(std::filesystem::path const &filename, + int flags, + int mode = 0777) + -> std::expected +{ + auto fdno = ::open(filename.c_str(), flags, mode); + if (fdno != -1) + return fd(fdno); + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +} // namespace nihil diff --git a/nihil/process.ccm b/nihil/process.ccm index 7a4f1f0..ba8a399 100644 --- a/nihil/process.ccm +++ b/nihil/process.ccm @@ -6,7 +6,9 @@ module; #include #include +#include #include +#include #include #include @@ -18,10 +20,43 @@ import :generic_error; namespace nihil { +/* + * Exception thrown when a process operation fails. + */ +export struct process_error : generic_error { + process_error(std::string what) + : generic_error(std::move(what)) + {} +}; + +// A waitpid() call failed. +export struct waitpid_error : process_error { + waitpid_error(::pid_t pid, std::error_code error) + : process_error(std::format("waitpid({}): {}", + pid, error.message())) + , _pid(pid) + , _error(error) + {} + + auto pid(this waitpid_error const &self) -> ::pid_t + { + return self._pid; + } + + auto error(this waitpid_error const &self) -> std::error_code + { + return self._error; + } + +private: + ::pid_t _pid; + std::error_code _error; +}; + /* * wait_result: the exit status of a process. */ -struct wait_result final { +export struct wait_result final { // Return true if the process exited normally with an exit code of // zero, otherwise false. auto okay(this wait_result const &self) -> bool @@ -64,7 +99,7 @@ private: /* * process: represents a process we created, which can be waited for. */ -struct process final { +export struct process final { process() = delete; /* @@ -105,19 +140,11 @@ struct process final { { auto status = int{}; auto ret = waitpid(self._pid, &status, WEXITED); + if (ret != -1) + return wait_result(status); - self._pid = -1; - - switch (ret) { - case -1: - throw generic_error("waitpid({}): failed: {}", - self._pid, strerror(errno)); - case 0: - throw generic_error("waitpid({}): no child to wait", - self._pid); - } - - return wait_result(status); + throw waitpid_error(self._pid, + std::make_error_code(std::errc(errno))); } /* diff --git a/nihil/read_file.ccm b/nihil/read_file.ccm new file mode 100644 index 0000000..fd26d8d --- /dev/null +++ b/nihil/read_file.ccm @@ -0,0 +1,49 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +#include +#include + +export module nihil:read_file; + +import :fd; +import :open_file; + +namespace nihil { + +/* + * Load the contents of a file into an output iterator. + */ +export auto read_file(std::filesystem::path const &filename, + std::output_iterator auto &&iter) + -> std::expected +{ + auto do_write = [&](fd &&file) -> std::expected + { + auto constexpr bufsize = std::size_t{1024}; + auto buffer = std::array{}; + + for (;;) { + auto err = read(file, buffer); + if (!err) + return std::unexpected(err.error()); + + auto data = std::span(buffer).subspan(0, *err); + std::ranges::copy(data, iter); + } + }; + + return open_file(filename, O_RDONLY).and_then(do_write); +} + +} // namespace nihil diff --git a/nihil/spawn.ccm b/nihil/spawn.ccm index f33eaa0..2b1a8c6 100644 --- a/nihil/spawn.ccm +++ b/nihil/spawn.ccm @@ -10,6 +10,7 @@ module; #include #include +#include #include #include #include @@ -43,7 +44,8 @@ export struct fd_pipe final { fd_pipe(int fdno, fd &ret) : _fdno(fdno), _fd(&ret) { auto fds = pipe(); if (!fds) - throw exec_error("pipe: {}", fds.error().message()); + throw exec_error(std::format("pipe: {}", + fds.error().message())); std::tie(_parent_fd, _child_fd) = std::move(*fds); } @@ -129,7 +131,8 @@ struct capture final { { auto fds = pipe(); if (!fds) - throw exec_error("pipe: {}", fds.error().message()); + throw exec_error(std::format("pipe: {}", + fds.error().message())); std::tie(_parent_fd, _child_fd) = std::move(*fds); } @@ -183,7 +186,7 @@ export auto spawn(executor auto &&executor, auto &&...actions) -> process { auto const pid = ::fork(); if (pid == -1) - throw exec_error("fork: {}", std::strerror(errno)); + throw exec_error(std::format("fork: {}", std::strerror(errno))); auto proc = process(pid); diff --git a/nihil/tabulate.ccm b/nihil/tabulate.ccm index debb784..73e251d 100644 --- a/nihil/tabulate.ccm +++ b/nihil/tabulate.ccm @@ -39,10 +39,7 @@ namespace nihil { // Exception thrown when a table spec is invalid. export struct table_spec_error : generic_error { - template - table_spec_error(std::format_string fmt, Args &&...args) - : generic_error(fmt, std::forward(args)...) - {} + table_spec_error(std::string what) : generic_error(std::move(what)) {} }; /* @@ -145,8 +142,8 @@ auto parse_field_flags(field_spec &field, Iterator &pos, Sentinel end) case '}': return; default: - throw table_spec_error( - "Invalid table spec: unknown flag character"); + throw table_spec_error("Invalid table spec: " + "unknown flag character"); } if (++pos == end) @@ -168,7 +165,7 @@ auto parse_field(Iterator &pos, Sentinel end) // The field spec should start with a '{'. if (*pos != '{') - throw table_spec_error("Invalid table spec: expected '{{'"); + throw table_spec_error("Invalid table spec: expected '{'"); if (++pos == end) throw table_spec_error("Invalid table spec: unterminated field"); @@ -178,7 +175,7 @@ auto parse_field(Iterator &pos, Sentinel end) auto brace = std::ranges::find(pos, end, '}'); if (brace == end) - throw table_spec_error("Invalid table spec: expected '}}'"); + throw table_spec_error("Invalid table spec: expected '}'"); field.name = std::basic_string_view(pos, brace); pos = std::next(brace); diff --git a/nihil/tests/CMakeLists.txt b/nihil/tests/CMakeLists.txt index abeab88..25111c2 100644 --- a/nihil/tests/CMakeLists.txt +++ b/nihil/tests/CMakeLists.txt @@ -8,10 +8,12 @@ add_executable(nihil.test generic_error.cc getenv.cc guard.cc + monad.cc next_word.cc skipws.cc spawn.cc - tabulate.cc) + tabulate.cc +) target_link_libraries(nihil.test PRIVATE nihil diff --git a/nihil/tests/generic_error.cc b/nihil/tests/generic_error.cc index b213af9..ee3eccd 100644 --- a/nihil/tests/generic_error.cc +++ b/nihil/tests/generic_error.cc @@ -6,12 +6,27 @@ import nihil; -using namespace std::literals; - TEST_CASE("generic_error: basic", "[generic_error]") { + using namespace std::literals; + + // C string + try { + throw nihil::generic_error("test error"); + } catch (nihil::generic_error const &exc) { + REQUIRE(exc.what() == "test error"sv); + } + + // std::string + try { + throw nihil::generic_error("test error"s); + } catch (nihil::generic_error const &exc) { + REQUIRE(exc.what() == "test error"sv); + } + + // std::string_view try { - throw nihil::generic_error("{} + {} = {}", 1, 2, 3); + throw nihil::generic_error("test error"sv); } catch (nihil::generic_error const &exc) { - REQUIRE(exc.what() == "1 + 2 = 3"s); + REQUIRE(exc.what() == "test error"sv); } } diff --git a/nihil/tests/monad.cc b/nihil/tests/monad.cc new file mode 100644 index 0000000..3964494 --- /dev/null +++ b/nihil/tests/monad.cc @@ -0,0 +1,68 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +import nihil; + +TEST_CASE("monad: co_await std::optional<> with value", "[nihil]") +{ + auto get_value = [] -> std::optional { + return 42; + }; + + auto try_get_value = [&get_value] -> std::optional { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(o == 42); +} + +TEST_CASE("monad: co_await std::optional<> without value", "[nihil]") +{ + auto get_value = [] -> std::optional { + return {}; + }; + + auto try_get_value = [&get_value] -> std::optional { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(!o.has_value()); +} + +TEST_CASE("monad: co_await std::expected<> with value", "[nihil]") +{ + auto get_value = [] -> std::expected { + return 42; + }; + + auto try_get_value = [&get_value] -> std::expected { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(o == 42); +} + +TEST_CASE("monad: co_await std::expected<> with error", "[nihil]") +{ + auto get_value = [] -> std::expected { + return std::unexpected("error"); + }; + + auto try_get_value = [&get_value] -> std::expected { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(!o); + REQUIRE(o.error() == "error"); +} diff --git a/nihil/usage_error.ccm b/nihil/usage_error.ccm index 9b56991..41de29c 100644 --- a/nihil/usage_error.ccm +++ b/nihil/usage_error.ccm @@ -17,10 +17,7 @@ namespace nihil { * Exception thrown to indicate invalid command-line arguments. */ export struct usage_error : generic_error { - template - usage_error(std::format_string fmt, Args &&...args) - : generic_error(fmt, std::forward(args)...) - {} + usage_error(std::string what) : generic_error(std::move(what)) {} }; } // namespace nihil diff --git a/nihil/write_file.ccm b/nihil/write_file.ccm new file mode 100644 index 0000000..64cfd29 --- /dev/null +++ b/nihil/write_file.ccm @@ -0,0 +1,79 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +#include +#include + +export module nihil:write_file; + +import :guard; +import :open_file; + +namespace nihil { + +/* + * Write the contents of a range to a file. Returns the number of bytes + * written. + */ +export [[nodiscard]] +auto write_file(std::filesystem::path const &filename, + std::ranges::contiguous_range auto &&range, + int mode = 0777) +-> std::expected +{ + return open_file(filename, O_CREAT|O_WRONLY, mode) + .and_then([&] (auto &&fd) { + return write(fd, range); + }); +} + +/* + * Utility wrapper for non-contiguous ranges. + */ +export [[nodiscard]] +auto write_file(std::filesystem::path const &filename, + std::ranges::range auto &&range) +-> std::expected +requires(!std::ranges::contiguous_range) +{ + return write_file(filename, std::vector(std::from_range, range)); +} + +/* + * Write the contents of a range to a file safely. The data will be written + * to ".tmp", and if the write succeeds, the temporary file will be + * renamed to the target filename. If an error occurs, the target file will + * not be modified. + */ +export [[nodiscard]] +auto safe_write_file(std::filesystem::path const &filename, + std::ranges::range auto &&range) +-> std::expected +{ + auto tmpfile = filename; + tmpfile.remove_filename(); + tmpfile /= (filename.filename().native() + ".tmp"); + + auto tmpfile_guard = guard([tmpfile] { ::unlink(tmpfile.c_str()); }); + + if (auto err = write_file(tmpfile, range); !err) + return std::unexpected(err.error()); + + if (::rename(tmpfile.c_str(), filename.c_str()) == -1) + return std::unexpected(std::make_error_code(std::errc(errno))); + + tmpfile_guard.release(); + return {}; +} + + +} // namespace nihil -- cgit v1.2.3