Serialize numbers at full precision instead of truncating to 6 digits - #14
Conversation
JSON.str routed every JSON.Num through Double.str, which is snprintf "%g" — six significant digits — so serialization silently corrupted numbers: 3.141592653589793 became "3.14159" and 1234567890.0 became "1.23457e+09". parse read full precision back, but str threw it away. num-str instead renders the fewest significant digits that parse back to exactly the input (searching %g precision 1..17), preferring a plain decimal and falling back to exponent form only for magnitudes no decimal precision can round-trip. Simple values are unchanged (3.14, 42, 10).
There was a problem hiding this comment.
Build & Tests
Checked out claude/serialize-number-precision at e7f575d (merge-base = origin/main HEAD b271233, so the branch is current):
carp -x test/json.carp— 284 / 0 (was 277; +7 new).carp-fmt --check,angler,carp -x gendocs.carp— all clean. Matches green CI (ubuntu + macos). No CHANGELOG in this repo.
Findings
The bug is real and the fix is correct — I didn't just trust the suite.
Bug confirmed: JSON.str routed every Num through Double.str = snprintf("%g") = 6 significant digits, so 3.141592653589793 serialized as 3.14159 and 1234567890.0 as 1.23457e+09 — a silent parse→serialize corruption on the core path.
Fix reviewed (json.carp:522-544): num-str walks %g precision 1→17, keeps the shortest rendering that Double.from-string round-trips back to the exact input, prefers a decimal over an exponent form, and falls back to %.17g. Pure Carp over the existing Double.format primitive — no new C. The %.17g fallback is effectively unreachable (17 sig digits always round-trips a double) but is a safe default.
Non-finite handling: verified num-str never sees NaN/inf — the (if (or (Double.nan? n) (Double.inf? n)) …) guard at json.carp:567 catches them upstream and routes to the existing NonFiniteNumber / "null" path, untouched by this change.
Stress test (beyond the suite): I round-tripped 3116 doubles through JSON.str→JSON.parse — pi, 0.1+0.2, max-double, min-normal, min-subnormal (5e-324), 2^53±1, powers of 1.7/3/10, and ~2500 fractions — asserting exact equality. 0 failures. Simple values stay byte-identical to before (3.14→3.14, 10→10 not 1e+01, 1234567890 in decimal form), which the new tests also pin.
Verdict: merge
Genuine silent-data-corruption fix on a flagship lib's serialize path, correct across every double I threw at it, no new C, all gates green.
The bug
JSON.str/JSON.pretty-strserialize everyJSON.NumthroughDouble.str, which issnprintf("%g", …)— six significant digits. So serialization silently loses precision:JSON.strbefore3.1415926535897933.141593.1415926535897931234567890.01.23457e+0912345678900.12345670.1234570.1234567JSON.parsereads full precision back (viaDouble.from-string), butstrthen throws most of it away, so a parse → serialize round-trip corrupts numbers. The existing tests missed it because every serialized number was ≤ 6 significant digits.The fix
A private
num-strrenders the fewest significant digits that parse back to exactly the input — it walks%gprecision from 1 to 17 and keeps the first rendering whoseDouble.from-stringequals the original. It prefers a plain-decimal form and only falls back to exponent notation for magnitudes no decimal precision can round-trip. This is pure Carp on top of the existingDouble.formatprimitive — no new C.Output stays clean and, for numbers already representable in ≤ 6 digits, byte-identical to before:
NaN / infinity are unaffected — they're still caught by the existing guard above the number path and reported as
NonFiniteNumber.Tests
Added 7 assertions to the serialization block: full-precision preservation, high-precision
parse→strround-trip, the0.1 + 0.2case, large-whole-number decimal form, and exact round-trips for magnitudes that require exponent notation. Full suite: 284 / 0 (was 277).carp-fmt --check,angler, andgendocsall clean.