diff --git a/research/try.cpp b/research/try.cpp index 972451b90..3fc23c55f 100644 --- a/research/try.cpp +++ b/research/try.cpp @@ -1,6 +1,7 @@ /* try.cpp - to try out and experiment with new features.... * scons will create the binary bin/try */ +// 09/26 - ways to build a cyclically wrapping number domain // 03/26 - runtime index access and iteration over tuple-likes // 06/25 - provide a concept to accept _tuple-like_ objects // 06/25 - investigate function type detection of std::bind Binders @@ -11,425 +12,146 @@ /** @file try.cpp - * Work out a construction scheme to build an efficient element-accessor to data - * in a tuple-like with run-time indexing. As an extension, run-time pulled iteration - * can be established, using the IterExplorer framework. - * @see lib::meta::FunTrampoline - * @see lib::meta::TupleIdxAdaptor + * Investigate techniques for creating a cyclically wrapping numeric domain. + * Actually this could be achieved with my `floordiv()` utility, yet the ramifications + * were not clear -- so I settled to build a stand-alone solution, based on the naive modulus + * technique, attempting to fix the negative offsets. It became clear that this approach would + * have to consider the domain boundaries and identify cases that could not be handlet. At that + * point, I used Claude to scrutinise and improve my draft, leading to a generic tool that is + * now relocated into the \ref util-quant.hpp header + * @see lib::par::Scale */ #include "lib/format-cout.hpp" -#include "lib/format-util.hpp" -#include "test/test-helper.hpp" -#include "test/diagnostic-output.hpp" -#include "lib/meta/common-result.hpp" -#include "lib/meta/tuple-concept.hpp" -#include "lib/meta/tuple-helper.hpp" -#include "lib/meta/function.hpp" -#include "lib/iter-adapter.hpp" -#include "lib/hetero-data.hpp" -#include "lib/meta/trait.hpp" +#include "lib/util-quant.hpp" #include "lib/util.hpp" -#include - -using std::string; - - -template +#include + + template + constexpr NUM _MAX = std::numeric_limits::max(); + + template + constexpr NUM _MIN = std::numeric_limits::lowest(); + + + + +template void -show() - { - SHOW_TYPE(X) - } +showCase (N val, N minVal, N maxVal) +{ + cout << "val:"<"<< util::cyclicWrap (val,minVal,maxVal)< +template void -show() - { - cout << "Tulik!! "<< ::test::showType() < ([](auto i) - { - using Elm = std::tuple_element_t; - cout <<" "<() <"<< util::floordiv(num,den)<` - */ - template - concept typeseq = requires (TSEQ seq) - { - // TSEQ can invoke a function with a Types subclass - [](Types&){/*placeholder*/}(seq); - }; -#endif ///////////////////////////////////////////////////////////////////////////////////////////////////(End) : new generic library functions - - - template class BUILD, typeseq TSEQ> - class FunTrampoline - { - using AllElm = ElmTypes; - using Indices = AllElm::Idx; - using Builder = BUILD; - using Functor = Builder::Functor; - using Table = std::array; - - template - static constexpr Table - buildFunctors (std::index_sequence) - { - return {Builder().template build()...}; - } - - static constexpr Table functors = buildFunctors (Indices{}); - - public: - static constexpr size_t size() { return functors.size(); } - static constexpr size_t memSiz() { return sizeof(functors); } - - static Functor const& - dispatch (size_t idx) - { - ENSURE (idx < functors.size()); - return functors[idx]; - } - - using ResType = _Fun::Ret; - }; - - -#if false ///////////////////////////////////////////////////////////////////////////////////////////////////TODO : new generic library functions - template - using isConst = std::is_const>; - template - using isLRef = std::is_lvalue_reference; - - - template - using Any_Const = ElmTypes::template OrAll; - - template - using All_LRef = ElmTypes::template AndAll; - - template - static constexpr bool Any_Const_v = Any_Const::value; - - template - static constexpr bool All_LRef_v = All_LRef::value; - - - - - template - struct ProbeCommonType - { - - template - using Reconciled = decltype(true ? std::declval() : std::declval()); - - - template - struct FoundResult - : std::true_type - { - using Type = RES; - }; - struct NoResultFound - : std::false_type - { }; - - template> - static FoundResult> - check(int); - - template - static NoResultFound - check(...); - - using FirstResult = decltype(check(0)); - - template - struct Join - { - using Result = FirstResult; - }; - - template - struct Join - { - //wenn RES erfolgreich - using CT = FirstResult::Type; - using Result = ProbeCommonType::Result; - }; - - using Result = Join::Result; - }; - - - - template - struct CommonResult - : ProbeCommonType::Result - { }; - - template - struct CommonResult - : CommonResult - { }; - - template<> - struct CommonResult<> - : std::false_type - { }; - - template - struct CommonResult> - : CommonResult - { }; - - template - using CommonResult_t = CommonResult::Type; - - template - static constexpr bool has_CommonResult_v = CommonResult::value; - - - template - concept reconcilable = requires - { - typename CommonResult::Type; - }; - - template - concept reconcilable_ref = reconcilable - and isLRef_v>; - - template - struct CommonResultTraits - { - static_assert (reconcilable, "unable to reconcile these types"); - - using CommonVal = std::common_type_t; - using ResType = CommonResult_t; - //NOTE: std::common_type decays (strips CV and ref) - static constexpr bool makeConst = isConst_v; - static constexpr bool exposeRef = isLRef_v; - }; -#endif ///////////////////////////////////////////////////////////////////////////////////////////////////(End) : new generic library functions - - - - template - class TupleIndexAdaptor - : public TUP - { - template - struct SubscriptBuilder - { - using Tuple = RefTraits::value_type; - using AddRefs = ElmTypes::template Apply; - using ResType = CommonResult_t; - using Functor = ResType(*)(Tuple&); - - template - constexpr auto - build() - { - return [](Tuple& tuple) -> ResType - { - return getElm (tuple); - }; - } - }; - - using TypeSeq = ElmTypes::Seq; - using Accessor = FunTrampoline; - public: - using ResType = Accessor::ResType; - using reference = ResType; - using value_Type = RefTraits::value_type; - - template - TupleIndexAdaptor(T&& dataSrc) - : TUP{forward (dataSrc)} - { } - - using TUP::TUP; - - static constexpr size_t size() { return Accessor::size(); } - - ResType - operator[] (size_t idx) - { - return Accessor::dispatch(idx)(*this); - } - }; - - template - TupleIndexAdaptor(TUP) -> TupleIndexAdaptor>; -}} - -using namespace lib::meta; - -template -struct Stringify - { - using Tuple = ElmTypes::Tup; - using Functor = string(*)(Tuple const&); - - template - constexpr auto - build() - { - using ElmT = std::tuple_element_t; - - return [](Tuple const& tuple) - { - return util::toString (getElm (tuple)); - }; - } - - }; - - -struct Uber { }; -struct Untr : Uber { }; -struct Unta : Uber { }; +template +void +showFloorWrap (N num, N den) +{ + auto fwrap = util::floorwrap (num,den); + cout << "FloorWrap:"< q="< )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable_ref )) -SHOW_EXPR(( reconcilable_ref )) -SHOW_EXPR(( reconcilable_ref )) -SHOW_EXPR(( reconcilable_ref )) - using TX = CommonResult_t; -SHOW_TYPE(TX) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) - using TX2 = CommonResult_t; -SHOW_TYPE(TX2) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable_ref )) -SHOW_EXPR(( reconcilable_ref )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) - using TX3 = CommonResult_t; -SHOW_TYPE(TX3) - using TX4 = CommonResult_t; -SHOW_TYPE(TX4) -SHOW_EXPR(( reconcilable )) - using TX5 = CommonResult_t; -SHOW_TYPE(TX5) - using TX6 = CommonResult_t; -SHOW_TYPE(TX6) -SHOW_EXPR(( CommonResult::value )) -SHOW_EXPR(( CommonResult::value )) -SHOW_EXPR(( CommonResult::value )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) -SHOW_EXPR(( reconcilable )) + showCase(0.2 ,-2,-1.8); + showCase(0 ,-2,-1.8); + showCase(-0.8,-2,-1.8); + showCase(-1.79,-2,-1.8); + showCase(-1.8,-2,-1.8); + showCase(-1.81,-2,-1.8); + showCase(-1.99,-2,-1.8); + showCase(-2 ,-2,-1.8); + showCase(-2.01,-2,-1.8); + cout << "\n.gulp." < - +#ifdef __cplusplus + #include +#else + #include +#endif /** * storage for a Lumiera unique ID, @@ -44,8 +47,6 @@ typedef lumiera_uid* LumieraUid; #ifdef __cplusplus /* =========== C++ definitions ====================== */ -#include - namespace lib { /** a STL compatible hash value */ diff --git a/src/lib/integral.hpp b/src/lib/integral.hpp index b9d34c6b7..7026d778f 100644 --- a/src/lib/integral.hpp +++ b/src/lib/integral.hpp @@ -21,7 +21,6 @@ #ifndef LIB_INTEGRAL_H #define LIB_INTEGRAL_H -#include #include #include @@ -38,5 +37,5 @@ static_assert(10 <= sizeof(f128)); const uint LIFE_AND_UNIVERSE_4EVER = 42; - + #endif /*LIB_INTEGRAL_H*/ diff --git a/src/lib/luid.c b/src/lib/luid.c index 59bc72db1..9cb1b3bb3 100644 --- a/src/lib/luid.c +++ b/src/lib/luid.c @@ -26,6 +26,7 @@ #include #include #include +#include diff --git a/src/lib/luid.h b/src/lib/luid.h index 3fae6be0b..e9af58656 100644 --- a/src/lib/luid.h +++ b/src/lib/luid.h @@ -46,7 +46,6 @@ #include "lib/hash-value.h" /* Types lumiera_uid and LumieraUid */ -#include diff --git a/src/lib/util-quant.hpp b/src/lib/util-quant.hpp index d8114e590..5c913afdd 100644 --- a/src/lib/util-quant.hpp +++ b/src/lib/util-quant.hpp @@ -22,12 +22,15 @@ #include "lib/integral.hpp" +#include #include #include #include -#include +#include #include +#include + namespace util { @@ -136,7 +139,7 @@ namespace util { } - /** quantise towards ceiling. + /** quantise towards ceiling. * This function models to fit something into a tiled structure. */ template requires std::unsigned_integral @@ -154,6 +157,103 @@ namespace util { + /** + * Transform into a cyclically wrapping domain. + * @tparam N any integral type, signed or unsigned + * @return mapping into the range [minVal...maxVal( + * @remark This function exploits the equivalence between signed and corresponding unsigned type + * to perform the modulus computation with positive numbers, because the modulus operator of C++ + * works flipped for negative numbers, and oriented towards zero, while we want here always + * the same cyclic wrap-around, irrespective of the position relative to zero. + * - mathematically we want: `minVal + (rawVal-minVal) modulus PERIOD` + * - the unsigned representation of a negative number wraps cleanly, and thus maxVal - minVal, + * when converted into unsigned, is off by multiples of the whole number domain length, which + * is again absorbed by wrapping. Thus surprisingly `PERIOD = U(U(maxVal) - U(minVal))` is exact + * and can always be represented in the unsigned type. + * - for rawVal < minVal a flipped definition is used that can also be represented as unsigned: + * ** define dist ≔ minVal - rawval, then dist ≥ 1 for rawVal < minVal + * ** mathematically we need offset ≡ (-dist) modulus PERIOD + * ** we could compute PERIOD - (dist % PERIOD) -- but that would flip for dist ≡ PERIOD + * ** thus both arguments are shifted by -1, which keeps them in-range and computable as unsigned + * @note the repeated casts `U(signed)` prevent C++ integer promotion to kick in (esp. by short and char) + * @remark Claude was used (9/26) to improve and then formally verify this solution. + */ + template + inline constexpr N + cyclicWrap (N rawVal, N minVal, N maxVal) + { + REQUIRE (minVal < maxVal); + using U = std::make_unsigned_t; + const U PERIOD = U(U(maxVal) - U(minVal)); // always representable + U offset; + if (rawVal >= minVal) + offset = U(U(rawVal) - U(minVal)) % PERIOD; + else + { + const U dist = U(U(minVal) - U(rawVal)); // dist ≥ 1 + offset = PERIOD-1 - (dist-1) % PERIOD; // ≡ (-dist) modulus PERIOD + } + return N(U(U(minVal) + offset)); + } + + + /** + * Transform into a cyclically wrapping domain: floating point implementation. + * @return mapping into the range [minVal...maxVal( + * @warning this implementation can not cover extreme numeric values beyond #limit_cyclicWrap() + * @remark within the usable numeric range, the flipped modulus for values below minVal can simply + * be fixed by shifting once by PERIOD. But there is a caveat: extremely small negative offsets + * could be absorbed, so that the result lands on +PERIOD + */ + template + inline constexpr F + cyclicWrap (F rawVal, F minVal, F maxVal) + { + REQUIRE (minVal < maxVal); + const F PERIOD = maxVal - minVal; + F offset = std::fmod (rawVal - minVal, PERIOD); + if (offset < 0) + offset += PERIOD; + if (not (offset < PERIOD)) // can happen due to ε or non-finite values + offset = 0; + F res = minVal + offset; // could be rounded to maxVal + return res < maxVal? res : minVal; + } + + + /** + * Validation helper for the floating-point implementation of #cyclicWrap(). + * Values beyond the computed limit can not be handled by the implementation, + * due to the limited internal resolution of floating-point computations. + * @param period length of the cyclic wrap period (≔ maxVal - minVal) + * @return a limit for each parameter values, taken absolutely + * @remark at the core of the #cyclicWrap() computation sits a numeric difference, + * taken modulo the period length. Each floating point number has a minimum resolution ("ULP"). + * When the ULP of the difference computation surpasses the periodic cycle length, the cyclicWrap + * becomes meaningless (albeit computed correct without glitch). And since that offset is then + * added on top of meanValue, the same argument holds, so `std::abs (minVal) <= limit` is required, + * and by transitive argument also for `maxVal` and `rawVal`. Furthermore, the difference and the + * period length must also be finite numbers by themselves. + * @note for performance reasons, that limit should be computed once, checked against `minVal` and `maxVal`, + * and then before each computation of #cyclicWrap(), `std::abs (rawVal) <= limit` should be checked. + * It is recommended to fail with an hard error, since resolving theses settings typically requires + * an intervention by the user (parameters need to be adjusted). + * @remark Claude was used (9/26) to analyse the computation and derive a boundary criterion. + */ + template + inline constexpr F + limit_cyclicWrap (F period) + { + REQUIRE (std::isfinite (period)); + F limit = period / std::numeric_limits::epsilon(); + if (not std::isfinite (limit)) + limit = std::numeric_limits::max(); + return limit / 2; // exact + } + + + + /** * epsilon comparison of doubles. * @remarks Floating point calculations are only accurate up to a certain degree, @@ -170,12 +270,13 @@ namespace util { * @see https://en.wikipedia.org/wiki/Unit_in_the_last_place * @todo 3/2024 seems we have solved this problem several times meanwhile /////////////////////////////////TICKET #1360 sort out floating-point rounding and precision */ + template inline constexpr bool - almostEqual (double d1, double d2, uint ulp =2) noexcept + almostEqual (F d1, F d2, uint ulp =2) noexcept { using std::fabs; - return fabs (d1-d2) < DBL_EPSILON * fabs (d1+d2) * ulp - or fabs (d1-d2) < DBL_MIN; // special treatment for subnormal results + return fabs (d1-d2) < std::numeric_limits::epsilon() * fabs (d1+d2) * ulp + or fabs (d1-d2) < std::numeric_limits::min(); // special treatment for subnormal results } diff --git a/src/tool/luidgen.c b/src/tool/luidgen.c index bd2478f80..eae964afa 100644 --- a/src/tool/luidgen.c +++ b/src/tool/luidgen.c @@ -32,6 +32,7 @@ #include #include +#include #include #include #include diff --git a/tests/stage/bus-term-test.cpp b/tests/stage/bus-term-test.cpp index 64b8c2de5..350bfcccb 100644 --- a/tests/stage/bus-term-test.cpp +++ b/tests/stage/bus-term-test.cpp @@ -36,7 +36,7 @@ #include "lib/format-string.hpp" #include "lib/format-cout.hpp" #include "lib/time/timevalue.hpp" -#include "lib/luid.h" +#include "lib/hash-indexed.hpp" #include "lib/util.hpp" #include diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index e98a36c47..9d26551c6 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -27951,7 +27951,7 @@

