From fb01c968f4453d25cba076505a6f09a4e5cfe4f7 Mon Sep 17 00:00:00 2001 From: Sergey Shandar Date: Tue, 25 Aug 2026 22:59:46 -0700 Subject: [PATCH] nanvm: format bigint string coercion as decimal --- fjs/nanvm/module.f.mjs | 7 +-- nanvm-lib/src/vm/bigint/display.rs | 41 ++++++++++++++ nanvm-lib/src/vm/bigint/mod.rs | 1 + nanvm-lib/src/vm/string_coercion.rs | 3 +- nanvm-lib/tests/test/generated.rs | 6 +-- nanvm-lib/tests/test/main.rs | 19 +++++++ .../todo/bigint-decimal-string-coercion.md | 54 ------------------- 7 files changed, 66 insertions(+), 65 deletions(-) create mode 100644 nanvm-lib/src/vm/bigint/display.rs delete mode 100644 nanvm-lib/todo/bigint-decimal-string-coercion.md diff --git a/fjs/nanvm/module.f.mjs b/fjs/nanvm/module.f.mjs index f04658ee9..f64bfdafc 100644 --- a/fjs/nanvm/module.f.mjs +++ b/fjs/nanvm/module.f.mjs @@ -137,9 +137,6 @@ const mulCases = [ { name: 'numberByBigint', args: [1, 1n], expected: throws }, ] -const hexadecimalBigint = - 'nanvm-lib prints bigints in hexadecimal; see nanvm-lib/todo/bigint-decimal-string-coercion.md' - /** * `String(x)`. * @@ -162,8 +159,8 @@ const stringCoercionCases = [ { name: 'null', args: [null], expected: 'null' }, { name: 'undefined', args: [undefined], expected: 'undefined' }, { name: 'string', args: ['already'], expected: 'already' }, - { name: 'bigint', args: [123n], expected: '123', rust: hexadecimalBigint }, - { name: 'negativeBigint', args: [-456n], expected: '-456', rust: hexadecimalBigint }, + { name: 'bigint', args: [123n], expected: '123' }, + { name: 'negativeBigint', args: [-456n], expected: '-456' }, { name: 'emptyArray', args: [[]], expected: '' }, { name: 'singletonArray', args: [[1]], expected: '1' }, { name: 'array', args: [[1, 2, 3]], expected: '1,2,3' }, diff --git a/nanvm-lib/src/vm/bigint/display.rs b/nanvm-lib/src/vm/bigint/display.rs new file mode 100644 index 000000000..d5a7ab83e --- /dev/null +++ b/nanvm-lib/src/vm/bigint/display.rs @@ -0,0 +1,41 @@ +use crate::{ + common::sized_index::SizedIndex, + sign::Sign, + vm::{BigInt, IContainer, IVm}, +}; +use core::fmt::{Display, Formatter, Result, Write}; + +const DECIMAL_BASE: u64 = 10_000_000_000_000_000_000; + +impl Display for BigInt { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + if self.is_zero() { + return f.write_char('0'); + } + if *self.0.header() == Sign::Negative { + f.write_char('-')?; + } + + let items = self.0.items(); + let mut words: Vec = (0..items.length()).map(|i| items[i]).collect(); + let mut groups = Vec::new(); + while !words.is_empty() { + let mut remainder = 0u128; + for word in words.iter_mut().rev() { + let dividend = (remainder << 64) | *word as u128; + *word = (dividend / DECIMAL_BASE as u128) as u64; + remainder = dividend % DECIMAL_BASE as u128; + } + groups.push(remainder as u64); + while words.last() == Some(&0) { + words.pop(); + } + } + + write!(f, "{}", groups.pop().unwrap())?; + for group in groups.iter().rev() { + write!(f, "{group:019}")?; + } + Ok(()) + } +} diff --git a/nanvm-lib/src/vm/bigint/mod.rs b/nanvm-lib/src/vm/bigint/mod.rs index 7b7ba93bf..93fcfacf1 100644 --- a/nanvm-lib/src/vm/bigint/mod.rs +++ b/nanvm-lib/src/vm/bigint/mod.rs @@ -2,6 +2,7 @@ mod add; mod cmp; mod debug; mod default; +mod display; mod from; mod index; mod mul; diff --git a/nanvm-lib/src/vm/string_coercion.rs b/nanvm-lib/src/vm/string_coercion.rs index 5ba35e36e..8e3d1a27c 100644 --- a/nanvm-lib/src/vm/string_coercion.rs +++ b/nanvm-lib/src/vm/string_coercion.rs @@ -61,8 +61,7 @@ impl Dispatch for StringCoercion { } fn bigint(self, v: BigInt) -> Self::Result { - // TODO: we should use different algorithm for large numbers. - to_result(&format!("{v:?}")) + to_result(&v.to_string()) } fn object(self, v: Object) -> Self::Result { diff --git a/nanvm-lib/tests/test/generated.rs b/nanvm-lib/tests/test/generated.rs index 1f714d52b..21b10c830 100644 --- a/nanvm-lib/tests/test/generated.rs +++ b/nanvm-lib/tests/test/generated.rs @@ -188,10 +188,8 @@ fn string_coercion() { check::("null", Nullish::Null.to_any().to_string().map(|v| v.to_any()), string_any("null")); check::("undefined", Nullish::Undefined.to_any().to_string().map(|v| v.to_any()), string_any("undefined")); check::("string", string_any("already").to_string().map(|v| v.to_any()), string_any("already")); - // TODO: nanvm-lib prints bigints in hexadecimal; see nanvm-lib/todo/bigint-decimal-string-coercion.md - // check::("bigint", bigint_any(123).to_string().map(|v| v.to_any()), string_any("123")); - // TODO: nanvm-lib prints bigints in hexadecimal; see nanvm-lib/todo/bigint-decimal-string-coercion.md - // check::("negativeBigint", bigint_any(-456).to_string().map(|v| v.to_any()), string_any("-456")); + check::("bigint", bigint_any(123).to_string().map(|v| v.to_any()), string_any("123")); + check::("negativeBigint", bigint_any(-456).to_string().map(|v| v.to_any()), string_any("-456")); check::("emptyArray", Array::default().to_any().to_string().map(|v| v.to_any()), string_any("")); check::("singletonArray", [(1f64).to_any()].to_array().to_any().to_string().map(|v| v.to_any()), string_any("1")); check::("array", [(1f64).to_any(), (2f64).to_any(), (3f64).to_any()].to_array().to_any().to_string().map(|v| v.to_any()), string_any("1,2,3")); diff --git a/nanvm-lib/tests/test/main.rs b/nanvm-lib/tests/test/main.rs index 3f4e870ff..753f87cc2 100644 --- a/nanvm-lib/tests/test/main.rs +++ b/nanvm-lib/tests/test/main.rs @@ -114,6 +114,24 @@ fn bigint_debug_format() { } } +/// Decimal display across limb and decimal-group boundaries. +fn bigint_display_format() { + let zero: BigInt = 0u64.into(); + assert_eq!(zero.to_string(), "0"); + + let two_to_64 = BigInt::::normalize_new(Sign::Positive, [0, 1]); + assert_eq!(two_to_64.to_string(), "18446744073709551616"); + + let max_u128 = BigInt::::normalize_new(Sign::Positive, [u64::MAX, u64::MAX]); + assert_eq!( + max_u128.to_string(), + "340282366920938463463374607431768211455" + ); + + let negative = BigInt::::normalize_new(Sign::Negative, [0, 1]); + assert_eq!(negative.to_string(), "-18446744073709551616"); +} + fn format_fn() { let f = Function::(A::InternalFunction::new_ok( ("myfunc".into(), 2), @@ -185,6 +203,7 @@ fn gen_test() { conversions::(); debug_format::(); bigint_debug_format::(); + bigint_display_format::(); unary_plus_bigint_message::(); bigint_add::(); bigint_mul::(); diff --git a/nanvm-lib/todo/bigint-decimal-string-coercion.md b/nanvm-lib/todo/bigint-decimal-string-coercion.md deleted file mode 100644 index cd79016dc..000000000 --- a/nanvm-lib/todo/bigint-decimal-string-coercion.md +++ /dev/null @@ -1,54 +0,0 @@ -## Coerce bigints to decimal strings - -**Priority:** P3 -**Status:** open - -### Problem - -`StringCoercion::bigint` (`src/vm/string_coercion.rs`) formats through `Debug`: - -```rust -fn bigint(self, v: BigInt) -> Self::Result { - // TODO: we should use different algorithm for large numbers. - to_result(&format!("{v:?}")) -} -``` - -`Debug` for `BigInt` prints hexadecimal with an `n` suffix, so `String(123n)` -returns `"0x7Bn"` where JavaScript returns `"123"`, and `String(-456n)` returns -`"-0x1C8n"` instead of `"-456"`. `ToString` on a bigint is -[decimal by specification](https://tc39.es/ecma262/#sec-numeric-types-bigint-tostring) -unless an explicit radix is passed to `BigInt.prototype.toString`. - -Two cases in the shared operator test data -([`fjs/nanvm/module.f.mjs`](../../fjs/nanvm/module.f.mjs), `stringCoercion`) -carry a `rust` reason pointing here and are therefore commented out in -`tests/test/generated.rs`. Deleting those two `rust` reasons and regenerating -is the acceptance test for this issue. - -### Proposal - -Convert the limb vector to decimal digits: repeatedly divide the magnitude by -the largest power of ten that fits in a limb (`10^19` for `u64`), emitting 19 -digits per step and zero-padding all but the most significant group, then -prefix `-` for a negative sign. That is O(n²) in the number of limbs, which is -the same complexity the existing `Debug` path has and is fine at the sizes the -VM sees today; a divide-and-conquer split is a later optimization, not a -blocker. - -`Debug` keeps its hexadecimal form — it is a developer-facing dump of the limb -representation, and the bigint formatting tests in -[`tests/test/main.rs`](../tests/test/main.rs) pin it deliberately. - -### Tasks - -- [ ] Add decimal conversion for `BigInt`. -- [ ] Use it from `StringCoercion::bigint`. -- [ ] Remove the two `rust` reasons from `stringCoercion` in the shared test - data and regenerate `tests/test/generated.rs`. - -### Related - -- [`nanvm-lib/tests/README.md`](../tests/README.md) — how the shared operator - test data records divergences like this one. -- [mvp-roadmap](./mvp-roadmap.md) — the operators task this belongs to.