From 92b7b05ea294675114ceba9ff303628b5eb791c8 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sun, 20 Sep 2026 23:49:41 +0200 Subject: [PATCH] Parameters: solve mixed signed / unsigned computation There are some corner cases where the C++ number promotion rules can hurt us badly. Devised a type selection logic to pick a suitable common numeric type explicitly, using `std::common_type` in most cases, yet handling explicitly the case when the computation would wrap into unsigned; in such a mixed sign integral case, we either use int64_t (when its precision suffices), or long double, which can hold 64bit of mantissa without precision loss. --- src/lib/par/scale.hpp | 13 +- src/lib/par/type-handler.hpp | 46 ++++++- tests/lib/par/parameter-scale-test.cpp | 8 +- wiki/thinkPad.ichthyo.mm | 159 +++++++++++++++++-------- 4 files changed, 171 insertions(+), 55 deletions(-) diff --git a/src/lib/par/scale.hpp b/src/lib/par/scale.hpp index f1b4a68e7..ff30ec83f 100644 --- a/src/lib/par/scale.hpp +++ b/src/lib/par/scale.hpp @@ -207,6 +207,7 @@ namespace par { return rawVal; } + /** * Join and combine an additional feed value with an anchor value. * @note actual joining operation is picked based on both scales and types involved. @@ -218,6 +219,8 @@ namespace par { Scale::join (VAL const& value, SRC const& srcFeed, Scale const& feedScale) const { VAL res{}; + using JoinT = NumberJoinType; + if (NOM == metric) UNIMPLEMENTED ("nominal and ordinal scales"); if (LIN == metric) @@ -226,9 +229,9 @@ namespace par { assignConverted (res, value * feedScale.template asFactor (srcFeed)); else if (feedScale.isFactor()) - assignConverted (res, value * srcFeed); + assignConverted (res, JoinT(value) * JoinT(srcFeed)); else - assignConverted (res, value + srcFeed); + assignConverted (res, JoinT(value) + JoinT(srcFeed)); } else if (this->isLogarithmic()) @@ -245,7 +248,8 @@ namespace par { NOTREACHED ("Unexpected metric conversion case"); return conform (res); - } + } // last step: re-establish target scale rules + /** * Simplified join() variant, assuming the feed is based on the same scale. @@ -259,6 +263,9 @@ namespace par { ); } + + + /** transform a logarithmic value into an exponential factor */ template template diff --git a/src/lib/par/type-handler.hpp b/src/lib/par/type-handler.hpp index fc4e4b9ff..1e79acc00 100644 --- a/src/lib/par/type-handler.hpp +++ b/src/lib/par/type-handler.hpp @@ -46,6 +46,11 @@ namespace par { using std::signed_integral; using std::floating_point; using std::numeric_limits; + using std::common_type_t; + using std::is_signed_v; + using std::is_unsigned_v; + using std::is_integral_v; + using std::is_floating_point_v; /* ===== type conversion implementation details ===== */ @@ -154,9 +159,44 @@ _Pragma("GCC diagnostic pop") * the best-precision floating point type to carry out * a numeric operation to combine two values */ - template - using CommonComputeType = std::conditional_t and integral, double - , std::common_type_t>; + template + using CommonComputeType = std::conditional_t and integral, double + , common_type_t>; + + namespace { + /** @internal decision logic for combining numeric parameters */ + template + auto + selectNumberJoinType() + { + if constexpr (is_floating_point_v or is_floating_point_v) + return std::type_identity>{}; + else + if constexpr (is_signed_v == is_signed_v) + return std::type_identity>{}; + else + { // mixed signed/unsigned integral: need a type wide enough to accommodate. + using U = std::conditional_t, W,V>; + if constexpr (8 > sizeof(U)) + return std::type_identity{}; + else + return std::type_identity{}; + // no standard integral type can hold uint64_t's full range signed. + // Expand to long double, which can hold 64bit without precision loss + } + } + } + + /** + * @return A common type to perform simple numeric operations safely. + * - when one of the type parameters is floating-point, computations switch over there + * - on integral types with same signedness, std::common_type handles a sane common ground + * - when mixed computations can be managed within signed 64bit, switch up to that + * - in the remaining problematic case(s), long double is used, as it can accommodate + * the full 64bit value range without loss on platforms relevant for Lumiera. + */ + template + using NumberJoinType = typename decltype(selectNumberJoinType())::type; diff --git a/tests/lib/par/parameter-scale-test.cpp b/tests/lib/par/parameter-scale-test.cpp index b11178c79..19b4f905b 100644 --- a/tests/lib/par/parameter-scale-test.cpp +++ b/tests/lib/par/parameter-scale-test.cpp @@ -177,8 +177,12 @@ namespace test{ feedScale.metric = DEC; // in dB CHECK ( 7 == scale.join (4, int64_t(3), feedScale)); // 4 + 3dB ⟼ 4 * 1.995 ≡ 7.98 ⟼ truncated to 7 - ////////////////////////////OOO die folgende Konvertierung entgleist.... -SHOW_EXPR(short(-3) * uint64_t(2)) + Scale sscale; + Scale uscale = {.neutral = 1}; + + // Note: number promotion rules of C++ would backfire nastily here... + CHECK (short(-3) * uint64_t(2) > numeric_limits::max()); + CHECK (short(-6) == sscale.join (-3, uint64_t(2), uscale)); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index d20e6e7a5..b0ccb82a3 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -85574,9 +85574,7 @@ - - - +

Zwar könnte man auch so argumentieren, daß zunächst der Quell-Feed so adaptiert werden sollte, daß der Basistyp gleich ist... @@ -85593,22 +85591,18 @@ - - + - - - +

...weil wir die Basis-Typen für Parameter entsprechend eingeschränkt haben

- -
+
@@ -85660,29 +85654,23 @@ - - - +

man könnte hier zwar delogarithmieren / relogarithmieren, aber was ist z.B. bei negativem Ergebnis...? und, noch mehr: welchen Sinn hat es zu einem multiplikativen Faktor etwas hinzuzuaddieren (was aber nicht logarithmisch ist)

- -
+
- - - +

Logarithmische Metrik verwendet man nur, wenn auch die Größe, in ihrer Bedeutung für die Wahrnehmung, selber logarithmisch ist. Zwar könnte man sich rein-formal vorstellen, daß so ein Fall durchaus mal auftreten kann — aber dann wäre es besser, die Situation zu hinterfragen: fehlt da vielleicht die Markierung des Feed als logarithmisch?

- -
+
@@ -85695,37 +85683,29 @@ - - - +

d.h. wenn beide Seiten float sind, dann ist das wohl auch so beabsichtigt, und wir bleiben in der float-Domäne. Ebenso wenn eine Seite long-double ist, dann alles dort. Sonst double verwenden

- -
+
- - - +

...weil sich die Constraints typischerweise überlappen

- -
+
- - - +
@@ -85766,22 +85746,79 @@
- -
+
- - - + + + + + + + - + + + + + + + + + + + + + + + + +

+ sonst: Promotion in long double +

+ +
+ + + +

+ bei letzterem hab ich nachgehakt, Claude hat zunächst double vorgeschlagen. +

+

+ Einsichten: +

+
    +
  • + double kann keinen uint64_t exakt halten (das wußte ich) +
  • +
  • + long double ist auf x64 entweder 80bit oder 128 bit, also ausreichend +
  • +
+ +
+
+
+ + + + + + + + + + +
+
+ @@ -87214,8 +87251,18 @@ Prop.1: dist%3 1 2 0 1 2 0 - - + + + + + + + + + + + +
@@ -87455,9 +87502,7 @@ Prop.1: dist%3 1 2 0 1 2 0 - - - +

...da ein Test eigentlich mehr machen sollte, als nur nochmal den Code zu wiederholen; und hier passiert nicht mehr, als ein cap an der boundrary, auf einem komplett offensichtlichen und einfachen Code-Pfad @@ -87472,9 +87517,7 @@ Prop.1: dist%3 1 2 0 1 2 0 - - - +

...denn die eigentlich trickreiche Funktionalität steckt in der Library-Funktion und ist dort schon per Unit-Test abgedeckt, und das korrekte Setup wird innerhalb des Scale-Descriptors gar nicht geprüft (weil diese Prüfung in den umschließenden Kontext gehört, wo die Scale in einen Parameter-Typ eingebunden wird) @@ -188878,6 +188921,28 @@ std::cout << tmpl.render({"what", "World"}) << s + + + + +

    +
  • + Ist nur eine ansonsten leere Struct mit einer typedef "type = T" +
  • +
  • + also die »Identity«-Metafunktion +
  • +
  • + nützlich um eine Typ-Entscheidungslogik in einer Funktion per constexpr-if zu formulieren +
  • +
  • + und natürlich allgemein, um Typen als first-class-citizens weiterzugeben +
  • +
+ +
+ +