mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
fix(codec): a whole-valued float still decodes as an integer (#32)
The signedness/range check in #31 went one step too far: it rejected 3.0 for an `int`, not just 3.7. That broke four long-standing test_basic_module_cpp cases (`addInts(3.0, 4.0)`, `echoInt(42.0)`, `isPositive(5.0)`, `twoArgs(hi, 3.0)`) which pass a whole-valued double where the contract declares an integer. They are right and the check was wrong. JSON does not distinguish 3 from 3.0, and this codec already says so in the other direction — Codec<double> accepts an integral number because "2 and 2.0 are the same value to JSON, and every encoder that sees a whole double may emit either". The two directions have to agree. It also matters in practice rather than in principle: logoscore's CLI types its arguments by parsing, so `logoscore call m addInts 3.0 4.0` produces JSON floats. Refusing them rejects a caller over a spelling of the same number. So a float decodes as an integer when it has no fractional part and fits; 3.7 is still refused, which is what the original change was actually for. Bounds are strict on the upper end for the same reason as the QJsonValue guard: double(int64max) rounds UP to 2^63, so `<=` would admit a value the cast cannot represent. verified: test-modules 176/176 with the four cases green again, and the conformance matrix unchanged at 170 pass / 2 xfail — hostile/int/fractional still expects dispatch_failed and gets it. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c0df466172
commit
3da8de93df
@@ -43,6 +43,7 @@
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
@@ -286,11 +287,38 @@ template <> struct Codec<nlohmann::json, void> {
|
||||
// This rejects rather than coerces, matching the rest of the codec: a value the
|
||||
// declared type cannot represent must not reach business logic wearing a
|
||||
// different one.
|
||||
//
|
||||
// A WHOLE-VALUED float is not such a value. JSON does not distinguish 3 from
|
||||
// 3.0, and every encoder that sees a whole double may emit either — which is
|
||||
// exactly the reasoning Codec<double> already gives for accepting an integral
|
||||
// number. The two directions have to agree, so 3.0 decodes as 3 while 3.7 is
|
||||
// still refused. A CLI that types its arguments by parsing (logoscore's does)
|
||||
// produces 3.0 for `3.0`, so refusing it breaks callers over a spelling.
|
||||
template <class T>
|
||||
struct Codec<T, std::enable_if_t<std::is_integral_v<T> && !std::is_same_v<T, bool>>> {
|
||||
static nlohmann::json to(T v) { return v; }
|
||||
static T from(const nlohmann::json& j, const std::string& path)
|
||||
{
|
||||
if (j.is_number_float()) {
|
||||
const double d = j.get<double>();
|
||||
double intPart = 0.0;
|
||||
if (std::modf(d, &intPart) != 0.0)
|
||||
typeError(path, "integer", j);
|
||||
// Strict bounds: double(int64max) rounds UP to 2^63, so `<=` would
|
||||
// admit a value the cast cannot represent (undefined behaviour).
|
||||
if constexpr (std::is_unsigned_v<T>) {
|
||||
if (d < 0.0 || d >= 18446744073709551616.0) // 2^64
|
||||
typeError(path, "unsigned integer in range", j);
|
||||
} else {
|
||||
if (d < -9223372036854775808.0 || d >= 9223372036854775808.0) // ±2^63
|
||||
typeError(path, "signed integer in range", j);
|
||||
}
|
||||
const auto whole = static_cast<long double>(d);
|
||||
if (whole < static_cast<long double>(std::numeric_limits<T>::min()) ||
|
||||
whole > static_cast<long double>(std::numeric_limits<T>::max()))
|
||||
typeError(path, "integer in range", j);
|
||||
return static_cast<T>(d);
|
||||
}
|
||||
if (!j.is_number_integer() && !j.is_number_unsigned())
|
||||
typeError(path, "integer", j);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user