Skip to content

feat(bigint): decode BigInt from JSON numbers - #4213

Open
bobzhang wants to merge 1 commit into
mainfrom
hongbo/bigint-from-json-number
Open

feat(bigint): decode BigInt from JSON numbers#4213
bobzhang wants to merge 1 commit into
mainfrom
hongbo/bigint-from-json-number

Conversation

@bobzhang

@bobzhang bobzhang commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Motivation

Json carries the exact source text of a number literal in Number(_, repr~) — the parser attaches it to every integer literal past the exact-integer range of a Double and to every literal that overflows to an infinity. Nothing ever read it back: stringify wrote it, and every FromJson impl ignored it.

BigInt::from_json accepted only the JSON string that BigInt::to_json emits, so a document from another producer could not be decoded:

let json = @json.parse("12345678901234567890123")
// Number(1.2345678901234568e22, repr=Some("12345678901234567890123"))
let big : BigInt = @json.from_json(json)  // before: decode error

The digits were right there in repr; going through the Double instead would have turned the last six of them into 741440.

Change

BigInt::from_json now also accepts a JSON number that denotes an integer:

  • repr present — it is authoritative and must spell a plain integer literal. An exponent-form literal such as 1e400 is rejected (`1e400` is not a plain integer literal) rather than decoded from the infinity it rounded to; BigInt decoding should not quietly return a rounded value.
  • repr absent — the Double must be finite and integral, and is converted exactly through its significand and exponent. An Int64 detour would cap the range at 2^63, well short of the integers a double holds, so 1e300 decodes to the 301-digit integer that double actually is.

BigInt::to_json is unchanged — it still emits a string, and its round trip still works. This only widens the decoder.

One behavior change: Json::number(123) previously failed to decode, and json/from_json_test.mbt asserted that. It now decodes to 123; the error test covers true and 1.5 instead.

Out of scope

  • Int64/UInt64 from_json accepting numbers — they need a range check and a separate decision.
  • Exposing the exact DoubleBigInt conversion as public API; it is a private helper here.

Verification

  • Mutation-checked twice: ignoring repr fails 4 tests (the doc test decodes 12345678901234567741440); routing through Int64 fails 2 (2^63 saturates to …807, and 1e300 comes out 19 digits long).
  • Boundaries covered: -0.0, 2^52+1, 2^53, 2^53+1, ±2^63, 2^64, @double.max_value (asserted equal to (2N.pow(53N) - 1N) << 971, 309 digits), a subnormal, 1.5, NaN, ±infinity.
  • moon test: 7533 passed, 0 failed.
  • moon test --target all -p moonbitlang/core/bigint -p moonbitlang/core/json: wasm 391, wasm-gc 418, js 392, native 391 — all passed. The js backend matters because it uses the host BigInt.
  • moon fmt, moon check, moon info clean; no .mbti change.

Reviewed and signed off by Codex CLI, which independently cross-checked the significand/exponent conversion against 47,499 finite integral doubles including the requested boundaries.

Signed-off-by: Codex CLI codex@openai.com

🤖 Generated with Claude Code

https://claude.ai/code/session_01VubGDsJHzgC6ykiq7t4hrY

`Json` keeps the source text of a number literal in `Number(_, repr~)`,
but nothing ever read it back: `stringify` wrote it and every `FromJson`
impl ignored it. `BigInt::from_json` accepted only the JSON string that
`BigInt::to_json` emits, so a document from another producer carrying
`12345678901234567890123` could not be decoded without the caller
destructuring `Number` by hand — and going through the `Double` would
have changed the last six digits.

`BigInt::from_json` now also accepts a JSON number that denotes an
integer:

- with `repr` present it is authoritative and must spell a plain integer
  literal, so an exponent-form literal such as `1e400` is rejected rather
  than decoded from the infinity it rounded to;
- with `repr` absent the `Double` must be finite and integral, and is
  converted exactly through its significand and exponent — an `Int64`
  detour would cap the range at 2^63, well short of the integers a double
  holds, so `1e300` decodes to the 301-digit integer that double is.

`Json::number(123)` previously failed to decode; the error test in
json/from_json_test.mbt now covers `true` and `1.5` instead.

Mutation-verified: ignoring `repr` fails 4 tests (the doc test decodes
12345678901234567741440), and routing through `Int64` fails 2 (2^63
saturates, 1e300 comes out 19 digits). All 7533 tests pass, and bigint
plus json pass on all four backends — the js one matters here because it
uses the host `BigInt`.

Reviewed with Codex CLI: "The revised comments match the lexer, and the
new boundary assertions match the arithmetic. No blocking issues." It
also cross-checked the bit surgery against 47,499 integral doubles.

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
Copilot AI lite review requested due to automatic review settings September 8, 2026 04:14
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 6585

Coverage increased (+0.009%) to 89.279%

Details

  • Coverage increased (+0.009%) from the base build.
  • Patch coverage: 20 of 20 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 18348
Covered Lines: 16381
Line Coverage: 89.28%
Coverage Strength: 273877.3 hits per line

💛 - Coveralls

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The decoder widening is internally consistent with existing Json::Number(repr) semantics and is backed by targeted tests covering key correctness boundaries and failure cases.

Pull request overview

This PR widens @json.FromJson decoding for @bigint.BigInt to accept JSON numbers that denote integers, preserving exact digits when the JSON parser provided the original numeric source text (repr) and otherwise converting finite integral Double values exactly via IEEE-754 significand/exponent decomposition.

Changes:

  • Extend BigInt::from_json to accept Json::Number in addition to the existing string encoding, with clear failure modes for non-finite and non-integral values.
  • Add a focused BigInt JSON-number decoding test suite covering large-integer repr, boundary doubles, exponent-form overflow, and non-integer rejection.
  • Update existing JSON decode error tests to reflect the broadened decoder behavior and new error messaging.
File summaries
File Description
bigint/bigint.mbt Implements Number decoding path for BigInt::from_json, including exact DoubleBigInt conversion helper and updated error message for type mismatch.
bigint/from_json_number_test.mbt Adds comprehensive tests for decoding BigInt from JSON numbers (including repr preservation and IEEE-754 boundary cases).
bigint/moon.pkg Adds moonbitlang/core/double as a test-only dependency to support new boundary tests using @double.* constants.
json/from_json_test.mbt Updates the BigInt decode error handling test to reflect that integer numbers now decode successfully and adjusts the expected error messages.
Review details
  • Files reviewed: 4/4 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants