diff --git a/src/util-newtype.h b/src/util-newtype.h new file mode 100644 index 00000000..3816af9d --- /dev/null +++ b/src/util-newtype.h @@ -0,0 +1,110 @@ +/* + * Copyright © 2025 Red Hat, Inc. + * + * 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 (including the next + * paragraph) 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. + */ + +#pragma once + +#include "config.h" +#include "util-macros.h" + +/** + * This is a C version of the Rust newtypes in the + * form struct Foo(u32); + * + * Use: DECLARE_NEWTYPE(foo, int) + * + * Defines a single-value struct called foo_t + * with the following helper functions: + * + * - int foo_as_int(foo_t f); + * - int foo(foo f); + * - foo_t foo_from_int(int); + * - foo_t foo_copy(foo_t f); + * - foo_t foo_min(foo_t a, foo b); + * - foo_t foo_max(foo_t a, foo b); + * - foo_t foo_cmp(foo_t a, foo b); + * - bool foo_eq(foo_t a, int b); + * - bool foo_ne(foo_t a, int b); + * - bool foo_le(foo_t a, int b); + * - bool foo_lt(foo_t a, int b); + * - bool foo_ge(foo_t a, int b); + * - bool foo_gt(foo_t a, int b); + * + * Since all the structs are single-value the provided functions don't + * use pointers but pass the struct around as value. The compiler + * hopefully optimizes things so this is the same as passing ints around + * but having some better type-safety. + * + * For example, this logic error is no longer possible using newtypes: + * ``` + * double cm_to_inches(int cm) { return cm / 2.54; } + * + * struct person { + * int age; + * int height; + * }; + * struct person p = { .age = 20, .height = 180 }; + * double inches = cm_to_inches(p.age); + * ``` + * With newtypes this would be: + * ``` + * DECLARE_NEWTYPE(year, int); + * DECLARE_NEWTYPE(cm, int); + * DECLARE_NEWTYPE(in, double); + * + * in_t cm_to_inches(cm_t cm) { return in_from_double(cm_as_int(cm) / 2.54); } + * + * struct person { + * year_t age; + * cm_t height; + * }; + * struct person p = { .age = year_from_int(20), .height = cm_from_int(180) }; + * in_t inches = cm_to_inches(p.age); // Compiler error! + * in_t inches = cm_to_inches(p.height); // Yay + * ``` + * And a side-effect is that the units are documented as part of the type. + */ +#define DECLARE_NEWTYPE(name_, type_) \ + typedef struct { \ + type_ v; \ + } name_##_t; \ + \ + static inline name_##_t name_##_from_##type_(type_ v) { return (name_##_t) { .v = v }; }; \ + static inline type_ name_##_as_##type_(name_##_t name_) { return name_.v; }; \ + static inline type_ name_(name_##_t name_) { return name_##_as_##type_(name_); } \ + static inline name_##_t name_##_copy(name_##_t name_) { return name_##_from_##type_(name_.v); }; \ + static inline name_##_t name_##_min(name_##_t a, name_##_t b) { \ + return name_##_from_##type_(min(a.v, b.v)); \ + }; \ + static inline name_##_t name_##_max(name_##_t a, name_##_t b) { \ + return name_##_from_##type_(max(a.v, b.v)); \ + }; \ + static inline int name_##_cmp(name_##_t a, name_##_t b) { \ + return a.v < b.v ? -1 : (a.v > b.v ? 1 : 0); \ + }; \ + static inline int name_##_eq(name_##_t a, type_ b) { return a.v == b; }\ + static inline int name_##_ne(name_##_t a, type_ b) { return a.v != b; }\ + static inline int name_##_le(name_##_t a, type_ b) { return a.v <= b; }\ + static inline int name_##_lt(name_##_t a, type_ b) { return a.v < b; }\ + static inline int name_##_ge(name_##_t a, type_ b) { return a.v >= b; }\ + static inline int name_##_gt(name_##_t a, type_ b) { return a.v > b; }\ + struct __useless_struct_to_allow_trailing_semicolon__ diff --git a/test/test-utils.c b/test/test-utils.c index eeed7dd1..56511500 100644 --- a/test/test-utils.c +++ b/test/test-utils.c @@ -41,6 +41,7 @@ #include "util-stringbuf.h" #include "util-matrix.h" #include "util-input-event.h" +#include "util-newtype.h" #include "litest.h" @@ -1954,6 +1955,84 @@ START_TEST(multivalue_test) } END_TEST +DECLARE_NEWTYPE(newint, int); +DECLARE_NEWTYPE(newdouble, double); + +START_TEST(newtype_test) +{ + { + newint_t n1 = newint_from_int(1); + newint_t n2 = newint_from_int(2); + + litest_assert_int_eq(newint(n1), 1); + litest_assert_int_eq(newint_as_int(n1), 1); + litest_assert_int_eq(newint(n2), 2); + litest_assert_int_eq(newint_as_int(n2), 2); + + litest_assert_int_eq(newint_cmp(n1, n2), -1); + litest_assert_int_eq(newint_cmp(n1, n1), 0); + litest_assert_int_eq(newint_cmp(n2, n1), 1); + + newint_t copy = newint_copy(n1); + litest_assert_int_eq(newint_cmp(n1, copy), 0); + + newint_t min = newint_min(n1, n2); + newint_t max = newint_max(n1, n2); + litest_assert_int_eq(newint_cmp(min, n1), 0); + litest_assert_int_eq(newint_cmp(max, n2), 0); + + litest_assert(newint_gt(n1, 0)); + litest_assert(newint_eq(n1, 1)); + litest_assert(newint_ge(n1, 1)); + litest_assert(newint_le(n1, 1)); + litest_assert(newint_ne(n1, 2)); + litest_assert(newint_lt(n1, 2)); + + litest_assert(!newint_gt(n1, 1)); + litest_assert(!newint_eq(n1, 0)); + litest_assert(!newint_ge(n1, 2)); + litest_assert(!newint_le(n1, 0)); + litest_assert(!newint_ne(n1, 1)); + litest_assert(!newint_lt(n1, 1)); + } + { + newdouble_t n1 = newdouble_from_double(1.2); + newdouble_t n2 = newdouble_from_double(2.3); + + litest_assert_double_eq(newdouble(n1), 1.2); + litest_assert_double_eq(newdouble_as_double(n1), 1.2); + litest_assert_double_eq(newdouble(n2), 2.3); + litest_assert_double_eq(newdouble_as_double(n2), 2.3); + + litest_assert_int_eq(newdouble_cmp(n1, n2), -1); + litest_assert_int_eq(newdouble_cmp(n1, n1), 0); + litest_assert_int_eq(newdouble_cmp(n2, n1), 1); + + newdouble_t copy = newdouble_copy(n1); + litest_assert_int_eq(newdouble_cmp(n1, copy), 0); + + newdouble_t min = newdouble_min(n1, n2); + newdouble_t max = newdouble_max(n1, n2); + litest_assert_int_eq(newdouble_cmp(min, n1), 0); + litest_assert_int_eq(newdouble_cmp(max, n2), 0); + + litest_assert(newdouble_gt(n1, 0.0)); + litest_assert(newdouble_eq(n1, 1.2)); + litest_assert(newdouble_ge(n1, 1.2)); + litest_assert(newdouble_le(n1, 1.2)); + litest_assert(newdouble_ne(n1, 2.3)); + litest_assert(newdouble_lt(n1, 2.3)); + + litest_assert(!newdouble_gt(n1, 1.2)); + litest_assert(!newdouble_eq(n1, 0.0)); + litest_assert(!newdouble_ge(n1, 2.3)); + litest_assert(!newdouble_le(n1, 0.0)); + litest_assert(!newdouble_ne(n1, 1.2)); + litest_assert(!newdouble_lt(n1, 1.2)); + } +} +END_TEST + int main(void) { struct litest_runner *runner = litest_runner_new(); @@ -2020,6 +2099,8 @@ int main(void) ADD_TEST(stringbuf_test); ADD_TEST(multivalue_test); + ADD_TEST(newtype_test); + enum litest_runner_result result = litest_runner_run_tests(runner); litest_runner_destroy(runner);