- +

Return value @@ -28104,7 +28104,7 @@ matches that space by default, but can be changed for the above reasons. The helper functions

- cairo_user_to_device() and cairo_user_to_device_distance()  tell you what the device-coordinates are for a user-coordinates position or distance. + cairo_user_to_device() and cairo_user_to_device_distance()  tell you what the device-coordinates are for a user-coordinates position or distance.

@@ -34862,7 +34862,7 @@ -

+

If the question is more than 365 days old, and ...

    @@ -85119,7 +85119,7 @@ -
    void hun(T* obj, class C* c)
    +    
    void hun(T* obj, class C* c)
     {
         //do some checking on c
         if (some conditions from c are true)
    @@ -85694,7 +85694,7 @@
       
       
         

    - Separate hard bounds from the control range. These get conflated constantly and they are different things. A hard bound says values outside are illegal, and conform either clamps or errors. A control range says "this is the travel of the fader", and values outside are perfectly legal but unusual. Gain is the obvious case: the useful range is maybe −60 to +12 dB, but +30 dB is not an error. If you have one min/max pair it will silently become whichever of the two you happened to need first. Two pairs, or one pair plus a flag, costs almost nothing today. + Separate hard bounds from the control range. These get conflated constantly and they are different things. A hard bound says values outside are illegal, and conform either clamps or errors. A control range says "this is the travel of the fader", and values outside are perfectly legal but unusual. Gain is the obvious case: the useful range is maybe −60 to +12 dB, but +30 dB is not an error. If you have one min/max pair it will silently become whichever of the two you happened to need first. Two pairs, or one pair plus a flag, costs almost nothing today.

    @@ -85704,7 +85704,7 @@

    - Decide whether the stored value is in the metric's own units or in a canonical unit. This is the one place where your design page makes a claim that the implementation can quietly break. The page says combining is addition in a logarithmic scale and multiplication in a linear one, and that this is why scale belongs to the type. That's only true if the stored representation actually is in dB. If you store canonical linear and treat the metric as a display attribute, the joining operator becomes ambiguous again, and you're back to the thing the page argues against. My suggestion: store in the metric's units, so "the metric determines the join" is literally true rather than an interpretive claim. + Decide whether the stored value is in the metric's own units or in a canonical unit. This is the one place where your design page makes a claim that the implementation can quietly break. The page says combining is addition in a logarithmic scale and multiplication in a linear one, and that this is why scale belongs to the type. That's only true if the stored representation actually is in dB. If you store canonical linear and treat the metric as a display attribute, the joining operator becomes ambiguous again, and you're back to the thing the page argues against. My suggestion: store in the metric's units, so "the metric determines the join" is literally true rather than an interpretive claim.

    @@ -85716,7 +85716,7 @@ Cyclic interacts with min/max in a way you'll want to be explicit about: for a cyclic scale, min/max define the period rather than a bound, and conform wraps instead of clamping. That's cheap. What isn't cheap, and what you should defer, is cyclic

    - interpolation: shortest arc versus explicit winding for an automation curve crossing the seam. That belongs to the automation function, not to Scale. + interpolation: shortest arc versus explicit winding for an automation curve crossing the seam. That belongs to the automation function, not to Scale.

    @@ -85725,19 +85725,19 @@ -

    +

    Sentinels

    -

    +

    Worth separating two things that both look like sentinels.

    -

    +

    Some are genuine domain members. Minus-infinity gain, "off", the extremal end of a fader. These have a definite position in the ordering and definite algebraic behaviour: −∞ dB is the absorbing element for gain combination, 0 dB is the identity. They belong in the domain, and a log-metric numeric scale that can represent its own absorbing element is more useful than one that can't. IEEE gets this part right, and borrowing -inf for exactly this is reasonable.

    -

    +

    Others are not values at all. "Unset", "indeterminate", "multiple values selected in the GUI", "bypassed". These have no position in the ordering, no join behaviour, and every arithmetic operation on them is a bug. Putting them in-band is how you get NaN's contagion problem, plus a per-type encoding story (NaN payloads for float, INT_MIN for int, nothing sensible for bool). You have four 64-bit slots and only need one for the value. A small out-of-band state word in the container is uniform across every implementation type, costs you nothing, and keeps comparison honest.

    -

    +

    Then make conform the single choke point that recognises and resolves both kinds. Everything downstream gets to assume it's looking at a real value.

    @@ -85749,7 +85749,7 @@ -

    +

    If you want a rule for where to stop, it's this: build now only what you couldn't add later without changing code you can't reach. Behind your vtable, everything is reachable. Two things are not.

      @@ -86059,8 +86059,772 @@ class ScaleRegistry - - + + + + + + + + + + + + + + + + + + + + + + + + + + +

      + famous last words... +

      +

      + Also sollten wir zumindest den pathologischen Fall erkennen, und zwar effizient +

      + +
      + + + + + + +

      + ...weil das auch in einem Release-Build passiert und dann (hoffentlich) zu einem sichtbaren Crash führt. +

      + +
      +
      + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + +

      + wie üblich habe ich Claude ganz bewußt nicht direkt den Stand meiner Überlegungen mitgeteilt; insofern ist es eine Bestätigung daß Claude ebenfalls (nur) zwei Hauptfälle verfolgt: Integral ⟶ Unsigned, sowie Floating-Point. Ebenfalls interessant, daß Claude auf die naheliegende Lösung mit Float-128 überhaupt nicht eingeht (vermutlich weil Claude von den vorhergehenden Chants den Kontext der Frage erkennen kann, und daher weiß, daß ich eine Library-Lösung brauche) +

      + +
      +
      + + + + +

      + Fallunterscheidung std::integral vs std::floating_point +

      + +
      + + + +

      + "Two problems hide behind one symptom here, and they have opposite YAGNI answers: for the integral case, total correctness is cheap and worth having unconditionally; for floating point it is impossible, so the only real decision is where to reject." +

      + +
      + + + + + + +

      + +

      +

      + Integral: do the arithmetic in the unsigned domain +

      +

      + The trick is not to widen the type but to change the domain. For any VAL of width N, the true span maxVal - minVal is at most 2^N-1, so it always fits into make_unsigned_t<VAL> — and unsigned arithmetic is modular by definition, so no UB even when the intermediate "overflows". +

      +
      +
      +
      +
      + +
      +
      +
      +
      +

      + template<std::integral VAL> +

      +

      + constexpr VAL +

      +

      + wrapCyclic (VAL rawVal, VAL minVal, VAL maxVal) +

      +

      +   { +

      +

      +      REQUIRE (minVal < maxVal); +

      +

      +      using U = std::make_unsigned_t<VAL>; +

      +

      +      const U PERIOD = U(U(maxVal) - U(minVal)); // exact, always representable in U +

      +

      +      U off; +

      +

      +      if (rawVal >= minVal) +

      +

      +        off = U(U(rawVal) - U(minVal)) % PERIOD; +

      +

      +      else +

      +

      +        { +

      +

      +           const U dist = U(U(minVal) - U(rawVal)); // ≥ 1, so dist-1 cannot underflow +

      +

      +           off = PERIOD-1 - (dist-1) % PERIOD; // ≡ (-dist) mod PERIOD +

      +

      +        } +

      +

      +      return VAL(U(U(minVal) + off)); +

      +

      +    } +

      +
      +
      +
      +
      + +
      +
      +
      +
      +

      + Three things worth noting. The PERIOD-1 - (dist-1) % PERIOD form is what avoids the usual double-modulus dance and gets the dist % PERIOD == 0 case right. The redundant-looking outer U(...) casts are not redundant for types narrower than int: integral promotion drags unsigned short operands up to int, and the modular wrap you rely on then doesn't happen. And the final VAL(U)  conversion is well-defined modular since C++20, though in practice it never wraps — minVal + off is mathematically in range by construction. +

      +

      + +

      +

      + This covers the full type range, including minVal = INT_MIN, maxVal = INT_MAX. Given that your Scale carries optional min/max qualifiers, that case is not exotic at all: toggle cyclic on a scale with no explicit bounds and the naive version is immediately UB. That alone makes the five extra lines worth it. And since make_unsigned_t is identity for unsigned types, one implementation serves signed and unsigned alike. +

      + +
      + + +
      +
      + + + + + + +

      + Floating point: the half-open invariant is the thing that actually bites +

      +

      + template<std::floating_point VAL> +

      +

      + constexpr VAL +

      +

      + wrapCyclic (VAL rawVal, VAL minVal, VAL maxVal) +

      +

      +   { +

      +

      +     REQUIRE (minVal < maxVal); +

      +

      +     const VAL PERIOD = maxVal - minVal; +

      +

      +     VAL off = std::fmod (rawVal - minVal, PERIOD); +

      +

      +     if (off < 0) off += PERIOD;      // this addition rounds... +

      +

      +     if (not (off < PERIOD)) off = 0; // ...and can land exactly on PERIOD +

      +

      +     const VAL res = minVal + off;    // so can this one +

      +

      +     return res < maxVal? res : minVal; +

      +

      +    } +

      +
      +
      +
      +
      + +
      +
      +
      +
      +

      + std::fmod is exact — IEEE 754 specifies the result as exactly representable, no rounding introduced. Everything around it is not. The classic failure: off is a tiny negative like -1e-300, off + PERIOD rounds to exactly PERIOD, and you return maxVal  from a half-open interval. That is not a boundary exotic case; it happens for any input a hair below minVal, which in an automation context is routine. Same for the final minVal + off rounding up to maxVal. Returning minVal there is not a fudge — maxVal ≡ minVal  in a cyclic domain, so it is the correct representative. +

      +

      + The cases I would not write code for: +

      +
        +
      • + rawVal - minVal overflowing to ±∞ (needs fmod(fmod(rawVal,P) - fmod(minVal,P), P), valid because both inner results are exact and their difference is bounded by 2·PERIOD). +
      • +
      • + PERIOD itself overflowing to ∞. +
      • +
      • + Resolution collapse: once |rawVal - minVal| / PERIOD exceeds 1/epsilon, the ulp spacing at rawVal exceeds the period and the answer, while exactly computed, carries no information. +
      • +
      +

      + The reason to skip these is not that they're rare but that the third one is unfixable. If someone wraps 1e18 into a 0.1-wide cycle, no implementation recovers the resolution — so the honest response is to reject, not to compute. One ASSERT at scale definition (PERIOD  finite and PERIOD > 0) plus one in the wrap covering the ratio bound gives you a diagnosis instead of silent nonsense, and costs nothing in release builds. +

      + +
      + +
      +
      +
      + + + + +

      + Auf weitere Nachfrage und Bitte um Erläuterung der gewählten speziellen Formulierungen stellt Claude einen gemeinsamen Lösungsansatz heraus... +

      + +
      + + + + + +

      + Both questions turn out to have a common shape: the awkwardness comes from having moved into a domain (unsigned, resp. finite-precision) where the convenient operation is no longer available, and the fix is to precompute the right quantity instead of patching after the fact. +

      +

      + (1) Why PERIOD-1 - (dist-1) % PERIOD +

      +

      + Start with why the obvious form isn't available. In C++, %  truncates toward zero, so (-7) % 3 is -1, not 2 — the result lands in (-P, 0] for negative dividends, not in [0, P). The usual repair is +

      +
      +
      +
      +
      + +
      +
      +
      +
      +

      + VAL r = (rawVal - minVal) % PERIOD; +

      +

      + if (r < 0) r += PERIOD; +

      +

      + +

      +

      + which is correct, cheap, and unavailable to us, because the whole reason for the unsigned domain is that rawVal - minVal can overflow. Once you're in U, there is no negative dividend to correct — you only have the magnitude dist = minVal - rawVal ≥ 1, and you need (-dist) mod PERIOD. +

      +

      + +

      +

      + The textbook identity for that is +

      +

      + offset = (PERIOD - dist % PERIOD) % PERIOD; +

      +

      + +

      +

      + The outer % is there solely because of one input: when dist % PERIOD == 0, the inner expression yields PERIOD itself, which is out of range. So you pay a second division to fix a single boundary value. +

      +

      + +

      +

      + The shifted form removes that boundary instead of patching it. +

      +

      + +

      +

      + Write +

      +

      +  dist - 1 = q·PERIOD + s with s = (dist-1) mod PERIOD ∈ [0, PERIOD). +

      +

      + Then +

      +

      +   dist = q·PERIOD + s + 1, +

      +

      + so +

      +

      +  -dist ≡ -(s+1) ≡ PERIOD-1-s   (mod PERIOD) +

      +

      + +

      +

      + and PERIOD-1-s lies in [0, PERIOD-1], i.e. already in range — no second reduction possible or needed. The -1/+1  shift maps the problematic residue 0 to the interior, and maps the interior residue PERIOD-1 to 0, which is precisely the value that was missing. +

      +

      + +

      +

      + So the preference is: one division instead of two (and integer division is the only expensive operation in this function — tens of cycles on 64-bit operands), no branch, and no intermediate that can transiently equal PERIOD and leak out through a later refactoring. dist - 1 cannot underflow because the enclosing branch guarantees rawVal < minVal, hence dist ≥ 1. +

      +

      + +

      +

      + (2) One rejection predicate, not two —
            and the near-minVal case isn't one of them +

      +

      + Your instinct that there are two conditions is right in the sense that overflow of rawVal - minVal and resolution collapse are genuinely independent: neither implies the other. With PERIOD = 0.1 the resolution limit sits at |rawVal - minVal| ≈ 4.5e14, nowhere near overflow; with PERIOD ≈ 1e300 the subtraction can overflow while the ratio is still fine. But both are bounds on the same quantity, and if you avoid computing the difference you can cover them with one comparison. +

      +

      + +

      +

      + Let the builder precompute a magnitude limit and store it in the scale: +

      +

      + +

      +

      + // at scale definition, cyclic + floating point +

      +

      + VAL lim = PERIOD / std::numeric_limits<VAL>::epsilon(); +

      +

      + if (not std::isfinite(lim)) +

      +

      +   lim = std::numeric_limits<VAL>::max(); +

      +

      +   lim /= 2; // exact +

      +

      + +

      +

      + The builder then rejects the definition unless std::abs(minVal) <= lim and std::abs(maxVal) <= lim (plus minVal < maxVal, both finite, and PERIOD normal rather than subnormal). A cycle of width 0.1 anchored at 1e300 is not a scale anyone can use, and that judgment belongs at definition time. +

      +

      + +

      +

      + Invocation then needs exactly one check: +

      +

      + +

      +

      + if (not (std::abs(rawVal) <= lim)) +

      +

      +   throw error::Invalid{...}; +

      +

      + +

      +

      + Since |rawVal| ≤ lim and |minVal| ≤ lim, the difference is bounded by 2·lim ≤ PERIOD/ε, which both keeps ulp(rawVal - minVal) ≤ PERIOD and makes the subtraction non-overflowing. And because the predicate is written as a negated <=, NaN and ±∞ fall into the reject branch for free — no separate isfinite test. That matters: without it, fmod(NaN, PERIOD) is NaN, every comparison in the wrap is false, and your not (off < PERIOD) clamp quietly turns it into minVal. A silently invented value is the worst outcome available here. +

      +

      + +

      +

      + Your second candidate — rawVal very close to minVal — is not a rejection case. Cancellation there loses absolute precision in the difference, but the result stays inside the cycle and remains the nearest representable answer; what needs protecting is only the half-open invariant, and the two clamps already do that. Rejecting would be wrong: values just below minVal are the single most common input to a wrap. +

      +

      + +

      +

      + On sanitising the resolution case: I'd argue against it, precisely because this is library code. Clamping or wrapping anyway produces a result indistinguishable from a legitimate one, and the caller has no way to learn that their automation curve has gone numerically meaningless. The information was destroyed in the input — no post-hoc treatment restores it, so there's nothing to sanitise, only something to conceal. +

      +

      + +

      +

      + Throwing is the right call, and it's affordable: one comparison on the hot path, well predicted, with the throw only on the path where the value is already garbage. Two caveats specific to your setting. First, an exception thrown inside a render job must not escape a worker thread — you'll want the job invocation wrapped so it fails the job rather than reaching std::terminate; I assume the scheduler already has that boundary, but this makes it load-bearing. Second, if the "sentinel values outside the continuous range" idea from the Parametrisation design ever lands on NaN, the sentinel has to be intercepted before the cyclic conform, never inside it. +

      +

      + +

      +

      + A nice side effect: if wrapCyclic is constexpr, the throw makes a violating call ill-formed at compile time, so constant-folded scale definitions get rejected by the compiler rather than at runtime. +

      +

      + +

      +

      + Given that Scale already exposes a "conform" operation, the natural home for this is there — cyclic wrapping is a conform, and conform is where out-of-domain inputs are supposed to be adjudicated. +

      + +
      + + + + +

      + Complete Reasoning for the Unsigned-case +

      +

      + +

      +

      + Below, everything named with a lower-case letter is a mathematical integer (element of ℤ, unbounded). Machine operations are written with distinct symbols so the two layers never blur. The proof is in two layers, and your question — wrapped arithmetic or equivalence transformation? — is answered by the split: wrapping is used exactly once, in Lemma 1, to establish that one machine value equals one mathematical value. Everything after that is ordinary arithmetic on non-negative integers, with no wrapping and no appeal to modular representation. +

      +

      + Definitions +

      +

      + Let VAL have width N. Write +

      +
        +
      • + A = [VAL_MIN, VAL_MAX] ⊂ ℤ for the set of values representable in VAL, with |A| = 2^N; +
      • +
      • + B = [0, 2^N - 1] ⊂ ℤ for the set representable in U = make_unsigned_t<VAL>; +
      • +
      • + ι : A → B, ι(v) = v mod 2^N, the value-preserving-mod conversion VAL → U (identity if v ≥ 0, else v + 2^N); +
      • +
      • + ⊖, ⊕ for the machine's unsigned operations on U: a ⊖ b = (a - b) mod 2^N, a ⊕ b = (a + b) mod 2^N. +
      • +
      +

      + For a ∈ ℤ and p ≥ 1, let a mod p  denote the mathematical modulo:
      the unique r ∈ [0, p)  with a ≡ r (mod p). +

      +

      + The modulus operator % in C++ is truncating and therefore agrees with this only on non-negative dividends; that restriction is the sole property of % used below. +

      +

      + +

      +

      + Given m = minVal, M = maxVal, x = rawVal, all in A, with m < M. +

      +

      + Thus  p ≔ M - m is the period-span. +

      +

      + +

      +

      + Goal. Compute the unique offset off ∈ [0, p) with off ≡ x - m (mod p), +

      +

      + then return m + off. +

      +

      + +

      +

      + Observation 0. 1 ≤ p ≤ VAL_MAX - VAL_MIN = 2^N - 1, so p ∈ B. +

      +

      + The span is always representable in U even when it is not representable in VAL. +

      +

      + +

      +

      + Lemma 1 (the only wrapping step) +

      +

      + Let x < m and put dist = m - x ∈ ℤ. Then dist ∈ [1, 2^N - 1] ⊆ B, and the machine expression U(ι(m) ⊖ ι(x)) evaluates to exactly dist. +

      +

      + Proof:  +

      +

      + Lower bound: x < m gives dist ≥ 1. +

      +

      + Upper bound: x ≥ VAL_MIN and m ≤ VAL_MAX give +

      +

      + dist ≤ VAL_MAX - VAL_MIN = 2^N - 1. Hence dist ∈ B. +

      +

      + For the value: ι(v) ≡ v (mod 2^N) by definition,
      so ι(m) - ι(x) ≡ m - x = dist (mod 2^N). +

      +

      + +

      +

      + Therefore +

      +
      +
      +
      +
        ι(m) ⊖ ι(x) = (ι(m) - ι(x)) mod 2^N = dist mod 2^N = dist,
      +
      +
      +
      +

      + the last equality because dist ∈ [0, 2^N). ∎ +

      +

      + +

      +

      + This is the whole role of unsigned wrap-around: it hands us the mathematical dist unharmed, despite x - m being possibly outside A. (The outer cast U(...) in the code is needed when N  is narrower than int, where integral promotion would lift the operands to int and suppress the mod 2^N reduction.) +

      +

      + From here on, all quantities lie in [0, 2^N) and every operation below is checked to stay there, so no further machine wrapping occurs and the reasoning is purely in ℤ. +

      +

      + +

      +

      + Restated goal. Since x - m = -dist, we need off = (-dist) mod p +

      +

      + +

      +

      + Proposition 1 (the two-division form) +

      +

      + Let s₀ = dist mod p. Then (p - s₀) mod p = (-dist) mod p. +

      +

      + Proof:  +

      +

      + dist ≥ 0, so C++ dist % p yields s₀  (truncating and mathematical modulo agree). From dist ≡ s₀, we get -dist ≡ -s₀ ≡ p - s₀ (mod p). So p - s₀ is a correct representative, but s₀ ∈ [0, p)  gives p - s₀ ∈ (0, p] — in range except at the single point s₀ = 0, where the value is p ∉ [0, p). The outer reduction maps p ↦ 0 and fixes [0, p)  pointwise, yielding the representative in [0, p). ∎ +

      +

      + The second division exists solely to repair one input. +

      +

      + Note also that p - s₀ > 0 always, so no unsigned underflow — +

      +

      + the form is safe, just wasteful. +

      +

      + +

      +

      + Proposition 2 (the one-division form) +

      +

      + Let s = (dist - 1) mod p. Then p - 1 - s = (-dist) mod p. +

      +

      + Proof:  +

      +

      + By Lemma 1, dist ≥ 1, hence dist - 1 ≥ 0  and C++ (dist-1) % p yields s. +

      +

      + Congruence. dist - 1 ≡ s (mod p) implies dist ≡ s + 1 (mod p), hence +

      +
      +
      +
      +
        -dist ≡ -(s + 1) ≡ p - s - 1  (mod p).
      +
      +
      +
      +

      + Range. s ∈ [0, p - 1], therefore p - 1 - s ∈ [0, p - 1] ⊂ [0, p). +

      +

      + Since the representative of a residue class in [0, p) is unique,
      and p - 1 - s is both congruent to -dist and inside [0, p), it is (-dist) mod p. ∎ +

      +

      + +

      +

      + No repair step is possible or needed. +

      +

      + Intermediates: p - 1 ≥ 0  since p ≥ 1, and (p-1) - s ≥ 0 since s ≤ p-1, so neither subtraction underflows in U. (Precedence gives PERIOD-1 - (dist-1) % PERIOD = (p-1) - ((dist-1) % p), as intended.) +

      +

      + +

      +

      + Why the shift works +

      +

      + Both propositions factor the same map d ↦ (-d) mod p, but through different intermediate maps on the residue: +

      +
        +
      • + Prop. 1 uses s₀ ↦ p - s₀, which sends [0, p-1]  onto [1, p] — a bijection onto the wrong  interval, off by one at both ends. +
      • +
      • + Prop. 2 uses s ↦ p - 1 - s, the reflection of [0, p-1]  onto itself — an involution of the correct interval. +
      • +
      +

      + The -1/+1 shift is precisely the reindexing that converts the first map into the second: it moves the problematic residue 0 into the interior and supplies the residue p-1 that was otherwise unreachable. +

      +

      + +

      +

      + Worked check, p = 3: +

      +
      +
      +
      +
      + +
      +
      +
      +
      dist                 1  2  3  4  5  6
      +(-dist) mod 3        2  1  0  2  1  0
      +
      +Prop.2: (dist-1)%3   0  1  2  0  1  2
      +        2 - s        2  1  0  2  1  0   ✓ always in range
      +
      +Prop.1: dist%3       1  2  0  1  2  0
      +        3 - s₀       2  1  3  2  1  3   ← out of range at dist = 3, 6
      +        (…)%3        2  1  0  2  1  0   ✓ only after repair
      +
      +
      +
      +

      + Closing the loop +

      +

      + off ∈ [0, p) gives m ≤ m + off ≤ m + p - 1 = M - 1, so m + off ∈ A: the result is representable in VAL, always. The machine computes ι(m) ⊕ off ≡ m + off (mod 2^N), and the conversion back to VAL (modular since C++20) recovers m + off exactly, since that value lies in A. +

      +

      + The positive branch is the same argument with Lemma 1 applied in the other direction: x ≥ m gives x - m ∈ [0, 2^N - 1] ⊆ B, so ι(x) ⊖ ι(m) = x - m exactly, and % on a non-negative dividend is the mathematical modulo directly. +

      + +
      + + +
      +
      + + + + + + +

      + Ich sehe zwar intuitiv die Logik, aber die Beschreibung von Claude ist zweideutig. Daher verlange ich von Claude einen voll ausformulierten mathematischen Beweis +

      + +
      + + + +
      + + + + + + + +

      + zwar sind die nach unsigned gewrappeden werte ι(rawVal) und ι(minVal) rein numerisch andere Zahlen, aber sie haben den gleichen Abstand dist. Das ist tatsächlich der Kern des Ansatzes und das bedarf eines Beweises, da minVal >0 sein kann +

      + +
      + + + + + + + + + + +
      + + + + + +

      + Und zwar, weil sich damit der Code organisieren läßt, ohne den Namepsace mit Spezial-Implementierungen zu belasten, aber auch ohne jetzt wieder eine neue generische Library-Funktion aufzumachen, die ich dann doch wieder nicht sauber ausentwickle und teste.... +

      + +
      + +
      + +
      @@ -104681,7 +105445,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
      -
      +
      #include <functional>
      @@ -114156,7 +114920,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
      -
      template<class RES>
      +    
      template<class RES>
       pair<RES, Ctx>
       runBuild(Builder<RES> buildr, Ctx c)
       {
      @@ -114197,7 +114961,7 @@ runBuild(Builder<RES> buildr, Ctx c)
       
         
         
      -    
      template<class R1, class R2>
      +    
      template<class R1, class R2>
       auto
       StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
       {
      @@ -174376,7 +175140,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
       
         
         
      -    
      mustache tmpl{"Hello {{what}}!"};
      +    
      mustache tmpl{"Hello {{what}}!"};
       std::cout << tmpl.render({"what", "World"}) << std::en
      @@ -180680,7 +181444,7 @@ std::cout << tmpl.render({"what", "World"}) << s

      - Earliest deadline first (EDF) or least time to go is a dynamic priority scheduling algorithm used in real-time operating systems to place processes in a priority queue. Whenever a scheduling event occurs (task finishes, new task released, etc.) the queue will be searched for the process closest to its deadline. This process is the next to be scheduled for execution. + Earliest deadline first (EDF) or least time to go is a dynamic priority scheduling algorithm used in real-time operating systems to place processes in a priority queue. Whenever a scheduling event occurs (task finishes, new task released, etc.) the queue will be searched for the process closest to its deadline. This process is the next to be scheduled for execution.

      @@ -183827,7 +184591,7 @@ std::cout << tmpl.render({"what", "World"}) << s -
      +
      struct bar : public sigc::trackable {};
      @@ -183879,7 +184643,7 @@ std::cout << tmpl.render({"what", "World"}) << s The event mask determines which events a widget will receive. Keep in mind that different widgets have different default event masks, and by changing the event mask you may disrupt a widget’s functionality, so be careful. This function must be called while a widget is unrealized. Consider

      - add_events() for widgets that are already realized, or if you want to preserve the existing event mask. This function can’t be used with widgets that have no window. (See get_has_window()). To get events on those widgets, place them inside a Gtk::EventBox and receive events on the event box. + add_events() for widgets that are already realized, or if you want to preserve the existing event mask. This function can’t be used with widgets that have no window. (See get_has_window()). To get events on those widgets, place them inside a Gtk::EventBox and receive events on the event box.

      @@ -186019,10 +186783,10 @@ std::cout << tmpl.render({"what", "World"}) << s Beispiel:

      -    cairo_scale (cr, 100, 100); +    cairo_scale (cr, 100, 100);

      -    cairo_translate (cr, 0.1, 0.1); +    cairo_translate (cr, 0.1, 0.1);

      @@ -186512,7 +187276,7 @@ std::cout << tmpl.render({"what", "World"}) << s -
      +
      template<

      @@ -186664,7 +187428,7 @@ std::cout << tmpl.render({"what", "World"}) << s -