fix(json): encode non-finite numbers as strings in stringify - #4212
Open
bobzhang wants to merge 1 commit into
Open
fix(json): encode non-finite numbers as strings in stringify#4212bobzhang wants to merge 1 commit into
bobzhang wants to merge 1 commit into
Conversation
`Json::stringify` wrote a `Number` holding NaN or an infinity as a bare `NaN` / `Infinity` / `-Infinity` literal, which is not JSON: no RFC 8259 parser reads it back. Encode those three the way `Double::to_json` already does, as the strings "NaN", "Infinity" and "-Infinity" — exactly what `Double::from_json` accepts — so the value survives a round trip. Only a directly constructed `Number` reaches this path: the parser records the source text in `repr` for every literal that overflows to an infinity, and that branch writes `repr` verbatim without consulting the double. The guard is a tiny entry with the non-finite tail outlined, so inlining it into the `stringify` loop stays cheap for ordinary numbers. Also document the `repr` contract this relies on: when present, `repr` must be valid JSON number syntax denoting the accompanying `Double`, since `stringify` writes it in place of the double; which literals the parser attaches it to (every integer past the exact-integer range, whether or not it lands on one, plus everything overflowing to an infinity); and that equality compares only the numeric value, so two equal `Json` values can stringify differently. Reverting the `write_number` call fails exactly the two tests added here (the `Json::number` doc test and `stringify non-finite number`); with it, 7529 pass on wasm-gc and json+builtin pass on all four backends. Reviewed with Codex CLI: "No blocking issues in the two revised passages. They correctly distinguish invalid JSON, a different JSON type, and a mismatched numeric value." Signed-off-by: Codex CLI <codex@openai.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VubGDsJHzgC6ykiq7t4hrY
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped, preserves existing repr behavior, and is backed by targeted tests validating both correctness and round-trip semantics.
Pull request overview
This PR fixes Json::stringify so that Json::number values holding non-finite Doubles (NaN / ±Infinity) are emitted as valid JSON by encoding them as strings ("NaN", "Infinity", "-Infinity"), matching the existing Double JSON round-trip behavior and preserving the existing repr precedence contract.
Changes:
- Add a small
write_numberhelper injson/json.mbtthat routes non-finite values to a dedicated encoder while keeping the common finite-number path fast. - Add tests covering non-finite number stringification and round-tripping through
@json.parse+@json.from_json. - Document the
reprcontract and clarify non-finite behavior inbuiltin/json.mbt(including a doc example).
File summaries
| File | Description |
|---|---|
| json/json.mbt | Routes Number stringification through a non-finite guard to ensure valid JSON output when repr is absent. |
| json/json_test.mbt | Adds coverage for NaN/±Infinity stringification and round-trip decoding; asserts repr still wins when present. |
| builtin/json.mbt | Documents Number.repr semantics, parser attachment cases, and the non-finite stringify behavior for Json::number. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Collaborator
Coverage Report for CI Build 6583Coverage increased (+0.005%) to 89.275%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
bobzhang
commented
Sep 8, 2026
| } | ||
|
|
||
| ///| | ||
| fn write_non_finite_number(buf : StringBuilder, number : Double) -> Unit { |
Contributor
Author
There was a problem hiding this comment.
inine this function?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Json::stringifywrote aNumberholding NaN or an infinity as a bare token:None of those are JSON — no RFC 8259 parser reads them back,
@json.parseincluded.Fix
Encode the three non-finite values the way
Double::to_jsonalready does — as the strings"NaN","Infinity"and"-Infinity"— which is exactly the setDouble::from_jsonaccepts, so the value survives a round trip:Only a directly constructed
Numberreaches this path. The parser records the source text inreprfor every literal that overflows to an infinity (1e400→Number(Infinity, repr=Some("1e400"))), and that branch writesreprverbatim without consulting the double — so parsed documents are unaffected, andreprstill takes precedence where it is present.The guard is a tiny
#inlineentry with the non-finite tail outlined, so inlining it into thestringifyloop stays cheap for ordinary numbers.Docs
The fix leans on the
reprcontract, which was undocumented (a bare// 1.0000000000000000000e100comment on the constructor). Now stated on bothpub enum JsonandJson::number:reprmust be valid JSON number syntax denoting the accompanyingDouble—stringifywrites it in place of the double, so text that is not a JSON number makes the output invalid JSON or gives it a different JSON type, and text denoting another value silently changes what the document says;9007199254740992keeps its text too) plus everything overflowing to an infinity, and nothing else (0.1and1e-400getNone);repr, so two equalJsonvalues can stringify differently.Verification
None => buf.write_object(n)fails exactly the two tests added here — theJson::numberdoc test andstringify non-finite number(3227 passed / 2 failed).moon test: 7529 passed, 0 failed.moon test --target all -p moonbitlang/core/json -p moonbitlang/core/builtin: wasm 3229, wasm-gc 3229, js 3199, native 3188 — all passed.moon fmt,moon check,moon infoclean; no.mbtichange (both new helpers are private).Reviewed and signed off by Codex CLI (three rounds; it blocked twice on doc claims that overstated when
repris attached and what a badreprproduces).Signed-off-by: Codex CLI codex@openai.com
🤖 Generated with Claude Code
https://claude.ai/code/session_01VubGDsJHzgC6ykiq7t4hrY