aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README3
-rw-r--r--nihil.config/CMakeLists.txt14
-rw-r--r--nihil.config/error.ccm5
-rw-r--r--nihil.config/nihil.config.ccm1
-rw-r--r--nihil.config/option.ccm26
-rw-r--r--nihil.config/read.ccm63
-rw-r--r--nihil.config/store.ccm137
-rw-r--r--nihil.config/string.ccm31
-rw-r--r--nihil.config/tests/CMakeLists.txt15
-rw-r--r--nihil.config/tests/string.cc34
-rw-r--r--nihil.config/write.ccm42
-rw-r--r--nihil.ucl/error.ccm5
-rw-r--r--nihil.ucl/map.ccm3
-rw-r--r--nihil.ucl/nihil.ucl.ccm1
-rw-r--r--nihil.ucl/parser.ccm8
-rw-r--r--nihil.ucl/type.ccm6
-rw-r--r--nihil/CMakeLists.txt8
-rw-r--r--nihil/exec.ccm21
-rw-r--r--nihil/format_filesystem.ccm35
-rw-r--r--nihil/generic_error.ccm15
-rw-r--r--nihil/monad.ccm271
-rw-r--r--nihil/nihil.ccm5
-rw-r--r--nihil/open_file.ccm35
-rw-r--r--nihil/process.ccm55
-rw-r--r--nihil/read_file.ccm49
-rw-r--r--nihil/spawn.ccm9
-rw-r--r--nihil/tabulate.ccm13
-rw-r--r--nihil/tests/CMakeLists.txt4
-rw-r--r--nihil/tests/generic_error.cc23
-rw-r--r--nihil/tests/monad.cc68
-rw-r--r--nihil/usage_error.ccm5
-rw-r--r--nihil/write_file.ccm79
32 files changed, 914 insertions, 175 deletions
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<typename... Args>
- error(std::format_string<Args...> fmt, Args &&...args)
- : generic_error(fmt, std::forward<Args>(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 <iostream>
#include <string>
#include <ucl++.h>
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 <filesystem>
+#include <format>
+#include <iterator>
+#include <string>
+
+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 <coroutine>
+#include <filesystem>
#include <format>
#include <map>
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<int>(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<char *>(
- ::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 <stdexcept>
+#include <format>
#include <string>
-#include <ucl++.h>
-
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<ucl::string>(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 <string>
+
+#include <catch2/catch_test_macros.hpp>
+
+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 <filesystem>
+#include <format>
+#include <utility>
+
+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<ucl::object>();
+
+ // 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<typename... Args>
- error(std::format_string<Args...> fmt, Args &&...args)
- : generic_error(fmt, std::forward<Args>(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 <cassert>
#include <cstdint>
#include <cstdlib>
+#include <format>
#include <memory>
#include <optional>
#include <string>
@@ -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<typename... Args>
- parse_error(std::format_string<Args...> fmt, Args &&...args)
- : error(fmt, std::forward<Args>(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 <concepts>
+#include <format>
#include <string>
#include <ucl.h>
@@ -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<typename... Args>
- exec_error(std::format_string<Args...> fmt, Args &&...args)
- : generic_error(fmt, std::forward<Args>(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 <filesystem>
+#include <format>
+#include <ranges>
+
+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<std::filesystem::path, char>
+{
+ template<typename ParseContext>
+ constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator
+ {
+ return ctx.begin();
+ }
+
+ template<typename FmtContext>
+ 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<typename... Args>
- generic_error(std::format_string<Args...> fmt, Args &&...args)
- : std::runtime_error(std::format(fmt, std::forward<Args>(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 <coroutine>
+#include <exception>
+#include <expected>
+#include <optional>
+#include <utility>
+
+export module nihil:monad;
+
+namespace nihil {
+
+/**********************************************************************
+ * return_object_holder
+ */
+
+// An object that starts out unitialized. Initialized by a call to emplace.
+template <typename T>
+using deferred = std::optional<T>;
+
+template <typename T>
+struct return_object_holder {
+ // The staging object that is returned (by copy/move) to the caller of
+ // the coroutine.
+ deferred<T> 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 <typename... Args>
+ void emplace(Args&&... args)
+ {
+ stage.emplace(std::forward<Args>(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 <typename T>
+auto make_return_object_holder(return_object_holder<T>*& p)
+{
+ return return_object_holder<T>{p};
+}
+
+/**********************************************************************
+ * std::optional
+ */
+
+template <typename T>
+struct optional_promise {
+ return_object_holder<std::optional<T>>* 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<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>;
+};
+
+namespace nihil {
+
+template <typename T>
+struct optional_awaitable {
+ std::optional<T> o;
+
+ auto await_ready()
+ {
+ return o.has_value();
+ }
+
+ auto await_resume()
+ {
+ return *o;
+ }
+
+ template <typename U>
+ void await_suspend(std::coroutine_handle<optional_promise<U>> h)
+ {
+ h.promise().data->emplace(std::nullopt);
+ h.destroy();
+ }
+};
+
+} // namespace nihil
+
+namespace std {
+
+export template <typename T>
+auto operator co_await(std::optional<T> o) {
+ return nihil::optional_awaitable<T>{std::move(o)};
+}
+
+} // namespace std
+
+/**********************************************************************
+ * std::expected
+ */
+
+namespace nihil {
+
+template <typename T, typename E>
+struct expected_promise {
+ return_object_holder<std::expected<T, E>>* 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<E> err)
+ {
+ data->emplace(std::move(err));
+ }
+
+ void unhandled_exception()
+ {
+ std::rethrow_exception(std::current_exception());
+ }
+};
+
+} // namespace nihil
+
+// 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.
+
+export template <typename T, typename E, typename... Args>
+struct std::coroutine_traits<std::expected<T, E>, Args...> {
+ using promise_type = nihil::expected_promise<T, E>;
+};
+
+namespace nihil {
+
+template <typename T, typename E>
+struct expected_awaitable {
+ std::expected<T, E> o;
+
+ auto await_ready()
+ {
+ return o.has_value();
+ }
+
+ auto await_resume()
+ {
+ return *o;
+ }
+
+ template <typename P>
+ void await_suspend(std::coroutine_handle<P> h)
+ {
+ h.promise().data->emplace(std::unexpected(o.error()));
+ h.destroy();
+ }
+};
+
+} // namespace nihil
+
+namespace std {
+
+export template <typename T, typename E>
+auto operator co_await(std::expected<T, E> o) {
+ return nihil::expected_awaitable<T, E>{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 <expected>
+#include <filesystem>
+#include <system_error>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+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<fd, std::error_code>
+{
+ 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 <cerrno>
#include <cstring>
+#include <format>
#include <optional>
+#include <system_error>
#include <utility>
#include <sys/types.h>
@@ -19,9 +21,42 @@ 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 <expected>
+#include <filesystem>
+#include <iterator>
+#include <ranges>
+#include <span>
+#include <system_error>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+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<char> auto &&iter)
+ -> std::expected<void, std::error_code>
+{
+ auto do_write = [&](fd &&file) -> std::expected<void, std::error_code>
+ {
+ auto constexpr bufsize = std::size_t{1024};
+ auto buffer = std::array<char, bufsize>{};
+
+ 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 <algorithm>
#include <cerrno>
+#include <format>
#include <iterator>
#include <string>
#include <utility>
@@ -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<typename... Args>
- table_spec_error(std::format_string<Args...> fmt, Args &&...args)
- : generic_error(fmt, std::forward<Args>(args)...)
- {}
+ table_spec_error(std::string what) : generic_error(std::move(what)) {}
};
/*
@@ -145,8 +142,8 @@ auto parse_field_flags(field_spec<Char> &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<Char>(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 <coroutine>
+#include <expected>
+#include <optional>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("monad: co_await std::optional<> with value", "[nihil]")
+{
+ auto get_value = [] -> std::optional<int> {
+ return 42;
+ };
+
+ auto try_get_value = [&get_value] -> std::optional<int> {
+ 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<int> {
+ return {};
+ };
+
+ auto try_get_value = [&get_value] -> std::optional<int> {
+ 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<int, std::string> {
+ return 42;
+ };
+
+ auto try_get_value = [&get_value] -> std::expected<int, std::string> {
+ 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<int, std::string> {
+ return std::unexpected("error");
+ };
+
+ auto try_get_value = [&get_value] -> std::expected<int, std::string> {
+ 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<typename... Args>
- usage_error(std::format_string<Args...> fmt, Args &&...args)
- : generic_error(fmt, std::forward<Args>(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 <expected>
+#include <filesystem>
+#include <ranges>
+#include <system_error>
+#include <vector>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+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<std::size_t, std::error_code>
+{
+ 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<std::size_t, std::error_code>
+requires(!std::ranges::contiguous_range<decltype(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 "<filename>.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<void, std::error_code>
+{
+ 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