diff options
| -rw-r--r-- | nihil.config/read.cc | 13 | ||||
| -rw-r--r-- | nihil.config/read.ccm | 8 | ||||
| -rw-r--r-- | nihil.config/write.cc | 15 | ||||
| -rw-r--r-- | nihil.config/write.ccm | 11 | ||||
| -rw-r--r-- | nihil.ucl/integer.ccm | 1 | ||||
| -rw-r--r-- | nihil.ucl/object.ccm | 10 | ||||
| -rw-r--r-- | nihil.ucl/real.ccm | 1 | ||||
| -rw-r--r-- | nihil/CMakeLists.txt | 5 | ||||
| -rw-r--r-- | nihil/error.cc | 139 | ||||
| -rw-r--r-- | nihil/error.ccm | 170 | ||||
| -rw-r--r-- | nihil/fd.cc | 63 | ||||
| -rw-r--r-- | nihil/fd.ccm | 47 | ||||
| -rw-r--r-- | nihil/generator.ccm | 1 | ||||
| -rw-r--r-- | nihil/guard.ccm | 1 | ||||
| -rw-r--r-- | nihil/match.ccm | 23 | ||||
| -rw-r--r-- | nihil/monad.ccm | 67 | ||||
| -rw-r--r-- | nihil/nihil.ccm | 3 | ||||
| -rw-r--r-- | nihil/open_file.cc | 4 | ||||
| -rw-r--r-- | nihil/open_file.ccm | 3 | ||||
| -rw-r--r-- | nihil/read_file.ccm | 5 | ||||
| -rw-r--r-- | nihil/rename_file.cc | 32 | ||||
| -rw-r--r-- | nihil/rename_file.ccm | 23 | ||||
| -rw-r--r-- | nihil/spawn.ccm | 4 | ||||
| -rw-r--r-- | nihil/tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | nihil/tests/error.cc | 136 | ||||
| -rw-r--r-- | nihil/write_file.ccm | 30 |
26 files changed, 685 insertions, 131 deletions
diff --git a/nihil.config/read.cc b/nihil.config/read.cc index d55703d..3c20566 100644 --- a/nihil.config/read.cc +++ b/nihil.config/read.cc @@ -4,6 +4,7 @@ module; +#include <expected> #include <filesystem> #include <format> #include <iterator> @@ -16,7 +17,8 @@ module nihil.config; namespace nihil::config { -auto read_from(std::filesystem::path const &filename) -> void +auto read_from(std::filesystem::path const &filename) + -> std::expected<void, nihil::error> { // TODO: nihil.ucl should have a way to load UCL from a filename. @@ -26,10 +28,9 @@ auto read_from(std::filesystem::path const &filename) -> void // 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())); + return {}; + auto errstr = std::format("cannot read {}", filename.string()); + return std::unexpected(nihil::error(errstr, err.error())); } // Parse the UCL. @@ -52,6 +53,8 @@ auto read_from(std::filesystem::path const &filename) -> void throw error(std::format("{}: {}", filename.string(), err.what())); } + + return {}; } } // namespace nihil::config diff --git a/nihil.config/read.ccm b/nihil.config/read.ccm index 74b0bc0..18f7213 100644 --- a/nihil.config/read.ccm +++ b/nihil.config/read.ccm @@ -4,15 +4,19 @@ module; +#include <expected> #include <filesystem> +import nihil; + export module nihil.config:read; namespace nihil::config { /* - * Load the configuration from a file. Throws config::error on failure. + * Load the configuration from a file. */ -export auto read_from(std::filesystem::path const &filename) -> void; +export [[nodiscard]] auto read_from(std::filesystem::path const &filename) + -> std::expected<void, nihil::error>; } // namespace nihil::config diff --git a/nihil.config/write.cc b/nihil.config/write.cc index 4b2a232..2b451bd 100644 --- a/nihil.config/write.cc +++ b/nihil.config/write.cc @@ -4,6 +4,7 @@ module; +#include <expected> #include <filesystem> #include <format> #include <utility> @@ -15,7 +16,8 @@ module nihil.config; namespace nihil::config { -auto write_to(std::filesystem::path const &filename) -> void +auto write_to(std::filesystem::path const &filename) + -> std::expected<void, nihil::error> try { auto uclconfig = ucl::map<ucl::object>(); @@ -30,10 +32,15 @@ try { 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())); + return std::unexpected(nihil::error( + std::format("cannot write {}", filename.string()), + ret.error())); + + return {}; } catch (ucl::error const &exc) { - throw error(std::format("{}: {}", filename.string(), exc.what())); + return std::unexpected(nihil::error( + "failed to serialize configuration", + nihil::error(exc.what()))); } }; diff --git a/nihil.config/write.ccm b/nihil.config/write.ccm index 71cdbb3..1a07dd7 100644 --- a/nihil.config/write.ccm +++ b/nihil.config/write.ccm @@ -4,16 +4,19 @@ module; +#include <expected> #include <filesystem> +import nihil; + export module nihil.config:write; -namespace nihil::config { +export namespace nihil::config { /* - * Write all config values (except defaults) to disk. Throws config::error - * on failure. + * Write all config values (except defaults) to disk. */ -auto write_to(std::filesystem::path const &filename) -> void; +auto write_to(std::filesystem::path const &filename) -> + std::expected<void, nihil::error>; }; diff --git a/nihil.ucl/integer.ccm b/nihil.ucl/integer.ccm index e43ae8b..7a87df3 100644 --- a/nihil.ucl/integer.ccm +++ b/nihil.ucl/integer.ccm @@ -13,6 +13,7 @@ module; export module nihil.ucl:integer; import :object; +import :type; namespace nihil::ucl { diff --git a/nihil.ucl/object.ccm b/nihil.ucl/object.ccm index 5becfa8..9b48256 100644 --- a/nihil.ucl/object.ccm +++ b/nihil.ucl/object.ccm @@ -16,16 +16,15 @@ module; #include <ucl.h> +import nihil; + export module nihil.ucl:object; -import nihil; import :error; import :type; namespace nihil::ucl { -export struct parser; - /*********************************************************************** * The basic object type. */ @@ -75,11 +74,6 @@ protected: ::ucl_object_t *_object = nullptr; friend auto swap(object &a, object &b) -> void; - -private: - - friend struct parser; - friend struct iterator; }; /*********************************************************************** diff --git a/nihil.ucl/real.ccm b/nihil.ucl/real.ccm index e0ecbb5..4e2748b 100644 --- a/nihil.ucl/real.ccm +++ b/nihil.ucl/real.ccm @@ -11,6 +11,7 @@ module; export module nihil.ucl:real; import :object; +import :type; namespace nihil::ucl { diff --git a/nihil/CMakeLists.txt b/nihil/CMakeLists.txt index 8aa4e27..73eea0b 100644 --- a/nihil/CMakeLists.txt +++ b/nihil/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(nihil command_map.ccm ctype.ccm ensure_dir.ccm + error.ccm exec.ccm fd.ccm find_in_path.ccm @@ -16,11 +17,13 @@ target_sources(nihil generic_error.ccm getenv.ccm guard.ccm + match.ccm monad.ccm next_word.ccm open_file.ccm process.ccm read_file.ccm + rename_file.ccm skipws.ccm spawn.ccm tabulate.ccm @@ -31,12 +34,14 @@ target_sources(nihil PRIVATE command_map.cc ensure_dir.cc + error.cc exec.cc fd.cc find_in_path.cc getenv.cc open_file.cc process.cc + rename_file.cc ) if(NIHIL_TESTS) diff --git a/nihil/error.cc b/nihil/error.cc new file mode 100644 index 0000000..de05855 --- /dev/null +++ b/nihil/error.cc @@ -0,0 +1,139 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <memory> +#include <optional> +#include <string> +#include <system_error> +#include <variant> + +module nihil; + +namespace nihil { + +error::error() +{ +} + +error::error(std::string_view what, error cause) + : m_error(std::string(what)) + , m_cause(std::make_shared<error>(std::move(cause))) +{ +} +error::error(std::string_view what) + : m_error(std::string(what)) +{ +} + +error::error(std::error_condition what, error cause) + : m_error(what) + , m_cause(std::make_shared<error>(std::move(cause))) +{ +} + +error::error(std::error_condition what) + : m_error(what) +{ +} + +error::error(std::error_code what, error cause) + : m_error(what) + , m_cause(std::make_shared<error>(std::move(cause))) +{ +} + +error::error(std::error_code what) + : m_error(what) +{ +} + +error::error(error const &) = default; +error::error(error &&) noexcept = default; +auto error::operator=(this error &, error const &) -> error & = default; +auto error::operator=(this error &, error &&) noexcept -> error & = default; + +auto error::cause(this error const &self) -> std::optional<error> +{ + if (self.m_cause) + return *self.m_cause; + return {}; +} + +auto error::str(this error const &self) -> std::string +{ + return self.m_error | match { + [] (std::monostate) -> std::string { + return "No error"; + }, + [] (std::error_code const &m) { + return m.message(); + }, + [] (std::error_condition const &m) { + return m.message(); + }, + [] (std::string const &m) { + return m; + } + }; +} + +auto error::what(this error const &self) -> std::string +{ + auto ret = self.str(); + + auto cause = self.m_cause; + while (cause) { + ret += ": " + cause->str(); + cause = cause->m_cause; + } + + return ret; +} + +auto error::code(this error const &self) -> std::optional<std::error_code> +{ + auto const *code = std::get_if<std::error_code>(&self.m_error); + if (code) + return {*code}; + return {}; +} + +auto error::condition(this error const &self) + -> std::optional<std::error_condition> +{ + auto const *condition = std::get_if<std::error_condition>(&self.m_error); + if (condition) + return {*condition}; + return {}; +} + +auto operator==(error const &lhs, error const &rhs) -> bool +{ + return lhs.m_error == rhs.m_error; +} + +auto operator<=>(error const &lhs, error const &rhs) -> std::strong_ordering +{ + return lhs.m_error <=> rhs.m_error; +} + +auto operator==(error const &lhs, std::error_code const &rhs) -> bool +{ + return lhs.code() == rhs; +} + +// Compare an error to an std::error_condition. +auto operator==(error const &lhs, std::error_condition const &rhs) -> bool +{ + return lhs.condition() == rhs; +} + +auto operator<<(std::ostream &strm, error const &e) -> std::ostream & +{ + return strm << e.what(); +} + +} // namespace nihil diff --git a/nihil/error.ccm b/nihil/error.ccm new file mode 100644 index 0000000..4000cdb --- /dev/null +++ b/nihil/error.ccm @@ -0,0 +1,170 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * error: a type representing an error. + * + * An error consists of an immediate cause, which may be a string or + * std:error_code, and an optional proximate cause, which is another error + * object. Any number of error objects may be stacked. + * + * For example, a failure to open a file might be a stack of two errors: + * + * - string, "failed to open /etc/somefile", + * - std::error_code, "No such file or directory". + * + * Calling .str() will format the entire stack starting at that error, + * for example: "failed to open /etc/somefile: No such file or directory". + * + * Errors may be copied, moved and thrown, although throwing errors is probably + * not very useful since there's no way to distinguish different errors. + * + */ + +#include <iosfwd> +#include <format> +#include <memory> +#include <optional> +#include <string> +#include <system_error> +#include <utility> +#include <variant> + +export module nihil:error; + +namespace nihil { + +// Things which can be errors. +using error_t = std::variant< + std::monostate, + std::string, + std::error_code, + std::error_condition + >; + +export struct error { + // Create an empty error, representing success. + error(); + + // Create an error from a freeform string. + error(std::string_view what, error cause); + explicit error(std::string_view what); + + template<typename Cause> + requires(std::is_error_code_enum<Cause>::value || + std::is_error_condition_enum<Cause>::value) + error(std::string_view what, Cause &&cause) + : error(what, error(std::forward<Cause>(cause))) + {} + + // Create an error from an std::error_code. + error(std::error_condition what, error cause); + explicit error(std::error_condition what); + + // Create an error from an std::error_condition. + error(std::error_code what, error cause); + explicit error(std::error_code what); + + // Create an error from an std::error_code enum. + error(auto errc, error cause) + requires(std::is_error_code_enum<decltype(errc)>::value) + : error(std::move(cause), std::make_error_code(errc)) + {} + + explicit error(auto errc) + requires(std::is_error_code_enum<decltype(errc)>::value) + : error(std::make_error_code(errc)) + {} + + // Create an error from an std::error_condition enum. + error(auto errc, error cause) + requires(std::is_error_condition_enum<decltype(errc)>::value) + : error(std::move(cause), std::make_error_condition(errc)) + {} + + explicit error(auto errc) + requires(std::is_error_condition_enum<decltype(errc)>::value) + : error(std::make_error_condition(errc)) + {} + + error(error const &); + error(error &&) noexcept; + + auto operator=(this error &, error const &) -> error &; + auto operator=(this error &, error &&) noexcept -> error &; + + // Return the cause of this error. + auto cause(this error const &) -> std::optional<error>; + + // Format this error and its cause(s) as a string. + auto what(this error const &) -> std::string; + + // Format this error as a string. + auto str(this error const &) -> std::string; + + // Return this error's error_code, if any. + auto code(this error const &) -> std::optional<std::error_code>; + + // Return this error's error_condition, if any. + auto condition(this error const &) + -> std::optional<std::error_condition>; + +private: + friend auto operator==(error const &, error const &) -> bool; + friend auto operator<=>(error const &, error const &) + -> std::strong_ordering; + + error_t m_error = std::make_error_code(std::errc()); + std::shared_ptr<error> m_cause; +}; + +// Compare an error to another error. This only compares the error itself, +// not any nested causes. +export auto operator==(error const &, error const &) -> bool; +export auto operator<=>(error const &, error const &) -> std::strong_ordering; + +// Compare an error to an std::error_code. +export auto operator==(error const &, std::error_code const &) -> bool; + +// Compare an error to an std::error_condition. +export auto operator==(error const &, std::error_condition const &) -> bool; + +// Compare an error to an std::error_code enum. +export auto operator==(error const &lhs, auto rhs) -> bool +requires(std::is_error_code_enum<decltype(rhs)>::value) +{ + return lhs.code() == rhs; +} + +// Compare an error to an std::error_condition enum. +export auto operator==(error const &lhs, auto rhs) -> bool +requires(std::is_error_condition_enum<decltype(rhs)>::value) +{ + return lhs.condition() == rhs; +} + +// Print an error to an ostream. +export auto operator<<(std::ostream &, error const &) -> std::ostream &; + +} // namespace nihil + +// Make error formattable. +export template<> +struct std::formatter<nihil::error, char> +{ + template<typename ParseContext> + constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator + { + return ctx.begin(); + } + + template<typename FormatContext> + auto format(nihil::error const &e, FormatContext &ctx) const + -> FormatContext::iterator + { + return std::ranges::copy(e.what(), ctx.out()).out; + } +}; diff --git a/nihil/fd.cc b/nihil/fd.cc index e25ba28..20c3411 100644 --- a/nihil/fd.cc +++ b/nihil/fd.cc @@ -51,7 +51,7 @@ fd::operator bool(this fd const &self) noexcept return self._fd != _invalid_fd; } -auto fd::close(this fd &self) -> std::expected<void, std::error_code> +auto fd::close(this fd &self) -> std::expected<void, error> { auto const ret = ::close(self.get()); self._fd = _invalid_fd; @@ -59,7 +59,7 @@ auto fd::close(this fd &self) -> std::expected<void, std::error_code> if (ret == 0) return {}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } auto fd::get(this fd const &self) -> int @@ -76,7 +76,7 @@ auto fd::release(this fd &&self) -> int throw fd_logic_error("Attempt to release an invalid fd"); } -auto dup(fd const &self) -> std::expected<fd, std::error_code> +auto dup(fd const &self) -> std::expected<fd, error> { auto thisfd = self.get(); @@ -84,10 +84,10 @@ auto dup(fd const &self) -> std::expected<fd, std::error_code> if (newfd != -1) return {newfd}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } -auto dup(fd const &self, int newfd) -> std::expected<fd, std::error_code> +auto dup(fd const &self, int newfd) -> std::expected<fd, error> { auto thisfd = self.get(); @@ -95,10 +95,10 @@ auto dup(fd const &self, int newfd) -> std::expected<fd, std::error_code> if (ret != -1) return {newfd}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } -auto raw_dup(fd const &self) -> std::expected<int, std::error_code> +auto raw_dup(fd const &self) -> std::expected<int, error> { auto thisfd = self.get(); @@ -106,10 +106,10 @@ auto raw_dup(fd const &self) -> std::expected<int, std::error_code> if (newfd != -1) return {newfd}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } -auto raw_dup(fd const &self, int newfd) -> std::expected<int, std::error_code> +auto raw_dup(fd const &self, int newfd) -> std::expected<int, error> { auto thisfd = self.get(); @@ -117,30 +117,28 @@ auto raw_dup(fd const &self, int newfd) -> std::expected<int, std::error_code> if (ret != -1) return {newfd}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } -auto getflags(fd const &self) -> std::expected<int, std::error_code> +auto getflags(fd const &self) -> std::expected<int, error> { auto const flags = ::fcntl(self.get(), F_GETFL); if (flags != -1) return {flags}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } -auto replaceflags(fd &self, int newflags) - -> std::expected<void, std::error_code> +auto replaceflags(fd &self, int newflags) -> std::expected<void, error> { auto const ret = ::fcntl(self.get(), F_SETFL, newflags); if (ret == 0) return {}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } -auto setflags(fd &self, int newflags) - -> std::expected<int, std::error_code> +auto setflags(fd &self, int newflags) -> std::expected<int, error> { auto flags = getflags(self); if (!flags) @@ -154,8 +152,7 @@ auto setflags(fd &self, int newflags) return {*flags}; } -auto clearflags(fd &self, int clrflags) - -> std::expected<int, std::error_code> +auto clearflags(fd &self, int clrflags) -> std::expected<int, error> { auto flags = getflags(self); if (!flags) @@ -169,27 +166,25 @@ auto clearflags(fd &self, int clrflags) return {*flags}; } -auto getfdflags(fd const &self) -> std::expected<int, std::error_code> +auto getfdflags(fd const &self) -> std::expected<int, error> { auto const flags = ::fcntl(self.get(), F_GETFD); if (flags != -1) return {flags}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } -auto replacefdflags(fd &self, int newflags) - -> std::expected<void, std::error_code> +auto replacefdflags(fd &self, int newflags) -> std::expected<void, error> { auto const ret = ::fcntl(self.get(), F_SETFD, newflags); if (ret != -1) return {}; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } -auto setfdflags(fd &self, int newflags) - -> std::expected<int, std::error_code> +auto setfdflags(fd &self, int newflags) -> std::expected<int, error> { auto flags = getfdflags(self); if (!flags) @@ -203,8 +198,7 @@ auto setfdflags(fd &self, int newflags) return {*flags}; } -auto clearfdflags(fd &self, int clrflags) - -> std::expected<int, std::error_code> +auto clearfdflags(fd &self, int clrflags) -> std::expected<int, error> { auto flags = getfdflags(self); if (!flags) @@ -218,33 +212,34 @@ auto clearfdflags(fd &self, int clrflags) return {*flags}; } -auto pipe() -> std::expected<std::pair<fd, fd>, std::error_code> { +auto pipe() -> std::expected<std::pair<fd, fd>, error> +{ auto fds = std::array<int, 2>{}; if (auto const ret = ::pipe(fds.data()); ret != 0) - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); return {{fd(fds[0]), fd(fds[1])}}; } auto fd::write(this fd &self, std::span<std::byte const> buffer) - -> std::expected<std::size_t, std::error_code> + -> std::expected<std::size_t, error> { auto const ret = ::write(self.get(), buffer.data(), buffer.size()); if (ret >= 0) return ret; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } auto fd::read(this fd &self, std::span<std::byte> buffer) - -> std::expected<std::size_t, std::error_code> + -> std::expected<std::size_t, error> { auto const ret = ::read(self.get(), buffer.data(), buffer.size()); if (ret >= 0) return ret; - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } } // namespace nihil diff --git a/nihil/fd.ccm b/nihil/fd.ccm index 352eab3..576f3e8 100644 --- a/nihil/fd.ccm +++ b/nihil/fd.ccm @@ -12,6 +12,7 @@ module; export module nihil:fd; +import :error; import :generic_error; namespace nihil { @@ -50,7 +51,7 @@ export struct fd final { explicit operator bool(this fd const &self) noexcept; // Close the wrapped fd. - auto close(this fd &self) -> std::expected<void, std::error_code>; + auto close(this fd &self) -> std::expected<void, error>; // Return the stored fd. auto get(this fd const &self) -> int; @@ -61,12 +62,12 @@ export struct fd final { // Write data from the provided buffer to the fd. Returns the // number of bytes written. auto write(this fd &self, std::span<std::byte const>) - -> std::expected<std::size_t, std::error_code>; + -> std::expected<std::size_t, error>; // Read data from the fd to the provided buffer. Returns the // number of bytes read. auto read(this fd &self, std::span<std::byte>) - -> std::expected<std::size_t, std::error_code>; + -> std::expected<std::size_t, error>; private: static constexpr int _invalid_fd = -1; @@ -75,7 +76,7 @@ private: }; // Create a copy of this fd by calling dup(). -export auto dup(fd const &self) -> std::expected<fd, std::error_code>; +export auto dup(fd const &self) -> std::expected<fd, error>; // Create a copy of this fd by calling dup2(). Note that because this results // in the existing fd and the new fd both being managed by an fd instance, @@ -86,51 +87,41 @@ export auto dup(fd const &self) -> std::expected<fd, std::error_code>; // // In both of these cases, either use raw_dup() instead, or immediately call // release() on the returned fd to prevent the fd instance from closing it. -export auto dup(fd const &self, int newfd) - -> std::expected<fd, std::error_code>; +export auto dup(fd const &self, int newfd) -> std::expected<fd, error>; // Create a copy of this fd by calling dup(). -export auto raw_dup(fd const &self) - -> std::expected<int, std::error_code>; +export auto raw_dup(fd const &self) -> std::expected<int, error>; // Create a copy of this fd by calling dup2(). -export auto raw_dup(fd const &self, int newfd) - -> std::expected<int, std::error_code>; +export auto raw_dup(fd const &self, int newfd) -> std::expected<int, error>; // Return the fnctl flags for this fd. -export auto getflags(fd const &self) - -> std::expected<int, std::error_code>; +export auto getflags(fd const &self) -> std::expected<int, error>; // Replace the fnctl flags for this fd. -export auto replaceflags(fd &self, int newflags) - -> std::expected<void, std::error_code>; +export auto replaceflags(fd &self, int newflags) -> std::expected<void, error>; // Add bits to the fcntl flags for this fd. Returns the new flags. -export auto setflags(fd &self, int newflags) - -> std::expected<int, std::error_code>; +export auto setflags(fd &self, int newflags) -> std::expected<int, error>; // Remove bits from the fcntl flags for this fd. Returns the new flags. -export auto clearflags(fd &self, int clrflags) - -> std::expected<int, std::error_code>; +export auto clearflags(fd &self, int clrflags) -> std::expected<int, error>; // Return the fd flags for this fd. -export auto getfdflags(fd const &self) - -> std::expected<int, std::error_code>; +export auto getfdflags(fd const &self) -> std::expected<int, error>; // Replace the fd flags for this fd. export auto replacefdflags(fd &self, int newflags) - -> std::expected<void, std::error_code>; + -> std::expected<void, error>; // Add bits to the fd flags for this fd. Returns the new flags. -export auto setfdflags(fd &self, int newflags) - -> std::expected<int, std::error_code>; +export auto setfdflags(fd &self, int newflags) -> std::expected<int, error>; // Remove bits from the fd flags for this fd. Returns the new flags. -export auto clearfdflags(fd &self, int clrflags) - -> std::expected<int, std::error_code>; +export auto clearfdflags(fd &self, int clrflags) -> std::expected<int, error>; // Create two fds by calling pipe() and return them. -export auto pipe() -> std::expected<std::pair<fd, fd>, std::error_code>; +export auto pipe() -> std::expected<std::pair<fd, fd>, error>; /* * Write data to a file descriptor from the provided range. Returns the @@ -138,7 +129,7 @@ export auto pipe() -> std::expected<std::pair<fd, fd>, std::error_code>; * partial object to be written. */ export auto write(fd &file, std::ranges::contiguous_range auto &&range) - -> std::expected<std::size_t, std::error_code> + -> std::expected<std::size_t, error> { return file.write(as_bytes(std::span(range))); } @@ -149,7 +140,7 @@ export auto write(fd &file, std::ranges::contiguous_range auto &&range) * object to be read. */ export auto read(fd &file, std::ranges::contiguous_range auto &&range) - -> std::expected<std::size_t, std::error_code> + -> std::expected<std::size_t, error> { return file.read(as_writable_bytes(std::span(range))); } diff --git a/nihil/generator.ccm b/nihil/generator.ccm index 82bcb27..b5a23ee 100644 --- a/nihil/generator.ccm +++ b/nihil/generator.ccm @@ -19,6 +19,7 @@ module; #include <memory> #include <ranges> #include <type_traits> +#include <utility> export module nihil:generator; diff --git a/nihil/guard.ccm b/nihil/guard.ccm index 18c6d70..92305f2 100644 --- a/nihil/guard.ccm +++ b/nihil/guard.ccm @@ -5,6 +5,7 @@ module; #include <concepts> +#include <functional> #include <optional> #include <utility> diff --git a/nihil/match.ccm b/nihil/match.ccm new file mode 100644 index 0000000..9859ea4 --- /dev/null +++ b/nihil/match.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <variant> + +export module nihil:match; + +namespace nihil { + +template<class... Ts> +struct match : Ts... { using Ts::operator()...; }; + +template<typename... Ts, typename... Fs> +constexpr decltype(auto) operator| (std::variant<Ts...> const &v, + match<Fs...> const &match) +{ + return std::visit(match, v); +} + +} // namespace nihil diff --git a/nihil/monad.ccm b/nihil/monad.ccm index f6f48eb..20371c5 100644 --- a/nihil/monad.ccm +++ b/nihil/monad.ccm @@ -137,11 +137,6 @@ struct optional_promise { } // namespace nihil -// This makes std::optional<T> 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 <typename T, typename... Args> struct std::coroutine_traits<std::optional<T>, Args...> { using promise_type = nihil::optional_promise<T>; @@ -188,8 +183,8 @@ auto operator co_await(std::optional<T> o) { namespace nihil { -template <typename T, typename E> -struct expected_promise { +export template <typename T, typename E> +struct expected_promise_base { return_object_holder<std::expected<T, E>>* data; auto get_return_object() @@ -207,28 +202,39 @@ struct expected_promise { return {}; } - void return_value(T o) + void unhandled_exception() { - data->emplace(std::move(o)); + std::rethrow_exception(std::current_exception()); } +}; - void return_value(std::unexpected<E> err) +export template <typename T, typename E> +struct expected_promise : expected_promise_base<T, E> { + void return_value(this expected_promise &self, std::unexpected<E> err) { - data->emplace(std::move(err)); + self.data->emplace(std::move(err)); } - void unhandled_exception() + void return_value(this expected_promise &self, T o) { - std::rethrow_exception(std::current_exception()); + self.data->emplace(std::move(o)); } }; -} // namespace nihil +export template <typename E> +struct expected_promise<void, E> : expected_promise_base<void, E> { + void return_value(this expected_promise &self, std::unexpected<E> err) + { + self.data->emplace(std::move(err)); + } + + void return_value(this expected_promise &self, int) + { + self.data->emplace(); + } +}; -// This makes std::expected<T> 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. +} // namespace nihil export template <typename T, typename E, typename... Args> struct std::coroutine_traits<std::expected<T, E>, Args...> { @@ -237,8 +243,8 @@ struct std::coroutine_traits<std::expected<T, E>, Args...> { namespace nihil { -template <typename T, typename E> -struct expected_awaitable { +export template<typename T, typename E> +struct expected_awaitable_base { std::expected<T, E> o; auto await_ready() @@ -246,11 +252,6 @@ struct expected_awaitable { return o.has_value(); } - auto await_resume() - { - return *o; - } - template <typename P> void await_suspend(std::coroutine_handle<P> h) { @@ -259,6 +260,22 @@ struct expected_awaitable { } }; +export template <typename T, typename E> +struct expected_awaitable : expected_awaitable_base<T, E> { + auto await_resume(this expected_awaitable &self) + { + return std::move(*self.o); + } +}; + +export template <typename E> +struct expected_awaitable<void, E> : expected_awaitable_base<void, E> { + auto await_resume(this expected_awaitable &) + { + return std::expected<void, E>(); + } +}; + } // namespace nihil namespace std { diff --git a/nihil/nihil.ccm b/nihil/nihil.ccm index ac3b13f..2a18b6e 100644 --- a/nihil/nihil.ccm +++ b/nihil/nihil.ccm @@ -10,6 +10,7 @@ export import :argv; export import :command_map; export import :ctype; export import :ensure_dir; +export import :error; export import :exec; export import :fd; export import :find_in_path; @@ -18,11 +19,13 @@ export import :generator; export import :generic_error; export import :getenv; export import :guard; +export import :match; export import :monad; export import :next_word; export import :open_file; export import :process; export import :read_file; +export import :rename_file; export import :skipws; export import :spawn; export import :tabulate; diff --git a/nihil/open_file.cc b/nihil/open_file.cc index d86478c..49a3930 100644 --- a/nihil/open_file.cc +++ b/nihil/open_file.cc @@ -16,13 +16,13 @@ module nihil; namespace nihil { auto open_file(std::filesystem::path const &filename, int flags, int mode) --> std::expected<fd, std::error_code> + -> std::expected<fd, error> { auto fdno = ::open(filename.c_str(), flags, mode); if (fdno != -1) return fd(fdno); - return std::unexpected(std::make_error_code(std::errc(errno))); + return std::unexpected(error(std::errc(errno))); } } // namespace nihil diff --git a/nihil/open_file.ccm b/nihil/open_file.ccm index 0266177..89af4e6 100644 --- a/nihil/open_file.ccm +++ b/nihil/open_file.ccm @@ -10,6 +10,7 @@ module; export module nihil:open_file; +import :error; import :fd; namespace nihil { @@ -19,6 +20,6 @@ namespace nihil { */ export auto open_file(std::filesystem::path const &filename, int flags, int mode = 0777) - -> std::expected<fd, std::error_code>; + -> std::expected<fd, error>; } // namespace nihil diff --git a/nihil/read_file.ccm b/nihil/read_file.ccm index fd26d8d..5f332fd 100644 --- a/nihil/read_file.ccm +++ b/nihil/read_file.ccm @@ -16,6 +16,7 @@ module; export module nihil:read_file; +import :error; import :fd; import :open_file; @@ -26,9 +27,9 @@ namespace nihil { */ export auto read_file(std::filesystem::path const &filename, std::output_iterator<char> auto &&iter) - -> std::expected<void, std::error_code> + -> std::expected<void, error> { - auto do_write = [&](fd &&file) -> std::expected<void, std::error_code> + auto do_write = [&](fd &&file) -> std::expected<void, error> { auto constexpr bufsize = std::size_t{1024}; auto buffer = std::array<char, bufsize>{}; diff --git a/nihil/rename_file.cc b/nihil/rename_file.cc new file mode 100644 index 0000000..c712e44 --- /dev/null +++ b/nihil/rename_file.cc @@ -0,0 +1,32 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> + +module nihil; + +namespace nihil { + +/* + * Rename a file. + */ +auto rename_file(std::filesystem::path const &oldp, + std::filesystem::path const &newp) + -> std::expected<void, error> +{ + auto err = std::error_code(); + + std::filesystem::rename(oldp, newp, err); + + if (err) + return std::unexpected(error(err)); + + return {}; +} + + +} // namespace nihil diff --git a/nihil/rename_file.ccm b/nihil/rename_file.ccm new file mode 100644 index 0000000..bffb3fa --- /dev/null +++ b/nihil/rename_file.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> + +export module nihil:rename_file; + +import :error; + +export namespace nihil { + +/* + * Rename a file (or directory). + */ +auto rename_file(std::filesystem::path const &oldp, + std::filesystem::path const &newp) + -> std::expected<void, error>; + +} // namespace nihil diff --git a/nihil/spawn.ccm b/nihil/spawn.ccm index 2b1a8c6..1ff5776 100644 --- a/nihil/spawn.ccm +++ b/nihil/spawn.ccm @@ -45,7 +45,7 @@ export struct fd_pipe final { auto fds = pipe(); if (!fds) throw exec_error(std::format("pipe: {}", - fds.error().message())); + fds.error().what())); std::tie(_parent_fd, _child_fd) = std::move(*fds); } @@ -132,7 +132,7 @@ struct capture final { auto fds = pipe(); if (!fds) throw exec_error(std::format("pipe: {}", - fds.error().message())); + fds.error().what())); std::tie(_parent_fd, _child_fd) = std::move(*fds); } diff --git a/nihil/tests/CMakeLists.txt b/nihil/tests/CMakeLists.txt index 71de926..e3e7c06 100644 --- a/nihil/tests/CMakeLists.txt +++ b/nihil/tests/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(nihil.test command_map.cc ctype.cc + error.cc fd.cc generator.cc generic_error.cc diff --git a/nihil/tests/error.cc b/nihil/tests/error.cc new file mode 100644 index 0000000..7b079fd --- /dev/null +++ b/nihil/tests/error.cc @@ -0,0 +1,136 @@ +/* + * This source code is released into the public domain. + */ + +#include <cerrno> +#include <cstring> +#include <system_error> + +#include <catch2/catch_test_macros.hpp> + +import nihil; + +TEST_CASE("error: invariants", "[nihil]") +{ + static_assert(std::destructible<nihil::error>); + static_assert(std::default_initializable<nihil::error>); + static_assert(std::move_constructible<nihil::error>); + static_assert(std::copy_constructible<nihil::error>); + static_assert(std::equality_comparable<nihil::error>); + static_assert(std::totally_ordered<nihil::error>); + static_assert(std::swappable<nihil::error>); + static_assert(std::regular<nihil::error>); +} + +TEST_CASE("error: construct from string", "[nihil]") +{ + auto e = nihil::error("an error"); + REQUIRE(e.str() == e.what()); + REQUIRE(e.what() == "an error"); + REQUIRE(std::format("{}", e) == e.what()); +} + +TEST_CASE("error: construct from std::error_condition", "[nihil]") +{ + auto code = std::make_error_condition(std::errc::invalid_argument); + auto e = nihil::error(code); + + REQUIRE(e.cause().has_value() == false); + REQUIRE(e.code().has_value() == false); + REQUIRE(e.condition().has_value() == true); + + REQUIRE(e == std::errc::invalid_argument); + REQUIRE(e != std::errc::no_such_file_or_directory); + + REQUIRE(e.str() == e.what()); + REQUIRE(e.what() == std::strerror(EINVAL)); + REQUIRE(std::format("{}", e) == e.what()); +} + +TEST_CASE("error: construct from std::errc", "[nihil]") +{ + auto e = nihil::error(std::errc::invalid_argument); + + REQUIRE(e.cause().has_value() == false); + REQUIRE(e.code().has_value() == false); + REQUIRE(e.condition().has_value() == true); + + REQUIRE(e == std::errc::invalid_argument); + REQUIRE(e != std::errc::no_such_file_or_directory); + + REQUIRE(e.str() == e.what()); + REQUIRE(e.what() == std::strerror(EINVAL)); + REQUIRE(std::format("{}", e) == e.what()); +} + +TEST_CASE("error: compound error", "[nihil]") +{ + using namespace std::literals; + + auto e = nihil::error("cannot open file", + nihil::error(std::errc::no_such_file_or_directory)); + + REQUIRE(e.cause().has_value() == true); + REQUIRE(e.code().has_value() == false); + REQUIRE(e.condition().has_value() == false); + + REQUIRE(e.cause() == std::errc::no_such_file_or_directory); + REQUIRE(e.str() == "cannot open file"); + REQUIRE(e.what() == ("cannot open file: "s + std::strerror(ENOENT))); + REQUIRE(std::format("{}", e) == e.what()); +} + +TEST_CASE("error: operator== with strings", "[nihil]") +{ + auto e1 = nihil::error("error"); + auto e2 = nihil::error("error"); + auto e3 = nihil::error("an error"); + + REQUIRE(e1 == e2); + REQUIRE(e1 != e3); +} + +TEST_CASE("error: operator< with strings", "[nihil]") +{ + auto e1 = nihil::error("aaa"); + auto e2 = nihil::error("zzz"); + + REQUIRE(e1 < e2); +} + +TEST_CASE("error: operator== with a cause", "[nihil]") +{ + auto e1 = nihil::error("error", nihil::error("cause 1")); + auto e2 = nihil::error("error", nihil::error("cause 2")); + + REQUIRE(e1 == e2); +} + +TEST_CASE("error: operator== with error_conditions", "[nihil]") +{ + auto e1 = nihil::error(std::errc::invalid_argument); + auto e2 = nihil::error(std::errc::invalid_argument); + auto e3 = nihil::error(std::errc::permission_denied); + + REQUIRE(e1 == e2); + REQUIRE(e1 != e3); +} + +TEST_CASE("error: std::format with string", "[nihil]") +{ + auto err = nihil::error("an error"); + REQUIRE(std::format("{}", err) == "an error"); +} + +TEST_CASE("error: std::format with std::errc", "[nihil]") +{ + auto err = nihil::error(std::errc::invalid_argument); + REQUIRE(std::format("{}", err) == std::strerror(EINVAL)); +} + +TEST_CASE("error: std::format with cause", "[nihil]") +{ + auto err = nihil::error("an error", std::errc::invalid_argument); + REQUIRE(std::format("{}", err) == "an error: Invalid argument"); +} + diff --git a/nihil/write_file.ccm b/nihil/write_file.ccm index 64cfd29..6722d5f 100644 --- a/nihil/write_file.ccm +++ b/nihil/write_file.ccm @@ -4,6 +4,7 @@ module; +#include <coroutine> #include <expected> #include <filesystem> #include <ranges> @@ -15,8 +16,11 @@ module; export module nihil:write_file; +import :error; import :guard; +import :monad; import :open_file; +import :rename_file; namespace nihil { @@ -28,12 +32,11 @@ export [[nodiscard]] auto write_file(std::filesystem::path const &filename, std::ranges::contiguous_range auto &&range, int mode = 0777) --> std::expected<std::size_t, std::error_code> + -> std::expected<std::size_t, error> { - return open_file(filename, O_CREAT|O_WRONLY, mode) - .and_then([&] (auto &&fd) { - return write(fd, range); - }); + auto file = co_await open_file(filename, O_CREAT|O_WRONLY, mode); + auto nbytes = co_await write(file, range); + co_return nbytes; } /* @@ -42,7 +45,7 @@ auto write_file(std::filesystem::path const &filename, export [[nodiscard]] auto write_file(std::filesystem::path const &filename, std::ranges::range auto &&range) --> std::expected<std::size_t, std::error_code> + -> std::expected<std::size_t, error> requires(!std::ranges::contiguous_range<decltype(range)>) { return write_file(filename, std::vector(std::from_range, range)); @@ -57,22 +60,21 @@ requires(!std::ranges::contiguous_range<decltype(range)>) export [[nodiscard]] auto safe_write_file(std::filesystem::path const &filename, std::ranges::range auto &&range) --> std::expected<void, std::error_code> + -> std::expected<void, error> { auto tmpfile = filename; tmpfile.remove_filename(); tmpfile /= (filename.filename().native() + ".tmp"); - auto tmpfile_guard = guard([tmpfile] { ::unlink(tmpfile.c_str()); }); + 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))); + co_await write_file(tmpfile, range); + co_await rename_file(tmpfile, filename); tmpfile_guard.release(); - return {}; + co_return {}; } |
