aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-25 21:15:34 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-25 21:15:34 +0100
commit257b6719ed89104018f6a521f6141ecd3eec3fd9 (patch)
tree590217da5f5131494ee3f002b44da25fa600a96d
parent0fc4b95e63370b9131c4e76f5f7137a4764dc341 (diff)
downloadnihil-257b6719ed89104018f6a521f6141ecd3eec3fd9.tar.gz
nihil-257b6719ed89104018f6a521f6141ecd3eec3fd9.tar.bz2
nihil: add an error type
-rw-r--r--nihil.config/read.cc13
-rw-r--r--nihil.config/read.ccm8
-rw-r--r--nihil.config/write.cc15
-rw-r--r--nihil.config/write.ccm11
-rw-r--r--nihil.ucl/integer.ccm1
-rw-r--r--nihil.ucl/object.ccm10
-rw-r--r--nihil.ucl/real.ccm1
-rw-r--r--nihil/CMakeLists.txt5
-rw-r--r--nihil/error.cc139
-rw-r--r--nihil/error.ccm170
-rw-r--r--nihil/fd.cc63
-rw-r--r--nihil/fd.ccm47
-rw-r--r--nihil/generator.ccm1
-rw-r--r--nihil/guard.ccm1
-rw-r--r--nihil/match.ccm23
-rw-r--r--nihil/monad.ccm67
-rw-r--r--nihil/nihil.ccm3
-rw-r--r--nihil/open_file.cc4
-rw-r--r--nihil/open_file.ccm3
-rw-r--r--nihil/read_file.ccm5
-rw-r--r--nihil/rename_file.cc32
-rw-r--r--nihil/rename_file.ccm23
-rw-r--r--nihil/spawn.ccm4
-rw-r--r--nihil/tests/CMakeLists.txt1
-rw-r--r--nihil/tests/error.cc136
-rw-r--r--nihil/write_file.ccm30
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 {};
}