Skip to content

Commit b93a00e

Browse files
bobzhangclaude
andcommitted
fix(json): preserve the sign of -0 in the integer fast path
Fixes #4053. parse("-0") returned +0.0 while parse("-0.0") / parse("-0e0") returned -0.0: the safe-integer fast path in lex_number_end negated the mantissa as an Int64 (where -0 == 0) before converting to Double. Negate after the conversion so every spelling of negative zero keeps the IEEE-754 sign bit, per RFC 8259 number semantics. The conversion-then-negation is exact for the whole safe-integer range. Deterministic regression tests in lex_number_test.mbt cover every spelling of negative zero, including values that underflow to zero (which already preserved the sign). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d5a4518 commit b93a00e

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

json/lex_number.mbt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,12 +399,16 @@ fn ParseContext::lex_number_end(
399399
// returned Double lossless. `reinterpret_as_uint64` / `reinterpret_as_int64`
400400
// are value-preserving here because both operands sit in [0, 2^53), well
401401
// inside the overlap of Int64+ and UInt64.
402+
//
403+
// The sign is applied after the Int64 -> Double conversion so that `-0`
404+
// parses to the IEEE-754 negative zero (`-(0L)` is still `0L`, but
405+
// `-(0.0)` is `-0.0`), matching the `-0.0` / `-0e0` paths below.
402406
if !scan.many_digits &&
403407
scan.exponent == 0L &&
404408
scan.mantissa <= SAFE_INTEGER_LIMIT.reinterpret_as_uint64() {
405-
let v = scan.mantissa.reinterpret_as_int64()
406-
let signed = if scan.negative { -v } else { v }
407-
return { value: signed.to_double(), repr: None }
409+
let v = scan.mantissa.reinterpret_as_int64().to_double()
410+
let value = if scan.negative { -v } else { v }
411+
return { value, repr: None }
408412
}
409413
return ctx.lex_integer_end(start, end)
410414
}

json/lex_number_test.mbt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,27 @@ test "parse number with huge exponent" {
170170
),
171171
)
172172
}
173+
174+
///|
175+
/// Every spelling of a negative zero — including values that underflow to
176+
/// zero — must keep the IEEE-754 sign bit. The integer spelling `-0` used to
177+
/// lose it: the integer fast path negated an `Int64` (where `-0 == 0`)
178+
/// before converting to `Double`.
179+
test "parse preserves the sign of negative zero" {
180+
fn is_negative(text : String) -> Bool raise {
181+
guard @json.parse(text) is Number(n, ..) else { fail("not a number") }
182+
n.reinterpret_as_int64() < 0L
183+
}
184+
185+
// Zero literals.
186+
assert_true(is_negative("-0"))
187+
assert_true(is_negative("-0.0"))
188+
assert_true(is_negative("-0e0"))
189+
assert_true(is_negative("-0.00E-7"))
190+
assert_false(is_negative("0"))
191+
assert_false(is_negative("0.0"))
192+
// Negative values that underflow to zero.
193+
assert_true(is_negative("-1e-400"))
194+
assert_true(is_negative("-4.9e-325"))
195+
assert_true(is_negative("-1e-999999999999999999999999999999999999"))
196+
}

0 commit comments

Comments
 (0)