diff --git a/libnum/CMakeLists.txt b/libnum/CMakeLists.txt index 3cc6c59..b05443a 100644 --- a/libnum/CMakeLists.txt +++ b/libnum/CMakeLists.txt @@ -8,9 +8,9 @@ include(CTest) add_library(num STATIC - src/static_checks.cpp) + src/str.cpp) -target_include_directories(num PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_include_directories(num PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") # Library file: libnum.a install(TARGETS num DESTINATION ${LIBDIR}) @@ -18,13 +18,29 @@ install(TARGETS num DESTINATION ${LIBDIR}) install(DIRECTORY include/ DESTINATION ${INCDIR}) #--- -# Testing +# Unit tests #--- -set(TESTS - test/isel_num8.cpp) +set(UNIT_TESTS + test/unit_scalar.cpp + test/unit_static.cpp) -foreach(testfile IN LISTS TESTS) +if(NOT CMAKE_CROSSCOMPILING) + add_executable(numtest ${UNIT_TESTS}) + target_link_libraries(numtest PUBLIC num) + + add_test(NAME "UnitTests" COMMAND numtest) +endif() + +#--- +# Performance tests +#--- + +set(PERF_TESTS + test/isel_num8.cpp + test/isel_num16.cpp) + +foreach(testfile IN LISTS PERF_TESTS) add_test(NAME "${testfile}" COMMAND python "${CMAKE_CURRENT_SOURCE_DIR}/test/isel.py" "${CMAKE_CURRENT_SOURCE_DIR}/${testfile}" diff --git a/libnum/include/num/num.h b/libnum/include/num/num.h index d5a132a..71c3124 100644 --- a/libnum/include/num/num.h +++ b/libnum/include/num/num.h @@ -19,10 +19,6 @@ General idea for an fp -> num conversion: 1. Literally just shift mantissa by exponent - num_fixed_position */ -/* TODO: Template specializations for std::integral_constant that - inlines at compile time to either (1) true/false if out of bounds, or (2) - coerce the int to the fixed point type */ - #pragma once #include @@ -103,15 +99,19 @@ struct num8 inline constexpr bool operator<(int const &i) { return i >= 1; } + inline constexpr bool operator>=(int const &i) { + return i <= 0; + } inline constexpr bool operator<=(int const &i) { return i + !v > 0; } inline constexpr bool operator>(int const &i) { return i + !v <= 0; } - inline constexpr bool operator>=(int const &i) { - return i <= 0; - } + + /* Limits as double */ + static constexpr double minDouble = 0.0; + static constexpr double maxDouble = double(0xff) / 256; }; /* num16: Signed 8:8 fixed-point type @@ -148,7 +148,7 @@ struct num16 inline constexpr explicit operator double() { return (double)v / 256; } /* num16 x num16 -> num32 multiplication - This is efficiently implemented with a muls.l instruction. */ + This is efficiently implemented with a muls.w instruction. */ static constexpr num32 dmul(num16 const &x, num16 const &y); /* Basic arithmetic */ @@ -162,7 +162,7 @@ struct num16 return *this; } inline constexpr num16 &operator*=(num16 const &other) { - v = (v * other.v) >> 8; + v = (v * other.v) / 256; return *this; } inline constexpr num16 &operator/=(num16 const &other) { @@ -177,20 +177,27 @@ struct num16 /* Comparisons with int */ inline constexpr bool operator==(int const &i) { - return ((v & 0xff) == 0) && (v >> 8) == i; + return (int16_t)i == i && (i << 8) == v; } inline constexpr bool operator<(int const &i) { return (v >> 8) < i; } + inline constexpr bool operator>=(int const &i) { + return (v >> 8) >= i; + } + /* Unfortunately the branchless version for this test is expressed in terms + of `v`, not `i`, so it does not simplify well when `i` is known. In that + case, writing eg. `x > num16(0)` is faster than `x > 0`. */ inline constexpr bool operator<=(int const &i) { return (v >> 8) + ((v & 0xff) != 0) <= i; } inline constexpr bool operator>(int const &i) { return (v >> 8) + ((v & 0xff) != 0) > i; } - inline constexpr bool operator>=(int const &i) { - return (v >> 8) >= i; - } + + /* Limits as double */ + static constexpr double minDouble = -128.0; + static constexpr double maxDouble = double(0x7fff) / 256; }; /* num32: Signed 16:16 fixed-point type @@ -257,6 +264,28 @@ struct num32 v %= other.v; return *this; } + + /* Comparisons with int */ + + inline constexpr bool operator==(int const &i) { + return (int16_t)i == i && (i << 16) == v; + } + inline constexpr bool operator<(int const &i) { + return (v >> 16) < i; + } + inline constexpr bool operator>=(int const &i) { + return (v >> 16) >= i; + } + inline constexpr bool operator<=(int const &i) { + return (v >> 16) + ((v & 0xffff) != 0) <= i; + } + inline constexpr bool operator>(int const &i) { + return (v >> 16) + ((v & 0xffff) != 0) > i; + } + + /* Limits as double */ + static constexpr double minDouble = -32768.0; + static constexpr double maxDouble = double(0x7fffffff) / 65536; }; /* Arithmetic with integers */ @@ -331,6 +360,11 @@ struct num64 v %= other.v; return *this; } + + /* Limits as double; note that the double doesn't have enough precision to + represent the entirety of the maximum value. */ + static constexpr double minDouble = -2147483648.0; + static constexpr double maxDouble = 2147483648.0 - double(1) / 2147483648; }; /* The following concept identifies the four num types */ @@ -355,7 +389,7 @@ inline constexpr num16::num16(num32 n): v((uint32_t)n.v >> 8) {} inline constexpr num16::num16(num64 n): v(n.v >> 24) {} inline constexpr num32::num32(num8 n): v(n.v * 256) {} -inline constexpr num32::num32(num16 n): v(n.v * 256) {} +inline constexpr num32::num32(num16 n): v((int32_t)n.v * 256) {} inline constexpr num32::num32(num64 n): v(n.v >> 16) {} inline constexpr num64::num64(num8 n): v((uint64_t)n.v * 16777216) {} @@ -369,7 +403,6 @@ template requires(is_num) inline constexpr bool operator==(T const &left, T const &right) { return left.v == right.v; } - template requires(is_num) inline constexpr bool operator!=(T const &left, T const &right) { return left.v != right.v; diff --git a/libnum/src/str.cpp b/libnum/src/str.cpp index 19ffcad..3713410 100644 --- a/libnum/src/str.cpp +++ b/libnum/src/str.cpp @@ -1,8 +1,10 @@ #include +using namespace num; /* Digits of the decimal part, from most to least significant. Returns the number of digits (which is 0 when x=0) */ static int decimal_digits(char *str, num64 x) { -// x = mod_64(x, num64_const(1)); +// x = mod_64(x, num64_const(1)); + return 0; } diff --git a/libnum/test/isel.py b/libnum/test/isel.py index 9135954..9ece295 100644 --- a/libnum/test/isel.py +++ b/libnum/test/isel.py @@ -35,7 +35,7 @@ # is a shortcut for the number of "non-trivial" instructions, and currently # expands to `[!mov && !rts]`. # -# A test is a normal C++ source built using the library, which exposes +# A test is a normal C++ source file built using the library, which exposes # functions with C linkage (ie. no name mangling) and has specifications in # comments of the form: # diff --git a/libnum/test/isel_num16.cpp b/libnum/test/isel_num16.cpp new file mode 100644 index 0000000..7f373ba --- /dev/null +++ b/libnum/test/isel_num16.cpp @@ -0,0 +1,78 @@ +#include +using namespace num; + +extern "C" { + +// num16_of_num8: %=1 && [extu.b] +num16 num16_of_num8(num8 x) +{ + return num16(x); +} + +// num16_of_num32: %=1 && [shlr8] +num16 num16_of_num32(num32 x) +{ + return num16(x); +} + +// num16_of_num64: %=[sh* || or] +num16 num16_of_num64(num64 x) +{ + return num16(x); +} + +// num16_mul: [shad] && ![jsr] +num16 num16_mul(num16 x, num16 y) +{ + return x * y; +} + +// num16_dmul: [muls.w] +num32 num16_dmul(num16 x, num16 y) +{ + return num16::dmul(x, y); +} + +// num16_eq: [bt* || bf*] && [shll8] +bool num16_eq(num16 x, int i) +{ + return x == i; +} + +// num16_le: ![bt* || bf*] +bool num16_le(num16 x, int i) +{ + return x <= i; +} + +// num16_gt: ![bt* || bf*] +bool num16_gt(num16 x, int i) +{ + return x > i; +} + +// num16_le_0: %<=3 +bool num16_le_0(num16 x) +{ + return x <= num16(0); +} + +// num16_ge_0: %<=3 || (%=4 && [mov.l]) +bool num16_ge_0(num16 x) +{ + return x >= num16(0); +} + +// num16_gt_0: %<=3 +bool num16_gt_0(num16 x) +{ + return x > num16(0); +} + +// num16_lt_0: %<=3 || (%=4 && [mov.l]) +bool num16_lt_0(num16 x) +{ + return x < num16(0); +} + +} /* extern "C" */ diff --git a/libnum/test/unit_check.h b/libnum/test/unit_check.h new file mode 100644 index 0000000..3bfb6da --- /dev/null +++ b/libnum/test/unit_check.h @@ -0,0 +1,102 @@ +//---------------------------------------------------------------------------// +// ," /\ ", Azur: A game engine for CASIO fx-CG and PC // +// | _/__\_ | Designed by Lephe' and the Planète Casio community. // +// "._`\/'_." License: MIT // +//---------------------------------------------------------------------------// +// unit_check.h: Utilities for asserting and printing +// +// This header defines a ToString typeclass and the utility functions +// runWithChecker() which extend the provided function with a Checker argument. +// This object accumulates the results of tests, and stores the names and +// values of the test subjects so they can be printed if any assertion fails. +//--- + +#include +#include +#include +#include +#include + +template +struct ToString {}; + +template +concept to_string = requires { ToString::str; }; + +template requires(is_num) +struct ToString +{ + static std::string str(T x) { + char s[64]; + uint64_t v = (uint64_t)x.v; + if constexpr (sizeof x < 8) + v &= ((1ull << 8 * sizeof x) - 1); + sprintf(s, "%0*lx (%lf)", 2 * (int)sizeof(x), v, (double)x); + return s; + } +}; + +template<> +struct ToString +{ + static std::string str(int i) { + return std::to_string(i); + } +}; + +template requires(to_string && ...) +class Checker +{ +public: + Checker(): m_success(true) {} + + void vars(std::initializer_list names) { + m_names = std::vector(names); + } + void values(Ts... values) { + m_values = std::make_tuple(values...); + } + + bool check(bool b, char const *expr) { + if(!b) { + m_success = false; + fprintf(stderr, "FAILED: %s\n", expr); + printValues(std::index_sequence_for {}); + } + return b; + } + + bool successful() const { + return m_success; + } + +private: + /* Iterate on `m_names` and `m_values` following the index sequence Is; for + each index, prints the variable name and value. */ + template + void printValues(std::index_sequence) { + (fprintf(stderr, " %s: %s\n", m_names[Is], + ToString::type> + ::str(std::get(m_values)).c_str()), ...); + } + + std::vector m_names; + std::tuple m_values; + bool m_success; +}; + +template requires(to_string) +bool runWithChecker(std::function &)> f) +{ + Checker c; + runOnSampleInputs([f, &c](T x) { f(x, c); }); + return c.successful(); +} + +template requires(to_string && to_string) +bool runWithChecker(std::function &)> f) +{ + Checker c; + runOnSampleInputs([f, &c](T x, U y) { f(x, y, c); }); + return c.successful(); +} diff --git a/libnum/test/unit_sample.h b/libnum/test/unit_sample.h new file mode 100644 index 0000000..b01525b --- /dev/null +++ b/libnum/test/unit_sample.h @@ -0,0 +1,174 @@ +//---------------------------------------------------------------------------// +// ," /\ ", Azur: A game engine for CASIO fx-CG and PC // +// | _/__\_ | Designed by Lephe' and the Planète Casio community. // +// "._`\/'_." License: MIT // +//---------------------------------------------------------------------------// +// unit_sample.h: Utility for generating sample values for testing +// +// This file mainly provides the runOnSampleInputs() function which runs a +// boolean function f() on a series of generated inputs. It achieves this by +// reading a storage of pre-computed inputs for each of the arguments of f. +// This requires a storage to exist for each argument's type, which is provided +// by SampleBase and subclassed by Sample. +// +// The meat of the generation is the generateIntSample() function, which +// generates sparse sets of integers of varied size that omit needlessly +// redundant input while densely covering special cases. +//--- + +#pragma once + +#include +#include +#include +#include +#include + +#include +using namespace num; + +//--- +// Integer sampling +//--- + +template +void generateIntSample(std::vector &v, std::function ctor, + Uint start, Uint length, int count) +{ + /* When we get to a set of size 16, do all values. */ + if(count <= 16) { + /* If we have an even-only range, force in some odd numbers too. */ + bool switch_odd = (length >= (Uint)(2*count)); + Uint step = length / count; + while(count-- > 0) { + v.push_back(ctor(start)); + start += switch_odd ? step ^ (count & 1) : step; + } + return; + } + + /* Otherwise, fractally divide into 16 segments and assign a portion of the + available points to each section. We divide the count of points in 16ths + so that there is at least one point in each segment. */ + Uint sublength = length / 16; + int subcount = count / 16; + /* This array must add up to 16. */ + int props[16] = { 4, 1, 1, 0, 0, 2, 0, 0, 1, 0, 2, 0, 0, 0, 1, 4 }; + for(int i = 0; i < 16; i++) { + if(props[i]) + generateIntSample(v, ctor, (Uint)(start + i * sublength), + sublength, subcount * props[i]); + } +} + +//--- +// Input sampling +//--- + +template +struct SampleBase { + static std::vector v; +}; + +template +std::vector SampleBase::v; + +template +struct Sample {}; + +template +concept has_sample = requires { Sample::get; }; + +template<> +struct Sample: SampleBase +{ + static std::vector const &get() { + if(v.size() > 0) + return v; + for(int i = 0; i <= 0xff; i++) { + num8 x; + x.v = i; + v.push_back(x); + } + return v; + } +}; + +template<> +struct Sample: SampleBase +{ + static std::vector const &get() { + if(v.size() > 0) + return v; + auto f = [](uint16_t i) { num16 x; x.v = i; return x; }; + generateIntSample(v, f, 0, 1 << 15, 512); + generateIntSample(v, f, -(1 << 15), 1 << 15, 512); + return v; + } +}; + +template<> +struct Sample: SampleBase +{ + static std::vector const &get() { + if(v.size() > 0) + return v; + auto f = [](uint32_t i) { num32 x; x.v = i; return x; }; + generateIntSample(v, f, 0, 1 << 15, 512); + generateIntSample(v, f, -(1 << 15), 1 << 15, 512); + generateIntSample(v, f, 0, 1ul << 31, 512); + generateIntSample(v, f, 1ul << 31, 1ul << 31, 512); + return v; + } +}; + +template<> +struct Sample: SampleBase +{ + static std::vector const &get() { + if(v.size() > 0) + return v; + auto f = [](uint32_t i) { return i; }; + generateIntSample(v, f, 0, 1 << 15, 512); + generateIntSample(v, f, -(1 << 15), 1 << 15, 512); + generateIntSample(v, f, 0, 1ul << 31, 512); + generateIntSample(v, f, 1ul << 31, 1ul << 31, 512); + return v; + } +}; + +template<> +struct Sample: SampleBase +{ + static std::vector const &get() { + if(v.size() > 0) + return v; + auto f = [](uint64_t i) { num64 x; x.v = i; return x; }; + generateIntSample(v, f, 0, 1 << 15, 512); + generateIntSample(v, f, -(1 << 15), 1 << 15, 512); + generateIntSample(v, f, 0, 1ul << 31, 512); + generateIntSample(v, f, -(1ul << 31), 1ul << 31, 512); + generateIntSample(v, f, 0, 1ull << 63, 512); + generateIntSample(v, f, 1ull << 63, 1ull << 63, 512); + return v; + } +}; + +//--- +// Automatic test functions +//--- + +template requires(has_sample) +void runOnSampleInputs(std::function f) +{ + for(auto t: Sample::get()) + f(t); +} + +template requires(has_sample && has_sample) +void runOnSampleInputs(std::function f) +{ + for(auto t: Sample::get()) + for(auto u: Sample::get()) + f(t, u); +} diff --git a/libnum/test/unit_scalar.cpp b/libnum/test/unit_scalar.cpp new file mode 100644 index 0000000..6a56f3d --- /dev/null +++ b/libnum/test/unit_scalar.cpp @@ -0,0 +1,131 @@ +//---------------------------------------------------------------------------// +// ," /\ ", Azur: A game engine for CASIO fx-CG and PC // +// | _/__\_ | Designed by Lephe' and the Planète Casio community. // +// "._`\/'_." License: MIT // +//---------------------------------------------------------------------------// +// unit_scalar.cpp: Unit tests for scalar arithmetic + +#include +#include "unit_sample.h" +#include "unit_check.h" + +/* Automatically stringify expressions in arguments to Checker.check() */ +#define CHECK(EXPR) check(EXPR, #EXPR) + +/* Test equality of num values up to slight variations */ +template requires(is_num) +bool isEqUpTo(T x, T y, int i) +{ + return abs(x.v - y.v) <= i; +} + +template +bool runInternalArithmeticTest(int error) +{ + return runWithChecker([error](T x, T y, Checker &c) { + c.vars({ "x", "y" }); + c.values(x, y); + + /* Surprisinly the cast back to integer in T() does saturation + arithmetic, so we can't require equality in case of overflow. */ + + double sum = double(x) + double(y); + if(sum >= T::minDouble && sum <= T::maxDouble) + c.CHECK(x + y == T(sum)); + + double diff = double(x) - double(y); + if(diff >= T::minDouble && diff <= T::maxDouble) + c.CHECK(x - y == T(diff)); + + double prod = double(x) * double(y); + if(prod >= T::minDouble && prod <= T::maxDouble) + c.CHECK(isEqUpTo(x * y, T(prod), error)); + + if(y != 0) { + double quot = double(x) / double(y); + if(quot >= T::minDouble && quot <= T::maxDouble) + c.CHECK(x / y == T(quot)); + } + + c.CHECK(y <= 0 || x < 0 || x % y < y); + c.CHECK((x < y) + (x == y) + (x > y) == 1); + c.CHECK((x <= y) - (x == y) + (x >= y) == 1); + }); +} + +template +bool runIntegerComparisonTest() +{ + return runWithChecker([](T x, int i, Checker &c) { + c.vars({ "x", "i" }); + c.values(x, i); + c.CHECK((x < i) + (x == i) + (x > i) == 1); + c.CHECK((x <= i) - (x == i) + (x >= i) == 1); + }); +} + +int main(void) +{ + bool success = true; + + printf("Testing unary laws on num8...\n"); + success &= runWithChecker([](num8 x, Checker &c) { + c.vars({ "x" }); + c.values(x); + c.CHECK(num8(num16(x)) == x); + c.CHECK(num8(num32(x)) == x); + c.CHECK(num8(num64(x)) == x); + c.CHECK(-x + x == 0); + }); + + printf("Testing binary laws on num8...\n"); + success &= runInternalArithmeticTest(0); + + printf("Testing comparisons of num8 with integers...\n"); + success &= runIntegerComparisonTest(); + + printf("Testing unary laws on num16...\n"); + success &= runWithChecker([](num16 x, Checker &c) { + c.vars({ "x" }); + c.values(x); + c.CHECK(num16(num8(x)).v == (x.v & 0xff)); + c.CHECK(x < 0 || num16(num8(x)) == x % num16(1)); + c.CHECK(num16(num32(x)) == x); + c.CHECK(num16(num64(x)) == x); + c.CHECK(-x + x == 0); + }); + + printf("Testing binary laws on num16...\n"); + success &= runInternalArithmeticTest(1); + + printf("Testing comparisons of num16 with integers...\n"); + success &= runIntegerComparisonTest(); + + printf("Testing unary laws on num32...\n"); + success &= runWithChecker([](num32 x, Checker &c) { + c.vars({ "x" }); + c.values(x); + c.CHECK(num32(num8(x)).v == (x.v & 0x0000ff00)); + c.CHECK(num32(num16(x)).v >> 8 == (int16_t)(x.v >> 8)); + c.CHECK(num32(num64(x)) == x); + c.CHECK(-x + x == 0); + }); + + printf("Testing binary laws on num32...\n"); + success &= runInternalArithmeticTest(1); + + printf("Testing comparisons of num32 with integers...\n"); + success &= runIntegerComparisonTest(); + + printf("Testing unary laws on num64...\n"); + success &= runWithChecker([](num64 x, Checker &c) { + c.vars({ "x" }); + c.values(x); + c.CHECK(num64(num8(x)).v == (x.v & 0x00000000ff000000ll)); + c.CHECK(num64(num16(x)).v >> 24 == (int16_t)(x.v >> 24)); + c.CHECK(num64(num32(x)).v >> 16 == (int32_t)(x.v >> 16)); + c.CHECK(-x + x == num64(0)); + }); + + return (success ? 0 : 1); +} diff --git a/libnum/src/static_checks.cpp b/libnum/test/unit_static.cpp similarity index 70% rename from libnum/src/static_checks.cpp rename to libnum/test/unit_static.cpp index 3767a54..9dc8491 100644 --- a/libnum/src/static_checks.cpp +++ b/libnum/test/unit_static.cpp @@ -1,5 +1,11 @@ -#include +//---------------------------------------------------------------------------// +// ," /\ ", Azur: A game engine for CASIO fx-CG and PC // +// | _/__\_ | Designed by Lephe' and the Planète Casio community. // +// "._`\/'_." License: MIT // +//---------------------------------------------------------------------------// +// unit_static.cpp: Compile-time evaluation tests +#include using namespace num; static_assert(sizeof(num8) == 1); @@ -23,31 +29,16 @@ static_assert(num32(num16(-15)) == num32(-15)); static_assert(num64(num16(1)) == num64(1)); static_assert(num64(num16(-1)) == num64(-1)); -/* Comparisons between num8 and int */ - static_assert(num8(0) == 0); static_assert(num8(0) != 1); static_assert(num8(0.5) != 0); -static_assert(num8(0.5) != 1); static_assert(num8(1) == 0); // overflow -static_assert(num8(1) != 1); // overflow static_assert(!(num8(0) < 0)); static_assert(num8(0) < 1); -static_assert(!(num8(0.5) < 0)); -static_assert(num8(0.5) < 1); - -static_assert(num8(0) <= 0); -static_assert(num8(0) <= 1); static_assert(!(num8(0.5) <= 0)); static_assert(num8(0.5) <= 1); - static_assert(!(num8(0) > 0)); -static_assert(!(num8(0) > 1)); static_assert(num8(0.5) > 0); -static_assert(!(num8(0.5) > 1)); - static_assert(num8(0) >= 0); -static_assert(!(num8(0) >= 1)); -static_assert(num8(0.5) >= 0); static_assert(!(num8(0.5) >= 1));