Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion builtin/json.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,34 @@
///|
/// JSON value type used by core serialization APIs.
///
/// `Number` carries the numeric value as a `Double` together with an optional
/// `repr`, the exact source text of the literal. `stringify` writes `repr` out
/// verbatim in place of the double, so it must be valid JSON number syntax and
/// must denote the accompanying `Double`: text that is not a JSON number makes
/// the output invalid JSON, or turns the value into a different JSON type
/// altogether (`repr="null"` emits `null`), and text denoting some other value
/// silently changes what the document says.
///
/// The parser attaches a `repr` to every integer literal whose digits run past
/// the exact-integer range of a `Double` — whether or not the value happens to
/// land on one, so `9007199254740992` keeps its text too — and to every literal
/// that overflows to an infinity. It leaves `repr` as `None` everywhere else,
/// including decimals that round (`0.1`) and those that underflow to zero
/// (`1e-400`).
///
/// Equality compares numeric values and ignores `repr`, so two equal `Json`
/// values may stringify differently:
///
/// ```mbt check
/// test {
/// let one : Json = Json::number(1)
/// let padded : Json = Json::number(1, repr="1.000")
/// inspect(one == padded, content="true")
/// inspect(one.stringify(), content="1")
/// inspect(padded.stringify(), content="1.000")
/// }
/// ```
///
/// Example:
///
/// ```mbt check
Expand All @@ -27,7 +55,7 @@ pub enum Json {
Null
True
False
Number(Double, repr~ : String?) // 1.0000000000000000000e100
Number(Double, repr~ : String?) // 1.0000000000000000000e100
String(String)
Array(Array[Json])
Object(Map[String, Json])
Expand Down Expand Up @@ -80,9 +108,22 @@ pub fn Json::empty_object() -> Json {
///
/// * `value` : A double-precision floating-point number to be converted to a
/// JSON number.
/// * `repr` : The exact source text of the literal, if it is worth preserving.
/// `stringify` writes it verbatim in place of `value`, so it must be valid JSON
/// number syntax and must denote `value` itself: text that is not a JSON number
/// makes the output invalid JSON or gives it a different JSON type, and text
/// denoting some other value makes the document disagree with the `Double`
/// every consumer reads. Supply it only for literals a `Double` cannot render
/// back as written, such as integers beyond the exact-integer range or
/// magnitudes beyond the range of a `Double` altogether.
///
/// Returns a JSON value representing the given number.
///
/// `NaN` and the infinities have no JSON syntax. Passing one without a `repr`
/// is accepted and stringifies to the strings `"NaN"`, `"Infinity"` and
/// `"-Infinity"`, matching how `Double::to_json` encodes them and what
/// `Double::from_json` accepts back.
///
/// Example:
///
/// ```mbt check
Expand All @@ -92,6 +133,7 @@ pub fn Json::empty_object() -> Json {
/// Json::number(@double.infinity, repr="1e9999999999999999999999999999999").stringify(),
/// content="1e9999999999999999999999999999999",
/// )
/// inspect(Json::number(@double.infinity).stringify(), content="\"Infinity\"")
/// }
/// ```
#owned(repr)
Expand Down
34 changes: 33 additions & 1 deletion json/json.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,38 @@ fn write_indent(
buf.write_string(cache[level])
}

///|
#inline
fn write_number(buf : StringBuilder, number : Double) -> Unit {
// The guard is kept tiny and the non-finite tail outlined so that inlining
// this into the `stringify` loop stays cheap on every ordinary number.
if number.is_nan() || number.is_inf() {
write_non_finite_number(buf, number)
} else {
buf.write_object(number)
}
}

///|
fn write_non_finite_number(buf : StringBuilder, number : Double) -> Unit {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inine this function?

// JSON has no syntax for NaN or the infinities, so a `Number` holding one
// cannot be written as a bare literal. Encode them the way `Double::to_json`
// does — as the strings "NaN", "Infinity" and "-Infinity" — since
// `Double::from_json` accepts exactly those three, so the value still
// survives a round trip.
//
// Only a directly constructed `Number` reaches this: the parser stores the
// source text in `repr` for every literal that overflows to an infinity, and
// that branch writes `repr` verbatim without consulting the double.
if number.is_nan() {
buf.write_string("\"NaN\"")
} else if number.is_pos_inf() {
buf.write_string("\"Infinity\"")
} else {
buf.write_string("\"-Infinity\"")
}
}

///|
/// Internal stack frame used by iterative stringify to avoid recursion
priv enum WriteFrame {
Expand Down Expand Up @@ -325,7 +357,7 @@ pub fn Json::stringify(
}
Number(n, repr~) =>
match repr {
None => buf.write_object(n)
None => write_number(buf, n)
Some(r) => buf.write_string(r)
}
True => buf.write_string("true")
Expand Down
45 changes: 45 additions & 0 deletions json/json_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,51 @@ test "stringify number" {
debug_inspect(err, content="[]")
}

///|
test "stringify non-finite number" {
// A directly constructed `Number` can hold a value JSON has no syntax for.
// It is written the way `Double::to_json` encodes one, so the output stays
// valid JSON and `Double::from_json` reads it back.
let nan = Json::number(@double.not_a_number)
let inf = Json::number(@double.infinity)
let neg_inf = Json::number(@double.neg_infinity)
inspect(
nan.stringify(),
content=(
#|"NaN"
),
)
inspect(
inf.stringify(),
content=(
#|"Infinity"
),
)
inspect(
neg_inf.stringify(),
content=(
#|"-Infinity"
),
)
inspect(
[nan, inf, neg_inf].to_json().stringify(),
content=(
#|["NaN","Infinity","-Infinity"]
),
)
let back : Double = @json.from_json(@json.parse(inf.stringify()))
inspect(back == @double.infinity, content="true")
let back : Double = @json.from_json(@json.parse(neg_inf.stringify()))
inspect(back == @double.neg_infinity, content="true")
let back : Double = @json.from_json(@json.parse(nan.stringify()))
inspect(back.is_nan(), content="true")
// `repr` still takes precedence over the double it accompanies.
inspect(
Json::number(@double.infinity, repr="1e999999999").stringify(),
content="1e999999999",
)
}

///|
test "stringify with indent" {
let json : Json = {
Expand Down
Loading