fix(core): encode a float as its shortest decimal
A float was widened to double before encoding, so every float watch, argument default and result read as the double nearest it: gravity -9.81 showed as -9.8100004196167, a radius of 0.1 as 0.10000000149011612, in `get`, `help` and call results alike. Agents read these, and copy them back into `set` and `call`. Codec<float> now encodes the shortest decimal that reads back as the same float (std::to_chars), as a double. Decoding is unchanged, and so is every value: 0.1 decodes to the float 0.1f came from. A double is encoded as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
15e0b4954d
commit
6987033d6b
3 changed files with 25 additions and 3 deletions
|
|
@ -192,7 +192,20 @@ export namespace Crafter::Debug {
|
||||||
|
|
||||||
template<std::floating_point T>
|
template<std::floating_point T>
|
||||||
struct Codec<T> {
|
struct Codec<T> {
|
||||||
static Value Encode(T value) { return Value(static_cast<double>(value)); }
|
static Value Encode(T value) {
|
||||||
|
// A float as the shortest decimal that reads back as it: 0.1f
|
||||||
|
// widened is 0.10000000149011612, which is what an agent would
|
||||||
|
// otherwise read for every float.
|
||||||
|
if constexpr (sizeof(T) < sizeof(double)) {
|
||||||
|
if (std::isfinite(value)) {
|
||||||
|
std::array<char, 32> text{};
|
||||||
|
const auto written = std::to_chars(text.data(), text.data() + text.size(), value);
|
||||||
|
double shortest = 0;
|
||||||
|
if (written.ec == std::errc() && std::from_chars(text.data(), written.ptr, shortest).ec == std::errc()) return Value(shortest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Value(static_cast<double>(value));
|
||||||
|
}
|
||||||
|
|
||||||
static void Decode(const Value& value, T& out, DecodeContext& context) {
|
static void Decode(const Value& value, T& out, DecodeContext& context) {
|
||||||
if (value.IsNumber()) {
|
if (value.IsNumber()) {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ int main() {
|
||||||
});
|
});
|
||||||
Watch("score", [] { return 1234; });
|
Watch("score", [] { return 1234; });
|
||||||
|
|
||||||
Check(registry().Resolve("physics.gravity").value.AsNumber() == static_cast<double>(-9.81F), "a variable watch reads the variable");
|
Check(registry().Resolve("physics.gravity").value.AsNumber() == -9.81, "a variable watch reads the variable, a float as its shortest decimal");
|
||||||
Check(registry().Resolve("bodies[1].pos.y").value.AsNumber() == 4, "a path reads into a watched value");
|
Check(registry().Resolve("bodies[1].pos.y").value.AsNumber() == 4, "a path reads into a watched value");
|
||||||
Check(registry().Resolve("bodies[1]").value.Find("id")->AsInteger() == 2, "an index reads an item");
|
Check(registry().Resolve("bodies[1]").value.Find("id")->AsInteger() == 2, "an index reads an item");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
// maps, a scoped enum, Required) through Value and its text, with no
|
// maps, a scoped enum, Required) through Value and its text, with no
|
||||||
// hand-written serialization. Fields the input leaves out keep their default
|
// hand-written serialization. Fields the input leaves out keep their default
|
||||||
// member initializers, null clears an optional, and an enum also accepts its
|
// member initializers, null clears an optional, and an enum also accepts its
|
||||||
// integer. Guards the reflection that command arguments and results rely on.
|
// integer. A float encodes as its shortest decimal, 0.1 rather than the
|
||||||
|
// 0.10000000149011612 it widens to, which every float watch used to show.
|
||||||
|
// Guards the reflection that command arguments and results rely on.
|
||||||
import std;
|
import std;
|
||||||
import Crafter.Debug.Core;
|
import Crafter.Debug.Core;
|
||||||
using namespace Crafter::Debug;
|
using namespace Crafter::Debug;
|
||||||
|
|
@ -74,6 +76,13 @@ int main() {
|
||||||
Check(!partial.target.has_value(), "null clears an optional");
|
Check(!partial.target.has_value(), "null clears an optional");
|
||||||
Check(partial.mode == Game::Mode::Build, "an enum accepts its integer");
|
Check(partial.mode == Game::Mode::Build, "an enum accepts its integer");
|
||||||
|
|
||||||
|
Check(Encode(0.1F).Dump() == "0.1" && Encode(-9.81F).Dump() == "-9.81" && Encode(4.0F).Dump() == "4.0",
|
||||||
|
std::format("a float reads as its shortest decimal ({}, {})", Encode(0.1F).Dump(), Encode(-9.81F).Dump()));
|
||||||
|
float tenth = 0;
|
||||||
|
DecodeContext tenthContext;
|
||||||
|
Decode(Value::Parse(Encode(0.1F).Dump()), tenth, tenthContext);
|
||||||
|
Check(tenthContext.Ok() && tenth == 0.1F && Encode(0.1).Dump() == "0.1", "and decodes to the same float; a double is unchanged");
|
||||||
|
|
||||||
float nan = 0;
|
float nan = 0;
|
||||||
DecodeContext nanContext;
|
DecodeContext nanContext;
|
||||||
Decode(Encode(std::numeric_limits<float>::quiet_NaN()), nan, nanContext);
|
Decode(Encode(std::numeric_limits<float>::quiet_NaN()), nan, nanContext);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue