Skip to content

Float canonicalization differs from Go on exact decimal ties (round-half-even) #57

Closed

Description

@sinkog

Found by the Rust-vs-Go generated-corpus check added for #54 (20 000 generated documents: the first divergence was at line 8270).

The same JSON number produces different canonical bytes, hence different digests, in Rust and Go:

input   : {"v":[1124191380420304536, 912968984149762.3]}
rust    : ...912968984149762.3
go      : ...912968984149762.2

Both languages parse the literal to the same double (0x4309f2b93fedf812), whose exact value is 912968984149762.25 — exactly halfway between two 16-digit decimals that both round-trip. Isolated reproduction (same bit pattern on both sides):

912968984149762.3, .2, .25 all print as
Rust format!("{f}") (used by canonical_float) 912968984149762.3
Go strconv.FormatFloat(f, 'f', -1, 64) (the reference, canonicaljson.go) 912968984149762.2

Cause

On an exact tie between two equally short candidates, Go's Ryū-based shortest formatting rounds half to even (strconv/ftoaryu.go, ryuDigits32: cNextDigit == 5 && c0 && central&1 == 1). Rust's Display/{:e} picked the odd neighbour here. A3 says "matches Go", so this is a defect in the Rust port, in the digest-bearing path: canonical_float → to_canonical_json → digest. The same shortest-digit generation is used by collection.rs::go_float_display (the Go %v port), which has the same tie behaviour in Go.

The differential corpus did not catch it because no hand-written vector is an exact tie; the existing float vectors are all unambiguous.

Scope

  • One shared shortest-digit routine in the Rust engine that applies Go's tie rule (exact tie ⇒ the candidate with an even last digit, provided it round-trips), used by canonical_float and go_float_display.
  • Unit tests for ties (odd and even neighbour, negative values, a carry case) and for "no change off ties" against the current output.
  • A bulk Rust-vs-Go check over floats with short exact expansions (j · 2^-q), the family where ties occur, kept as a test or a differential vector.
  • Add the example above to conformance/differential / canonicalize so the Go peer pins it too.

Related: #54 (the cross-language property check that found it).

Activity

  1. sinkog commented on Oct 17, 2026

    @sinkog
    MemberAuthor

    Resolved by #58 (merge f5f8bdb). Float canonicalization now matches Go on exact decimal ties (round-half-even). PR check was green on the merged head; main gate is green at 463ffde.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions