Motivation
FloatValue is currently Value[float], i.e. IEEE-754 binary floating point. This produces the classic surprises:
0.1 + 0.2 == 0.3 # False -> 0.30000000000000004
We worked around this for the new Equal/NotEqual comparison nodes by adding rel_tol/abs_tol parameters (via math.isclose). That's a correct local band-aid, but the rounding error is already baked into the values before they reach the comparison node — so tolerance masks the symptom rather than fixing the cause.
The real fix for the decimal-rounding class of bugs is to carry Decimal end-to-end through the numeric value type and arithmetic nodes (Add, Sum, etc.), so 0.1 + 0.2 is exactly 0.3.
Proposed exploration
Change FloatValue to be driven by Decimal under the hood (or introduce a parallel DecimalValue and migrate). Investigate the blast radius and whether Pydantic cooperates.
Key open questions / risks
-
Pydantic support. Value[T] is a RootModel-style generic. Confirm Pydantic v2 cleanly supports Decimal as a root type for validation and JSON (de)serialization. Pydantic supports Decimal natively, but verify behavior with our model_validate_json casts (cast_string_to_float, etc.) and JSON output. If it resists, determine whether a custom serializer/validator ("strongarming") is needed.
-
End-to-end Decimal, or it's pointless. The win only materializes if a value never round-trips through a binary float:
- JSON/YAML parsing must parse numbers to
Decimal (stdlib json needs parse_float=Decimal; check ruamel.yaml behavior).
- Every numeric node must do Decimal arithmetic, not
float.
- Casts (Integer<->Float<->String) must preserve Decimal.
-
Decimal is not a silver bullet. It's exact only for terminating decimal fractions. 1/3, sqrt(2), etc. still don't terminate (truncated at getcontext().prec, default 28 digits). Division/roots/irrationals will still need tolerance. Scope: this eliminates the 0.1 + 0.2 class, not all float error.
-
Context precision. Decide on and manage decimal.getcontext().prec (global vs. per-operation context). Determines rounding of non-terminating results.
-
Performance. Decimal is meaningfully slower than float. Probably fine for this engine's workloads, but note it.
-
Migration / compatibility. Existing serialized workflows and stored values use float JSON. Decide on a node-version bump + migration story, and whether FloatValue is replaced in place or DecimalValue is added alongside (with casts).
Out of scope
The Equal/NotEqual tolerance params stay regardless — they're still the right tool for division/irrational results even after a Decimal value type lands.
Motivation
FloatValueis currentlyValue[float], i.e. IEEE-754 binary floating point. This produces the classic surprises:We worked around this for the new
Equal/NotEqualcomparison nodes by addingrel_tol/abs_tolparameters (viamath.isclose). That's a correct local band-aid, but the rounding error is already baked into the values before they reach the comparison node — so tolerance masks the symptom rather than fixing the cause.The real fix for the decimal-rounding class of bugs is to carry
Decimalend-to-end through the numeric value type and arithmetic nodes (Add,Sum, etc.), so0.1 + 0.2is exactly0.3.Proposed exploration
Change
FloatValueto be driven byDecimalunder the hood (or introduce a parallelDecimalValueand migrate). Investigate the blast radius and whether Pydantic cooperates.Key open questions / risks
Pydantic support.
Value[T]is a RootModel-style generic. Confirm Pydantic v2 cleanly supportsDecimalas a root type for validation and JSON (de)serialization. Pydantic supportsDecimalnatively, but verify behavior with ourmodel_validate_jsoncasts (cast_string_to_float, etc.) and JSON output. If it resists, determine whether a custom serializer/validator ("strongarming") is needed.End-to-end Decimal, or it's pointless. The win only materializes if a value never round-trips through a binary
float:Decimal(stdlibjsonneedsparse_float=Decimal; check ruamel.yaml behavior).float.Decimal is not a silver bullet. It's exact only for terminating decimal fractions.
1/3,sqrt(2), etc. still don't terminate (truncated atgetcontext().prec, default 28 digits). Division/roots/irrationals will still need tolerance. Scope: this eliminates the0.1 + 0.2class, not all float error.Context precision. Decide on and manage
decimal.getcontext().prec(global vs. per-operation context). Determines rounding of non-terminating results.Performance. Decimal is meaningfully slower than
float. Probably fine for this engine's workloads, but note it.Migration / compatibility. Existing serialized workflows and stored values use float JSON. Decide on a node-version bump + migration story, and whether
FloatValueis replaced in place orDecimalValueis added alongside (with casts).Out of scope
The
Equal/NotEqualtolerance params stay regardless — they're still the right tool for division/irrational results even after a Decimal value type lands.