aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.ucl
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-22 16:56:39 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-22 16:56:39 +0100
commitd6c3858418c4c00adb18d927135f73ed5a54564a (patch)
tree6a3bc3e70b3a63eadbf99955070adf1fa2e303e7 /nihil.ucl
parent429be0c13e16b51b8fc7695c5f3ff65ac057fca7 (diff)
downloadnihil-d6c3858418c4c00adb18d927135f73ed5a54564a.tar.gz
nihil-d6c3858418c4c00adb18d927135f73ed5a54564a.tar.bz2
nihil.ucl: improve type safety
Diffstat (limited to 'nihil.ucl')
-rw-r--r--nihil.ucl/CMakeLists.txt2
-rw-r--r--nihil.ucl/array.ccm4
-rw-r--r--nihil.ucl/nihil.ucl.ccm3
-rw-r--r--nihil.ucl/object.ccm75
-rw-r--r--nihil.ucl/object_cast.ccm81
-rw-r--r--nihil.ucl/tests/array.cc123
-rw-r--r--nihil.ucl/type.ccm93
7 files changed, 293 insertions, 88 deletions
diff --git a/nihil.ucl/CMakeLists.txt b/nihil.ucl/CMakeLists.txt
index f01fb25..9c6b3f3 100644
--- a/nihil.ucl/CMakeLists.txt
+++ b/nihil.ucl/CMakeLists.txt
@@ -10,7 +10,9 @@ target_sources(nihil.ucl PUBLIC
emit.ccm
error.ccm
object.ccm
+ object_cast.ccm
parser.ccm
+ type.ccm
array.ccm
boolean.ccm
diff --git a/nihil.ucl/array.ccm b/nihil.ucl/array.ccm
index 26cd9b9..7488bde 100644
--- a/nihil.ucl/array.ccm
+++ b/nihil.ucl/array.ccm
@@ -150,9 +150,7 @@ auto operator-(array_iterator<T> const &lhs,
return copy;
}
-static_assert(std::random_access_iterator<array_iterator<object>>);
-
-export template<datatype T>
+export template<datatype T = object>
struct array final : object {
inline static constexpr object_type ucl_type = object_type::array;
diff --git a/nihil.ucl/nihil.ucl.ccm b/nihil.ucl/nihil.ucl.ccm
index d0924d8..909fe18 100644
--- a/nihil.ucl/nihil.ucl.ccm
+++ b/nihil.ucl/nihil.ucl.ccm
@@ -5,9 +5,12 @@
module;
export module nihil.ucl;
+
export import :emit;
export import :object;
+export import :object_cast;
export import :parser;
+export import :type;
export import :array;
export import :boolean;
diff --git a/nihil.ucl/object.ccm b/nihil.ucl/object.ccm
index 43f36fe..7286301 100644
--- a/nihil.ucl/object.ccm
+++ b/nihil.ucl/object.ccm
@@ -23,6 +23,7 @@ export module nihil.ucl:object;
import nihil;
import :error;
+import :type;
namespace nihil::ucl {
@@ -32,26 +33,13 @@ export struct parser;
* The basic object type.
*/
-enum struct object_type {
- object = UCL_OBJECT,
- array = UCL_ARRAY,
- integer = UCL_INT,
- real = UCL_FLOAT,
- string = UCL_STRING,
- boolean = UCL_BOOLEAN,
- time = UCL_TIME,
- userdata = UCL_USERDATA,
- null = UCL_NULL,
-};
-
-template<typename T>
-concept datatype = requires(T o) {
- { T::ucl_type } -> std::convertible_to<object_type>;
-};
-
export struct object {
inline static constexpr object_type ucl_type = object_type::object;
+ // Create an object from an existing ucl_object_t. We assume the
+ // object has already been referenced.
+ object(::ucl_object_t *object) : _object(object) {}
+
// Free our object on destruction.
virtual ~object() {
if (_object)
@@ -100,28 +88,8 @@ export struct object {
// Return the type of this object.
auto type(this object const &self) -> object_type
{
- switch (ucl_object_type(self.get_ucl_object())) {
- case UCL_OBJECT:
- return object_type::object;
- case UCL_ARRAY:
- return object_type::array;
- case UCL_INT:
- return object_type::integer;
- case UCL_FLOAT:
- return object_type::real;
- case UCL_STRING:
- return object_type::string;
- case UCL_BOOLEAN:
- return object_type::boolean;
- case UCL_TIME:
- return object_type::time;
- case UCL_USERDATA:
- return object_type::userdata;
- case UCL_NULL:
- return object_type::null;
- default:
- std::abort();
- }
+ auto utype = ::ucl_object_type(self.get_ucl_object());
+ return static_cast<object_type>(utype);
}
// Return the underlying object.
@@ -156,10 +124,6 @@ export struct object {
}
protected:
- // Create an object from an existing ucl_object_t. We assume the
- // object has already been referenced.
- object(::ucl_object_t *object) : _object(object) {}
-
// The object we're wrapping.
::ucl_object_t *_object = nullptr;
@@ -274,29 +238,4 @@ export auto end(object const &) -> iterator::sentinel
return {};
}
-/***********************************************************************
- * Value access by object_cast.
- */
-
-// Exception thrown when object_cast fails.
-export struct bad_object_cast : error {
- bad_object_cast()
- : error("bad object_cast<>: object does not match target type")
- {}
-};
-
-//export template<typename To>
-////auto object_cast(object const &from) -> To = delete;
-
-export template<datatype To>
-auto object_cast(object const &from) -> To
-{
- if (from.type() != To::ucl_type)
- throw bad_object_cast();
-
- auto const *uobj = from.get_ucl_object();
- auto *refptr = ::ucl_object_ref(uobj);
- return To(refptr);
-}
-
} // namespace nihil::ucl
diff --git a/nihil.ucl/object_cast.ccm b/nihil.ucl/object_cast.ccm
new file mode 100644
index 0000000..7291960
--- /dev/null
+++ b/nihil.ucl/object_cast.ccm
@@ -0,0 +1,81 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cstdlib>
+
+#include <ucl.h>
+
+export module nihil.ucl:object_cast;
+
+import :type;
+import :object;
+import :array;
+
+namespace nihil::ucl {
+
+/*
+ * Ensure a UCL object is convertible to another type. Throws type_mismatch
+ * if not.
+ */
+
+// Implementation for basic types.
+template<datatype To>
+struct convert_check
+{
+ auto check(::ucl_object_t const *from) -> void
+ {
+ auto from_type = static_cast<object_type>(::ucl_object_type(from));
+ auto to_type = To::ucl_type;
+
+ // Converting from anything to object is permitted.
+ if (to_type == object_type::object)
+ return;
+
+ // Converting between two equal types is permitted.
+ if (from_type == to_type)
+ return;
+
+ // Otherwise, this is an error.
+ throw type_mismatch(to_type, from_type);
+ }
+};
+
+// Implementation for array.
+template<typename T>
+struct convert_check<array<T>>
+{
+ auto check(::ucl_object_t const *from) -> void
+ {
+ using To = array<T>;
+ auto from_type = static_cast<object_type>(::ucl_object_type(from));
+ auto to_type = To::ucl_type;
+
+ // If the source type is not an array, this is an error.
+ if (from_type != object_type::array)
+ throw type_mismatch(to_type, from_type);
+
+ for (std::size_t i = 0, size = ::ucl_array_size(from);
+ i < size; ++i) {
+ auto const *arr_obj = ::ucl_array_find_index(from, i);
+ convert_check<typename To::value_type>{}.check(arr_obj);
+ }
+ }
+};
+
+/*
+ * Convert a UCL object to another type.
+ */
+export template<datatype To>
+auto object_cast(object const &from) -> To
+{
+ convert_check<To>{}.check(from.get_ucl_object());
+
+ auto const *uobj = from.get_ucl_object();
+ auto *refptr = ::ucl_object_ref(uobj);
+ return To(refptr);
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/tests/array.cc b/nihil.ucl/tests/array.cc
index 60cb61d..220564d 100644
--- a/nihil.ucl/tests/array.cc
+++ b/nihil.ucl/tests/array.cc
@@ -12,18 +12,23 @@ import nihil.ucl;
TEST_CASE("ucl: array: construct", "[ucl]")
{
- auto arr = nihil::ucl::array<nihil::ucl::integer>();
+ using namespace nihil::ucl;
+
+ auto arr = array<integer>();
REQUIRE(arr.size() == 0);
+ REQUIRE(str(arr.type()) == "array");
}
TEST_CASE("ucl: array: push_back", "[ucl]")
{
- auto arr = nihil::ucl::array<nihil::ucl::integer>();
+ using namespace nihil::ucl;
+
+ auto arr = array<integer>();
REQUIRE(arr.size() == 0);
- arr.push_back(nihil::ucl::integer(1));
- arr.push_back(nihil::ucl::integer(42));
- arr.push_back(nihil::ucl::integer(666));
+ arr.push_back(integer(1));
+ arr.push_back(integer(42));
+ arr.push_back(integer(666));
REQUIRE(arr.size() == 3);
REQUIRE(arr[0].value() == 1);
@@ -34,17 +39,18 @@ TEST_CASE("ucl: array: push_back", "[ucl]")
REQUIRE(arr.front() == 1);
REQUIRE(arr.back() == 666);
-
}
TEST_CASE("ucl: array: compare", "[ucl]")
{
- auto arr = nihil::ucl::array<nihil::ucl::integer>();
+ using namespace nihil::ucl;
+
+ auto arr = array<integer>();
arr.push_back(1);
arr.push_back(42);
arr.push_back(666);
- auto arr2 = nihil::ucl::array<nihil::ucl::integer>();
+ auto arr2 = array<integer>();
REQUIRE(arr != arr2);
arr2.push_back(1);
@@ -52,7 +58,7 @@ TEST_CASE("ucl: array: compare", "[ucl]")
arr2.push_back(666);
REQUIRE(arr == arr2);
- auto arr3 = nihil::ucl::array<nihil::ucl::integer>();
+ auto arr3 = array<integer>();
arr3.push_back(1);
arr3.push_back(1);
arr3.push_back(1);
@@ -61,10 +67,15 @@ TEST_CASE("ucl: array: compare", "[ucl]")
TEST_CASE("ucl: array: iterator", "[ucl]")
{
- auto arr = nihil::ucl::array<nihil::ucl::integer>{1, 42, 666};
+ using namespace nihil::ucl;
+
+ auto arr = array<integer>{1, 42, 666};
auto it = arr.begin();
REQUIRE(*it == 1);
+ auto end = arr.end();
+ REQUIRE(it != end);
+ REQUIRE(it < end);
++it;
REQUIRE(*it == 42);
@@ -74,20 +85,26 @@ TEST_CASE("ucl: array: iterator", "[ucl]")
--it;
REQUIRE(*it == 42);
+
+ ++it;
+ REQUIRE(it != end);
+ ++it;
+ REQUIRE(it == end);
}
TEST_CASE("ucl: array: parse", "[ucl]")
{
using namespace std::literals;
+ using namespace nihil::ucl;
auto input = "value = [1, 42, 666]"sv;
- auto obj = nihil::ucl::parse(input);
+ auto obj = parse(input);
auto v = obj.lookup("value");
REQUIRE(v);
REQUIRE(v->key() == "value");
- auto arr = object_cast<nihil::ucl::array<nihil::ucl::integer>>(*v);
+ auto arr = object_cast<array<integer>>(*v);
REQUIRE(arr.size() == 3);
REQUIRE(arr[0] == 1);
REQUIRE(arr[1] == 42);
@@ -96,7 +113,9 @@ TEST_CASE("ucl: array: parse", "[ucl]")
TEST_CASE("ucl: array: emit", "[ucl]")
{
- auto ucl = nihil::ucl::parse("array = [1, 42, 666];");
+ using namespace nihil::ucl;
+
+ auto ucl = parse("array = [1, 42, 666];");
auto output = std::format("{:c}", ucl);
REQUIRE(output ==
"array [\n"
@@ -108,7 +127,9 @@ TEST_CASE("ucl: array: emit", "[ucl]")
TEST_CASE("ucl: array is a sized_range", "[ucl]")
{
- auto arr = nihil::ucl::array<nihil::ucl::integer>{1, 42, 666};
+ using namespace nihil::ucl;
+
+ auto arr = array<integer>{1, 42, 666};
static_assert(std::ranges::sized_range<decltype(arr)>);
auto size = std::ranges::size(arr);
@@ -116,19 +137,87 @@ TEST_CASE("ucl: array is a sized_range", "[ucl]")
auto begin = std::ranges::begin(arr);
static_assert(std::random_access_iterator<decltype(begin)>);
+
auto end = std::ranges::end(arr);
static_assert(std::sentinel_for<decltype(end), decltype(begin)>);
REQUIRE(std::distance(begin, end) == 3);
- auto vec = std::vector<nihil::ucl::integer>();
+ auto vec = std::vector<integer>();
std::ranges::copy(arr, std::back_inserter(vec));
REQUIRE(std::ranges::equal(arr, vec));
auto arr_as_ints =
- arr | std::views::transform(&nihil::ucl::integer::value);
- auto int_vec = std::vector<nihil::ucl::integer::value_type>();
+ arr | std::views::transform(&integer::value);
+ auto int_vec = std::vector<integer::value_type>();
std::ranges::copy(arr_as_ints, std::back_inserter(int_vec));
REQUIRE(int_vec == std::vector<std::int64_t>{1, 42, 666});
}
+
+TEST_CASE("ucl: array: bad object_cast", "[ucl]")
+{
+ using namespace nihil::ucl;
+
+ auto arr = array<integer>();
+
+ REQUIRE_THROWS_AS(object_cast<integer>(arr), type_mismatch);
+}
+
+TEST_CASE("ucl: array: heterogeneous elements", "[ucl]")
+{
+ using namespace std::literals;
+ using namespace nihil::ucl;
+
+ auto obj = parse("array [ 42, true, \"test\" ];");
+ auto v = obj.lookup("array");
+ REQUIRE(v);
+ auto arr = object_cast<array<>>(*v);
+
+ REQUIRE(arr.size() == 3);
+
+ auto int_obj = object_cast<integer>(arr[0]);
+ REQUIRE(int_obj == 42);
+
+ auto bool_obj = object_cast<boolean>(arr[1]);
+ REQUIRE(bool_obj == true);
+
+ auto string_obj = object_cast<string>(arr[2]);
+ REQUIRE(string_obj == "test");
+}
+
+TEST_CASE("ucl: array: heterogenous cast", "[ucl]")
+{
+ using namespace nihil::ucl;
+
+ auto arr = array<>();
+ arr.push_back(integer(42));
+ arr.push_back(boolean(true));
+
+ // Converting to an array<integer> should fail.
+ REQUIRE_THROWS_AS(object_cast<array<integer>>(arr), type_mismatch);
+
+ // Converting to array<object> should succeed.
+ auto obj_arr = object_cast<array<object>>(arr);
+ REQUIRE(obj_arr[0] == integer(42));
+}
+
+TEST_CASE("ucl: array: homogeneous cast", "[ucl]")
+{
+ using namespace nihil::ucl;
+
+ auto arr = array<>();
+ arr.push_back(integer(1));
+ arr.push_back(integer(42));
+
+ auto obj = object(arr.get_ucl_object());
+
+ // Converting to array<string> should fail.
+ REQUIRE_THROWS_AS(object_cast<array<string>>(obj), type_mismatch);
+
+ // Converting to an array<integer> should succeed.
+ auto obj_arr = object_cast<array<integer>>(obj);
+ REQUIRE(obj_arr[0] == 1);
+ REQUIRE(obj_arr[1] == 42);
+}
+
diff --git a/nihil.ucl/type.ccm b/nihil.ucl/type.ccm
new file mode 100644
index 0000000..2aef1a6
--- /dev/null
+++ b/nihil.ucl/type.ccm
@@ -0,0 +1,93 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <concepts>
+#include <string>
+
+#include <ucl.h>
+
+export module nihil.ucl:type;
+
+import :error;
+
+namespace nihil::ucl {
+
+// Our strongly-typed version of ::ucl_type.
+export enum struct object_type {
+ object = UCL_OBJECT,
+ array = UCL_ARRAY,
+ integer = UCL_INT,
+ real = UCL_FLOAT,
+ string = UCL_STRING,
+ boolean = UCL_BOOLEAN,
+ time = UCL_TIME,
+ userdata = UCL_USERDATA,
+ null = UCL_NULL,
+};
+
+// Get the name of a type.
+export auto str(object_type type) -> std::string_view {
+ using namespace std::literals;
+
+ switch (type) {
+ case object_type::object:
+ return "object"sv;
+ case object_type::array:
+ return "array"sv;
+ case object_type::integer:
+ return "integer"sv;
+ case object_type::real:
+ return "real"sv;
+ case object_type::string:
+ return "string"sv;
+ case object_type::boolean:
+ return "boolean"sv;
+ case object_type::time:
+ return "time"sv;
+ case object_type::userdata:
+ return "userdata"sv;
+ case object_type::null:
+ return "null"sv;
+ default:
+ // Don't fail here, since UCL might add more types that we
+ // don't know about.
+ return "unknown"sv;
+ }
+}
+
+// Concept of a UCL data type.
+export template<typename T>
+concept datatype = requires(T o) {
+ { o.get_ucl_object() } -> std::convertible_to<::ucl_object_t const *>;
+ { o.type() } -> std::same_as<object_type>;
+ { T::ucl_type } -> std::convertible_to<object_type>;
+};
+
+// 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))
+ , _expected_type(expected_type)
+ , _actual_type(actual_type)
+ {}
+
+ // The type we expected.
+ auto expected_type(this type_mismatch const &self) -> object_type {
+ return self._expected_type;
+ }
+
+ // The type we got.
+ auto actual_type(this type_mismatch const &self) -> object_type {
+ return self._actual_type;
+ }
+
+private:
+ object_type _expected_type;
+ object_type _actual_type;
+};
+
+} // namespace nihil::ucl